diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt index 8e0fbcc..1b7bd02 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -440,7 +440,10 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) { composable { RecommendedGlobalProxyScreen(vm::setRecommendedGlobalProxy, ::navigateUp) } - composable { NetworkLoggingScreen(::navigateUp) } + composable { + NetworkLoggingScreen(vm::getNetworkLoggingEnabled, vm::setNetworkLoggingEnabled, + vm::getNetworkLogsCount, vm::exportNetworkLogs, vm::deleteNetworkLogs, ::navigateUp) + } //composable { WifiAuthKeypairScreen(::navigateUp) } composable { PreferentialNetworkServiceScreen(vm::getPnsEnabled, vm::setPnsEnabled, vm.pnsConfigs, diff --git a/app/src/main/java/com/bintianqi/owndroid/MyDbHelper.kt b/app/src/main/java/com/bintianqi/owndroid/MyDbHelper.kt index f49867f..4e5544b 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MyDbHelper.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MyDbHelper.kt @@ -4,7 +4,7 @@ import android.content.Context import android.database.sqlite.SQLiteDatabase 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) { db.execSQL("CREATE TABLE dhizuku_clients (uid INTEGER PRIMARY KEY," + "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," + "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)" + ) + } } } \ No newline at end of file diff --git a/app/src/main/java/com/bintianqi/owndroid/MyRepository.kt b/app/src/main/java/com/bintianqi/owndroid/MyRepository.kt index 4dc4b57..8e6af20 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MyRepository.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MyRepository.kt @@ -6,7 +6,10 @@ import android.database.DatabaseUtils import android.database.sqlite.SQLiteDatabase import android.os.Build.VERSION import androidx.annotation.RequiresApi +import androidx.core.database.getIntOrNull +import androidx.core.database.getLongOrNull import androidx.core.database.getStringOrNull +import com.bintianqi.owndroid.dpm.NetworkLog import com.bintianqi.owndroid.dpm.SecurityEvent import com.bintianqi.owndroid.dpm.SecurityEventWithData import com.bintianqi.owndroid.dpm.transformSecurityEventData @@ -154,4 +157,71 @@ class MyRepository(val dbHelper: MyDbHelper) { fun deleteSecurityLogs() { dbHelper.writableDatabase.execSQL("DELETE FROM security_logs") } + + fun getNetworkLogsCount(): Long { + return DatabaseUtils.queryNumEntries(dbHelper.readableDatabase, "network_logs") + } + fun writeNetworkLogs(logs: List) { + 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") + } } \ No newline at end of file diff --git a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt index 5083798..4be2f61 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt @@ -1753,6 +1753,28 @@ class MyViewModel(application: Application): AndroidViewModel(application) { fun removeApnConfig(id: Int): Boolean { 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) fun getPasswordComplexity(): PasswordComplexity { diff --git a/app/src/main/java/com/bintianqi/owndroid/NotificationUtils.kt b/app/src/main/java/com/bintianqi/owndroid/NotificationUtils.kt index 2b664c8..2c326f8 100644 --- a/app/src/main/java/com/bintianqi/owndroid/NotificationUtils.kt +++ b/app/src/main/java/com/bintianqi/owndroid/NotificationUtils.kt @@ -72,10 +72,15 @@ enum class NotificationType( 12, R.string.security_logs_collected, R.drawable.description_fill0, 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) { LockTaskMode("LockTaskMode", R.string.lock_task_mode, NotificationManagerCompat.IMPORTANCE_HIGH), 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) } diff --git a/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt b/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt index a47eba3..ec7b1f7 100644 --- a/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt +++ b/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt @@ -173,7 +173,7 @@ fun AppChooserScreen( } } } - item { Spacer(Modifier.height(60.dp)) } + item { Spacer(Modifier.height(BottomPadding)) } } } } diff --git a/app/src/main/java/com/bintianqi/owndroid/Receiver.kt b/app/src/main/java/com/bintianqi/owndroid/Receiver.kt index 117f185..652f894 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Receiver.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Receiver.kt @@ -11,12 +11,9 @@ import android.os.Build.VERSION import android.os.UserHandle import android.os.UserManager import androidx.core.app.NotificationCompat -import com.bintianqi.owndroid.dpm.handleNetworkLogs import com.bintianqi.owndroid.dpm.handlePrivilegeChange +import com.bintianqi.owndroid.dpm.retrieveNetworkLogs import com.bintianqi.owndroid.dpm.retrieveSecurityLogs -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch class Receiver : DeviceAdminReceiver() { 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) { super.onNetworkLogsAvailable(context, intent, batchToken, networkLogsCount) - if(VERSION.SDK_INT >= 26) { - CoroutineScope(Dispatchers.IO).launch { - handleNetworkLogs(context, batchToken) - } + if (VERSION.SDK_INT >= 26) { + retrieveNetworkLogs(context.applicationContext as MyApplication, batchToken) } } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt index cb8f9a1..dfb1691 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt @@ -273,7 +273,7 @@ fun ApplicationDetailsScreen( ) 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 } - Spacer(Modifier.height(40.dp)) + Spacer(Modifier.height(BottomPadding)) } if(dialog == 1 && VERSION.SDK_INT >= 28) ClearAppStorageDialog(packageName, vm::clearAppData) { dialog = 0 } @@ -606,7 +606,7 @@ fun CredentialManagerPolicyScreen( ) { 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)) Notes(note, HorizontalPadding) - Spacer(Modifier.height(40.dp)) + Spacer(Modifier.height(BottomPadding)) } } } @@ -771,7 +771,7 @@ fun PackageFunctionScreen( Text(stringResource(R.string.add)) } if (notes != null) Notes(notes, HorizontalPadding) - Spacer(Modifier.height(40.dp)) + Spacer(Modifier.height(BottomPadding)) } } } \ No newline at end of file diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt index 89c9ef3..fb64d9b 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt @@ -29,12 +29,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json 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") 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) ).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?, + val address: String?, val port: Int? +) + @RequiresApi(26) -fun handleNetworkLogs(context: Context, batchToken: Long) { - val networkEvents = Privilege.DPM.retrieveNetworkLogs(Privilege.DAR, batchToken) ?: return - val file = context.filesDir.resolve("NetworkLogs.json") - val fileExist = file.exists() - val json = Json { ignoreUnknownKeys = true; explicitNulls = false } - val buffer = file.bufferedWriter() - networkEvents.forEachIndexed { index, event -> - if(fileExist && index == 0) buffer.write(",") - val item = buildJsonObject { - if(VERSION.SDK_INT >= 28) put("id", event.id) - put("time", event.timestamp) - put("package", event.packageName) - if(event is DnsEvent) { - put("type", "dns") - put("host", event.hostname) - put("count", event.totalResolvedAddressCount) - putJsonArray("addresses") { - event.inetAddresses.forEach { inetAddresses -> - add(inetAddresses.hostAddress) - } - } - } - if(event is ConnectEvent) { - put("type", "connect") - put("address", event.inetAddress.hostAddress) - put("port", event.port) +fun retrieveNetworkLogs(app: MyApplication, token: Long) { + CoroutineScope(Dispatchers.IO).launch { + val logs = Privilege.DPM.retrieveNetworkLogs(Privilege.DAR, token)?.mapNotNull { + when (it) { + is DnsEvent -> NetworkLog( + if (VERSION.SDK_INT >= 28) it.id else null, it.packageName, it.timestamp, "dns", + it.hostname, it.totalResolvedAddressCount, + it.inetAddresses.mapNotNull { address -> address.hostAddress }, null, null + ) + is ConnectEvent -> NetworkLog( + if (VERSION.SDK_INT >= 28) it.id else null, it.packageName, it.timestamp, + "connect", null, null, null, it.inetAddress.hostAddress, it.port + ) + else -> null } } - buffer.write(json.encodeToString(item)) - if(index < networkEvents.size - 1) buffer.write(",") + if (logs.isNullOrEmpty()) return@launch + app.myRepo.writeNetworkLogs(logs) + NotificationUtils.sendBasicNotification( + app, NotificationType.NetworkLogsCollected, + app.getString(R.string.n_logs_in_total, logs.size) + ) } - buffer.close() } @Serializable @@ -493,7 +486,8 @@ fun transformSecurityEventData(tag: Int, payload: Any): SecurityEventData? { @RequiresApi(24) fun retrieveSecurityLogs(app: MyApplication) { 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) NotificationUtils.sendBasicNotification( app, NotificationType.SecurityLogsCollected, 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 85368d9..5e1abd2 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt @@ -10,6 +10,7 @@ import android.app.admin.DevicePolicyManager.WIFI_SECURITY_PERSONAL import android.app.admin.WifiSsidPolicy import android.app.usage.NetworkStats import android.net.ConnectivityManager +import android.net.Uri import android.net.wifi.WifiConfiguration import android.os.Build.VERSION 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.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.bintianqi.owndroid.BottomPadding import com.bintianqi.owndroid.HorizontalPadding import com.bintianqi.owndroid.MyViewModel import com.bintianqi.owndroid.Privilege @@ -116,6 +118,7 @@ import com.bintianqi.owndroid.formatFileSize import com.bintianqi.owndroid.adaptiveInsets import com.bintianqi.owndroid.popToast import com.bintianqi.owndroid.showOperationResultToast +import com.bintianqi.owndroid.ui.CircularProgressDialog import com.bintianqi.owndroid.ui.ErrorDialog import com.bintianqi.owndroid.ui.FullWidthCheckBoxItem import com.bintianqi.owndroid.ui.FullWidthRadioButtonItem @@ -135,6 +138,9 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.serialization.Serializable +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale @Serializable object Network @@ -803,7 +809,7 @@ private fun AddNetworkScreen( ) { 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) @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 logFile = context.filesDir.resolve("NetworkLogs.json") - var fileSize by remember { mutableLongStateOf(0) } - LaunchedEffect(Unit) { fileSize = logFile.length() } - val exportNetworkLogsLauncher = rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("application/json")) { uri -> - if(uri != null) context.contentResolver.openOutputStream(uri)?.use { outStream -> - outStream.write("[".encodeToByteArray()) - logFile.inputStream().use { it.copyTo(outStream) } - outStream.write("]".encodeToByteArray()) - context.showOperationResultToast(true) + var enabled by remember { mutableStateOf(false) } + var count by remember { mutableIntStateOf(0) } + var dialog by rememberSaveable { mutableStateOf(false) } + var exporting by rememberSaveable { mutableStateOf(false) } + LaunchedEffect(Unit) { + enabled = getEnabled() + count = getCount() + } + val exportLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.CreateDocument("application/json") + ) { uri -> + if (uri != null) { + exporting = true + exportLogs(uri) { + exporting = false + context.showOperationResultToast(true) + } } } - MyScaffold(R.string.network_logging, onNavigateUp) { + MyScaffold(R.string.network_logging, onNavigateUp, 0.dp) { SwitchItem( - R.string.enable, - getState = { Privilege.DPM.isNetworkLoggingEnabled(Privilege.DAR) }, - onCheckedChange = { Privilege.DPM.setNetworkLoggingEnabled(Privilege.DAR, it) }, - padding = false + R.string.enable, enabled, { + setEnabled(it) + enabled = it + } ) - Text(stringResource(R.string.log_file_size_is, formatFileSize(fileSize))) - Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { - Button( - onClick = { - exportNetworkLogsLauncher.launch("NetworkLogs.json") - }, - enabled = fileSize > 0, - modifier = Modifier.fillMaxWidth(0.49F) - ) { - Text(stringResource(R.string.export_logs)) - } - Button( - onClick = { - logFile.delete() - fileSize = logFile.length() - }, - enabled = fileSize > 0, - modifier = Modifier.fillMaxWidth(0.96F) - ) { - Text(stringResource(R.string.delete_logs)) - } + Text( + stringResource(R.string.n_logs_in_total, count), + Modifier.padding(HorizontalPadding, 5.dp) + ) + Button( + { + val date = SimpleDateFormat("yyyyMMdd", Locale.getDefault()).format(Date()) + exportLauncher.launch("network_logs_$date") + }, + Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding), + count > 0 + ) { + Text(stringResource(R.string.export_logs)) } - Notes(R.string.info_network_log) + if (count > 0) Button( + { + dialog = true + }, + Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding), + ) { + Text(stringResource(R.string.delete_logs)) + } + Spacer(Modifier.height(10.dp)) + Notes(R.string.info_network_log, HorizontalPadding) } + 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 diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt index 55005d1..059071d 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt @@ -81,6 +81,7 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.bintianqi.owndroid.AppInfo +import com.bintianqi.owndroid.BottomPadding import com.bintianqi.owndroid.DhizukuClientInfo import com.bintianqi.owndroid.DhizukuPermissions import com.bintianqi.owndroid.HorizontalPadding @@ -615,7 +616,7 @@ fun AddDelegatedAdminScreen( ) { Text(stringResource(R.string.delete)) } - Spacer(Modifier.height(40.dp)) + Spacer(Modifier.height(BottomPadding)) } } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt index db4dcac..12407c9 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt @@ -101,6 +101,7 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.bintianqi.owndroid.AppInfo +import com.bintianqi.owndroid.BottomPadding import com.bintianqi.owndroid.HorizontalPadding import com.bintianqi.owndroid.MyViewModel import com.bintianqi.owndroid.Privilege @@ -1281,7 +1282,7 @@ private fun LockTaskPackages( Text(stringResource(R.string.add)) } 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)) } - Spacer(Modifier.height(40.dp)) + Spacer(Modifier.height(BottomPadding)) ErrorDialog(errorMessage) { errorMessage = null } } } @@ -1417,7 +1418,7 @@ fun CaCertScreen( HorizontalDivider() } item { - Spacer(Modifier.height(40.dp)) + Spacer(Modifier.height(BottomPadding)) } } if (selectedCaCert != null && (dialog == 1 || dialog == 2)) { diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/UserRestriction.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/UserRestriction.kt index 497e23c..8fe03b4 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/UserRestriction.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/UserRestriction.kt @@ -239,7 +239,7 @@ fun UserRestrictionEditorScreen( keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Done), keyboardActions = KeyboardActions { add() } ) - Spacer(Modifier.height(40.dp)) + Spacer(Modifier.height(BottomPadding)) } } } diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 5012319..236ebe7 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -277,7 +277,6 @@ Неверная конфигурация Исключить хосты Сетевой журнал - Размер файла журнала: %1$s Удалить журналы Экспортировать журналы Пара ключей Wi-Fi diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index d593b89..664810e 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -306,7 +306,6 @@ Geçersiz yapılandırma Hariç tutulan ana bilgisayarlar Ağ Kayıtları - Kayıt dosyası boyutu: %1$s Kayıtları Sil Kayıtları Dışa Aktar Wi-Fi Kimlik Doğrulama Anahtar Çifti diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 81262df..a33c9b5 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -291,7 +291,7 @@ 无效配置 排除的主机 网络日志 - 日志文件大小:%1$s + 网络日志已收集 删除日志 导出日志 Wi-Fi密钥对 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 55e9949..19b6ea4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -325,7 +325,7 @@ Invalid config Excluded hosts Network logging - Log file size: %1$s + Network logs collected Delete logs Export logs Wi-Fi keypair