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.TypedValue
28 import android.view.Menu
29 import android.view.MenuItem
30 import android.view.WindowManager
31 import android.widget.TextView
32 import android.widget.ScrollView
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
53 import java.lang.Exception
54 import java.nio.charset.StandardCharsets
56 // Define the class constants.
57 private const val SCROLLVIEW_POSITION = "scrollview_position"
59 class LogcatActivity : AppCompatActivity() {
60 // Define the class variables.
61 private var scrollViewYPositionInt = 0
63 // Define the class views.
64 private lateinit var swipeRefreshLayout: SwipeRefreshLayout
65 private lateinit var logcatScrollView: ScrollView
66 private lateinit var logcatTextView: TextView
68 // Define the save logcat activity result launcher. It must be defined before `onCreate()` is run or the app will crash.
69 private val saveLogcatActivityResultLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { fileUri ->
70 // Only save the file if the URI is not null, which happens if the user exited the file picker by pressing back.
71 if (fileUri != null) {
73 // Get the logcat string.
74 val logcatString = logcatTextView.text.toString()
76 // Open an output stream.
77 val outputStream = contentResolver.openOutputStream(fileUri)!!
79 // Save the logcat using a coroutine with Dispatchers.IO.
80 CoroutineScope(Dispatchers.Main).launch {
81 withContext(Dispatchers.IO) {
82 // Write the logcat string to the output stream.
83 outputStream.write(logcatString.toByteArray(StandardCharsets.UTF_8))
85 // Close the output stream.
90 // Get a cursor from the content resolver.
91 val contentResolverCursor = contentResolver.query(fileUri, null, null, null)!!
93 // Move to the fist row.
94 contentResolverCursor.moveToFirst()
96 // Get the file name from the cursor.
97 val fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
100 contentResolverCursor.close()
102 // Display a snackbar with the saved logcat information.
103 Snackbar.make(logcatTextView, getString(R.string.saved, fileNameString), Snackbar.LENGTH_SHORT).show()
104 } catch (exception: Exception) {
105 // Display a snackbar with the error message.
106 Snackbar.make(logcatTextView, getString(R.string.error_saving_logcat, exception.toString()), Snackbar.LENGTH_INDEFINITE).show()
111 public override fun onCreate(savedInstanceState: Bundle?) {
112 // Get a handle for the shared preferences.
113 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
115 // Get the preferences.
116 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
117 val bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false)
119 // Disable screenshots if not allowed.
120 if (!allowScreenshots) {
121 window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
124 // Run the default commands.
125 super.onCreate(savedInstanceState)
127 // Set the content view.
129 setContentView(R.layout.logcat_bottom_appbar)
131 setContentView(R.layout.logcat_top_appbar)
134 // Get handles for the views.
135 val toolbar = findViewById<Toolbar>(R.id.toolbar)
136 swipeRefreshLayout = findViewById(R.id.swiperefreshlayout)
137 logcatScrollView = findViewById(R.id.scrollview)
138 logcatTextView = findViewById(R.id.logcat_textview)
140 // Set the toolbar as the action bar.
141 setSupportActionBar(toolbar)
143 // Get a handle for the action bar.
144 val actionBar = supportActionBar!!
146 // Display the back arrow in the action bar.
147 actionBar.setDisplayHomeAsUpEnabled(true)
149 // Implement swipe to refresh.
150 swipeRefreshLayout.setOnRefreshListener {
151 // Get the current logcat.
155 // Set the swipe refresh color scheme according to the theme.
156 swipeRefreshLayout.setColorSchemeResources(R.color.blue_text)
158 // Initialize a color background typed value.
159 val colorBackgroundTypedValue = TypedValue()
161 // Get the color background from the theme.
162 theme.resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true)
164 // Get the color background int from the typed value.
165 val colorBackgroundInt = colorBackgroundTypedValue.data
167 // Set the swipe refresh background color.
168 swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt)
170 // Check to see if the activity has been restarted.
171 if (savedInstanceState != null) {
172 // Get the saved scrollview position.
173 scrollViewYPositionInt = savedInstanceState.getInt(SCROLLVIEW_POSITION)
180 override fun onCreateOptionsMenu(menu: Menu): Boolean {
181 // Inflate the menu. This adds items to the action bar.
182 menuInflater.inflate(R.menu.logcat_options_menu, menu)
188 override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
189 // Run the commands that correlate to the selected menu item.
190 return when (menuItem.itemId) {
191 R.id.copy -> { // Copy was selected.
192 // Get a handle for the clipboard manager.
193 val clipboardManager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
195 // Save the logcat in a clip data.
196 val logcatClipData = ClipData.newPlainText(getString(R.string.logcat), logcatTextView.text)
198 // Place the clip data on the clipboard.
199 clipboardManager.setPrimaryClip(logcatClipData)
201 // Display a snackbar if the API <= 32 (Android 12L). Beginning in Android 13 the OS displays a notification that covers up the snackbar.
202 if (Build.VERSION.SDK_INT <= 32)
203 Snackbar.make(logcatTextView, R.string.logcat_copied, Snackbar.LENGTH_SHORT).show()
205 // Consume the event.
209 R.id.save -> { // Save was selected.
210 // Open the file picker.
211 saveLogcatActivityResultLauncher.launch(getString(R.string.privacy_browser_logcat_txt, BuildConfig.VERSION_NAME))
213 // Consume the event.
217 R.id.clear -> { // Clear was selected.
219 // Clear the logcat. `-c` clears the logcat. `-b all` clears all the buffers (instead of just crash, main, and system).
220 val process = Runtime.getRuntime().exec("logcat -b all -c")
222 // Wait for the process to finish.
225 // Reset the scroll view Y position int.
226 scrollViewYPositionInt = 0
228 // Reload the logcat.
230 } catch (exception: Exception) {
234 // Consume the event.
238 else -> { // The home button was pushed.
239 // Do not consume the event. The system will process the home command.
240 super.onOptionsItemSelected(menuItem)
245 public override fun onSaveInstanceState(savedInstanceState: Bundle) {
246 // Run the default commands.
247 super.onSaveInstanceState(savedInstanceState)
249 // Get the scrollview Y position.
250 val scrollViewYPositionInt = logcatScrollView.scrollY
252 // Store the scrollview Y position in the bundle.
253 savedInstanceState.putInt(SCROLLVIEW_POSITION, scrollViewYPositionInt)
256 private fun getLogcat() {
258 // 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.
259 val getLogcatProcess = Runtime.getRuntime().exec("logcat -b all -v long -d")
261 // Wrap the logcat in a buffered reader.
262 val logcatBufferedReader = BufferedReader(InputStreamReader(getLogcatProcess.inputStream))
264 // Display the logcat.
265 logcatTextView.text = logcatBufferedReader.readText()
267 // Close the buffered reader.
268 logcatBufferedReader.close()
269 } catch (exception: IOException) {
273 // Update the scroll position after the text is populated.
274 logcatTextView.post {
275 // Set the scroll position.
276 logcatScrollView.scrollY = scrollViewYPositionInt
279 // Stop the swipe to refresh animation if it is displayed.
280 swipeRefreshLayout.isRefreshing = false