2 * Copyright © 2019-2022 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.net.Uri
25 import android.os.Build
26 import android.os.Bundle
27 import android.provider.OpenableColumns
28 import android.util.TypedValue
29 import android.view.Menu
30 import android.view.MenuItem
31 import android.view.WindowManager
32 import android.widget.TextView
33 import android.widget.ScrollView
35 import androidx.activity.result.contract.ActivityResultContracts
36 import androidx.appcompat.app.AppCompatActivity
37 import androidx.appcompat.widget.Toolbar
38 import androidx.preference.PreferenceManager
39 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
41 import com.google.android.material.snackbar.Snackbar
42 import com.stoutner.privacybrowser.BuildConfig
44 import com.stoutner.privacybrowser.R
46 import java.io.BufferedReader
47 import java.io.IOException
48 import java.io.InputStreamReader
49 import java.lang.Exception
50 import java.nio.charset.StandardCharsets
52 // Define the class constants.
53 private const val SCROLLVIEW_POSITION = "scrollview_position"
55 class LogcatActivity : AppCompatActivity() {
56 // Define the class variables.
57 private var scrollViewYPositionInt = 0
59 // Define the class views.
60 private lateinit var swipeRefreshLayout: SwipeRefreshLayout
61 private lateinit var logcatScrollView: ScrollView
62 private lateinit var logcatTextView: TextView
64 // Define the save logcat activity result launcher. It must be defined before `onCreate()` is run or the app will crash.
65 private val saveLogcatActivityResultLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument()) { fileUri: Uri? ->
66 // Only save the file if the URI is not null, which happens if the user exited the file picker by pressing back.
67 if (fileUri != null) {
69 // Get the logcat string.
70 val logcatString = logcatTextView.text.toString()
72 // Open an output stream.
73 val outputStream = contentResolver.openOutputStream(fileUri)!!
75 // Write the logcat string to the output stream.
76 outputStream.write(logcatString.toByteArray(StandardCharsets.UTF_8))
78 // Close the output stream.
81 // Initialize the file name string from the file URI last path segment.
82 var fileNameString = fileUri.lastPathSegment
84 // Query the exact file name if the API >= 26.
85 if (Build.VERSION.SDK_INT >= 26) {
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 fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
96 contentResolverCursor.close()
99 // Display a snackbar with the saved logcat information.
100 Snackbar.make(logcatTextView, getString(R.string.saved, fileNameString), Snackbar.LENGTH_SHORT).show()
101 } catch (exception: Exception) {
102 // Display a snackbar with the error message.
103 Snackbar.make(logcatTextView, getString(R.string.error_saving_logcat, exception.toString()), Snackbar.LENGTH_INDEFINITE).show()
108 public override fun onCreate(savedInstanceState: Bundle?) {
109 // Get a handle for the shared preferences.
110 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
112 // Get the preferences.
113 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
114 val bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false)
116 // Disable screenshots if not allowed.
117 if (!allowScreenshots) {
118 window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
121 // Run the default commands.
122 super.onCreate(savedInstanceState)
124 // Set the content view.
126 setContentView(R.layout.logcat_bottom_appbar)
128 setContentView(R.layout.logcat_top_appbar)
131 // Get handles for the views.
132 val toolbar = findViewById<Toolbar>(R.id.toolbar)
133 swipeRefreshLayout = findViewById(R.id.swiperefreshlayout)
134 logcatScrollView = findViewById(R.id.scrollview)
135 logcatTextView = findViewById(R.id.logcat_textview)
137 // Set the toolbar as the action bar.
138 setSupportActionBar(toolbar)
140 // Get a handle for the action bar.
141 val actionBar = supportActionBar!!
143 // Display the back arrow in the action bar.
144 actionBar.setDisplayHomeAsUpEnabled(true)
146 // Implement swipe to refresh.
147 swipeRefreshLayout.setOnRefreshListener {
148 // Get the current logcat.
152 // Set the swipe refresh color scheme according to the theme.
153 swipeRefreshLayout.setColorSchemeResources(R.color.blue_text)
155 // Initialize a color background typed value.
156 val colorBackgroundTypedValue = TypedValue()
158 // Get the color background from the theme.
159 theme.resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true)
161 // Get the color background int from the typed value.
162 val colorBackgroundInt = colorBackgroundTypedValue.data
164 // Set the swipe refresh background color.
165 swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt)
167 // Check to see if the activity has been restarted.
168 if (savedInstanceState != null) {
169 // Get the saved scrollview position.
170 scrollViewYPositionInt = savedInstanceState.getInt(SCROLLVIEW_POSITION)
177 override fun onCreateOptionsMenu(menu: Menu): Boolean {
178 // Inflate the menu. This adds items to the action bar.
179 menuInflater.inflate(R.menu.logcat_options_menu, menu)
185 override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
186 // Run the commands that correlate to the selected menu item.
187 return when (menuItem.itemId) {
188 R.id.copy -> { // Copy was selected.
189 // Get a handle for the clipboard manager.
190 val clipboardManager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
192 // Save the logcat in a clip data.
193 val logcatClipData = ClipData.newPlainText(getString(R.string.logcat), logcatTextView.text)
195 // Place the clip data on the clipboard.
196 clipboardManager.setPrimaryClip(logcatClipData)
198 // Display a snackbar.
199 Snackbar.make(logcatTextView, R.string.logcat_copied, Snackbar.LENGTH_SHORT).show()
201 // Consume the event.
205 R.id.save -> { // Save was selected.
206 // Open the file picker.
207 saveLogcatActivityResultLauncher.launch(getString(R.string.privacy_browser_logcat_txt, BuildConfig.VERSION_NAME))
209 // Consume the event.
213 R.id.clear -> { // Clear was selected.
215 // Clear the logcat. `-c` clears the logcat. `-b all` clears all the buffers (instead of just crash, main, and system).
216 val process = Runtime.getRuntime().exec("logcat -b all -c")
218 // Wait for the process to finish.
221 // Reset the scroll view Y position int.
222 scrollViewYPositionInt = 0
224 // Reload the logcat.
226 } catch (exception: Exception) {
230 // Consume the event.
234 else -> { // The home button was pushed.
235 // Do not consume the event. The system will process the home command.
236 super.onOptionsItemSelected(menuItem)
241 public override fun onSaveInstanceState(savedInstanceState: Bundle) {
242 // Run the default commands.
243 super.onSaveInstanceState(savedInstanceState)
245 // Get the scrollview Y position.
246 val scrollViewYPositionInt = logcatScrollView.scrollY
248 // Store the scrollview Y position in the bundle.
249 savedInstanceState.putInt(SCROLLVIEW_POSITION, scrollViewYPositionInt)
252 private fun getLogcat() {
254 // 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.
255 val getLogcatProcess = Runtime.getRuntime().exec("logcat -b all -v long -d")
257 // Wrap the logcat in a buffered reader.
258 val logcatBufferedReader = BufferedReader(InputStreamReader(getLogcatProcess.inputStream))
260 // Display the logcat.
261 logcatTextView.text = logcatBufferedReader.readText()
263 // Close the buffered reader.
264 logcatBufferedReader.close()
265 } catch (exception: IOException) {
269 // Update the scroll position after the text is populated.
270 logcatTextView.post {
271 // Set the scroll position.
272 logcatScrollView.scrollY = scrollViewYPositionInt
275 // Stop the swipe to refresh animation if it is displayed.
276 swipeRefreshLayout.isRefreshing = false