diff options
author | Harsh Shandilya <me@msfjarvis.dev> | 2020-03-24 12:12:25 +0530 |
---|---|---|
committer | Jason A. Donenfeld <Jason@zx2c4.com> | 2020-03-26 00:49:01 -0600 |
commit | 63a395125aa40ab9f30f3479d8e005306a16bd78 (patch) | |
tree | 81c21b7d379fb7894998c82a2de576f48ef79ff1 /ui/src | |
parent | 6f973afa36b01698509440c85ec943e75ffed871 (diff) |
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 <Jason@zx2c4.com>
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
Diffstat (limited to 'ui/src')
18 files changed, 444 insertions, 116 deletions
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"> <activity android:name=".activity.TunnelToggleActivity" android:theme="@style/NoBackgroundTheme"/> + <activity android:name=".activity.MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> @@ -58,6 +59,19 @@ android:screenOrientation="fullSensor" tools:replace="screenOrientation" /> + <activity android:name=".activity.LogViewerActivity" + android:label="@string/log_viewer_title"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + </intent-filter> + </activity> + + <provider + android:name=".activity.LogViewerActivity$ExportedLogContentProvider" + android:authorities="${applicationId}.exported-log" + android:exported="false" + android:grantUriPermissions="true" /> + <receiver android:name=".BootShutdownReceiver"> <intent-filter> <action android:name="android.intent.action.ACTION_SHUTDOWN" /> 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<LogLine>() + 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: + * + * <pre>05-26 11:02:36.886 5689 5689 D AndroidRuntime: CheckJNI is OFF.</pre> + */ + 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<String, ByteArray> = ConcurrentHashMap() + private var SHARE_ACTIVITY_REQUEST = 49133 + } + + private inner class LogEntryAdapter : RecyclerView.Adapter<LogEntryAdapter.ViewHolder>() { + + 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<MaterialTextView>(R.id.log_date).text = line.time.toString() + findViewById<MaterialTextView>(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<out String>?, selection: String?, selectionArgs: Array<out String>?, 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<out String>?): Int = 0 + + override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int = 0 + + override fun getType(uri: Uri): String? = logForUri(uri)?.let { "text/plain" } + + override fun getStreamTypes(uri: Uri, mimeTypeFilter: String): Array<String>? = 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<Preference>("log_viewer")?.setOnPreferenceClickListener { + startActivity(Intent(requireContext(), LogViewerActivity::class.java)) + true + } val moduleInstaller = preferenceManager.findPreference<Preference>("module_downloader") val kernelModuleDisabler = preferenceManager.findPreference<Preference>("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 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="#FFFFFFFF" + android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/> +</vector> 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 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright © 2020 WireGuard LLC. All Rights Reserved. + ~ SPDX-License-Identifier: Apache-2.0 + --> + +<androidx.coordinatorlayout.widget.CoordinatorLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/recycler_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:listitem="@layout/log_viewer_entry" + tools:itemCount="20" /> + + <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton + style="@style/Widget.MaterialComponents.ExtendedFloatingActionButton.Icon" + android:id="@+id/share_fab" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="bottom|end" + android:layout_margin="@dimen/fab_margin" + app:icon="@drawable/ic_action_share_white" /> + +</androidx.coordinatorlayout.widget.CoordinatorLayout> 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 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright © 2020 WireGuard LLC. All Rights Reserved. + ~ SPDX-License-Identifier: Apache-2.0 + --> + +<androidx.constraintlayout.widget.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="6dp"> + <com.google.android.material.textview.MaterialTextView + style="@style/TextAppearance.MaterialComponents.Caption" + android:id="@+id/log_date" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="?android:attr/textColorPrimary" + android:textSize="10sp" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" + tools:text="Fri Mar 13 10:17:37 GMT+05:30 2020" /> + + <com.google.android.material.textview.MaterialTextView + style="@style/TextAppearance.MaterialComponents.Caption" + android:id="@+id/log_msg" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textColor="?android:attr/textColorPrimary" + app:layout_constraintTop_toBottomOf="@id/log_date" + tools:text="FATAL EXCEPTION: Thread-2" /> + +</androidx.constraintlayout.widget.ConstraintLayout> 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 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + <item + android:id="@+id/save_log" + android:icon="@drawable/ic_action_save" + android:title="@string/log_export_title" + app:showAsAction="ifRoom"/> +</menu> 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 @@ <string name="key_length_explanation_binary">: वायरगार्ड कीज 32 बाइट होनी चाहिए</string> <string name="key_length_explanation_hex">: वायरगार्ड हेक्स कीज़ 64 अक्षरों की होनी चाहिए (32 बाइट्स)</string> <string name="listen_port">पोर्ट सूने</string> - <string name="log_export_error">लॉग निर्यात करने में असमर्थ: %s</string> <string name="log_export_success">“%s” में सहेजा गया</string> - <string name="log_export_summary">लॉग फ़ाइल को डाउनलोड फ़ोल्डर में सहेजा जाएगा</string> <string name="log_export_title">लॉग फ़ाइल निर्यात करें</string> <string name="logcat_error">लॉगकैट चलाने में असमर्थ: </string> <string name="module_version_error">कर्नेल मॉड्यूल संस्करण निर्धारित करने में असमर्थ</string> 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 @@ <string name="key_length_explanation_binary">: Kunci WireGuard harus terdiri dari 32 bit</string> <string name="key_length_explanation_hex">: Kunci hex WireGuard Harus terdiri dari 64 karakter (32 bit)</string> <string name="listen_port">Isi port</string> - <string name="log_export_error">Log %s tidak bisa diekspor</string> <string name="log_export_success">Simpan ke “%s”</string> - <string name="log_export_summary">File log akan disimpan di folder download</string> <string name="log_export_title">Ekspor file log</string> <string name="logcat_error">Tidak bisa menjalankan logcat: </string> <string name="module_version_error">Tidak dapat menentukan versi modul kernel</string> 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 @@ <string name="key_length_explanation_binary">: le chiavi di WireGuard devono essere di 32 byte</string> <string name="key_length_explanation_hex">: le chiavi hex di WireGuard devono essere di 64 caratteri (32 byte)</string> <string name="listen_port">Porta in ascolto</string> - <string name="log_export_error">Impossibile esportare il registro: %s</string> <string name="log_export_success">Salvato in “%s”</string> - <string name="log_export_summary">Il file del registro verrà salvato nella cartella di download</string> <string name="log_export_title">Esporta file registro</string> <string name="logcat_error">Impossibile eseguire logcat: </string> <string name="module_version_error">Impossibile determinare la versione modulo del kernel</string> 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 @@ <string name="key_length_explanation_binary">: WireGuard 鍵は32バイトでなければなりません</string> <string name="key_length_explanation_hex">: WireGuard hex 鍵は64文字(32バイト)でなければなりません</string> <string name="listen_port">Listen ポート</string> - <string name="log_export_error">ログをエクスポートできません: %s</string> <string name="log_export_success">“%s” に保存しました</string> - <string name="log_export_summary">ログはダウンロードフォルダに保存されます</string> <string name="log_export_title">ログのエクスポート</string> <string name="logcat_error">logcat を実行できません: </string> <string name="module_version_error">カーネルモジュールバージョンを特定できません</string> 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 @@ <color name="list_multiselect_background">#1aeeeeee</color> <color name="status_bar_color">#21242424</color> <color name="navigation_bar_color">#aa242424</color> + + <!-- Log viewer tag colors --> + <color name="debug_tag_color">#aaaaaa</color> + <color name="error_tag_color">#ff0000</color> + <color name="info_tag_color">#00ff00</color> + <color name="warning_tag_color">#ffff00</color> </resources> 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 @@ <string name="key_length_explanation_binary">: Ключи WireGuard должны быть 32 байта</string> <string name="key_length_explanation_hex">: HEX ключи WireGuard должны содержать 64 символа (32 байта)</string> <string name="listen_port">Порт</string> - <string name="log_export_error">Не удалось экспортировать логи: %s</string> <string name="log_export_success">Сохранено в “%s”</string> - <string name="log_export_summary">Файл логов будет сохранен в папке загрузок</string> <string name="log_export_title">Экспорт логов в файл</string> <string name="logcat_error">Не удалось запустить logcat: </string> <string name="module_version_error">Не удалось определить версию модуля ядра</string> 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 @@ <string name="key_length_explanation_binary">:WireGuard 密钥大小必须为 32 字节</string> <string name="key_length_explanation_hex">:WireGuard 的十六进制密钥长度必须为 64 个字符(32 字节)</string> <string name="listen_port">监听端口</string> - <string name="log_export_error">无法导出日志:%s</string> <string name="log_export_success">已保存至 “%s”</string> - <string name="log_export_summary">日志文件将保存至下载文件夹</string> <string name="log_export_title">导出日志文件</string> <string name="logcat_error">无法运行 logcat:</string> <string name="module_version_error">无法确定内核模块版本</string> 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 name="mtrl_textinput_default_box_stroke_color" tools:override="true">@color/secondary_color</color> <color name="white">#ffffffff</color> + <!-- Log viewer tag colors --> + <color name="debug_tag_color">#444444</color> + <color name="error_tag_color">#aa0000</color> + <color name="info_tag_color">#00aa00</color> + <color name="warning_tag_color">#aaaa00</color> </resources> 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 @@ <string name="key_length_explanation_binary">: WireGuard keys must be 32 bytes</string> <string name="key_length_explanation_hex">: WireGuard hex keys must be 64 characters (32 bytes)</string> <string name="listen_port">Listen port</string> - <string name="log_export_error">Unable to export log: %s</string> <string name="log_export_success">Saved to “%s”</string> - <string name="log_export_summary">Log file will be saved to downloads folder</string> <string name="log_export_title">Export log file</string> + <string name="log_export_subject">WireGuard Android Log File</string> <string name="logcat_error">Unable to run logcat: </string> <string name="module_version_error">Unable to determine kernel module version</string> <string name="module_installer_not_found">No modules are available for your device</string> @@ -186,4 +185,7 @@ <string name="zip_export_title">Export tunnels to zip file</string> <string name="key_length_error">Incorrect key length</string> <string name="key_contents_error">Bad characters in key</string> + <string name="log_viewer_title">View application log</string> + <string name="log_viewer_pref_summary">Logs may assist with debugging</string> + <string name="log_saver_activity_label">Save log</string> </resources> 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" /> <com.wireguard.android.preference.ZipExporterPreference /> - <com.wireguard.android.preference.LogExporterPreference /> + <Preference + android:key="log_viewer" + android:title="@string/log_viewer_title" + android:summary="@string/log_viewer_pref_summary" /> <CheckBoxPreference android:defaultValue="false" android:key="dark_theme" |