()
+ private var rawLogLines = StringBuffer()
+ private var recyclerView: RecyclerView? = null
+ private var saveButton: MenuItem? = null
+ private val coroutineScope = CoroutineScope(Dispatchers.Default)
+ private val year by lazy {
+ val yearFormatter: DateFormat = SimpleDateFormat("yyyy", Locale.US)
+ yearFormatter.format(Date())
+ }
+
+ @Suppress("Deprecation")
+ private val defaultColor by lazy { resources.getColor(R.color.primary_text_color) }
+
+ @Suppress("Deprecation")
+ private val debugColor by lazy { resources.getColor(R.color.debug_tag_color) }
+
+ @Suppress("Deprecation")
+ private val errorColor by lazy { resources.getColor(R.color.error_tag_color) }
+
+ @Suppress("Deprecation")
+ private val infoColor by lazy { resources.getColor(R.color.info_tag_color) }
+
+ @Suppress("Deprecation")
+ private val warningColor by lazy { resources.getColor(R.color.warning_tag_color) }
+
+ private var lastUri: Uri? = null
+
+ private fun revokeLastUri() {
+ lastUri?.let {
+ LOGS.remove(it.pathSegments.lastOrNull())
+ revokeUriPermission(it, Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ lastUri = null
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = LogViewerActivityBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ setUpFAB(binding.shareFab)
+ setUpRoot(binding.root)
+ setUpScrollingContent(binding.recyclerView, binding.shareFab)
+ logAdapter = LogEntryAdapter()
+ binding.recyclerView.apply {
+ recyclerView = this
+ layoutManager = LinearLayoutManager(context)
+ adapter = logAdapter
+ addItemDecoration(DividerItemDecoration(context, LinearLayoutManager.VERTICAL))
+ }
+
+ coroutineScope.launch { streamingLog() }
+
+ binding.shareFab.setOnClickListener {
+ revokeLastUri()
+ val key = KeyPair().privateKey.toHex()
+ LOGS[key] = rawLogLines.toString().toByteArray(Charsets.UTF_8)
+ lastUri = Uri.parse("content://${BuildConfig.APPLICATION_ID}.exported-log/$key")
+ val shareIntent = ShareCompat.IntentBuilder.from(this)
+ .setType("text/plain")
+ .setSubject(getString(R.string.log_export_subject))
+ .setStream(lastUri)
+ .setChooserTitle(R.string.log_export_title)
+ .createChooserIntent()
+ .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ grantUriPermission("android", lastUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ startActivityForResult(shareIntent, SHARE_ACTIVITY_REQUEST)
+ }
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ if (requestCode == SHARE_ACTIVITY_REQUEST) {
+ revokeLastUri()
+ }
+ super.onActivityResult(requestCode, resultCode, data)
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+ menuInflater.inflate(R.menu.log_viewer, menu)
+ saveButton = menu?.findItem(R.id.save_log)
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when (item.itemId) {
+ android.R.id.home -> {
+ finish()
+ true
+ }
+ R.id.save_log -> {
+ coroutineScope.launch { saveLog() }
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ coroutineScope.cancel()
+ }
+
+ private suspend fun saveLog() {
+ val context = this
+ withContext(Dispatchers.Main) {
+ saveButton?.isEnabled = false
+ withContext(Dispatchers.IO) {
+ val outputFile = DownloadsFileSaver.save(context, "wireguard-log.txt", "text/plain", true)
+ outputFile.outputStream.use {
+ it.write(rawLogLines.toString().toByteArray(Charsets.UTF_8))
+ }
+ withContext(Dispatchers.Main) {
+ Snackbar.make(findViewById(android.R.id.content),
+ getString(R.string.log_export_success, outputFile.fileName),
+ Snackbar.LENGTH_SHORT)
+ .setAnchorView(binding.shareFab)
+ .show()
+ saveButton?.isEnabled = true
+ }
+ }
+ }
+ }
+
+ private suspend fun streamingLog() = withContext(Dispatchers.IO) {
+ val builder = ProcessBuilder().command("logcat", "-b", "all", "-v", "threadtime", "*:V")
+ builder.environment()["LC_ALL"] = "C"
+ val process = try {
+ builder.start()
+ } catch (e: IOException) {
+ e.printStackTrace()
+ return@withContext
+ }
+ val stdout = BufferedReader(InputStreamReader(process!!.inputStream, StandardCharsets.UTF_8))
+ while (true) {
+ val line = stdout.readLine() ?: break
+ rawLogLines.append(line)
+ rawLogLines.append('\n')
+ val logLine = parseLine(line)
+ if (logLine != null) {
+ withContext(Dispatchers.Main) {
+ recyclerView?.let {
+ val shouldScroll = it.canScrollVertically(1)
+ logLines.add(logLine)
+ logAdapter.notifyDataSetChanged()
+ if (!shouldScroll)
+ it.scrollToPosition(logLines.size - 1)
+ }
+ }
+ }
+ }
+ }
+
+ private fun parseTime(timeStr: String): Date? {
+ val formatter: DateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.US)
+ return try {
+ formatter.parse("$year-$timeStr")
+ } catch (e: ParseException) {
+ null
+ }
+ }
+
+ private fun parseLine(line: String): LogLine? {
+ val m: Matcher = THREADTIME_LINE.matcher(line)
+ return if (m.matches()) {
+ LogLine(m.group(2)!!.toInt(), m.group(3)!!.toInt(), parseTime(m.group(1)!!), m.group(4)!!, m.group(5)!!, m.group(6)!!)
+ } else {
+ null
+ }
+ }
+
+ private data class LogLine(val pid: Int, val tid: Int, val time: Date?, val level: String, val tag: String, val msg: String)
+
+ companion object {
+ /**
+ * Match a single line of `logcat -v threadtime`, such as:
+ *
+ * 05-26 11:02:36.886 5689 5689 D AndroidRuntime: CheckJNI is OFF.
+ */
+ private val THREADTIME_LINE: Pattern = Pattern.compile("^(\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.\\d{3})(?:\\s+[0-9A-Za-z]+)?\\s+(\\d+)\\s+(\\d+)\\s+([A-Z])\\s+(.+?)\\s*: (.*)$")
+ private val LOGS: MutableMap = ConcurrentHashMap()
+ private var SHARE_ACTIVITY_REQUEST = 49133
+ }
+
+ private inner class LogEntryAdapter : RecyclerView.Adapter() {
+
+ private inner class ViewHolder(val layout: View, var isSingleLine: Boolean = true) : RecyclerView.ViewHolder(layout)
+
+ private fun levelToColor(level: String): Int {
+ return when (level) {
+ "D" -> debugColor
+ "E" -> errorColor
+ "I" -> infoColor
+ "W" -> warningColor
+ else -> defaultColor
+ }
+ }
+
+ override fun getItemCount() = logLines.size
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.log_viewer_entry, parent, false)
+ return ViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val line = logLines[position]
+ val spannable = if (position > 0 && logLines[position - 1].tag == line.tag)
+ SpannableString(line.msg)
+ else
+ SpannableString("${line.tag}: ${line.msg}").apply {
+ setSpan(StyleSpan(BOLD), 0, "${line.tag}:".length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
+ setSpan(ForegroundColorSpan(levelToColor(line.level)),
+ 0, "${line.tag}:".length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
+ }
+ holder.layout.apply {
+ findViewById(R.id.log_date).text = line.time.toString()
+ findViewById(R.id.log_msg).apply {
+ setSingleLine()
+ text = spannable
+ setOnClickListener {
+ isSingleLine = !holder.isSingleLine
+ holder.isSingleLine = !holder.isSingleLine
+ }
+ }
+ }
+ }
+ }
+
+ class ExportedLogContentProvider : ContentProvider() {
+ private fun logForUri(uri: Uri): ByteArray? = LOGS[uri.pathSegments.lastOrNull()]
+
+ override fun insert(uri: Uri, values: ContentValues?): Uri? = null
+
+ override fun query(uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?, sortOrder: String?): Cursor? =
+ logForUri(uri)?.let {
+ val m = MatrixCursor(arrayOf(android.provider.OpenableColumns.DISPLAY_NAME, android.provider.OpenableColumns.SIZE), 1)
+ m.addRow(arrayOf("wireguard-log.txt", it.size.toLong()))
+ m
+ }
+
+ override fun onCreate(): Boolean = true
+
+ override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array?): Int = 0
+
+ override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int = 0
+
+ override fun getType(uri: Uri): String? = logForUri(uri)?.let { "text/plain" }
+
+ override fun getStreamTypes(uri: Uri, mimeTypeFilter: String): Array? = getType(uri)?.let { if (compareMimeTypes(it, mimeTypeFilter)) arrayOf(it) else null }
+
+ override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
+ if (mode != "r") return null
+ val log = logForUri(uri) ?: return null
+ return openPipeHelper(uri, "text/plain", null, log) { output, _, _, _, l ->
+ FileOutputStream(output.fileDescriptor).write(l!!)
+ }
+ }
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/activity/SettingsActivity.kt b/ui/src/main/java/com/wireguard/android/activity/SettingsActivity.kt
index 8bd27d00..103b6b44 100644
--- a/ui/src/main/java/com/wireguard/android/activity/SettingsActivity.kt
+++ b/ui/src/main/java/com/wireguard/android/activity/SettingsActivity.kt
@@ -4,6 +4,7 @@
*/
package com.wireguard.android.activity
+import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
@@ -100,6 +101,10 @@ class SettingsActivity : ThemeChangeAwareActivity() {
wgQuickOnlyPrefs.forEach { it.parent?.removePreference(it) }
}
}
+ preferenceManager.findPreference("log_viewer")?.setOnPreferenceClickListener {
+ startActivity(Intent(requireContext(), LogViewerActivity::class.java))
+ true
+ }
val moduleInstaller = preferenceManager.findPreference("module_downloader")
val kernelModuleDisabler = preferenceManager.findPreference("kernel_module_disabler")
moduleInstaller?.isVisible = false
diff --git a/ui/src/main/java/com/wireguard/android/preference/LogExporterPreference.kt b/ui/src/main/java/com/wireguard/android/preference/LogExporterPreference.kt
deleted file mode 100644
index ede4b661..00000000
--- a/ui/src/main/java/com/wireguard/android/preference/LogExporterPreference.kt
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-package com.wireguard.android.preference
-
-import android.Manifest
-import android.content.Context
-import android.content.pm.PackageManager
-import android.os.Build
-import android.util.AttributeSet
-import android.util.Log
-import androidx.preference.Preference
-import com.google.android.material.snackbar.Snackbar
-import com.wireguard.android.Application
-import com.wireguard.android.R
-import com.wireguard.android.util.DownloadsFileSaver
-import com.wireguard.android.util.ErrorMessages
-import com.wireguard.android.util.FragmentUtils
-import java.io.BufferedReader
-import java.io.InputStreamReader
-
-/**
- * Preference implementing a button that asynchronously exports logs.
- */
-class LogExporterPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
- private var exportedFilePath: String? = null
- private fun exportLog() {
- Application.getAsyncWorker().supplyAsync {
- val outputFile = DownloadsFileSaver.save(context, "wireguard-log.txt", "text/plain", true)
- try {
- val process = Runtime.getRuntime().exec(arrayOf(
- "logcat", "-b", "all", "-d", "-v", "threadtime", "*:V"))
- BufferedReader(InputStreamReader(process.inputStream)).use { stdout ->
- BufferedReader(InputStreamReader(process.errorStream)).use { stderr ->
- while (true) {
- val line = stdout.readLine() ?: break
- outputFile.outputStream.write(line.toByteArray())
- outputFile.outputStream.write('\n'.toInt())
- }
- outputFile.outputStream.close()
- if (process.waitFor() != 0) {
- val errors = StringBuilder()
- errors.append(R.string.logcat_error)
- while (true) {
- val line = stderr.readLine() ?: break
- errors.append(line)
- }
- throw Exception(errors.toString())
- }
- }
- }
- } catch (e: Exception) {
- outputFile.delete()
- throw e
- }
- outputFile.fileName
- }.whenComplete(this::exportLogComplete)
- }
-
- private fun exportLogComplete(filePath: String, throwable: Throwable?) {
- if (throwable != null) {
- val error = ErrorMessages.get(throwable)
- val message = context.getString(R.string.log_export_error, error)
- Log.e(TAG, message, throwable)
- Snackbar.make(
- FragmentUtils.getPrefActivity(this).findViewById(android.R.id.content),
- message, Snackbar.LENGTH_LONG).show()
- isEnabled = true
- } else {
- exportedFilePath = filePath
- notifyChanged()
- }
- }
-
- override fun getSummary() = if (exportedFilePath == null)
- context.getString(R.string.log_export_summary)
- else
- context.getString(R.string.log_export_success, exportedFilePath)
-
- override fun getTitle() = context.getString(R.string.log_export_title)
-
- override fun onClick() {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
- FragmentUtils.getPrefActivity(this)
- .ensurePermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { _, grantResults ->
- if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- isEnabled = false
- exportLog()
- }
- }
- } else {
- isEnabled = false
- exportLog()
- }
- }
-
- companion object {
- private val TAG = "WireGuard/" + LogExporterPreference::class.java.simpleName
- }
-}
diff --git a/ui/src/main/res/drawable/ic_action_share_white.xml b/ui/src/main/res/drawable/ic_action_share_white.xml
new file mode 100644
index 00000000..4ada554b
--- /dev/null
+++ b/ui/src/main/res/drawable/ic_action_share_white.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/ui/src/main/res/layout/log_viewer_activity.xml b/ui/src/main/res/layout/log_viewer_activity.xml
new file mode 100644
index 00000000..7a08bc88
--- /dev/null
+++ b/ui/src/main/res/layout/log_viewer_activity.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/main/res/layout/log_viewer_entry.xml b/ui/src/main/res/layout/log_viewer_entry.xml
new file mode 100644
index 00000000..37f8941d
--- /dev/null
+++ b/ui/src/main/res/layout/log_viewer_entry.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/main/res/menu/log_viewer.xml b/ui/src/main/res/menu/log_viewer.xml
new file mode 100644
index 00000000..3a9da698
--- /dev/null
+++ b/ui/src/main/res/menu/log_viewer.xml
@@ -0,0 +1,9 @@
+
+
diff --git a/ui/src/main/res/values-hi/strings.xml b/ui/src/main/res/values-hi/strings.xml
index c79341d7..bda0cbb2 100644
--- a/ui/src/main/res/values-hi/strings.xml
+++ b/ui/src/main/res/values-hi/strings.xml
@@ -88,9 +88,7 @@
: वायरगार्ड कीज 32 बाइट होनी चाहिए
: वायरगार्ड हेक्स कीज़ 64 अक्षरों की होनी चाहिए (32 बाइट्स)
पोर्ट सूने
- लॉग निर्यात करने में असमर्थ: %s
“%s” में सहेजा गया
- लॉग फ़ाइल को डाउनलोड फ़ोल्डर में सहेजा जाएगा
लॉग फ़ाइल निर्यात करें
लॉगकैट चलाने में असमर्थ:
कर्नेल मॉड्यूल संस्करण निर्धारित करने में असमर्थ
diff --git a/ui/src/main/res/values-id/strings.xml b/ui/src/main/res/values-id/strings.xml
index 7c6c6a0b..3f0af404 100644
--- a/ui/src/main/res/values-id/strings.xml
+++ b/ui/src/main/res/values-id/strings.xml
@@ -88,9 +88,7 @@
: Kunci WireGuard harus terdiri dari 32 bit
: Kunci hex WireGuard Harus terdiri dari 64 karakter (32 bit)
Isi port
- Log %s tidak bisa diekspor
Simpan ke “%s”
- File log akan disimpan di folder download
Ekspor file log
Tidak bisa menjalankan logcat:
Tidak dapat menentukan versi modul kernel
diff --git a/ui/src/main/res/values-it/strings.xml b/ui/src/main/res/values-it/strings.xml
index 48f36a2b..2ab211e9 100644
--- a/ui/src/main/res/values-it/strings.xml
+++ b/ui/src/main/res/values-it/strings.xml
@@ -88,9 +88,7 @@
: le chiavi di WireGuard devono essere di 32 byte
: le chiavi hex di WireGuard devono essere di 64 caratteri (32 byte)
Porta in ascolto
- Impossibile esportare il registro: %s
Salvato in “%s”
- Il file del registro verrà salvato nella cartella di download
Esporta file registro
Impossibile eseguire logcat:
Impossibile determinare la versione modulo del kernel
diff --git a/ui/src/main/res/values-ja/strings.xml b/ui/src/main/res/values-ja/strings.xml
index c753a5d5..5d342d2d 100644
--- a/ui/src/main/res/values-ja/strings.xml
+++ b/ui/src/main/res/values-ja/strings.xml
@@ -84,9 +84,7 @@
: WireGuard 鍵は32バイトでなければなりません
: WireGuard hex 鍵は64文字(32バイト)でなければなりません
Listen ポート
- ログをエクスポートできません: %s
“%s” に保存しました
- ログはダウンロードフォルダに保存されます
ログのエクスポート
logcat を実行できません:
カーネルモジュールバージョンを特定できません
diff --git a/ui/src/main/res/values-night/colors.xml b/ui/src/main/res/values-night/colors.xml
index 314142d9..e1015da8 100644
--- a/ui/src/main/res/values-night/colors.xml
+++ b/ui/src/main/res/values-night/colors.xml
@@ -14,4 +14,10 @@
#1aeeeeee
#21242424
#aa242424
+
+
+ #aaaaaa
+ #ff0000
+ #00ff00
+ #ffff00
diff --git a/ui/src/main/res/values-ru/strings.xml b/ui/src/main/res/values-ru/strings.xml
index 557ce196..519ec24e 100644
--- a/ui/src/main/res/values-ru/strings.xml
+++ b/ui/src/main/res/values-ru/strings.xml
@@ -93,9 +93,7 @@
: Ключи WireGuard должны быть 32 байта
: HEX ключи WireGuard должны содержать 64 символа (32 байта)
Порт
- Не удалось экспортировать логи: %s
Сохранено в “%s”
- Файл логов будет сохранен в папке загрузок
Экспорт логов в файл
Не удалось запустить logcat:
Не удалось определить версию модуля ядра
diff --git a/ui/src/main/res/values-zh-rCN/strings.xml b/ui/src/main/res/values-zh-rCN/strings.xml
index 613abf46..4e3730f1 100644
--- a/ui/src/main/res/values-zh-rCN/strings.xml
+++ b/ui/src/main/res/values-zh-rCN/strings.xml
@@ -82,9 +82,7 @@
:WireGuard 密钥大小必须为 32 字节
:WireGuard 的十六进制密钥长度必须为 64 个字符(32 字节)
监听端口
- 无法导出日志:%s
已保存至 “%s”
- 日志文件将保存至下载文件夹
导出日志文件
无法运行 logcat:
无法确定内核模块版本
diff --git a/ui/src/main/res/values/colors.xml b/ui/src/main/res/values/colors.xml
index 06bcd143..bd304726 100644
--- a/ui/src/main/res/values/colors.xml
+++ b/ui/src/main/res/values/colors.xml
@@ -18,4 +18,9 @@
@color/secondary_color
#ffffffff
+
+ #444444
+ #aa0000
+ #00aa00
+ #aaaa00
diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml
index 74c5be69..972b6244 100644
--- a/ui/src/main/res/values/strings.xml
+++ b/ui/src/main/res/values/strings.xml
@@ -93,10 +93,9 @@
: WireGuard keys must be 32 bytes
: WireGuard hex keys must be 64 characters (32 bytes)
Listen port
- Unable to export log: %s
Saved to “%s”
- Log file will be saved to downloads folder
Export log file
+ WireGuard Android Log File
Unable to run logcat:
Unable to determine kernel module version
No modules are available for your device
@@ -186,4 +185,7 @@
Export tunnels to zip file
Incorrect key length
Bad characters in key
+ View application log
+ Logs may assist with debugging
+ Save log
diff --git a/ui/src/main/res/xml/preferences.xml b/ui/src/main/res/xml/preferences.xml
index 4668fab4..06d7ac7c 100644
--- a/ui/src/main/res/xml/preferences.xml
+++ b/ui/src/main/res/xml/preferences.xml
@@ -8,7 +8,10 @@
android:summaryOff="@string/restore_on_boot_summary_off"
android:title="@string/restore_on_boot_title" />
-
+