From 63a395125aa40ab9f30f3479d8e005306a16bd78 Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Tue, 24 Mar 2020 12:12:25 +0530 Subject: Introduce realtime log viewer This contains a share button and a save button, the former using a custom content provider. Co-authored-by: Jason A. Donenfeld Signed-off-by: Harsh Shandilya Signed-off-by: Jason A. Donenfeld --- ui/src/main/AndroidManifest.xml | 14 + .../android/activity/LogViewerActivity.kt | 324 +++++++++++++++++++++ .../wireguard/android/activity/SettingsActivity.kt | 5 + .../android/preference/LogExporterPreference.kt | 101 ------- ui/src/main/res/drawable/ic_action_share_white.xml | 9 + ui/src/main/res/layout/log_viewer_activity.xml | 30 ++ ui/src/main/res/layout/log_viewer_entry.xml | 34 +++ ui/src/main/res/menu/log_viewer.xml | 9 + ui/src/main/res/values-hi/strings.xml | 2 - ui/src/main/res/values-id/strings.xml | 2 - ui/src/main/res/values-it/strings.xml | 2 - ui/src/main/res/values-ja/strings.xml | 2 - ui/src/main/res/values-night/colors.xml | 6 + ui/src/main/res/values-ru/strings.xml | 2 - ui/src/main/res/values-zh-rCN/strings.xml | 2 - ui/src/main/res/values/colors.xml | 5 + ui/src/main/res/values/strings.xml | 6 +- ui/src/main/res/xml/preferences.xml | 5 +- 18 files changed, 444 insertions(+), 116 deletions(-) create mode 100644 ui/src/main/java/com/wireguard/android/activity/LogViewerActivity.kt delete mode 100644 ui/src/main/java/com/wireguard/android/preference/LogExporterPreference.kt create mode 100644 ui/src/main/res/drawable/ic_action_share_white.xml create mode 100644 ui/src/main/res/layout/log_viewer_activity.xml create mode 100644 ui/src/main/res/layout/log_viewer_entry.xml create mode 100644 ui/src/main/res/menu/log_viewer.xml (limited to 'ui/src') diff --git a/ui/src/main/AndroidManifest.xml b/ui/src/main/AndroidManifest.xml index 11c1102b..74bc7a20 100644 --- a/ui/src/main/AndroidManifest.xml +++ b/ui/src/main/AndroidManifest.xml @@ -31,6 +31,7 @@ android:banner="@mipmap/banner"> + @@ -58,6 +59,19 @@ android:screenOrientation="fullSensor" tools:replace="screenOrientation" /> + + + + + + + + diff --git a/ui/src/main/java/com/wireguard/android/activity/LogViewerActivity.kt b/ui/src/main/java/com/wireguard/android/activity/LogViewerActivity.kt new file mode 100644 index 00000000..c94bba8d --- /dev/null +++ b/ui/src/main/java/com/wireguard/android/activity/LogViewerActivity.kt @@ -0,0 +1,324 @@ +/* + * Copyright © 2020 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.wireguard.android.activity + +import android.content.ClipDescription.compareMimeTypes +import android.content.ContentProvider +import android.content.ContentValues +import android.content.Intent +import android.database.Cursor +import android.database.MatrixCursor +import android.graphics.Typeface.BOLD +import android.net.Uri +import android.os.Bundle +import android.os.ParcelFileDescriptor +import android.text.Spannable +import android.text.SpannableString +import android.text.style.ForegroundColorSpan +import android.text.style.StyleSpan +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ShareCompat +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.snackbar.Snackbar +import com.google.android.material.textview.MaterialTextView +import com.wireguard.android.BuildConfig +import com.wireguard.android.R +import com.wireguard.android.databinding.LogViewerActivityBinding +import com.wireguard.android.util.DownloadsFileSaver +import com.wireguard.android.widget.EdgeToEdge.setUpFAB +import com.wireguard.android.widget.EdgeToEdge.setUpRoot +import com.wireguard.android.widget.EdgeToEdge.setUpScrollingContent +import com.wireguard.crypto.KeyPair +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.BufferedReader +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStreamReader +import java.nio.charset.StandardCharsets +import java.text.DateFormat +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.concurrent.ConcurrentHashMap +import java.util.regex.Matcher +import java.util.regex.Pattern + +class LogViewerActivity : AppCompatActivity() { + + private lateinit var binding: LogViewerActivityBinding + private lateinit var logAdapter: LogEntryAdapter + private var logLines = arrayListOf() + 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" /> - +