Refactor network logging

This commit is contained in:
BinTianqi
2025-10-17 12:54:47 +08:00
parent 5b9ce9f984
commit fde191adc5
17 changed files with 232 additions and 98 deletions

View File

@@ -440,7 +440,10 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
composable<RecommendedGlobalProxy> { composable<RecommendedGlobalProxy> {
RecommendedGlobalProxyScreen(vm::setRecommendedGlobalProxy, ::navigateUp) RecommendedGlobalProxyScreen(vm::setRecommendedGlobalProxy, ::navigateUp)
} }
composable<NetworkLogging> { NetworkLoggingScreen(::navigateUp) } composable<NetworkLogging> {
NetworkLoggingScreen(vm::getNetworkLoggingEnabled, vm::setNetworkLoggingEnabled,
vm::getNetworkLogsCount, vm::exportNetworkLogs, vm::deleteNetworkLogs, ::navigateUp)
}
//composable<WifiAuthKeypair> { WifiAuthKeypairScreen(::navigateUp) } //composable<WifiAuthKeypair> { WifiAuthKeypairScreen(::navigateUp) }
composable<PreferentialNetworkService> { composable<PreferentialNetworkService> {
PreferentialNetworkServiceScreen(vm::getPnsEnabled, vm::setPnsEnabled, vm.pnsConfigs, PreferentialNetworkServiceScreen(vm::getPnsEnabled, vm::setPnsEnabled, vm.pnsConfigs,

View File

@@ -4,7 +4,7 @@ import android.content.Context
import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper import android.database.sqlite.SQLiteOpenHelper
class MyDbHelper(context: Context): SQLiteOpenHelper(context, "data", null, 2) { class MyDbHelper(context: Context): SQLiteOpenHelper(context, "data", null, 3) {
override fun onCreate(db: SQLiteDatabase) { override fun onCreate(db: SQLiteDatabase) {
db.execSQL("CREATE TABLE dhizuku_clients (uid INTEGER PRIMARY KEY," + db.execSQL("CREATE TABLE dhizuku_clients (uid INTEGER PRIMARY KEY," +
"signature TEXT, permissions TEXT)") "signature TEXT, permissions TEXT)")
@@ -14,5 +14,12 @@ class MyDbHelper(context: Context): SQLiteOpenHelper(context, "data", null, 2) {
db.execSQL("CREATE TABLE security_logs (id INTEGER, tag INTEGER, level INTEGER," + db.execSQL("CREATE TABLE security_logs (id INTEGER, tag INTEGER, level INTEGER," +
"time INTEGER, data TEXT)") "time INTEGER, data TEXT)")
} }
if (oldVersion < 3) {
db.execSQL(
"CREATE TABLE network_logs (id INTEGER, package INTEGER, time INTEGER," +
"type TEXT, host TEXT, count INTEGER, addresses TEXT, address TEXT," +
"port INTEGER)"
)
}
} }
} }

View File

@@ -6,7 +6,10 @@ import android.database.DatabaseUtils
import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase
import android.os.Build.VERSION import android.os.Build.VERSION
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.database.getIntOrNull
import androidx.core.database.getLongOrNull
import androidx.core.database.getStringOrNull import androidx.core.database.getStringOrNull
import com.bintianqi.owndroid.dpm.NetworkLog
import com.bintianqi.owndroid.dpm.SecurityEvent import com.bintianqi.owndroid.dpm.SecurityEvent
import com.bintianqi.owndroid.dpm.SecurityEventWithData import com.bintianqi.owndroid.dpm.SecurityEventWithData
import com.bintianqi.owndroid.dpm.transformSecurityEventData import com.bintianqi.owndroid.dpm.transformSecurityEventData
@@ -154,4 +157,71 @@ class MyRepository(val dbHelper: MyDbHelper) {
fun deleteSecurityLogs() { fun deleteSecurityLogs() {
dbHelper.writableDatabase.execSQL("DELETE FROM security_logs") dbHelper.writableDatabase.execSQL("DELETE FROM security_logs")
} }
fun getNetworkLogsCount(): Long {
return DatabaseUtils.queryNumEntries(dbHelper.readableDatabase, "network_logs")
}
fun writeNetworkLogs(logs: List<NetworkLog>) {
val db = dbHelper.writableDatabase
val statement = db.compileStatement(
"INSERT INTO network_logs VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
)
db.beginTransaction()
logs.forEach { event ->
if (event.id == null) statement.bindNull(1)
else statement.bindLong(1, event.id)
statement.bindString(2, event.packageName)
statement.bindLong(3, event.time)
statement.bindString(4, event.type)
if (event.host == null) statement.bindNull(5)
else statement.bindString(5, event.host)
if (event.count == null) statement.bindNull(6)
else statement.bindLong(6, event.count.toLong())
if (event.addresses == null) statement.bindNull(7)
else statement.bindString(7, event.addresses.joinToString(","))
if (event.address == null) statement.bindNull(8)
else statement.bindString(8, event.address)
if (event.port == null) statement.bindNull(9)
else statement.bindLong(9, event.port.toLong())
statement.executeInsert()
statement.clearBindings()
}
db.setTransactionSuccessful()
db.endTransaction()
statement.close()
}
fun exportNetworkLogs(stream: OutputStream) {
val bw = stream.bufferedWriter()
val json = Json {
explicitNulls = false
}
var offset = 0
var addComma = false
bw.write("[")
while (true) {
val cursor = dbHelper.readableDatabase.rawQuery(
"SELECT * FROM network_logs LIMIT ? OFFSET ?",
arrayOf(100.toString(), offset.toString())
)
if (cursor.count == 0) break
while (cursor.moveToNext()) {
if (addComma) bw.write(",")
addComma = true
val log = NetworkLog(
cursor.getLongOrNull(0), cursor.getString(1), cursor.getLong(2),
cursor.getString(3), cursor.getStringOrNull(4), cursor.getIntOrNull(5),
cursor.getStringOrNull(6)?.split(',')?.filter { it.isNotEmpty() },
cursor.getStringOrNull(7), cursor.getIntOrNull(8)
)
bw.write(json.encodeToString(log))
offset += 100
}
cursor.close()
}
bw.write("]")
bw.close()
}
fun deleteNetworkLogs() {
dbHelper.writableDatabase.execSQL("DELETE FROM network_logs")
}
} }

View File

@@ -1753,6 +1753,28 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
fun removeApnConfig(id: Int): Boolean { fun removeApnConfig(id: Int): Boolean {
return DPM.removeOverrideApn(DAR, id) return DPM.removeOverrideApn(DAR, id)
} }
@RequiresApi(26)
fun getNetworkLoggingEnabled(): Boolean {
return DPM.isNetworkLoggingEnabled(DAR)
}
@RequiresApi(26)
fun setNetworkLoggingEnabled(enabled: Boolean) {
DPM.setNetworkLoggingEnabled(DAR, enabled)
}
fun getNetworkLogsCount(): Int {
return myRepo.getNetworkLogsCount().toInt()
}
fun exportNetworkLogs(uri: Uri, callback: () -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
application.contentResolver.openOutputStream(uri)?.use {
myRepo.exportNetworkLogs(it)
}
withContext(Dispatchers.Main) { callback() }
}
}
fun deleteNetworkLogs() {
myRepo.deleteNetworkLogs()
}
@RequiresApi(29) @RequiresApi(29)
fun getPasswordComplexity(): PasswordComplexity { fun getPasswordComplexity(): PasswordComplexity {

View File

@@ -72,10 +72,15 @@ enum class NotificationType(
12, R.string.security_logs_collected, R.drawable.description_fill0, 12, R.string.security_logs_collected, R.drawable.description_fill0,
MyNotificationChannel.SecurityLogging MyNotificationChannel.SecurityLogging
), ),
NetworkLogsCollected(
13, R.string.network_logs_collected, R.drawable.description_fill0,
MyNotificationChannel.NetworkLogging
),
} }
enum class MyNotificationChannel(val id: String, val text: Int, val importance: Int) { enum class MyNotificationChannel(val id: String, val text: Int, val importance: Int) {
LockTaskMode("LockTaskMode", R.string.lock_task_mode, NotificationManagerCompat.IMPORTANCE_HIGH), LockTaskMode("LockTaskMode", R.string.lock_task_mode, NotificationManagerCompat.IMPORTANCE_HIGH),
Events("Events", R.string.events, NotificationManagerCompat.IMPORTANCE_LOW), Events("Events", R.string.events, NotificationManagerCompat.IMPORTANCE_LOW),
SecurityLogging("SecurityLogging", R.string.security_logging, NotificationManagerCompat.IMPORTANCE_MIN) SecurityLogging("SecurityLogging", R.string.security_logging, NotificationManagerCompat.IMPORTANCE_MIN),
NetworkLogging("NetworkLogging", R.string.network_logging, NotificationManagerCompat.IMPORTANCE_MIN)
} }

View File

@@ -173,7 +173,7 @@ fun AppChooserScreen(
} }
} }
} }
item { Spacer(Modifier.height(60.dp)) } item { Spacer(Modifier.height(BottomPadding)) }
} }
} }
} }

View File

@@ -11,12 +11,9 @@ import android.os.Build.VERSION
import android.os.UserHandle import android.os.UserHandle
import android.os.UserManager import android.os.UserManager
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import com.bintianqi.owndroid.dpm.handleNetworkLogs
import com.bintianqi.owndroid.dpm.handlePrivilegeChange import com.bintianqi.owndroid.dpm.handlePrivilegeChange
import com.bintianqi.owndroid.dpm.retrieveNetworkLogs
import com.bintianqi.owndroid.dpm.retrieveSecurityLogs import com.bintianqi.owndroid.dpm.retrieveSecurityLogs
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class Receiver : DeviceAdminReceiver() { class Receiver : DeviceAdminReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
@@ -47,10 +44,8 @@ class Receiver : DeviceAdminReceiver() {
override fun onNetworkLogsAvailable(context: Context, intent: Intent, batchToken: Long, networkLogsCount: Int) { override fun onNetworkLogsAvailable(context: Context, intent: Intent, batchToken: Long, networkLogsCount: Int) {
super.onNetworkLogsAvailable(context, intent, batchToken, networkLogsCount) super.onNetworkLogsAvailable(context, intent, batchToken, networkLogsCount)
if(VERSION.SDK_INT >= 26) { if (VERSION.SDK_INT >= 26) {
CoroutineScope(Dispatchers.IO).launch { retrieveNetworkLogs(context.applicationContext as MyApplication, batchToken)
handleNetworkLogs(context, batchToken)
}
} }
} }

View File

@@ -273,7 +273,7 @@ fun ApplicationDetailsScreen(
) )
if(VERSION.SDK_INT >= 28) FunctionItem(R.string.clear_app_storage, icon = R.drawable.mop_fill0) { dialog = 1 } if(VERSION.SDK_INT >= 28) FunctionItem(R.string.clear_app_storage, icon = R.drawable.mop_fill0) { dialog = 1 }
FunctionItem(R.string.uninstall, icon = R.drawable.delete_fill0) { dialog = 2 } FunctionItem(R.string.uninstall, icon = R.drawable.delete_fill0) { dialog = 2 }
Spacer(Modifier.height(40.dp)) Spacer(Modifier.height(BottomPadding))
} }
if(dialog == 1 && VERSION.SDK_INT >= 28) if(dialog == 1 && VERSION.SDK_INT >= 28)
ClearAppStorageDialog(packageName, vm::clearAppData) { dialog = 0 } ClearAppStorageDialog(packageName, vm::clearAppData) { dialog = 0 }
@@ -606,7 +606,7 @@ fun CredentialManagerPolicyScreen(
) { ) {
Text(stringResource(R.string.apply)) Text(stringResource(R.string.apply))
} }
Spacer(Modifier.height(40.dp)) Spacer(Modifier.height(BottomPadding))
} }
} }
} }
@@ -661,7 +661,7 @@ fun PermittedAsAndImPackages(
} }
Spacer(Modifier.height(10.dp)) Spacer(Modifier.height(10.dp))
Notes(note, HorizontalPadding) Notes(note, HorizontalPadding)
Spacer(Modifier.height(40.dp)) Spacer(Modifier.height(BottomPadding))
} }
} }
} }
@@ -771,7 +771,7 @@ fun PackageFunctionScreen(
Text(stringResource(R.string.add)) Text(stringResource(R.string.add))
} }
if (notes != null) Notes(notes, HorizontalPadding) if (notes != null) Notes(notes, HorizontalPadding)
Spacer(Modifier.height(40.dp)) Spacer(Modifier.height(BottomPadding))
} }
} }
} }

View File

@@ -29,12 +29,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.add
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonArray
@SuppressLint("PrivateApi") @SuppressLint("PrivateApi")
fun binderWrapperDevicePolicyManager(appContext: Context): DevicePolicyManager? { fun binderWrapperDevicePolicyManager(appContext: Context): DevicePolicyManager? {
@@ -137,39 +132,37 @@ val runtimePermissions = listOf(
PermissionItem(Manifest.permission.ACTIVITY_RECOGNITION, R.string.permission_ACTIVITY_RECOGNITION, R.drawable.history_fill0, true, 29) PermissionItem(Manifest.permission.ACTIVITY_RECOGNITION, R.string.permission_ACTIVITY_RECOGNITION, R.drawable.history_fill0, true, 29)
).filter { VERSION.SDK_INT >= it.requiresApi } ).filter { VERSION.SDK_INT >= it.requiresApi }
@Serializable
class NetworkLog(
val id: Long?, @SerialName("package") val packageName: String, val time: Long, val type: String,
val host: String?, val count: Int?, val addresses: List<String>?,
val address: String?, val port: Int?
)
@RequiresApi(26) @RequiresApi(26)
fun handleNetworkLogs(context: Context, batchToken: Long) { fun retrieveNetworkLogs(app: MyApplication, token: Long) {
val networkEvents = Privilege.DPM.retrieveNetworkLogs(Privilege.DAR, batchToken) ?: return CoroutineScope(Dispatchers.IO).launch {
val file = context.filesDir.resolve("NetworkLogs.json") val logs = Privilege.DPM.retrieveNetworkLogs(Privilege.DAR, token)?.mapNotNull {
val fileExist = file.exists() when (it) {
val json = Json { ignoreUnknownKeys = true; explicitNulls = false } is DnsEvent -> NetworkLog(
val buffer = file.bufferedWriter() if (VERSION.SDK_INT >= 28) it.id else null, it.packageName, it.timestamp, "dns",
networkEvents.forEachIndexed { index, event -> it.hostname, it.totalResolvedAddressCount,
if(fileExist && index == 0) buffer.write(",") it.inetAddresses.mapNotNull { address -> address.hostAddress }, null, null
val item = buildJsonObject { )
if(VERSION.SDK_INT >= 28) put("id", event.id) is ConnectEvent -> NetworkLog(
put("time", event.timestamp) if (VERSION.SDK_INT >= 28) it.id else null, it.packageName, it.timestamp,
put("package", event.packageName) "connect", null, null, null, it.inetAddress.hostAddress, it.port
if(event is DnsEvent) { )
put("type", "dns") else -> null
put("host", event.hostname)
put("count", event.totalResolvedAddressCount)
putJsonArray("addresses") {
event.inetAddresses.forEach { inetAddresses ->
add(inetAddresses.hostAddress)
} }
} }
if (logs.isNullOrEmpty()) return@launch
app.myRepo.writeNetworkLogs(logs)
NotificationUtils.sendBasicNotification(
app, NotificationType.NetworkLogsCollected,
app.getString(R.string.n_logs_in_total, logs.size)
)
} }
if(event is ConnectEvent) {
put("type", "connect")
put("address", event.inetAddress.hostAddress)
put("port", event.port)
}
}
buffer.write(json.encodeToString(item))
if(index < networkEvents.size - 1) buffer.write(",")
}
buffer.close()
} }
@Serializable @Serializable
@@ -493,7 +486,8 @@ fun transformSecurityEventData(tag: Int, payload: Any): SecurityEventData? {
@RequiresApi(24) @RequiresApi(24)
fun retrieveSecurityLogs(app: MyApplication) { fun retrieveSecurityLogs(app: MyApplication) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val logs = Privilege.DPM.retrieveSecurityLogs(Privilege.DAR) ?: return@launch val logs = Privilege.DPM.retrieveSecurityLogs(Privilege.DAR)
if (logs.isNullOrEmpty()) return@launch
app.myRepo.writeSecurityLogs(logs) app.myRepo.writeSecurityLogs(logs)
NotificationUtils.sendBasicNotification( NotificationUtils.sendBasicNotification(
app, NotificationType.SecurityLogsCollected, app, NotificationType.SecurityLogsCollected,

View File

@@ -10,6 +10,7 @@ import android.app.admin.DevicePolicyManager.WIFI_SECURITY_PERSONAL
import android.app.admin.WifiSsidPolicy import android.app.admin.WifiSsidPolicy
import android.app.usage.NetworkStats import android.app.usage.NetworkStats
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.Uri
import android.net.wifi.WifiConfiguration import android.net.wifi.WifiConfiguration
import android.os.Build.VERSION import android.os.Build.VERSION
import android.provider.Telephony import android.provider.Telephony
@@ -106,6 +107,7 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bintianqi.owndroid.BottomPadding
import com.bintianqi.owndroid.HorizontalPadding import com.bintianqi.owndroid.HorizontalPadding
import com.bintianqi.owndroid.MyViewModel import com.bintianqi.owndroid.MyViewModel
import com.bintianqi.owndroid.Privilege import com.bintianqi.owndroid.Privilege
@@ -116,6 +118,7 @@ import com.bintianqi.owndroid.formatFileSize
import com.bintianqi.owndroid.adaptiveInsets import com.bintianqi.owndroid.adaptiveInsets
import com.bintianqi.owndroid.popToast import com.bintianqi.owndroid.popToast
import com.bintianqi.owndroid.showOperationResultToast import com.bintianqi.owndroid.showOperationResultToast
import com.bintianqi.owndroid.ui.CircularProgressDialog
import com.bintianqi.owndroid.ui.ErrorDialog import com.bintianqi.owndroid.ui.ErrorDialog
import com.bintianqi.owndroid.ui.FullWidthCheckBoxItem import com.bintianqi.owndroid.ui.FullWidthCheckBoxItem
import com.bintianqi.owndroid.ui.FullWidthRadioButtonItem import com.bintianqi.owndroid.ui.FullWidthRadioButtonItem
@@ -135,6 +138,9 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@Serializable object Network @Serializable object Network
@@ -803,7 +809,7 @@ private fun AddNetworkScreen(
) { ) {
Text(stringResource(if (updating) R.string.update else R.string.add)) Text(stringResource(if (updating) R.string.update else R.string.add))
} }
Spacer(Modifier.height(60.dp)) Spacer(Modifier.height(BottomPadding))
} }
} }
@@ -1539,50 +1545,82 @@ fun RecommendedGlobalProxyScreen(
@RequiresApi(26) @RequiresApi(26)
@Composable @Composable
fun NetworkLoggingScreen(onNavigateUp: () -> Unit) { fun NetworkLoggingScreen(
getEnabled: () -> Boolean, setEnabled: (Boolean) -> Unit, getCount: () -> Int,
exportLogs: (Uri, () -> Unit) -> Unit, deleteLogs: () -> Unit, onNavigateUp: () -> Unit
) {
val context = LocalContext.current val context = LocalContext.current
val logFile = context.filesDir.resolve("NetworkLogs.json") var enabled by remember { mutableStateOf(false) }
var fileSize by remember { mutableLongStateOf(0) } var count by remember { mutableIntStateOf(0) }
LaunchedEffect(Unit) { fileSize = logFile.length() } var dialog by rememberSaveable { mutableStateOf(false) }
val exportNetworkLogsLauncher = rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("application/json")) { uri -> var exporting by rememberSaveable { mutableStateOf(false) }
if(uri != null) context.contentResolver.openOutputStream(uri)?.use { outStream -> LaunchedEffect(Unit) {
outStream.write("[".encodeToByteArray()) enabled = getEnabled()
logFile.inputStream().use { it.copyTo(outStream) } count = getCount()
outStream.write("]".encodeToByteArray()) }
val exportLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.CreateDocument("application/json")
) { uri ->
if (uri != null) {
exporting = true
exportLogs(uri) {
exporting = false
context.showOperationResultToast(true) context.showOperationResultToast(true)
} }
} }
MyScaffold(R.string.network_logging, onNavigateUp) { }
MyScaffold(R.string.network_logging, onNavigateUp, 0.dp) {
SwitchItem( SwitchItem(
R.string.enable, R.string.enable, enabled, {
getState = { Privilege.DPM.isNetworkLoggingEnabled(Privilege.DAR) }, setEnabled(it)
onCheckedChange = { Privilege.DPM.setNetworkLoggingEnabled(Privilege.DAR, it) }, enabled = it
padding = false }
)
Text(
stringResource(R.string.n_logs_in_total, count),
Modifier.padding(HorizontalPadding, 5.dp)
) )
Text(stringResource(R.string.log_file_size_is, formatFileSize(fileSize)))
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
Button( Button(
onClick = { {
exportNetworkLogsLauncher.launch("NetworkLogs.json") val date = SimpleDateFormat("yyyyMMdd", Locale.getDefault()).format(Date())
exportLauncher.launch("network_logs_$date")
}, },
enabled = fileSize > 0, Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding),
modifier = Modifier.fillMaxWidth(0.49F) count > 0
) { ) {
Text(stringResource(R.string.export_logs)) Text(stringResource(R.string.export_logs))
} }
Button( if (count > 0) Button(
onClick = { {
logFile.delete() dialog = true
fileSize = logFile.length()
}, },
enabled = fileSize > 0, Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding),
modifier = Modifier.fillMaxWidth(0.96F)
) { ) {
Text(stringResource(R.string.delete_logs)) Text(stringResource(R.string.delete_logs))
} }
Spacer(Modifier.height(10.dp))
Notes(R.string.info_network_log, HorizontalPadding)
} }
Notes(R.string.info_network_log) if (exporting) CircularProgressDialog { exporting = false }
if (dialog) AlertDialog(
text = {
Text(stringResource(R.string.delete_logs))
},
confirmButton = {
TextButton({
deleteLogs()
dialog = false
}) {
Text(stringResource(R.string.confirm))
} }
},
dismissButton = {
TextButton({ dialog = false }) {
Text(stringResource(R.string.cancel))
}
},
onDismissRequest = { dialog = false }
)
} }
@Serializable object PreferentialNetworkService @Serializable object PreferentialNetworkService

View File

@@ -81,6 +81,7 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bintianqi.owndroid.AppInfo import com.bintianqi.owndroid.AppInfo
import com.bintianqi.owndroid.BottomPadding
import com.bintianqi.owndroid.DhizukuClientInfo import com.bintianqi.owndroid.DhizukuClientInfo
import com.bintianqi.owndroid.DhizukuPermissions import com.bintianqi.owndroid.DhizukuPermissions
import com.bintianqi.owndroid.HorizontalPadding import com.bintianqi.owndroid.HorizontalPadding
@@ -615,7 +616,7 @@ fun AddDelegatedAdminScreen(
) { ) {
Text(stringResource(R.string.delete)) Text(stringResource(R.string.delete))
} }
Spacer(Modifier.height(40.dp)) Spacer(Modifier.height(BottomPadding))
} }
} }

View File

@@ -101,6 +101,7 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bintianqi.owndroid.AppInfo import com.bintianqi.owndroid.AppInfo
import com.bintianqi.owndroid.BottomPadding
import com.bintianqi.owndroid.HorizontalPadding import com.bintianqi.owndroid.HorizontalPadding
import com.bintianqi.owndroid.MyViewModel import com.bintianqi.owndroid.MyViewModel
import com.bintianqi.owndroid.Privilege import com.bintianqi.owndroid.Privilege
@@ -1281,7 +1282,7 @@ private fun LockTaskPackages(
Text(stringResource(R.string.add)) Text(stringResource(R.string.add))
} }
Notes(R.string.info_lock_task_packages) Notes(R.string.info_lock_task_packages)
Spacer(Modifier.height(40.dp)) Spacer(Modifier.height(BottomPadding))
} }
} }
} }
@@ -1331,7 +1332,7 @@ private fun LockTaskFeatures(
) { ) {
Text(stringResource(R.string.apply)) Text(stringResource(R.string.apply))
} }
Spacer(Modifier.height(40.dp)) Spacer(Modifier.height(BottomPadding))
ErrorDialog(errorMessage) { errorMessage = null } ErrorDialog(errorMessage) { errorMessage = null }
} }
} }
@@ -1417,7 +1418,7 @@ fun CaCertScreen(
HorizontalDivider() HorizontalDivider()
} }
item { item {
Spacer(Modifier.height(40.dp)) Spacer(Modifier.height(BottomPadding))
} }
} }
if (selectedCaCert != null && (dialog == 1 || dialog == 2)) { if (selectedCaCert != null && (dialog == 1 || dialog == 2)) {

View File

@@ -239,7 +239,7 @@ fun UserRestrictionEditorScreen(
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Done), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions { add() } keyboardActions = KeyboardActions { add() }
) )
Spacer(Modifier.height(40.dp)) Spacer(Modifier.height(BottomPadding))
} }
} }
} }

View File

@@ -277,7 +277,6 @@
<string name="invalid_config">Неверная конфигурация</string> <string name="invalid_config">Неверная конфигурация</string>
<string name="excluded_hosts">Исключить хосты</string> <string name="excluded_hosts">Исключить хосты</string>
<string name="network_logging">Сетевой журнал</string> <string name="network_logging">Сетевой журнал</string>
<string name="log_file_size_is">Размер файла журнала: %1$s</string>
<string name="delete_logs">Удалить журналы</string> <string name="delete_logs">Удалить журналы</string>
<string name="export_logs">Экспортировать журналы</string> <string name="export_logs">Экспортировать журналы</string>
<string name="wifi_auth_keypair">Пара ключей Wi-Fi</string> <string name="wifi_auth_keypair">Пара ключей Wi-Fi</string>

View File

@@ -306,7 +306,6 @@
<string name="invalid_config">Geçersiz yapılandırma</string> <string name="invalid_config">Geçersiz yapılandırma</string>
<string name="excluded_hosts">Hariç tutulan ana bilgisayarlar</string> <string name="excluded_hosts">Hariç tutulan ana bilgisayarlar</string>
<string name="network_logging">Ağ Kayıtları</string> <string name="network_logging">Ağ Kayıtları</string>
<string name="log_file_size_is">Kayıt dosyası boyutu: %1$s</string>
<string name="delete_logs">Kayıtları Sil</string> <string name="delete_logs">Kayıtları Sil</string>
<string name="export_logs">Kayıtları Dışa Aktar</string> <string name="export_logs">Kayıtları Dışa Aktar</string>
<string name="wifi_auth_keypair">Wi-Fi Kimlik Doğrulama Anahtar Çifti</string> <string name="wifi_auth_keypair">Wi-Fi Kimlik Doğrulama Anahtar Çifti</string>

View File

@@ -291,7 +291,7 @@
<string name="invalid_config">无效配置</string> <string name="invalid_config">无效配置</string>
<string name="excluded_hosts">排除的主机</string> <string name="excluded_hosts">排除的主机</string>
<string name="network_logging">网络日志</string> <string name="network_logging">网络日志</string>
<string name="log_file_size_is">日志文件大小:%1$s</string> <string name="network_logs_collected">网络日志已收集</string>
<string name="delete_logs">删除日志</string> <string name="delete_logs">删除日志</string>
<string name="export_logs">导出日志</string> <string name="export_logs">导出日志</string>
<string name="wifi_auth_keypair">Wi-Fi密钥对</string> <string name="wifi_auth_keypair">Wi-Fi密钥对</string>

View File

@@ -325,7 +325,7 @@
<string name="invalid_config">Invalid config</string> <string name="invalid_config">Invalid config</string>
<string name="excluded_hosts">Excluded hosts</string> <string name="excluded_hosts">Excluded hosts</string>
<string name="network_logging">Network logging</string> <string name="network_logging">Network logging</string>
<string name="log_file_size_is">Log file size: %1$s</string> <string name="network_logs_collected">Network logs collected</string>
<string name="delete_logs">Delete logs</string> <string name="delete_logs">Delete logs</string>
<string name="export_logs">Export logs</string> <string name="export_logs">Export logs</string>
<string name="wifi_auth_keypair">Wi-Fi keypair</string> <string name="wifi_auth_keypair">Wi-Fi keypair</string>