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>
This commit is contained in:
parent
6f973afa36
commit
63a395125a
@ -12,10 +12,11 @@ buildscript {
|
|||||||
cardviewVersion = '1.0.0'
|
cardviewVersion = '1.0.0'
|
||||||
collectionVersion = '1.1.0'
|
collectionVersion = '1.1.0'
|
||||||
coreKtxVersion = '1.2.0'
|
coreKtxVersion = '1.2.0'
|
||||||
|
coroutinesVersion = '1.3.5'
|
||||||
constraintLayoutVersion = '1.1.3'
|
constraintLayoutVersion = '1.1.3'
|
||||||
coordinatorLayoutVersion = '1.1.0'
|
coordinatorLayoutVersion = '1.1.0'
|
||||||
agpVersion = '3.6.1'
|
agpVersion = '3.6.1'
|
||||||
fragmentVersion = '1.2.2'
|
fragmentVersion = '1.2.3'
|
||||||
materialComponentsVersion = '1.1.0'
|
materialComponentsVersion = '1.1.0'
|
||||||
jsr305Version = '3.0.2'
|
jsr305Version = '3.0.2'
|
||||||
kotlinVersion = '1.3.71'
|
kotlinVersion = '1.3.71'
|
||||||
|
@ -22,6 +22,7 @@ android {
|
|||||||
}
|
}
|
||||||
compileSdkVersion 29
|
compileSdkVersion 29
|
||||||
dataBinding.enabled true
|
dataBinding.enabled true
|
||||||
|
viewBinding.enabled true
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId 'com.wireguard.android'
|
applicationId 'com.wireguard.android'
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
@ -84,6 +85,8 @@ dependencies {
|
|||||||
implementation "com.google.android.material:material:$materialComponentsVersion"
|
implementation "com.google.android.material:material:$materialComponentsVersion"
|
||||||
implementation "com.journeyapps:zxing-android-embedded:$zxingEmbeddedVersion"
|
implementation "com.journeyapps:zxing-android-embedded:$zxingEmbeddedVersion"
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion"
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
|
||||||
implementation "net.sourceforge.streamsupport:android-retrofuture:$streamsupportVersion"
|
implementation "net.sourceforge.streamsupport:android-retrofuture:$streamsupportVersion"
|
||||||
implementation "net.sourceforge.streamsupport:android-retrostreams:$streamsupportVersion"
|
implementation "net.sourceforge.streamsupport:android-retrostreams:$streamsupportVersion"
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
android:banner="@mipmap/banner">
|
android:banner="@mipmap/banner">
|
||||||
|
|
||||||
<activity android:name=".activity.TunnelToggleActivity" android:theme="@style/NoBackgroundTheme"/>
|
<activity android:name=".activity.TunnelToggleActivity" android:theme="@style/NoBackgroundTheme"/>
|
||||||
|
|
||||||
<activity android:name=".activity.MainActivity">
|
<activity android:name=".activity.MainActivity">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
@ -58,6 +59,19 @@
|
|||||||
android:screenOrientation="fullSensor"
|
android:screenOrientation="fullSensor"
|
||||||
tools:replace="screenOrientation" />
|
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">
|
<receiver android:name=".BootShutdownReceiver">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.ACTION_SHUTDOWN" />
|
<action android:name="android.intent.action.ACTION_SHUTDOWN" />
|
||||||
|
@ -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!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.wireguard.android.activity
|
package com.wireguard.android.activity
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@ -100,6 +101,10 @@ class SettingsActivity : ThemeChangeAwareActivity() {
|
|||||||
wgQuickOnlyPrefs.forEach { it.parent?.removePreference(it) }
|
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 moduleInstaller = preferenceManager.findPreference<Preference>("module_downloader")
|
||||||
val kernelModuleDisabler = preferenceManager.findPreference<Preference>("kernel_module_disabler")
|
val kernelModuleDisabler = preferenceManager.findPreference<Preference>("kernel_module_disabler")
|
||||||
moduleInstaller?.isVisible = false
|
moduleInstaller?.isVisible = false
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
9
ui/src/main/res/drawable/ic_action_share_white.xml
Normal file
9
ui/src/main/res/drawable/ic_action_share_white.xml
Normal file
@ -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>
|
30
ui/src/main/res/layout/log_viewer_activity.xml
Normal file
30
ui/src/main/res/layout/log_viewer_activity.xml
Normal file
@ -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>
|
34
ui/src/main/res/layout/log_viewer_entry.xml
Normal file
34
ui/src/main/res/layout/log_viewer_entry.xml
Normal file
@ -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>
|
9
ui/src/main/res/menu/log_viewer.xml
Normal file
9
ui/src/main/res/menu/log_viewer.xml
Normal file
@ -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>
|
@ -88,9 +88,7 @@
|
|||||||
<string name="key_length_explanation_binary">: वायरगार्ड कीज 32 बाइट होनी चाहिए</string>
|
<string name="key_length_explanation_binary">: वायरगार्ड कीज 32 बाइट होनी चाहिए</string>
|
||||||
<string name="key_length_explanation_hex">: वायरगार्ड हेक्स कीज़ 64 अक्षरों की होनी चाहिए (32 बाइट्स)</string>
|
<string name="key_length_explanation_hex">: वायरगार्ड हेक्स कीज़ 64 अक्षरों की होनी चाहिए (32 बाइट्स)</string>
|
||||||
<string name="listen_port">पोर्ट सूने</string>
|
<string name="listen_port">पोर्ट सूने</string>
|
||||||
<string name="log_export_error">लॉग निर्यात करने में असमर्थ: %s</string>
|
|
||||||
<string name="log_export_success">“%s” में सहेजा गया</string>
|
<string name="log_export_success">“%s” में सहेजा गया</string>
|
||||||
<string name="log_export_summary">लॉग फ़ाइल को डाउनलोड फ़ोल्डर में सहेजा जाएगा</string>
|
|
||||||
<string name="log_export_title">लॉग फ़ाइल निर्यात करें</string>
|
<string name="log_export_title">लॉग फ़ाइल निर्यात करें</string>
|
||||||
<string name="logcat_error">लॉगकैट चलाने में असमर्थ: </string>
|
<string name="logcat_error">लॉगकैट चलाने में असमर्थ: </string>
|
||||||
<string name="module_version_error">कर्नेल मॉड्यूल संस्करण निर्धारित करने में असमर्थ</string>
|
<string name="module_version_error">कर्नेल मॉड्यूल संस्करण निर्धारित करने में असमर्थ</string>
|
||||||
|
@ -88,9 +88,7 @@
|
|||||||
<string name="key_length_explanation_binary">: Kunci WireGuard harus terdiri dari 32 bit</string>
|
<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="key_length_explanation_hex">: Kunci hex WireGuard Harus terdiri dari 64 karakter (32 bit)</string>
|
||||||
<string name="listen_port">Isi port</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_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="log_export_title">Ekspor file log</string>
|
||||||
<string name="logcat_error">Tidak bisa menjalankan logcat: </string>
|
<string name="logcat_error">Tidak bisa menjalankan logcat: </string>
|
||||||
<string name="module_version_error">Tidak dapat menentukan versi modul kernel</string>
|
<string name="module_version_error">Tidak dapat menentukan versi modul kernel</string>
|
||||||
|
@ -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_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="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="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_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="log_export_title">Esporta file registro</string>
|
||||||
<string name="logcat_error">Impossibile eseguire logcat: </string>
|
<string name="logcat_error">Impossibile eseguire logcat: </string>
|
||||||
<string name="module_version_error">Impossibile determinare la versione modulo del kernel</string>
|
<string name="module_version_error">Impossibile determinare la versione modulo del kernel</string>
|
||||||
|
@ -84,9 +84,7 @@
|
|||||||
<string name="key_length_explanation_binary">: WireGuard 鍵は32バイトでなければなりません</string>
|
<string name="key_length_explanation_binary">: WireGuard 鍵は32バイトでなければなりません</string>
|
||||||
<string name="key_length_explanation_hex">: WireGuard hex 鍵は64文字(32バイト)でなければなりません</string>
|
<string name="key_length_explanation_hex">: WireGuard hex 鍵は64文字(32バイト)でなければなりません</string>
|
||||||
<string name="listen_port">Listen ポート</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_success">“%s” に保存しました</string>
|
||||||
<string name="log_export_summary">ログはダウンロードフォルダに保存されます</string>
|
|
||||||
<string name="log_export_title">ログのエクスポート</string>
|
<string name="log_export_title">ログのエクスポート</string>
|
||||||
<string name="logcat_error">logcat を実行できません: </string>
|
<string name="logcat_error">logcat を実行できません: </string>
|
||||||
<string name="module_version_error">カーネルモジュールバージョンを特定できません</string>
|
<string name="module_version_error">カーネルモジュールバージョンを特定できません</string>
|
||||||
|
@ -14,4 +14,10 @@
|
|||||||
<color name="list_multiselect_background">#1aeeeeee</color>
|
<color name="list_multiselect_background">#1aeeeeee</color>
|
||||||
<color name="status_bar_color">#21242424</color>
|
<color name="status_bar_color">#21242424</color>
|
||||||
<color name="navigation_bar_color">#aa242424</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>
|
</resources>
|
||||||
|
@ -93,9 +93,7 @@
|
|||||||
<string name="key_length_explanation_binary">: Ключи WireGuard должны быть 32 байта</string>
|
<string name="key_length_explanation_binary">: Ключи WireGuard должны быть 32 байта</string>
|
||||||
<string name="key_length_explanation_hex">: HEX ключи WireGuard должны содержать 64 символа (32 байта)</string>
|
<string name="key_length_explanation_hex">: HEX ключи WireGuard должны содержать 64 символа (32 байта)</string>
|
||||||
<string name="listen_port">Порт</string>
|
<string name="listen_port">Порт</string>
|
||||||
<string name="log_export_error">Не удалось экспортировать логи: %s</string>
|
|
||||||
<string name="log_export_success">Сохранено в “%s”</string>
|
<string name="log_export_success">Сохранено в “%s”</string>
|
||||||
<string name="log_export_summary">Файл логов будет сохранен в папке загрузок</string>
|
|
||||||
<string name="log_export_title">Экспорт логов в файл</string>
|
<string name="log_export_title">Экспорт логов в файл</string>
|
||||||
<string name="logcat_error">Не удалось запустить logcat: </string>
|
<string name="logcat_error">Не удалось запустить logcat: </string>
|
||||||
<string name="module_version_error">Не удалось определить версию модуля ядра</string>
|
<string name="module_version_error">Не удалось определить версию модуля ядра</string>
|
||||||
|
@ -82,9 +82,7 @@
|
|||||||
<string name="key_length_explanation_binary">:WireGuard 密钥大小必须为 32 字节</string>
|
<string name="key_length_explanation_binary">:WireGuard 密钥大小必须为 32 字节</string>
|
||||||
<string name="key_length_explanation_hex">:WireGuard 的十六进制密钥长度必须为 64 个字符(32 字节)</string>
|
<string name="key_length_explanation_hex">:WireGuard 的十六进制密钥长度必须为 64 个字符(32 字节)</string>
|
||||||
<string name="listen_port">监听端口</string>
|
<string name="listen_port">监听端口</string>
|
||||||
<string name="log_export_error">无法导出日志:%s</string>
|
|
||||||
<string name="log_export_success">已保存至 “%s”</string>
|
<string name="log_export_success">已保存至 “%s”</string>
|
||||||
<string name="log_export_summary">日志文件将保存至下载文件夹</string>
|
|
||||||
<string name="log_export_title">导出日志文件</string>
|
<string name="log_export_title">导出日志文件</string>
|
||||||
<string name="logcat_error">无法运行 logcat:</string>
|
<string name="logcat_error">无法运行 logcat:</string>
|
||||||
<string name="module_version_error">无法确定内核模块版本</string>
|
<string name="module_version_error">无法确定内核模块版本</string>
|
||||||
|
@ -18,4 +18,9 @@
|
|||||||
<color name="mtrl_textinput_default_box_stroke_color" tools:override="true">@color/secondary_color</color>
|
<color name="mtrl_textinput_default_box_stroke_color" tools:override="true">@color/secondary_color</color>
|
||||||
<color name="white">#ffffffff</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>
|
</resources>
|
||||||
|
@ -93,10 +93,9 @@
|
|||||||
<string name="key_length_explanation_binary">: WireGuard keys must be 32 bytes</string>
|
<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="key_length_explanation_hex">: WireGuard hex keys must be 64 characters (32 bytes)</string>
|
||||||
<string name="listen_port">Listen port</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_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_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="logcat_error">Unable to run logcat: </string>
|
||||||
<string name="module_version_error">Unable to determine kernel module version</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>
|
<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="zip_export_title">Export tunnels to zip file</string>
|
||||||
<string name="key_length_error">Incorrect key length</string>
|
<string name="key_length_error">Incorrect key length</string>
|
||||||
<string name="key_contents_error">Bad characters in key</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>
|
</resources>
|
||||||
|
@ -8,7 +8,10 @@
|
|||||||
android:summaryOff="@string/restore_on_boot_summary_off"
|
android:summaryOff="@string/restore_on_boot_summary_off"
|
||||||
android:title="@string/restore_on_boot_title" />
|
android:title="@string/restore_on_boot_title" />
|
||||||
<com.wireguard.android.preference.ZipExporterPreference />
|
<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
|
<CheckBoxPreference
|
||||||
android:defaultValue="false"
|
android:defaultValue="false"
|
||||||
android:key="dark_theme"
|
android:key="dark_theme"
|
||||||
|
Loading…
Reference in New Issue
Block a user