diff --git a/app/src/full/java/com/celzero/bravedns/ui/HomeScreenActivity.kt b/app/src/full/java/com/celzero/bravedns/ui/HomeScreenActivity.kt index bc32e4ed4..fe00aeb74 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/HomeScreenActivity.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/HomeScreenActivity.kt @@ -52,7 +52,6 @@ import androidx.work.WorkRequest import com.celzero.bravedns.BuildConfig import com.celzero.bravedns.NonStoreAppUpdater import com.celzero.bravedns.R -import com.celzero.bravedns.RethinkDnsApplication.Companion.DEBUG import com.celzero.bravedns.backup.BackupHelper import com.celzero.bravedns.backup.BackupHelper.Companion.BACKUP_FILE_EXTN import com.celzero.bravedns.backup.BackupHelper.Companion.INTENT_RESTART_APP diff --git a/app/src/full/java/com/celzero/bravedns/ui/fragment/AboutFragment.kt b/app/src/full/java/com/celzero/bravedns/ui/fragment/AboutFragment.kt index 0d7654b11..57730170c 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/fragment/AboutFragment.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/fragment/AboutFragment.kt @@ -64,6 +64,7 @@ import com.celzero.bravedns.util.Constants.Companion.INIT_TIME_MS import com.celzero.bravedns.util.Constants.Companion.RETHINKDNS_SPONSOR_LINK import com.celzero.bravedns.util.Constants.Companion.TIME_FORMAT_4 import com.celzero.bravedns.util.FirebaseErrorReporting.TOKEN_LENGTH +import com.celzero.bravedns.util.KernelProc import com.celzero.bravedns.util.Themes import com.celzero.bravedns.util.UIUtils import com.celzero.bravedns.util.UIUtils.htmlToSpannedText @@ -172,6 +173,7 @@ class AboutFragment : Fragment(R.layout.fragment_about), View.OnClickListener, K b.aboutAppContributors.setOnClickListener(this) b.aboutAppTranslate.setOnClickListener(this) b.aboutStats.setOnClickListener(this) + b.aboutProc.setOnClickListener(this) b.aboutDbStats.setOnClickListener(this) b.tokenTextView.setOnClickListener(this) b.aboutFlightRecord.setOnClickListener(this) @@ -392,6 +394,9 @@ class AboutFragment : Fragment(R.layout.fragment_about), View.OnClickListener, K b.aboutStats -> { openStatsDialog() } + b.aboutProc -> { + openProcDialog() + } b.aboutDbStats -> { openDatabaseDumpDialog() } @@ -464,8 +469,86 @@ class AboutFragment : Fragment(R.layout.fragment_about), View.OnClickListener, K } } + private fun openProcDialog() { + fun gatherProcDump(force: Boolean): String { + val stat = KernelProc.getStats(force) + val status = KernelProc.getStatus(force) + val mem = KernelProc.getMem(force) + val maps = KernelProc.getMaps(force) + val smaps = KernelProc.getSmaps(force) + val net = KernelProc.getNet(force) + val ns = KernelProc.getNs(force) + val sched = KernelProc.getSched(force) + val task = KernelProc.getTask(force) + return buildString { + append(stat) + append("\n\n") + append(status) + append("\n\n") + append(mem) + append("\n\n") + append(maps) + append("\n\n") + append(smaps) + append("\n\n") + append(net) + append("\n\n") + append(ns) + append("\n\n") + append(sched) + append("\n\n") + append(task) + } + } + + io { + val initial = gatherProcDump(force = false) + uiCtx { + if (!isAdded) return@uiCtx + val tv = android.widget.TextView(requireContext()) + val pad = resources.getDimensionPixelSize(R.dimen.dots_margin_bottom) + tv.setPadding(pad, pad, pad, pad) + tv.text = initial.ifEmpty { "No Proc stats" } + tv.setTextIsSelectable(true) + tv.typeface = android.graphics.Typeface.MONOSPACE + val scroll = android.widget.ScrollView(requireContext()) + scroll.addView(tv) + + val dialog = MaterialAlertDialogBuilder(requireContext(), R.style.App_Dialog_NoDim) + .setTitle(getString(R.string.title_proc)) + .setView(scroll) + .setPositiveButton(R.string.fapps_info_dialog_positive_btn) { d, _ -> d.dismiss() } + .setNegativeButton(R.string.dns_info_neutral) { _, _ -> + copyToClipboard("proc_dump", tv.text.toString()) + showToastUiCentered( + requireContext(), + getString(R.string.copied_clipboard), + Toast.LENGTH_SHORT + ) + } + .setNeutralButton("Refresh", null) + .create() + + dialog.setOnShowListener { + dialog.getButton(android.app.AlertDialog.BUTTON_NEUTRAL)?.setOnClickListener { + io { + val refreshed = gatherProcDump(force = true) + uiCtx { + if (isAdded) tv.text = refreshed + showToastUiCentered(requireContext(), getString(R.string.config_add_success_toast), + Toast.LENGTH_SHORT) + } + } + } + } + + dialog.show() + } + } + } + private fun copyToClipboard(label: String, text: String): ClipboardManager? { - val cb = ContextCompat.getSystemService(requireContext(), ClipboardManager::class.java) + val cb = getSystemService(requireContext(), ClipboardManager::class.java) cb?.setPrimaryClip(ClipData.newPlainText(label, text)) return cb } diff --git a/app/src/main/java/com/celzero/bravedns/util/KernelProc.kt b/app/src/main/java/com/celzero/bravedns/util/KernelProc.kt new file mode 100644 index 000000000..a7041d1e9 --- /dev/null +++ b/app/src/main/java/com/celzero/bravedns/util/KernelProc.kt @@ -0,0 +1,134 @@ +/* + * Copyright 2026 RethinkDNS and its authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.celzero.bravedns.util + +import android.os.Process +import java.io.File +import java.nio.ByteBuffer +import java.nio.ByteOrder + +/** + * Reads /proc/self/auxv once and returns a human readable breakdown of the aux vector entries. + * Result is cached so subsequent callers do not hit the filesystem again. + */ +object KernelProc { + private const val AUXV_PATH = "/proc/self/auxv" + private const val STATUS_PATH = "/proc/self/status" + private const val SCHED_PATH = "/proc/self/sched" + private const val NS_DIR = "/proc/self/ns" + private const val NET_DIR = "/proc/self/net" + private const val TASK_DIR = "/proc/self/task" + private const val MEM_PATH = "/proc/self/mem" // usually unreadable; handled gracefully + private const val SMAPS_PATH = "/proc/self/smaps" + private const val MAPS_PATH = "/proc/self/maps" + private const val MAX_READ_BYTES = 512 * 1024 // prevent OOM on large proc files + + // Lazy so we only read and parse once per process lifetime. + private val cachedStats: String by lazy { readAuxvHumanReadable() } + private val cachedText: MutableMap = mutableMapOf() + + fun getStats(forceRefresh: Boolean = false): String = if (forceRefresh) readAuxvHumanReadable() else cachedStats + + fun getStatus(forceRefresh: Boolean = false): String = readProcText(STATUS_PATH, title = "status", forceRefresh = forceRefresh) + fun getSched(forceRefresh: Boolean = false): String = readProcText(SCHED_PATH, title = "sched", forceRefresh = forceRefresh) + fun getNs(forceRefresh: Boolean = false): String = readDirAsLines(NS_DIR, title = "ns", forceRefresh = forceRefresh) + fun getNet(forceRefresh: Boolean = false): String = readDirAsLines(NET_DIR, title = "net", forceRefresh = forceRefresh) + fun getTask(forceRefresh: Boolean = false): String = readDirAsLines(TASK_DIR, title = "task", forceRefresh = forceRefresh) + fun getMem(forceRefresh: Boolean = false): String = readProcText(MEM_PATH, title = "mem", forceRefresh = forceRefresh) + fun getSmaps(forceRefresh: Boolean = false): String = readProcText(SMAPS_PATH, title = "smaps", forceRefresh = forceRefresh) + fun getMaps(forceRefresh: Boolean = false): String = readProcText(MAPS_PATH, title = "maps", forceRefresh = forceRefresh) + + private fun readProcText(path: String, title: String, forceRefresh: Boolean = false): String { + if (forceRefresh) cachedText.remove(path) + return cachedText.getOrPut(path) { + runCatching { + val file = File(path) + if (!file.exists()) return@getOrPut "$title: missing" + file.inputStream().buffered().use { stream -> + val limited = ByteArray(MAX_READ_BYTES) + val read = stream.read(limited) + if (read <= 0) return@getOrPut "$title: empty" + val content = String(limited, 0, read) + val body = if (stream.read() != -1) "$title (truncated to ${MAX_READ_BYTES}B)\n$content" else "$title\n$content" + body + } + }.getOrElse { err -> "$title: error: ${err.message ?: err::class.java.simpleName}" } + } + } + + private fun readDirAsLines(dirPath: String, title: String, forceRefresh: Boolean = false): String { + // Directory contents (like /proc/self/task) change often; read live instead of caching. + return runCatching { + val dir = File(dirPath) + if (!dir.exists()) return@runCatching "$title: missing" + val files = dir.listFiles()?.sortedBy { it.name } ?: emptyList() + if (files.isEmpty()) return@runCatching "$title: empty" + val body = files.joinToString(separator = "\n") { f -> + val target = runCatching { f.canonicalPath }.getOrDefault(f.path) + "${f.name} -> $target" + } + "$title\n$body" + }.getOrElse { err -> "$title: error: ${err.message ?: err::class.java.simpleName}" } + } + + private fun readAuxvHumanReadable(): String { + return runCatching { + val file = File(AUXV_PATH) + if (!file.exists()) return@runCatching "auxv: missing ($AUXV_PATH)" + + val bytes = file.readBytes() + val wordSize = if (Process.is64Bit()) 8 else 4 + val buffer = ByteBuffer.wrap(bytes).order(ByteOrder.nativeOrder()) + + val typeNames = mapOf( + 0L to "AT_NULL", + 3L to "AT_PHDR", + 4L to "AT_PHENT", + 5L to "AT_PHNUM", + 6L to "AT_PAGESZ", + 7L to "AT_BASE", + 8L to "AT_FLAGS", + 9L to "AT_ENTRY", + 11L to "AT_UID", + 12L to "AT_EUID", + 13L to "AT_GID", + 14L to "AT_EGID", + 15L to "AT_PLATFORM", + 16L to "AT_HWCAP", + 17L to "AT_CLKTCK", + 20L to "AT_HWCAP2", + 23L to "AT_SECURE", + 25L to "AT_RANDOM", + 31L to "AT_EXECFN", + 33L to "AT_SECURE_PLATFORM" + ) + + fun readWord(): Long = if (wordSize == 8) buffer.long else buffer.int.toLong() and 0xffffffffL + + val entries = mutableListOf() + while (buffer.remaining() >= wordSize * 2) { + val type = readWord() + val value = readWord() + if (type == 0L) break // AT_NULL terminator + val name = typeNames[type] ?: "UNKNOWN" + val annotated = "$name($type)=0x${value.toString(16)} ($value)" + entries.add(annotated) + } + + if (entries.isEmpty()) "auxv: empty" else entries.joinToString(separator = "\n") + }.getOrElse { err -> "auxv: error: ${err.message ?: err::class.java.simpleName}" } + } +} diff --git a/app/src/main/res/layout/fragment_about.xml b/app/src/main/res/layout/fragment_about.xml index 717390a4d..63c556a81 100644 --- a/app/src/main/res/layout/fragment_about.xml +++ b/app/src/main/res/layout/fragment_about.xml @@ -673,6 +673,22 @@ android:text="@string/title_statistics" android:textSize="@dimen/large_font_text_view" /> + + About Settings Stats + Proc taking a nap… A highly customizable DNS and Firewall Detect and block network security threats