diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d94f989..f405d2d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -26,8 +26,8 @@ android { applicationId = "com.bintianqi.owndroid" minSdk = 21 targetSdk = 36 - versionCode = 40 - versionName = "7.1" + versionCode = 41 + versionName = "7.2" multiDexEnabled = false } diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt index 9e89f07..165e88e 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -1,6 +1,7 @@ package com.bintianqi.owndroid import android.Manifest +import android.content.pm.PackageManager import android.os.Build.VERSION import android.os.Bundle import androidx.activity.compose.setContent @@ -242,7 +243,10 @@ class MainActivity : FragmentActivity() { val locale = context.resources?.configuration?.locale zhCN = locale == Locale.SIMPLIFIED_CHINESE || locale == Locale.CHINESE || locale == Locale.CHINA val vm by viewModels() - if (VERSION.SDK_INT >= 33) { + if ( + VERSION.SDK_INT >= 33 && + checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED + ) { val launcher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {} launcher.launch(Manifest.permission.POST_NOTIFICATIONS) } @@ -379,7 +383,11 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) { CaCertScreen(vm.installedCaCerts, vm::getCaCerts, vm::installCaCert, vm::parseCaCert, vm::exportCaCert, vm::uninstallCaCert, vm::uninstallAllCaCerts, ::navigateUp) } - composable { SecurityLoggingScreen(::navigateUp) } + composable { + SecurityLoggingScreen(vm::getSecurityLoggingEnabled, vm::setSecurityLoggingEnabled, + vm::exportSecurityLogs, vm::getSecurityLogsCount, vm::deleteSecurityLogs, + vm::getPreRebootSecurityLogs, vm::exportPreRebootSecurityLogs, ::navigateUp) + } composable { DisableAccountManagementScreen(vm.mdAccountTypes, vm::getMdAccountTypes, vm::setMdAccountType, ::navigateUp) diff --git a/app/src/main/java/com/bintianqi/owndroid/MyDbHelper.kt b/app/src/main/java/com/bintianqi/owndroid/MyDbHelper.kt index bb4bdf6..f49867f 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MyDbHelper.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MyDbHelper.kt @@ -4,12 +4,15 @@ import android.content.Context import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteOpenHelper -class MyDbHelper(context: Context): SQLiteOpenHelper(context, "data", null, 1) { +class MyDbHelper(context: Context): SQLiteOpenHelper(context, "data", null, 2) { override fun onCreate(db: SQLiteDatabase) { db.execSQL("CREATE TABLE dhizuku_clients (uid INTEGER PRIMARY KEY," + "signature TEXT, permissions TEXT)") } - override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) { - + override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + if (oldVersion < 2) { + db.execSQL("CREATE TABLE security_logs (id INTEGER, tag INTEGER, level INTEGER," + + "time INTEGER, data TEXT)") + } } } \ 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 0f08b0c..4dc4b57 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MyRepository.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MyRepository.kt @@ -1,7 +1,18 @@ package com.bintianqi.owndroid +import android.app.admin.SecurityLog import android.content.ContentValues +import android.database.DatabaseUtils import android.database.sqlite.SQLiteDatabase +import android.os.Build.VERSION +import androidx.annotation.RequiresApi +import androidx.core.database.getStringOrNull +import com.bintianqi.owndroid.dpm.SecurityEvent +import com.bintianqi.owndroid.dpm.SecurityEventWithData +import com.bintianqi.owndroid.dpm.transformSecurityEventData +import kotlinx.serialization.json.ClassDiscriminatorMode +import kotlinx.serialization.json.Json +import java.io.OutputStream class MyRepository(val dbHelper: MyDbHelper) { fun getDhizukuClients(): List { @@ -43,4 +54,104 @@ class MyRepository(val dbHelper: MyDbHelper) { fun deleteDhizukuClient(info: DhizukuClientInfo) { dbHelper.writableDatabase.delete("dhizuku_clients", "uid = ${info.uid}", null) } + + fun getSecurityLogsCount(): Long { + return DatabaseUtils.queryNumEntries(dbHelper.readableDatabase, "security_logs") + } + @RequiresApi(24) + fun writeSecurityLogs(events: List) { + val db = dbHelper.writableDatabase + val json = Json { + classDiscriminatorMode = ClassDiscriminatorMode.NONE + } + val statement = db.compileStatement("INSERT INTO security_logs VALUES (?, ?, ?, ?, ?)") + db.beginTransaction() + events.forEach { event -> + try { + if (VERSION.SDK_INT >= 28) { + statement.bindLong(1, event.id) + statement.bindLong(3, event.logLevel.toLong()) + } else { + statement.bindNull(1) + statement.bindNull(3) + } + statement.bindLong(2, event.tag.toLong()) + statement.bindLong(4, event.timeNanos / 1000000) + val dataObject = transformSecurityEventData(event.tag, event.data) + if (dataObject == null) { + statement.bindNull(5) + } else { + statement.bindString(5, json.encodeToString(dataObject)) + } + statement.executeInsert() + } catch (e: Exception) { + e.printStackTrace() + } finally { + statement.clearBindings() + } + } + db.setTransactionSuccessful() + db.endTransaction() + statement.close() + } + fun exportSecurityLogs(stream: OutputStream) { + var offset = 0 + val json = Json { + explicitNulls = false + } + var addComma = false + val bw = stream.bufferedWriter() + bw.write("[") + while (true) { + dbHelper.readableDatabase.rawQuery( + "SELECT * FROM security_logs LIMIT ? OFFSET ?", + arrayOf(100.toString(), offset.toString()) + ).use { cursor -> + if (cursor.count == 0) { + break + } + while (cursor.moveToNext()) { + if (addComma) bw.write(",") + addComma = true + val event = SecurityEvent( + cursor.getLong(0), cursor.getInt(1), cursor.getInt(2), cursor.getLong(3), + cursor.getStringOrNull(4)?.let { json.decodeFromString(it) } + ) + bw.write(json.encodeToString(event)) + } + offset += 100 + } + } + bw.write("]") + bw.close() + } + @RequiresApi(24) + fun exportPRSecurityLogs(logs: List, stream: OutputStream) { + val bw = stream.bufferedWriter() + bw.write("[") + val json = Json { + explicitNulls = false + classDiscriminatorMode = ClassDiscriminatorMode.NONE + } + var addComma = false + logs.forEach { log -> + try { + if (addComma) bw.write(",") + addComma = true + val event = SecurityEventWithData( + if (VERSION.SDK_INT >= 28) log.id else null, log.tag, + if (VERSION.SDK_INT >= 28) log.logLevel else null, log.timeNanos / 1000000, + transformSecurityEventData(log.tag, log.data) + ) + bw.write(json.encodeToString(event)) + } catch (e: Exception) { + e.printStackTrace() + } + } + bw.write("]") + bw.close() + } + fun deleteSecurityLogs() { + dbHelper.writableDatabase.execSQL("DELETE FROM security_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 dcea517..929ff3b 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt @@ -13,6 +13,7 @@ import android.app.admin.FactoryResetProtectionPolicy import android.app.admin.IDevicePolicyManager import android.app.admin.PackagePolicy import android.app.admin.PreferentialNetworkServiceConfig +import android.app.admin.SecurityLog import android.app.admin.SystemUpdateInfo import android.app.admin.SystemUpdatePolicy import android.app.admin.WifiSsidPolicy @@ -756,9 +757,12 @@ class MyViewModel(application: Application): AndroidViewModel(application) { getLockTaskPackages() } @RequiresApi(28) - fun startLockTaskMode(packageName: String, activity: String): Int { - if (!NotificationUtils.checkPermission(application)) return 0 - if (!DPM.isLockTaskPermitted(packageName)) return 1 + fun startLockTaskMode(packageName: String, activity: String): Boolean { + if (!DPM.isLockTaskPermitted(packageName)) { + val list = lockTaskPackages.value.map { it.name } + packageName + DPM.setLockTaskPackages(DAR, list.toTypedArray()) + getLockTaskPackages() + } val options = ActivityOptions.makeBasic().setLockTaskEnabled(true) val intent = if(activity.isNotEmpty()) { Intent().setComponent(ComponentName(packageName, activity)) @@ -766,9 +770,9 @@ class MyViewModel(application: Application): AndroidViewModel(application) { if (intent != null) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) application.startActivity(intent, options.toBundle()) - return 0 + return true } else { - return 2 + return false } } @RequiresApi(28) @@ -913,6 +917,53 @@ class MyViewModel(application: Application): AndroidViewModel(application) { } DPM.installSystemUpdate(DAR, uri, application.mainExecutor, callback) } + @RequiresApi(24) + fun getSecurityLoggingEnabled(): Boolean { + return DPM.isSecurityLoggingEnabled(DAR) + } + @RequiresApi(24) + fun setSecurityLoggingEnabled(enabled: Boolean) { + DPM.setSecurityLoggingEnabled(DAR, enabled) + } + fun exportSecurityLogs(uri: Uri, callback: () -> Unit) { + viewModelScope.launch(Dispatchers.IO) { + application.contentResolver.openOutputStream(uri)?.use { + myRepo.exportSecurityLogs(it) + } + withContext(Dispatchers.Main) { + callback() + } + } + } + fun getSecurityLogsCount(): Int { + return myRepo.getSecurityLogsCount().toInt() + } + fun deleteSecurityLogs() { + myRepo.deleteSecurityLogs() + } + var preRebootSecurityLogs = emptyList() + @RequiresApi(24) + fun getPreRebootSecurityLogs(): Boolean { + if (preRebootSecurityLogs.isNotEmpty()) return true + return try { + val logs = DPM.retrievePreRebootSecurityLogs(DAR) + if (logs != null && logs.isNotEmpty()) { + preRebootSecurityLogs = logs + true + } else false + } catch (_: SecurityException) { + false + } + } + @RequiresApi(24) + fun exportPreRebootSecurityLogs(uri: Uri, callback: () -> Unit) { + viewModelScope.launch(Dispatchers.IO) { + val stream = application.contentResolver.openOutputStream(uri) ?: return@launch + myRepo.exportPRSecurityLogs(preRebootSecurityLogs, stream) + stream.close() + withContext(Dispatchers.Main) { callback() } + } + } @RequiresApi(24) fun isCreatingWorkProfileAllowed(): Boolean { diff --git a/app/src/main/java/com/bintianqi/owndroid/NotificationUtils.kt b/app/src/main/java/com/bintianqi/owndroid/NotificationUtils.kt index 0ed50a6..5e90d18 100644 --- a/app/src/main/java/com/bintianqi/owndroid/NotificationUtils.kt +++ b/app/src/main/java/com/bintianqi/owndroid/NotificationUtils.kt @@ -1,48 +1,35 @@ package com.bintianqi.owndroid -import android.Manifest -import android.app.Notification -import android.app.NotificationChannel import android.app.NotificationManager import android.content.Context -import android.content.pm.PackageManager -import android.os.Build +import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat object NotificationUtils { - fun checkPermission(context: Context): Boolean { - return if(Build.VERSION.SDK_INT >= 33) - context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED - else false - } fun createChannels(context: Context) { - if (Build.VERSION.SDK_INT < 26) return - val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - val lockTaskMode = NotificationChannel( - MyNotificationChannel.LockTaskMode.id, - context.getString(MyNotificationChannel.LockTaskMode.text), - NotificationManager.IMPORTANCE_HIGH - ) - val events = NotificationChannel( - MyNotificationChannel.Events.id, - context.getString(MyNotificationChannel.Events.text), - NotificationManager.IMPORTANCE_HIGH - ) - nm.createNotificationChannels(listOf(lockTaskMode, events)) + val channels = MyNotificationChannel.entries.map { + NotificationChannelCompat.Builder(it.id, it.importance) + .setName(context.getString(it.text)) + .build() + } + NotificationManagerCompat.from(context).createNotificationChannelsCompat(channels) } - fun notifyEvent(context: Context, type: NotificationType, text: String) { - val notification = NotificationCompat.Builder(context, MyNotificationChannel.Events.id) + fun sendBasicNotification( + context: Context, type: NotificationType, channel: MyNotificationChannel, text: String + ) { + val notification = NotificationCompat.Builder(context, channel.id) .setSmallIcon(type.icon) .setContentTitle(context.getString(type.text)) .setContentText(text) .build() - notify(context, type, notification) + val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + nm.notify(type.id, notification) } - fun notify(context: Context, type: NotificationType, notification: Notification) { + fun notifyEvent(context: Context, type: NotificationType, text: String) { val enabledNotifications = SP.notifications?.split(',')?.mapNotNull { it.toIntOrNull() } if (enabledNotifications == null || type.id in enabledNotifications) { - val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - nm.notify(type.id, notification) + sendBasicNotification(context, type, MyNotificationChannel.Events, text) } } fun cancel(context: Context, type: NotificationType) { @@ -62,10 +49,12 @@ enum class NotificationType(val id: Int, val text: Int, val icon: Int) { BugReportShared(8, R.string.bug_report_shared, R.drawable.bug_report_fill0), BugReportSharingDeclined(9, R.string.bug_report_sharing_declined, R.drawable.bug_report_fill0), BugReportFailed(10, R.string.bug_report_failed, R.drawable.bug_report_fill0), - SystemUpdatePending(11, R.string.system_update_pending, R.drawable.system_update_fill0) + SystemUpdatePending(11, R.string.system_update_pending, R.drawable.system_update_fill0), + SecurityLogsCollected(12, R.string.security_logs_collected, R.drawable.description_fill0), } -enum class MyNotificationChannel(val id: String, val text: Int) { - LockTaskMode("LockTaskMode", R.string.lock_task_mode), - Events("Events", R.string.events) +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) } diff --git a/app/src/main/java/com/bintianqi/owndroid/Receiver.kt b/app/src/main/java/com/bintianqi/owndroid/Receiver.kt index 7dbadb7..117f185 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Receiver.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Receiver.kt @@ -1,5 +1,6 @@ package com.bintianqi.owndroid +import android.app.NotificationManager import android.app.PendingIntent import android.app.admin.DeviceAdminReceiver import android.content.ComponentName @@ -12,7 +13,7 @@ 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.processSecurityLogs +import com.bintianqi.owndroid.dpm.retrieveSecurityLogs import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -21,11 +22,10 @@ class Receiver : DeviceAdminReceiver() { override fun onReceive(context: Context, intent: Intent) { super.onReceive(context, intent) if(VERSION.SDK_INT >= 26 && intent.action == "com.bintianqi.owndroid.action.STOP_LOCK_TASK_MODE") { - val dpm = getManager(context) val receiver = ComponentName(context, this::class.java) - val packages = dpm.getLockTaskPackages(receiver) - dpm.setLockTaskPackages(receiver, arrayOf()) - dpm.setLockTaskPackages(receiver, packages) + val packages = Privilege.DPM.getLockTaskPackages(receiver) + Privilege.DPM.setLockTaskPackages(receiver, arrayOf()) + Privilege.DPM.setLockTaskPackages(receiver, packages) } } @@ -56,29 +56,23 @@ class Receiver : DeviceAdminReceiver() { override fun onSecurityLogsAvailable(context: Context, intent: Intent) { super.onSecurityLogsAvailable(context, intent) - if(VERSION.SDK_INT >= 24) { - CoroutineScope(Dispatchers.IO).launch { - val events = getManager(context).retrieveSecurityLogs(MyAdminComponent) ?: return@launch - val file = context.filesDir.resolve("SecurityLogs.json") - val fileExists = file.exists() - file.outputStream().use { - if(fileExists) it.write(",".encodeToByteArray()) - processSecurityLogs(events, it) - } - } + if (VERSION.SDK_INT >= 24) { + retrieveSecurityLogs(context.applicationContext as MyApplication) } } override fun onLockTaskModeEntering(context: Context, intent: Intent, pkg: String) { super.onLockTaskModeEntering(context, intent, pkg) - if (!NotificationUtils.checkPermission(context)) return - val intent = Intent(context, this::class.java).setAction("com.bintianqi.owndroid.action.STOP_LOCK_TASK_MODE") - val pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) - val builder = NotificationCompat.Builder(context, MyNotificationChannel.LockTaskMode.id) + val stopIntent = Intent(context, this::class.java) + .setAction("com.bintianqi.owndroid.action.STOP_LOCK_TASK_MODE") + val pendingIntent = PendingIntent.getBroadcast(context, 0, stopIntent, PendingIntent.FLAG_IMMUTABLE) + val notification = NotificationCompat.Builder(context, MyNotificationChannel.LockTaskMode.id) .setContentTitle(context.getText(R.string.lock_task_mode)) .setSmallIcon(R.drawable.lock_fill0) .addAction(NotificationCompat.Action.Builder(null, context.getString(R.string.stop), pendingIntent).build()) - NotificationUtils.notify(context, NotificationType.LockTaskMode, builder.build()) + .build() + val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + nm.notify(NotificationType.LockTaskMode.id, notification) } override fun onLockTaskModeExiting(context: Context, intent: Intent) { 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 0e2214e..c075f31 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt @@ -14,21 +14,28 @@ import android.content.pm.PackageInstaller import android.os.Build.VERSION import android.util.Log import androidx.annotation.RequiresApi +import com.bintianqi.owndroid.MyApplication +import com.bintianqi.owndroid.MyNotificationChannel +import com.bintianqi.owndroid.NotificationType +import com.bintianqi.owndroid.NotificationUtils import com.bintianqi.owndroid.Privilege import com.bintianqi.owndroid.R import com.bintianqi.owndroid.SP import com.bintianqi.owndroid.ShortcutUtils import com.rosan.dhizuku.api.Dhizuku import com.rosan.dhizuku.api.DhizukuBinderWrapper +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers 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.JsonElement -import kotlinx.serialization.json.JsonPrimitive +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 -import java.io.OutputStream @SuppressLint("PrivateApi") fun binderWrapperDevicePolicyManager(appContext: Context): DevicePolicyManager? { @@ -166,230 +173,336 @@ fun handleNetworkLogs(context: Context, batchToken: Long) { buffer.close() } -@RequiresApi(24) -fun processSecurityLogs(securityEvents: List, outputStream: OutputStream) { - val json = Json { ignoreUnknownKeys = true; explicitNulls = false } - val buffer = outputStream.bufferedWriter() - securityEvents.forEachIndexed { index, event -> - val item = buildJsonObject { - put("time", event.timeNanos / 1000) - put("tag", event.tag) - if(VERSION.SDK_INT >= 28) put("level", event.logLevel) - if(VERSION.SDK_INT >= 28) put("id", event.id) - parseSecurityEventData(event).let { if(it != null) put("data", it) } - } - buffer.write(json.encodeToString(item)) - if(index < securityEvents.size - 1) buffer.write(",") - } - buffer.close() +@Serializable +class SecurityEvent( + val id: Long?, val tag: Int, val level: Int?, val time: Long, val data: JsonObject? +) + +@Serializable +class SecurityEventWithData( + val id: Long?, val tag: Int, val level: Int?, val time: Long, val data: SecurityEventData? +) + +@Serializable +sealed class SecurityEventData { + @Serializable + class AdbShellCmd(val command: String): SecurityEventData() + @Serializable + class AppProcessStart( + val name: String, + val time: Long, + val uid: Int, + val pid: Int, + val seinfo: String, + val hash: String + ): SecurityEventData() + @Serializable + class BackupServiceToggled( + val admin: String, + val user: Int, + val state: Int + ): SecurityEventData() + @Serializable + class BluetoothConnection( + val mac: String, + val successful: Int, + @SerialName("failure_reason") val failureReason: String + ): SecurityEventData() + @Serializable + class BluetoothDisconnection( + val mac: String, + val reason: String + ): SecurityEventData() + @Serializable + class CameraPolicySet( + val admin: String, + @SerialName("admin_user") val adminUser: Int, + @SerialName("target_user") val targetUser: Int, + val disabled: Int + ): SecurityEventData() + @Serializable + class CaInstalledRemoved( + val result: Int, + val subject: String, + val user: Int + ): SecurityEventData() + @Serializable + class CertValidationFailure(val reason: String): SecurityEventData() + @Serializable + class CryptoSelfTestCompleted(val result: Int): SecurityEventData() + @Serializable + class KeyguardDisabledFeaturesSet( + val admin: String, + @SerialName("admin_user") val adminUser: Int, + @SerialName("target_user") val targetUser: Int, + val mask: Int + ): SecurityEventData() + @Serializable + class KeyguardDismissAuthAttempt( + val result: Int, + val strength: Int + ): SecurityEventData() + @Serializable + class KeyGeneratedImportDestruction( + val result: Int, + val alias: String, + val uid: Int + ): SecurityEventData() + @Serializable + class KeyIntegrityViolation( + val alias: String, + val uid: Int + ): SecurityEventData() + @Serializable + class MaxPasswordAttemptsSet( + val admin: String, + @SerialName("admin_user") val adminUser: Int, + @SerialName("target_user") val targetUser: Int, + val value: Int + ): SecurityEventData() + @Serializable + class MaxScreenLockTimeoutSet( + val admin: String, + @SerialName("admin_user") val adminUser: Int, + @SerialName("target_user") val targetUser: Int, + val timeout: Long + ): SecurityEventData() + @Serializable + class MediaMountUnmount( + @SerialName("mount_point") val mountPoint: String, + val label: String + ): SecurityEventData() + @Serializable + class OsStartup( + @SerialName("verified_boot_state") val verifiedBootState: String, + @SerialName("dm_verity_mode") val dmVerityMode: String + ): SecurityEventData() + @Serializable + class PackageInstalledUninstalledUpdated( + val name: String, + val version: Long, + val user: Int + ): SecurityEventData() + @Serializable + class PasswordChanged( + val complexity: Int, + val user: Int + ): SecurityEventData() + @Serializable + class PasswordComplexityRequired( + val admin: String, + @SerialName("admin_user") val adminUser: Int, + @SerialName("target_user") val targetUser: Int, + val complexity: Int + ): SecurityEventData() + @Serializable + class PasswordComplexitySet( + val admin: String, + @SerialName("admin_user") val adminUser: Int, + @SerialName("target_user") val targetUser: Int, + val length: Int, + val quality: Int, + val letters: Int, + @SerialName("non_letters") val nonLetters: Int, + val digits: Int, + val uppercase: Int, + val lowercase: Int, + val symbols: Int + ): SecurityEventData() + @Serializable + class PasswordExpirationSet( + val admin: String, + @SerialName("admin_user") val adminUser: Int, + @SerialName("target_user") val targetUser: Int, + val expiration: Long + ): SecurityEventData() + @Serializable + class PasswordHistoryLengthSet( + val admin: String, + @SerialName("admin_user") val adminUser: Int, + @SerialName("target_user") val targetUser: Int, + val length: Int + ): SecurityEventData() + @Serializable + class RemoteLock( + val admin: String, + @SerialName("admin_user") val adminUser: Int, + @SerialName("target_user") val targetUser: Int, + ): SecurityEventData() + @Serializable + class SyncRecvSendFile(val path: String): SecurityEventData() + @Serializable + class UserRestrictionAddedRemoved( + val admin: String, + val user: Int, + val restriction: String + ): SecurityEventData() + @Serializable + class WifiConnection( + val bssid: String, + val type: String, + @SerialName("failure_reason") val failureReason: String + ): SecurityEventData() + @Serializable + class WifiDisconnection( + val bssid: String, + val reason: String + ): SecurityEventData() } -@RequiresApi(24) -fun parseSecurityEventData(event: SecurityLog.SecurityEvent): JsonElement? { - return when(event.tag) { - SecurityLog.TAG_ADB_SHELL_CMD -> JsonPrimitive(event.data as String) +fun transformSecurityEventData(tag: Int, payload: Any): SecurityEventData? { + return when(tag) { + SecurityLog.TAG_ADB_SHELL_CMD -> SecurityEventData.AdbShellCmd(payload as String) SecurityLog.TAG_ADB_SHELL_INTERACTIVE -> null SecurityLog.TAG_APP_PROCESS_START -> { - val payload = event.data as Array<*> - buildJsonObject { - put("name", payload[0] as String) - put("time", payload[1] as Long) - put("uid", payload[2] as Int) - put("pid", payload[3] as Int) - put("seinfo", payload[4] as String) - put("apk_hash", payload[5] as String) - } + val data = payload as Array<*> + SecurityEventData.AppProcessStart( + data[0] as String, data[1] as Long, data[2] as Int, data[3] as Int, + data[4] as String, data[5] as String + ) } SecurityLog.TAG_BACKUP_SERVICE_TOGGLED -> { - val payload = event.data as Array<*> - buildJsonObject { - put("admin", payload[0] as String) - put("admin_user_id", payload[1] as Int) - put("state", payload[2] as Int) - } + val data = payload as Array<*> + SecurityEventData.BackupServiceToggled(data[0] as String, data[1] as Int, data[2] as Int) } SecurityLog.TAG_BLUETOOTH_CONNECTION -> { - val payload = event.data as Array<*> - buildJsonObject { - put("mac", payload[0] as String) - put("successful", payload[1] as Int) - (payload[2] as String).let { if(it != "") put("failure_reason", it) } - } + val data = payload as Array<*> + SecurityEventData.BluetoothConnection(data[0] as String, data[1] as Int, data[2] as String) } SecurityLog.TAG_BLUETOOTH_DISCONNECTION -> { - val payload = event.data as Array<*> - buildJsonObject { - put("mac", payload[0] as String) - (payload[1] as String).let { if(it != "") put("reason", it) } - } + val data = payload as Array<*> + SecurityEventData.BluetoothDisconnection(data[0] as String, data[1] as String) } SecurityLog.TAG_CAMERA_POLICY_SET -> { - val payload = event.data as Array<*> - buildJsonObject { - put("admin", payload[0] as String) - put("admin_user_id", payload[1] as Int) - put("target_user_id", payload[2] as Int) - put("disabled", payload[3] as Int) - } + val data = payload as Array<*> + SecurityEventData.CameraPolicySet( + data[0] as String, data[1] as Int, data[2] as Int, data[3] as Int + ) } SecurityLog.TAG_CERT_AUTHORITY_INSTALLED, SecurityLog.TAG_CERT_AUTHORITY_REMOVED -> { - val payload = event.data as Array<*> - buildJsonObject { - put("result", payload[0] as Int) - put("subject", payload[1] as String) - if(VERSION.SDK_INT >= 30) put("user", payload[2] as Int) - } + val data = payload as Array<*> + SecurityEventData.CaInstalledRemoved(data[0] as Int, data[1] as String, data[2] as Int) } - SecurityLog.TAG_CERT_VALIDATION_FAILURE -> JsonPrimitive(event.data as String) - SecurityLog.TAG_CRYPTO_SELF_TEST_COMPLETED -> JsonPrimitive(event.data as Int) + SecurityLog.TAG_CERT_VALIDATION_FAILURE -> + SecurityEventData.CertValidationFailure(payload as String) + SecurityLog.TAG_CRYPTO_SELF_TEST_COMPLETED -> + SecurityEventData.CryptoSelfTestCompleted(payload as Int) SecurityLog.TAG_KEYGUARD_DISABLED_FEATURES_SET -> { - val payload = event.data as Array<*> - buildJsonObject { - put("admin", payload[0] as String) - put("admin_user_id", payload[1] as Int) - put("target_user_id", payload[2] as Int) - put("mask", payload[3] as Int) - } + val data = payload as Array<*> + SecurityEventData.KeyguardDisabledFeaturesSet( + data[0] as String, data[1] as Int, data[2] as Int, data[3] as Int + ) } SecurityLog.TAG_KEYGUARD_DISMISSED -> null SecurityLog.TAG_KEYGUARD_DISMISS_AUTH_ATTEMPT -> { - val payload = event.data as Array<*> - buildJsonObject { - put("result", payload[0] as Int) - put("strength", payload[1] as Int) - } + val data = payload as Array<*> + SecurityEventData.KeyguardDismissAuthAttempt(data[0] as Int, data[1] as Int) } SecurityLog.TAG_KEYGUARD_SECURED -> null - SecurityLog.TAG_KEY_DESTRUCTION, SecurityLog.TAG_KEY_GENERATED, SecurityLog.TAG_KEY_IMPORT -> { - val payload = event.data as Array<*> - buildJsonObject { - put("result", payload[0] as Int) - put("alias", payload[1] as String) - put("uid", payload[2] as Int) - } - } - SecurityLog.TAG_KEY_INTEGRITY_VIOLATION -> { - val payload = event.data as Array<*> - buildJsonObject { - put("alias", payload[0] as String) - put("uid", payload[1] as Int) - } + SecurityLog.TAG_KEY_GENERATED, SecurityLog.TAG_KEY_IMPORT, SecurityLog.TAG_KEY_DESTRUCTION -> { + val data = payload as Array<*> + SecurityEventData.KeyGeneratedImportDestruction( + data[0] as Int, data[1] as String, data[2] as Int + ) } SecurityLog.TAG_LOGGING_STARTED, SecurityLog.TAG_LOGGING_STOPPED -> null SecurityLog.TAG_LOG_BUFFER_SIZE_CRITICAL -> null SecurityLog.TAG_MAX_PASSWORD_ATTEMPTS_SET -> { - val payload = event.data as Array<*> - buildJsonObject { - put("admin", payload[0] as String) - put("admin_user_id", payload[1] as Int) - put("target_user_id", payload[2] as Int) - put("value", payload[3] as Int) - } + val data = payload as Array<*> + SecurityEventData.MaxPasswordAttemptsSet( + data[0] as String, data[1] as Int, data[2] as Int, data[3] as Int + ) } SecurityLog.TAG_MAX_SCREEN_LOCK_TIMEOUT_SET -> { - val payload = event.data as Array<*> - buildJsonObject { - put("admin", payload[0] as String) - put("admin_user_id", payload[1] as Int) - put("target_user_id", payload[2] as Int) - put("timeout", payload[3] as Long) - } + val data = payload as Array<*> + SecurityEventData.MaxScreenLockTimeoutSet( + data[0] as String, data[1] as Int, data[2] as Int, data[3] as Long + ) } SecurityLog.TAG_MEDIA_MOUNT, SecurityLog.TAG_MEDIA_UNMOUNT -> { - val payload = event.data as Array<*> - buildJsonObject { - put("mount_point", payload[0] as String) - put("volume_label", payload[1] as String) - } + val data = payload as Array<*> + SecurityEventData.MediaMountUnmount(data[0] as String, data[1] as String) } + SecurityLog.TAG_NFC_ENABLED, SecurityLog.TAG_NFC_DISABLED -> null SecurityLog.TAG_OS_SHUTDOWN -> null SecurityLog.TAG_OS_STARTUP -> { - val payload = event.data as Array<*> - buildJsonObject { - put("verified_boot_state", payload[0] as String) - put("dm_verify_state", payload[1] as String) - } + val data = payload as Array<*> + SecurityEventData.OsStartup(data[0] as String, data[1] as String) } - SecurityLog.TAG_PACKAGE_INSTALLED, SecurityLog.TAG_PACKAGE_UNINSTALLED, SecurityLog.TAG_PACKAGE_UPDATED -> { - val payload = event.data as Array<*> - buildJsonObject { - put("name", payload[0] as String) - put("version", payload[1] as Long) - put("user_id", payload[2] as Int) - } + SecurityLog.TAG_PACKAGE_INSTALLED, SecurityLog.TAG_PACKAGE_UPDATED, + SecurityLog.TAG_PACKAGE_UNINSTALLED -> { + val data = payload as Array<*> + SecurityEventData.PackageInstalledUninstalledUpdated( + data[0] as String, data[1] as Long, data[2] as Int + ) } SecurityLog.TAG_PASSWORD_CHANGED -> { - val payload = event.data as Array<*> - buildJsonObject { - put("complexity", payload[0] as Int) - put("user_id", payload[1] as Int) - } + val data = payload as Array<*> + SecurityEventData.PasswordChanged(data[0] as Int, data[1] as Int) } - SecurityLog. TAG_PASSWORD_COMPLEXITY_REQUIRED -> { - val payload = event.data as Array<*> - buildJsonObject { - put("admin", payload[0] as String) - put("admin_user_id", payload[1] as Int) - put("target_user_id", payload[2] as Int) - put("complexity", payload[3] as Int) - } + SecurityLog.TAG_PASSWORD_COMPLEXITY_REQUIRED -> { + val data = payload as Array<*> + SecurityEventData.PasswordComplexityRequired( + data[0] as String, data[1] as Int, data[2] as Int, data[3] as Int + ) + } + SecurityLog.TAG_PASSWORD_COMPLEXITY_SET -> { + val data = payload as Array<*> + SecurityEventData.PasswordComplexitySet( + data[0] as String, data[1] as Int, data[2] as Int, data[3] as Int, data[4] as Int, + data[5] as Int, data[6] as Int, data[7] as Int, data[8] as Int, data[9] as Int, + data[10] as Int + ) } - SecurityLog.TAG_PASSWORD_COMPLEXITY_SET -> null //Deprecated SecurityLog.TAG_PASSWORD_EXPIRATION_SET -> { - val payload = event.data as Array<*> - buildJsonObject { - put("admin", payload[0] as String) - put("admin_user_id", payload[1] as Int) - put("target_user_id", payload[2] as Int) - put("timeout", payload[3] as Long) - } + val data = payload as Array<*> + SecurityEventData.PasswordExpirationSet( + data[0] as String, data[1] as Int, data[2] as Int, data[3] as Long + ) } SecurityLog.TAG_PASSWORD_HISTORY_LENGTH_SET -> { - val payload = event.data as Array<*> - buildJsonObject { - put("admin", payload[0] as String) - put("admin_user_id", payload[1] as Int) - put("target_user_id", payload[2] as Int) - put("length", payload[3] as Int) - } + val data = payload as Array<*> + SecurityEventData.PasswordHistoryLengthSet( + data[0] as String, data[1] as Int, data[2] as Int, data[3] as Int + ) } SecurityLog.TAG_REMOTE_LOCK -> { - val payload = event.data as Array<*> - buildJsonObject { - put("admin", payload[0] as String) - put("admin_user_id", payload[1] as Int) - put("target_user_id", payload[2] as Int) - } + val data = payload as Array<*> + SecurityEventData.RemoteLock(data[0] as String, data[1] as Int, data[2] as Int) } - SecurityLog.TAG_SYNC_RECV_FILE, SecurityLog.TAG_SYNC_SEND_FILE -> JsonPrimitive(event.data as String) + SecurityLog.TAG_SYNC_RECV_FILE, SecurityLog.TAG_SYNC_SEND_FILE -> + SecurityEventData.SyncRecvSendFile(payload as String) SecurityLog.TAG_USER_RESTRICTION_ADDED, SecurityLog.TAG_USER_RESTRICTION_REMOVED -> { - val payload = event.data as Array<*> - buildJsonObject { - put("admin", payload[0] as String) - put("admin_user_id", payload[1] as Int) - put("restriction", payload[2] as String) - } + val data = payload as Array<*> + SecurityEventData.UserRestrictionAddedRemoved( + data[0] as String, data[1] as Int, data[2] as String + ) } SecurityLog.TAG_WIFI_CONNECTION -> { - val payload = event.data as Array<*> - buildJsonObject { - put("bssid", payload[0] as String) - put("type", payload[1] as String) - (payload[2] as String).let { if(it != "") put("failure_reason", it) } - } + val data = payload as Array<*> + SecurityEventData.WifiConnection(data[0] as String, data[1] as String, data[2] as String) } SecurityLog.TAG_WIFI_DISCONNECTION -> { - val payload = event.data as Array<*> - buildJsonObject { - put("bssid", payload[0] as String) - (payload[1] as String).let { if(it != "") put("reason", it) } - } + val data = payload as Array<*> + SecurityEventData.WifiDisconnection(data[0] as String, data[1] as String) } SecurityLog.TAG_WIPE_FAILURE -> null else -> null } } +@RequiresApi(24) +fun retrieveSecurityLogs(app: MyApplication) { + CoroutineScope(Dispatchers.IO).launch { + val logs = Privilege.DPM.retrieveSecurityLogs(Privilege.DAR) ?: return@launch + app.myRepo.writeSecurityLogs(logs) + NotificationUtils.sendBasicNotification( + app, NotificationType.SecurityLogsCollected, MyNotificationChannel.SecurityLogging, + app.getString(R.string.n_logs_in_total, logs.size) + ) + } +} + fun setDefaultAffiliationID() { if (VERSION.SDK_INT < 26) return if(!SP.isDefaultAffiliationIdSet) { 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 df3e91f..5be58f1 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt @@ -1567,49 +1567,6 @@ fun NetworkLoggingScreen(onNavigateUp: () -> Unit) { } } -@Serializable object WifiAuthKeypair - -@RequiresApi(31) -@Composable -fun WifiAuthKeypairScreen(onNavigateUp: () -> Unit) { - val context = LocalContext.current - val focusMgr = LocalFocusManager.current - var keyPair by remember { mutableStateOf("") } - MyScaffold(R.string.wifi_auth_keypair, onNavigateUp) { - OutlinedTextField( - value = keyPair, - label = { Text(stringResource(R.string.alias)) }, - onValueChange = { keyPair = it }, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }), - modifier = Modifier.fillMaxWidth() - ) - Spacer(Modifier.padding(vertical = 5.dp)) - val isExist = try { - Privilege.DPM.isKeyPairGrantedToWifiAuth(keyPair) - } catch(e: java.lang.IllegalArgumentException) { - e.printStackTrace() - false - } - Text(stringResource(R.string.already_exist)+":$isExist") - Spacer(Modifier.padding(vertical = 5.dp)) - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - Button( - onClick = { context.showOperationResultToast(Privilege.DPM.grantKeyPairToWifiAuth(keyPair)) }, - modifier = Modifier.fillMaxWidth(0.49F) - ) { - Text(stringResource(R.string.grant)) - } - Button( - onClick = { context.showOperationResultToast(Privilege.DPM.revokeKeyPairFromWifiAuth(keyPair)) }, - modifier = Modifier.fillMaxWidth(0.96F) - ) { - Text(stringResource(R.string.revoke)) - } - } - } -} - @Serializable object PreferentialNetworkService @RequiresApi(33) 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 2e829b3..45b3555 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt @@ -88,7 +88,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -111,11 +110,11 @@ import com.bintianqi.owndroid.MyViewModel import com.bintianqi.owndroid.Privilege import com.bintianqi.owndroid.R import com.bintianqi.owndroid.SP -import com.bintianqi.owndroid.formatFileSize import com.bintianqi.owndroid.formatDate import com.bintianqi.owndroid.popToast import com.bintianqi.owndroid.showOperationResultToast import com.bintianqi.owndroid.ui.CheckBoxItem +import com.bintianqi.owndroid.ui.CircularProgressDialog import com.bintianqi.owndroid.ui.ErrorDialog import com.bintianqi.owndroid.ui.FullWidthCheckBoxItem import com.bintianqi.owndroid.ui.FullWidthRadioButtonItem @@ -132,7 +131,9 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.serialization.Serializable -import java.io.ByteArrayOutputStream +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale import java.util.TimeZone import kotlin.math.roundToLong @@ -178,7 +179,7 @@ fun SystemManagerScreen( if(VERSION.SDK_INT >= 31) { FunctionItem(R.string.nearby_streaming_policy, icon = R.drawable.share_fill0) { onNavigate(NearbyStreamingPolicy) } } - if(VERSION.SDK_INT >= 28 && privilege.device) { + if (VERSION.SDK_INT >= 28 && privilege.device && !privilege.dhizuku) { FunctionItem(R.string.lock_task_mode, icon = R.drawable.lock_fill0) { onNavigate(LockTaskMode) } } FunctionItem(R.string.ca_cert, icon = R.drawable.license_fill0) { onNavigate(CaCert) } @@ -676,7 +677,7 @@ fun ChangeTimeZoneScreen(setTimeZone: (String) -> Boolean, onNavigateUp: () -> U var dialog by remember { mutableStateOf(false) } val availableIds = TimeZone.getAvailableIDs() val validInput = inputTimezone in availableIds - MyScaffold(R.string.change_timezone, onNavigateUp) { + MyScaffold(R.string.change_timezone, onNavigateUp) { OutlinedTextField( value = inputTimezone, label = { Text(stringResource(R.string.timezone_id)) }, @@ -1147,7 +1148,7 @@ fun NearbyStreamingPolicyScreen( fun LockTaskModeScreen( chosenPackage: Channel, onChoosePackage: () -> Unit, lockTaskPackages: StateFlow>, getLockTaskPackages: () -> Unit, - setLockTaskPackage: (String, Boolean) -> Unit, startLockTaskMode: (String, String) -> Unit, + setLockTaskPackage: (String, Boolean) -> Unit, startLockTaskMode: (String, String) -> Boolean, getLockTaskFeatures: () -> Int, setLockTaskFeature: (Int) -> String?, onNavigateUp: () -> Unit ) { val coroutine = rememberCoroutineScope() @@ -1202,9 +1203,10 @@ fun LockTaskModeScreen( @RequiresApi(28) @Composable private fun StartLockTaskMode( - startLockTaskMode: (String, String) -> Unit, + startLockTaskMode: (String, String) -> Boolean, chosenPackage: Channel, onChoosePackage: () -> Unit ) { + val context = LocalContext.current val focusMgr = LocalFocusManager.current var packageName by rememberSaveable { mutableStateOf("") } var activity by rememberSaveable { mutableStateOf("") } @@ -1244,7 +1246,8 @@ private fun StartLockTaskMode( .fillMaxWidth() .padding(bottom = 5.dp), onClick = { - startLockTaskMode(packageName, activity) + val result = startLockTaskMode(packageName, activity) + if (!result) context.showOperationResultToast(false) }, enabled = packageName.isNotBlank() && (!specifyActivity || activity.isNotBlank()) ) { @@ -1525,77 +1528,104 @@ fun CaCertScreen( @RequiresApi(24) @Composable -fun SecurityLoggingScreen(onNavigateUp: () -> Unit) { +fun SecurityLoggingScreen( + getEnabled: () -> Boolean, setEnabled: (Boolean) -> Unit, exportLogs: (Uri, () -> Unit) -> Unit, + getCount: () -> Int, deleteLogs: () -> Unit, getPRLogs: () -> Boolean, + exportPRLogs: (Uri, () -> Unit) -> Unit, onNavigateUp: () -> Unit +) { val context = LocalContext.current - val logFile = context.filesDir.resolve("SecurityLogs.json") - var fileSize by remember { mutableLongStateOf(0) } - LaunchedEffect(Unit) { fileSize = logFile.length() } - var preRebootSecurityLogs by remember { mutableStateOf(byteArrayOf()) } - val exportPreRebootSecurityLogs = rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("application/json")) { uri -> - if(uri != null) context.contentResolver.openOutputStream(uri)?.use { outStream -> - preRebootSecurityLogs.inputStream().copyTo(outStream) + var enabled by remember { mutableStateOf(false) } + var logsCount by remember { mutableIntStateOf(0) } + var exporting by remember { mutableStateOf(false) } + var dialog by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + enabled = getEnabled() + logsCount = getCount() + } + val exportLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.CreateDocument("application/json") + ) { + if (it != null) { + exporting = true + exportLogs(it) { + exporting = false + context.showOperationResultToast(true) + } } } - val exportSecurityLogs = rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("application/json")) { uri -> - if(uri != null) context.contentResolver.openOutputStream(uri)?.use { outStream -> - outStream.write("[".toByteArray()) - logFile.inputStream().use { it.copyTo(outStream) } - outStream.write("]".toByteArray()) - context.showOperationResultToast(true) + val exportPRLogsLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.CreateDocument("application/json") + ) { + if (it != null) { + exporting = true + exportPRLogs(it) { + exporting = false + context.showOperationResultToast(true) + } } } - MyScaffold(R.string.security_logging, onNavigateUp) { + MyScaffold(R.string.security_logging, onNavigateUp, 0.dp) { SwitchItem( - R.string.enable, - getState = { Privilege.DPM.isSecurityLoggingEnabled(Privilege.DAR) }, - onCheckedChange = { Privilege.DPM.setSecurityLoggingEnabled(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 = { - exportSecurityLogs.launch("SecurityLogs.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, logsCount), + Modifier.padding(HorizontalPadding) + ) + Button( + { + val date = SimpleDateFormat("yyyyMMdd", Locale.getDefault()).format(Date()) + exportLauncher.launch("security_logs_$date") + }, + Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding), + logsCount > 0 + ) { + Text(stringResource(R.string.export_logs)) } - Notes(R.string.info_security_log) - Spacer(Modifier.padding(vertical = 5.dp)) + if (logsCount > 0) FilledTonalButton( + { dialog = true }, + Modifier.fillMaxWidth().padding(HorizontalPadding, 4.dp) + ) { + Text(stringResource(R.string.delete_logs)) + } + Notes(R.string.info_security_log, HorizontalPadding) Button( onClick = { - val logs = Privilege.DPM.retrievePreRebootSecurityLogs(Privilege.DAR) - if(logs == null) { - context.popToast(R.string.no_logs) - return@Button + if (getPRLogs()) { + val date = SimpleDateFormat("yyyyMMdd", Locale.getDefault()).format(Date()) + exportPRLogsLauncher.launch("pre_reboot_security_logs_$date") } else { - val outputStream = ByteArrayOutputStream() - outputStream.write("[".encodeToByteArray()) - processSecurityLogs(logs, outputStream) - outputStream.write("]".encodeToByteArray()) - preRebootSecurityLogs = outputStream.toByteArray() - exportPreRebootSecurityLogs.launch("PreRebootSecurityLogs.json") + context.showOperationResultToast(false) } }, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth().padding(HorizontalPadding, 15.dp) ) { Text(stringResource(R.string.pre_reboot_security_logs)) } - Notes(R.string.info_pre_reboot_security_log) + Notes(R.string.info_pre_reboot_security_log, HorizontalPadding) } + if (exporting) CircularProgressDialog { exporting = false } + if (dialog) AlertDialog( + text = { Text(stringResource(R.string.delete_logs)) }, + confirmButton = { + TextButton({ + deleteLogs() + logsCount = 0 + dialog = false + }) { + Text(stringResource(R.string.confirm)) + } + }, + dismissButton = { + TextButton({ dialog = false }) { + Text(stringResource(R.string.cancel)) + } + }, + onDismissRequest = { dialog = false } + ) } @Serializable object DisableAccountManagement diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 93940e9..5012319 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -35,7 +35,6 @@ Время (мс) Длина Нет - Нет журналов По умолчанию Применить Определять пользователем diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index dfad27d..d593b89 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -35,7 +35,6 @@ Zaman (ms) Uzunluk Yok - Kayıt Yok Varsayılan Uygula Kullanıcı Tarafından Karar Ver diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index dcaa54a..81262df 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -35,7 +35,6 @@ 时间(毫秒) 长度 - 无日志 默认 应用 由用户决定 @@ -194,6 +193,8 @@ 卸载所有用户证书 安全日志 重启前安全日志 + 安全日志已收集 + 总共 %1$d 条日志 清除数据 清除外部存储 清除受保护的数据 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7f0ff06..55e9949 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -37,7 +37,6 @@ Time(ms) Length None - No logs Default Apply Decide by user @@ -221,6 +220,8 @@ Uninstall all user CA certificate Security logging Pre-reboot security logs + Security logs collected + %1$d logs in total Wipe data Wipe external storage Wipe protected data