Retrieve network logs

This commit is contained in:
BinTianqi
2024-08-31 12:00:08 +08:00
parent 99e02df084
commit 03a42429b6
9 changed files with 155 additions and 20 deletions

View File

@@ -2,6 +2,7 @@ plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
alias(libs.plugins.cc) alias(libs.plugins.cc)
kotlin("plugin.serialization") version "2.0.0"
} }
android { android {
@@ -86,4 +87,5 @@ dependencies {
implementation(libs.androidx.biometric) implementation(libs.androidx.biometric)
implementation(libs.androidx.fragment) implementation(libs.androidx.fragment)
implementation(libs.hiddenApiBypass) implementation(libs.hiddenApiBypass)
implementation(libs.serialization)
} }

View File

@@ -17,16 +17,20 @@ import android.content.pm.PackageInstaller.STATUS_FAILURE_STORAGE
import android.content.pm.PackageInstaller.STATUS_FAILURE_TIMEOUT import android.content.pm.PackageInstaller.STATUS_FAILURE_TIMEOUT
import android.content.pm.PackageInstaller.STATUS_PENDING_USER_ACTION import android.content.pm.PackageInstaller.STATUS_PENDING_USER_ACTION
import android.content.pm.PackageInstaller.STATUS_SUCCESS import android.content.pm.PackageInstaller.STATUS_SUCCESS
import android.os.Build.VERSION
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity
import com.bintianqi.owndroid.dpm.getDPM import com.bintianqi.owndroid.dpm.getDPM
import com.bintianqi.owndroid.dpm.getReceiver import com.bintianqi.owndroid.dpm.getReceiver
import com.bintianqi.owndroid.dpm.handleNetworkLogs
import com.bintianqi.owndroid.dpm.isDeviceAdmin import com.bintianqi.owndroid.dpm.isDeviceAdmin
import com.bintianqi.owndroid.dpm.isDeviceOwner import com.bintianqi.owndroid.dpm.isDeviceOwner
import com.bintianqi.owndroid.dpm.isProfileOwner import com.bintianqi.owndroid.dpm.isProfileOwner
import com.bintianqi.owndroid.dpm.toggleInstallAppActivity import com.bintianqi.owndroid.dpm.toggleInstallAppActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
class Receiver : DeviceAdminReceiver() { class Receiver : DeviceAdminReceiver() {
override fun onEnabled(context: Context, intent: Intent) { override fun onEnabled(context: Context, intent: Intent) {
@@ -48,6 +52,14 @@ class Receiver : DeviceAdminReceiver() {
Toast.makeText(context, R.string.create_work_profile_success, Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.create_work_profile_success, Toast.LENGTH_SHORT).show()
} }
override fun onNetworkLogsAvailable(context: Context, intent: Intent, batchToken: Long, networkLogsCount: Int) {
super.onNetworkLogsAvailable(context, intent, batchToken, networkLogsCount)
if(VERSION.SDK_INT >= 28) {
CoroutineScope(Dispatchers.IO).launch {
handleNetworkLogs(context, batchToken)
}
}
}
} }
val installAppDone = MutableStateFlow(false) val installAppDone = MutableStateFlow(false)

View File

@@ -13,9 +13,12 @@ import androidx.activity.result.contract.ActivityResultContracts
import com.bintianqi.owndroid.dpm.addDeviceAdmin import com.bintianqi.owndroid.dpm.addDeviceAdmin
import com.bintianqi.owndroid.dpm.createManagedProfile import com.bintianqi.owndroid.dpm.createManagedProfile
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.nio.file.Files
import java.util.Locale
lateinit var getFile: ActivityResultLauncher<Intent> lateinit var getFile: ActivityResultLauncher<Intent>
val fileUriFlow = MutableStateFlow(Uri.parse("")) val fileUriFlow = MutableStateFlow(Uri.parse(""))
@@ -73,6 +76,7 @@ fun writeClipBoard(context: Context, string: String):Boolean{
} }
lateinit var requestPermission: ActivityResultLauncher<String> lateinit var requestPermission: ActivityResultLauncher<String>
lateinit var saveNetworkLogs: ActivityResultLauncher<Intent>
fun registerActivityResult(context: ComponentActivity){ fun registerActivityResult(context: ComponentActivity){
getFile = context.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult -> getFile = context.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult ->
@@ -92,6 +96,19 @@ fun registerActivityResult(context: ComponentActivity){
} }
} }
requestPermission = context.registerForActivityResult(ActivityResultContracts.RequestPermission()) { permissionGranted.value = it } requestPermission = context.registerForActivityResult(ActivityResultContracts.RequestPermission()) { permissionGranted.value = it }
saveNetworkLogs = context.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val intentData = result.data ?: return@registerForActivityResult
val uriData = intentData.data ?: return@registerForActivityResult
context.contentResolver.openOutputStream(uriData).use { outStream ->
if(outStream != null) {
val logFile = context.filesDir.resolve("NetworkLogs.json")
logFile.inputStream().use { inStream ->
inStream.copyTo(outStream)
}
Toast.makeText(context.applicationContext, R.string.success, Toast.LENGTH_SHORT).show()
}
}
}
} }
val permissionGranted = MutableStateFlow<Boolean?>(null) val permissionGranted = MutableStateFlow<Boolean?>(null)
@@ -108,3 +125,15 @@ suspend fun prepareForNotification(context: Context, action: ()->Unit) {
action() action()
} }
} }
fun formatFileSize(bytes: Long): String {
val kb = 1024
val mb = kb * 1024
val gb = mb * 1024
return when {
bytes >= gb -> String.format(Locale.US, "%.2f GB", bytes / gb.toDouble())
bytes >= mb -> String.format(Locale.US, "%.2f MB", bytes / mb.toDouble())
bytes >= kb -> String.format(Locale.US, "%.2f KB", bytes / kb.toDouble())
else -> "$bytes bytes"
}
}

View File

@@ -3,7 +3,9 @@ package com.bintianqi.owndroid.dpm
import android.Manifest import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.PendingIntent import android.app.PendingIntent
import android.app.admin.ConnectEvent
import android.app.admin.DevicePolicyManager import android.app.admin.DevicePolicyManager
import android.app.admin.DnsEvent
import android.app.admin.FactoryResetProtectionPolicy import android.app.admin.FactoryResetProtectionPolicy
import android.app.admin.IDevicePolicyManager import android.app.admin.IDevicePolicyManager
import android.app.admin.SystemUpdatePolicy import android.app.admin.SystemUpdatePolicy
@@ -13,9 +15,11 @@ import android.content.Intent
import android.content.pm.IPackageInstaller import android.content.pm.IPackageInstaller
import android.content.pm.PackageInstaller import android.content.pm.PackageInstaller
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build
import android.os.Build.VERSION import android.os.Build.VERSION
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.RequiresApi
import androidx.annotation.StringRes import androidx.annotation.StringRes
import com.bintianqi.owndroid.InstallAppActivity import com.bintianqi.owndroid.InstallAppActivity
import com.bintianqi.owndroid.PackageInstallerReceiver import com.bintianqi.owndroid.PackageInstallerReceiver
@@ -26,8 +30,18 @@ import com.rosan.dhizuku.api.Dhizuku
import com.rosan.dhizuku.api.Dhizuku.binderWrapper import com.rosan.dhizuku.api.Dhizuku.binderWrapper
import com.rosan.dhizuku.api.DhizukuBinderWrapper import com.rosan.dhizuku.api.DhizukuBinderWrapper
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.json.encodeToStream
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import kotlin.io.path.inputStream
import kotlin.io.path.notExists
import kotlin.io.path.outputStream
import kotlin.io.path.writeText
lateinit var createManagedProfile: ActivityResultLauncher<Intent> lateinit var createManagedProfile: ActivityResultLauncher<Intent>
lateinit var addDeviceAdmin: ActivityResultLauncher<Intent> lateinit var addDeviceAdmin: ActivityResultLauncher<Intent>
@@ -305,3 +319,54 @@ fun permissionList(): List<PermissionItem>{
} }
return list return list
} }
@RequiresApi(Build.VERSION_CODES.O)
@OptIn(ExperimentalSerializationApi::class)
fun handleNetworkLogs(context: Context, batchToken: Long) {
val events = context.getDPM().retrieveNetworkLogs(context.getReceiver(), batchToken) ?: return
val eventsList = mutableListOf<NetworkEventItem>()
val file = context.filesDir.toPath().resolve("NetworkLogs.json")
if(file.notExists()) file.writeText("[]")
val json = Json { ignoreUnknownKeys = true; explicitNulls = false }
var jsonObj: MutableList<NetworkEventItem>
file.inputStream().use {
jsonObj = json.decodeFromStream(it)
}
events.forEach { event ->
try {
val dnsEvent = event as DnsEvent
val addresses = mutableListOf<String?>()
dnsEvent.inetAddresses.forEach { inetAddresses ->
addresses += inetAddresses.hostAddress
}
eventsList += NetworkEventItem(
id = if(VERSION.SDK_INT >= 28) event.id else null, packageName = event.packageName
, timestamp = event.timestamp, type = "dns", hostName = dnsEvent.hostname,
hostAddresses = addresses, totalResolvedAddressCount = dnsEvent.totalResolvedAddressCount
)
} catch(e: Exception) {
val connectEvent = event as ConnectEvent
eventsList += NetworkEventItem(
id = if(VERSION.SDK_INT >= 28) event.id else null, packageName = event.packageName, timestamp = event.timestamp, type = "connect",
hostAddress = connectEvent.inetAddress.hostAddress, port = connectEvent.port
)
}
}
jsonObj.addAll(eventsList)
file.outputStream().use {
json.encodeToStream(jsonObj, it)
}
}
@Serializable
data class NetworkEventItem(
val id: Long? = null,
@SerialName("package_name") val packageName: String,
val timestamp: Long,
val type: String,
val port: Int? = null,
@SerialName("address") val hostAddress: String? = null,
@SerialName("host_name") val hostName: String? = null,
@SerialName("count") val totalResolvedAddressCount: Int? = null,
@SerialName("addresses") val hostAddresses: List<String?>? = null
)

View File

@@ -16,6 +16,7 @@ import android.app.admin.WifiSsidPolicy
import android.app.admin.WifiSsidPolicy.WIFI_SSID_POLICY_TYPE_ALLOWLIST import android.app.admin.WifiSsidPolicy.WIFI_SSID_POLICY_TYPE_ALLOWLIST
import android.app.admin.WifiSsidPolicy.WIFI_SSID_POLICY_TYPE_DENYLIST import android.app.admin.WifiSsidPolicy.WIFI_SSID_POLICY_TYPE_DENYLIST
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager.NameNotFoundException import android.content.pm.PackageManager.NameNotFoundException
import android.net.ProxyInfo import android.net.ProxyInfo
import android.net.Uri import android.net.Uri
@@ -38,7 +39,6 @@ import android.telephony.data.ApnSetting.PROTOCOL_IPV6
import android.telephony.data.ApnSetting.PROTOCOL_NON_IP import android.telephony.data.ApnSetting.PROTOCOL_NON_IP
import android.telephony.data.ApnSetting.PROTOCOL_PPP import android.telephony.data.ApnSetting.PROTOCOL_PPP
import android.telephony.data.ApnSetting.PROTOCOL_UNSTRUCTURED import android.telephony.data.ApnSetting.PROTOCOL_UNSTRUCTURED
import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
@@ -75,6 +75,7 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -98,6 +99,8 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.bintianqi.owndroid.R import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.formatFileSize
import com.bintianqi.owndroid.saveNetworkLogs
import com.bintianqi.owndroid.selectedPackage import com.bintianqi.owndroid.selectedPackage
import com.bintianqi.owndroid.toText import com.bintianqi.owndroid.toText
import com.bintianqi.owndroid.ui.Animations import com.bintianqi.owndroid.ui.Animations
@@ -624,28 +627,40 @@ private fun NetworkLog() {
val context = LocalContext.current val context = LocalContext.current
val dpm = context.getDPM() val dpm = context.getDPM()
val receiver = context.getReceiver() val receiver = context.getReceiver()
val logFile = context.filesDir.resolve("NetworkLogs.json")
var fileSize by remember { mutableLongStateOf(0) }
var fileExists by remember { mutableStateOf(logFile.exists()) }
LaunchedEffect(Unit) {
fileSize = logFile.length()
}
Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) {
Spacer(Modifier.padding(vertical = 10.dp)) Spacer(Modifier.padding(vertical = 10.dp))
Text(text = stringResource(R.string.retrieve_net_logs), style = typography.headlineLarge) Text(text = stringResource(R.string.retrieve_net_logs), style = typography.headlineLarge)
Spacer(Modifier.padding(vertical = 5.dp)) Spacer(Modifier.padding(vertical = 5.dp))
Text(text = stringResource(R.string.developing)) SwitchItem(R.string.enable, "", null, { dpm.isNetworkLoggingEnabled(receiver) }, { dpm.setNetworkLoggingEnabled(receiver,it) }, padding = false)
Spacer(Modifier.padding(vertical = 5.dp)) if(fileExists) {
SwitchItem(R.string.enable,"",null, { dpm.isNetworkLoggingEnabled(receiver) }, {dpm.setNetworkLoggingEnabled(receiver,it) }, padding = false) Text(stringResource(R.string.retrieved_logs_are, formatFileSize(fileSize)))
Spacer(Modifier.padding(vertical = 5.dp)) Button(
Button( onClick = {
onClick = { val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
val log = dpm.retrieveNetworkLogs(receiver,1234567890) intent.addCategory(Intent.CATEGORY_OPENABLE)
if(log != null) { intent.setType("application/json")
for(i in log) { Log.d("NetworkLog",i.toString()) } intent.putExtra(Intent.EXTRA_TITLE, "NetworkLogs.json")
Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() saveNetworkLogs.launch(intent)
}else{ },
Log.d("NetworkLog",context.getString(R.string.none)) modifier = Modifier.fillMaxWidth()
Toast.makeText(context, R.string.none, Toast.LENGTH_SHORT).show() ) {
} Text(stringResource(R.string.export_logs))
}, }
modifier = Modifier.fillMaxWidth() Button(
) { onClick = {
Text(stringResource(R.string.retrieve)) Toast.makeText(context, if(logFile.delete()) R.string.success else R.string.failed, Toast.LENGTH_SHORT).show()
fileExists = logFile.exists()
},
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.delete_logs))
}
} }
} }
} }

View File

@@ -241,6 +241,9 @@
<string name="invalid_config">Invalid config</string> <!--TODO--> <string name="invalid_config">Invalid config</string> <!--TODO-->
<string name="exclude_hosts">Exclude hosts</string> <!--TODO--> <string name="exclude_hosts">Exclude hosts</string> <!--TODO-->
<string name="retrieve_net_logs">Ağ kayıtları</string> <string name="retrieve_net_logs">Ağ kayıtları</string>
<string name="retrieved_logs_are">Retrieved logs: %1$s</string> <!--TODO-->
<string name="delete_logs">Delete logs</string> <!--TODO-->
<string name="export_logs">Export logs</string> <!--TODO-->
<string name="retrieve">Geri al</string> <string name="retrieve">Geri al</string>
<string name="wifi_auth_keypair">WiFi anahtar çifti</string> <string name="wifi_auth_keypair">WiFi anahtar çifti</string>
<string name="keypair">Anahtar çifti</string> <string name="keypair">Anahtar çifti</string>

View File

@@ -236,6 +236,9 @@
<string name="invalid_config">无效配置</string> <string name="invalid_config">无效配置</string>
<string name="exclude_hosts">排除列表</string> <string name="exclude_hosts">排除列表</string>
<string name="retrieve_net_logs">收集网络日志</string> <string name="retrieve_net_logs">收集网络日志</string>
<string name="retrieved_logs_are">已收集的日志:%1$s</string>
<string name="delete_logs">删除日志</string>
<string name="export_logs">导出日志</string>
<string name="retrieve">收集</string> <string name="retrieve">收集</string>
<string name="wifi_auth_keypair">WiFi密钥对</string> <string name="wifi_auth_keypair">WiFi密钥对</string>
<string name="keypair">密钥对</string> <string name="keypair">密钥对</string>

View File

@@ -245,6 +245,9 @@
<string name="invalid_config">Invalid config</string> <string name="invalid_config">Invalid config</string>
<string name="exclude_hosts">Exclude hosts</string> <string name="exclude_hosts">Exclude hosts</string>
<string name="retrieve_net_logs">Network logs</string> <string name="retrieve_net_logs">Network logs</string>
<string name="retrieved_logs_are">Retrieved logs: %1$s</string>
<string name="delete_logs">Delete logs</string>
<string name="export_logs">Export logs</string>
<string name="retrieve">Retrieve</string> <string name="retrieve">Retrieve</string>
<string name="wifi_auth_keypair">WiFi keypair</string> <string name="wifi_auth_keypair">WiFi keypair</string>
<string name="keypair">Keypair</string> <string name="keypair">Keypair</string>

View File

@@ -11,6 +11,7 @@ biometric = "1.2.0-alpha05"
fragment = "1.8.0-beta01" fragment = "1.8.0-beta01"
dhizuku = "2.5.2" dhizuku = "2.5.2"
hiddenApiBypass = "4.3" hiddenApiBypass = "4.3"
serialization = "1.7.1"
[libraries] [libraries]
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity-compose" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity-compose" }
@@ -28,6 +29,8 @@ dhizuku-api = { module = "io.github.iamr0s:Dhizuku-API", version.ref = "dhizuku"
hiddenApiBypass = { module = "org.lsposed.hiddenapibypass:hiddenapibypass", version.ref = "hiddenApiBypass" } hiddenApiBypass = { module = "org.lsposed.hiddenapibypass:hiddenapibypass", version.ref = "hiddenApiBypass" }
androidx-fragment = { group = "androidx.fragment", name = "fragment", version.ref = "fragment" } androidx-fragment = { group = "androidx.fragment", name = "fragment", version.ref = "fragment" }
serialization = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }