2 * Copyright 2019-2024 Soren Stoutner <soren@stoutner.com>.
4 * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android/>.
6 * Privacy Browser Android is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
11 * Privacy Browser Android is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with Privacy Browser Android. If not, see <http://www.gnu.org/licenses/>.
20 package com.stoutner.privacybrowser.activities
22 import android.content.ClipData
23 import android.content.ClipboardManager
24 import android.os.Build
25 import android.os.Bundle
26 import android.provider.OpenableColumns
27 import android.util.Base64
28 import android.util.TypedValue
29 import android.view.Menu
30 import android.view.MenuItem
31 import android.view.WindowManager
32 import android.webkit.WebView
34 import androidx.activity.result.contract.ActivityResultContracts
35 import androidx.appcompat.app.AppCompatActivity
36 import androidx.appcompat.widget.Toolbar
37 import androidx.preference.PreferenceManager
38 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
40 import com.google.android.material.snackbar.Snackbar
42 import com.stoutner.privacybrowser.BuildConfig
43 import com.stoutner.privacybrowser.R
45 import kotlinx.coroutines.CoroutineScope
46 import kotlinx.coroutines.Dispatchers
47 import kotlinx.coroutines.launch
48 import kotlinx.coroutines.withContext
50 import java.io.BufferedReader
51 import java.io.IOException
52 import java.io.InputStreamReader
54 import java.nio.charset.StandardCharsets
56 // Define the class constants.
57 private const val SCROLL_Y = "A"
59 class LogcatActivity : AppCompatActivity() {
60 // Declare the class variables.
61 private lateinit var logcatPlainTextStringBuilder: StringBuilder
63 // Declare the class views.
64 private lateinit var swipeRefreshLayout: SwipeRefreshLayout
65 private lateinit var logcatWebView: WebView
67 // Define the save logcat activity result launcher. It must be defined before `onCreate()` is run or the app will crash.
68 private val saveLogcatActivityResultLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { fileUri ->
69 // Only save the file if the URI is not null, which happens if the user exited the file picker by pressing back.
70 if (fileUri != null) {
72 // Open an output stream.
73 val outputStream = contentResolver.openOutputStream(fileUri)!!
75 // Save the logcat using a coroutine with Dispatchers.IO.
76 CoroutineScope(Dispatchers.Main).launch {
77 withContext(Dispatchers.IO) {
78 // Write the logcat string to the output stream.
79 outputStream.write(logcatPlainTextStringBuilder.toString().toByteArray(StandardCharsets.UTF_8))
81 // Close the output stream.
86 // Get a cursor from the content resolver.
87 val contentResolverCursor = contentResolver.query(fileUri, null, null, null)!!
89 // Move to the fist row.
90 contentResolverCursor.moveToFirst()
92 // Get the file name from the cursor.
93 val fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
96 contentResolverCursor.close()
98 // Display a snackbar with the saved logcat information.
99 Snackbar.make(logcatWebView, getString(R.string.saved, fileNameString), Snackbar.LENGTH_SHORT).show()
100 } catch (exception: Exception) {
101 // Display a snackbar with the error message.
102 Snackbar.make(logcatWebView, getString(R.string.error_saving_logcat, exception.toString()), Snackbar.LENGTH_INDEFINITE).show()
107 public override fun onCreate(savedInstanceState: Bundle?) {
108 // Get a handle for the shared preferences.
109 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
111 // Get the preferences.
112 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
113 val bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false)
115 // Disable screenshots if not allowed.
116 if (!allowScreenshots)
117 window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
119 // Run the default commands.
120 super.onCreate(savedInstanceState)
122 // Set the content view.
124 setContentView(R.layout.logcat_bottom_appbar)
126 setContentView(R.layout.logcat_top_appbar)
128 // Get handles for the views.
129 val toolbar = findViewById<Toolbar>(R.id.toolbar)
130 swipeRefreshLayout = findViewById(R.id.swiperefreshlayout)
131 logcatWebView = findViewById(R.id.logcat_webview)
133 // Set the toolbar as the action bar.
134 setSupportActionBar(toolbar)
136 // Get a handle for the action bar.
137 val actionBar = supportActionBar!!
139 // Display the back arrow in the action bar.
140 actionBar.setDisplayHomeAsUpEnabled(true)
142 // Implement swipe to refresh.
143 swipeRefreshLayout.setOnRefreshListener {
144 // Populate the current logcat.
148 // Set the swipe refresh color scheme according to the theme.
149 swipeRefreshLayout.setColorSchemeResources(R.color.blue_text)
151 // Initialize a color background typed value.
152 val colorBackgroundTypedValue = TypedValue()
154 // Get the color background from the theme.
155 theme.resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true)
157 // Get the color background int from the typed value.
158 val colorBackgroundInt = colorBackgroundTypedValue.data
160 // Set the swipe refresh background color.
161 swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt)
163 // Restore the WebView scroll position if the activity has been restarted.
164 if (savedInstanceState != null)
165 logcatWebView.scrollY = savedInstanceState.getInt(SCROLL_Y)
167 // Allow loading of file:// URLs.
168 logcatWebView.settings.allowFileAccess = true
170 // Populate the logcat.
174 override fun onCreateOptionsMenu(menu: Menu): Boolean {
175 // Inflate the menu. This adds items to the action bar.
176 menuInflater.inflate(R.menu.logcat_options_menu, menu)
182 override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
183 // Run the commands that correlate to the selected menu item.
184 return when (menuItem.itemId) {
185 R.id.copy -> { // Copy was selected.
186 // Get a handle for the clipboard manager.
187 val clipboardManager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
189 // Save the logcat in a clip data.
190 val logcatClipData = ClipData.newPlainText(getString(R.string.logcat), logcatPlainTextStringBuilder)
192 // Place the clip data on the clipboard.
193 clipboardManager.setPrimaryClip(logcatClipData)
195 // Display a snackbar if the API <= 32 (Android 12L). Beginning in Android 13 the OS displays a notification that covers up the snackbar.
196 if (Build.VERSION.SDK_INT <= 32)
197 Snackbar.make(logcatWebView, R.string.logcat_copied, Snackbar.LENGTH_SHORT).show()
199 // Consume the event.
203 R.id.save -> { // Save was selected.
204 // Open the file picker.
205 saveLogcatActivityResultLauncher.launch(getString(R.string.privacy_browser_logcat_txt, BuildConfig.VERSION_NAME))
207 // Consume the event.
211 R.id.clear -> { // Clear was selected.
213 // Clear the logcat. `-c` clears the logcat. `-b all` clears all the buffers (instead of just crash, main, and system).
214 val process = Runtime.getRuntime().exec("logcat -b all -c")
216 // Wait for the process to finish.
219 // Reload the logcat.
221 } catch (exception: Exception) {
225 // Consume the event.
229 else -> { // The home button was pushed.
230 // Do not consume the event. The system will process the home command.
231 super.onOptionsItemSelected(menuItem)
236 public override fun onSaveInstanceState(savedInstanceState: Bundle) {
237 // Run the default commands.
238 super.onSaveInstanceState(savedInstanceState)
240 // Store the scroll Y position in the bundle.
241 savedInstanceState.putInt(SCROLL_Y, logcatWebView.scrollY)
244 private fun populateLogcat() {
246 // Get the logcat. `-b all` gets all the buffers (instead of just crash, main, and system). `-v long` produces more complete information. `-d` dumps the logcat and exits.
247 val getLogcatProcess = Runtime.getRuntime().exec("logcat -b all -v long -d")
249 // Wrap the logcat in a buffered reader.
250 val logcatBufferedReader = BufferedReader(InputStreamReader(getLogcatProcess.inputStream))
252 // Reset the logcat plain text string.
253 logcatPlainTextStringBuilder = StringBuilder()
255 // Create a logcat HTML string builder.
256 val logcatHtmlStringBuilder = StringBuilder()
258 // Populate the initial HTML.
259 logcatHtmlStringBuilder.append("<html>")
260 logcatHtmlStringBuilder.append("<head>")
261 logcatHtmlStringBuilder.append("<style>")
263 // Set the word break so that lines never exceed the width of the screen.
264 logcatHtmlStringBuilder.append("body { word-break: break-word; }")
267 logcatHtmlStringBuilder.append("@media (prefers-color-scheme: dark) { body { color: #C1C1C1; /* Gray 350 */ background-color: #303030; /* Gray 860 */ } }")
268 logcatHtmlStringBuilder.append("span.header { color: #0D47A1; /* Blue 900 */ } @media (prefers-color-scheme: dark) { span.header { color: #8AB4F8; /* Violet 500 */ } }")
269 logcatHtmlStringBuilder.append("strong.crash { color: #B71C1C; /* Red 900. */ } @media (prefers-color-scheme: dark) { strong.crash { color: #E24B4C; /* Red Night. */ } }")
270 logcatHtmlStringBuilder.append("span.crash { color: #EF5350; /* Red 400. */ } @media (prefers-color-scheme: dark) { span.crash { color: #EF9A9A; /* Red Night. */ } }")
272 // Close the style tag.
273 logcatHtmlStringBuilder.append("</style>")
275 // Respect dark mode.
276 logcatHtmlStringBuilder.append("<meta name=\"color-scheme\" content=\"light dark\">")
278 // Start the HTML body.
279 logcatHtmlStringBuilder.append("</head>")
280 logcatHtmlStringBuilder.append("<body>")
282 // Create a logcat line string.
283 var logcatLineString: String?
285 while (logcatBufferedReader.readLine().also { logcatLineString = it } != null) {
286 // Populate the logcat plain text string builder.
287 logcatPlainTextStringBuilder.append(logcatLineString)
290 logcatPlainTextStringBuilder.append("\n")
292 // Trim the string, which is necessary for correct detection of lines that start with `at`.
293 logcatLineString = logcatLineString!!.trim()
295 // Apply syntax highlighting to the logcat.
296 if (logcatLineString!!.contains("crash") || logcatLineString!!.contains("Exception") ) { // Colorize crashes.
297 logcatHtmlStringBuilder.append("<strong class=\"crash\">")
298 logcatHtmlStringBuilder.append(logcatLineString)
299 logcatHtmlStringBuilder.append("</strong>")
300 } else if (logcatLineString!!.startsWith("at") || logcatLineString!!.startsWith("Process:") || logcatLineString!!.contains("FATAL")) { // Colorize lines relating to crashes.
301 logcatHtmlStringBuilder.append("<span class=\"crash\">")
302 logcatHtmlStringBuilder.append(logcatLineString)
303 logcatHtmlStringBuilder.append("</span>")
304 } else if (logcatLineString!!.startsWith("-")) { // Colorize the headers.
305 logcatHtmlStringBuilder.append("<span class=\"header\">")
306 logcatHtmlStringBuilder.append(logcatLineString)
307 logcatHtmlStringBuilder.append("</span>")
308 } else if (logcatLineString!!.startsWith("[ ")) { // Colorize the time stamps.
309 logcatHtmlStringBuilder.append("<span style=color:gray>")
310 logcatHtmlStringBuilder.append(logcatLineString)
311 logcatHtmlStringBuilder.append("</span>")
312 } else { // Display the standard lines.
313 logcatHtmlStringBuilder.append(logcatLineString)
317 logcatHtmlStringBuilder.append("<br>")
321 logcatHtmlStringBuilder.append("</body>")
322 logcatHtmlStringBuilder.append("</html>")
324 // Encode the logcat HTML.
325 val base64EncodedLogcatHtml: String = Base64.encodeToString(logcatHtmlStringBuilder.toString().toByteArray(Charsets.UTF_8), Base64.NO_PADDING)
327 // Load the encoded logcat.
328 logcatWebView.loadData(base64EncodedLogcatHtml, "text/html", "base64")
330 // Close the buffered reader.
331 logcatBufferedReader.close()
332 } catch (exception: IOException) {
336 // Stop the swipe to refresh animation if it is displayed.
337 swipeRefreshLayout.isRefreshing = false