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 kotlinx.coroutines.CoroutineScope
47 import kotlinx.coroutines.Dispatchers
48 import kotlinx.coroutines.launch
49 import kotlinx.coroutines.withContext
51 import java.io.BufferedReader
52 import java.io.IOException
53 import java.io.InputStreamReader
54 import java.lang.Exception
55 import java.nio.charset.StandardCharsets
57 // Define the class constants.
58 private const val SCROLLVIEW_POSITION = "scrollview_position"
60 class LogcatActivity : AppCompatActivity() {
61 // Define the class variables.
62 private var scrollViewYPositionInt = 0
64 // Define the class views.
65 private lateinit var swipeRefreshLayout: SwipeRefreshLayout
66 private lateinit var logcatScrollView: ScrollView
67 private lateinit var logcatTextView: TextView
69 // Define the save logcat activity result launcher. It must be defined before `onCreate()` is run or the app will crash.
70 private val saveLogcatActivityResultLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { fileUri: Uri? ->
71 // Only save the file if the URI is not null, which happens if the user exited the file picker by pressing back.
72 if (fileUri != null) {
74 // Get the logcat string.
75 val logcatString = logcatTextView.text.toString()
77 // Open an output stream.
78 val outputStream = contentResolver.openOutputStream(fileUri)!!
80 // Save the logcat using a coroutine with Dispatchers.IO.
81 CoroutineScope(Dispatchers.Main).launch {
82 withContext(Dispatchers.IO) {
83 // Write the logcat string to the output stream.
84 outputStream.write(logcatString.toByteArray(StandardCharsets.UTF_8))
86 // Close the output stream.
91 // Initialize the file name string from the file URI last path segment.
92 var fileNameString = fileUri.lastPathSegment
94 // Query the exact file name if the API >= 26.
95 if (Build.VERSION.SDK_INT >= 26) {
96 // Get a cursor from the content resolver.
97 val contentResolverCursor = contentResolver.query(fileUri, null, null, null)!!
99 // Move to the fist row.
100 contentResolverCursor.moveToFirst()
102 // Get the file name from the cursor.
103 fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
106 contentResolverCursor.close()
109 // Display a snackbar with the saved logcat information.
110 Snackbar.make(logcatTextView, getString(R.string.saved, fileNameString), Snackbar.LENGTH_SHORT).show()
111 } catch (exception: Exception) {
112 // Display a snackbar with the error message.
113 Snackbar.make(logcatTextView, getString(R.string.error_saving_logcat, exception.toString()), Snackbar.LENGTH_INDEFINITE).show()
118 public override fun onCreate(savedInstanceState: Bundle?) {
119 // Get a handle for the shared preferences.
120 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
122 // Get the preferences.
123 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
124 val bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false)
126 // Disable screenshots if not allowed.
127 if (!allowScreenshots) {
128 window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
131 // Run the default commands.
132 super.onCreate(savedInstanceState)
134 // Set the content view.
136 setContentView(R.layout.logcat_bottom_appbar)
138 setContentView(R.layout.logcat_top_appbar)
141 // Get handles for the views.
142 val toolbar = findViewById<Toolbar>(R.id.toolbar)
143 swipeRefreshLayout = findViewById(R.id.swiperefreshlayout)
144 logcatScrollView = findViewById(R.id.scrollview)
145 logcatTextView = findViewById(R.id.logcat_textview)
147 // Set the toolbar as the action bar.
148 setSupportActionBar(toolbar)
150 // Get a handle for the action bar.
151 val actionBar = supportActionBar!!
153 // Display the back arrow in the action bar.
154 actionBar.setDisplayHomeAsUpEnabled(true)
156 // Implement swipe to refresh.
157 swipeRefreshLayout.setOnRefreshListener {
158 // Get the current logcat.
162 // Set the swipe refresh color scheme according to the theme.
163 swipeRefreshLayout.setColorSchemeResources(R.color.blue_text)
165 // Initialize a color background typed value.
166 val colorBackgroundTypedValue = TypedValue()
168 // Get the color background from the theme.
169 theme.resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true)
171 // Get the color background int from the typed value.
172 val colorBackgroundInt = colorBackgroundTypedValue.data
174 // Set the swipe refresh background color.
175 swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt)
177 // Check to see if the activity has been restarted.
178 if (savedInstanceState != null) {
179 // Get the saved scrollview position.
180 scrollViewYPositionInt = savedInstanceState.getInt(SCROLLVIEW_POSITION)
187 override fun onCreateOptionsMenu(menu: Menu): Boolean {
188 // Inflate the menu. This adds items to the action bar.
189 menuInflater.inflate(R.menu.logcat_options_menu, menu)
195 override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
196 // Run the commands that correlate to the selected menu item.
197 return when (menuItem.itemId) {
198 R.id.copy -> { // Copy was selected.
199 // Get a handle for the clipboard manager.
200 val clipboardManager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
202 // Save the logcat in a clip data.
203 val logcatClipData = ClipData.newPlainText(getString(R.string.logcat), logcatTextView.text)
205 // Place the clip data on the clipboard.
206 clipboardManager.setPrimaryClip(logcatClipData)
208 // Display a snackbar.
209 Snackbar.make(logcatTextView, R.string.logcat_copied, Snackbar.LENGTH_SHORT).show()
211 // Consume the event.
215 R.id.save -> { // Save was selected.
216 // Open the file picker.
217 saveLogcatActivityResultLauncher.launch(getString(R.string.privacy_browser_logcat_txt, BuildConfig.VERSION_NAME))
219 // Consume the event.
223 R.id.clear -> { // Clear was selected.
225 // Clear the logcat. `-c` clears the logcat. `-b all` clears all the buffers (instead of just crash, main, and system).
226 val process = Runtime.getRuntime().exec("logcat -b all -c")
228 // Wait for the process to finish.
231 // Reset the scroll view Y position int.
232 scrollViewYPositionInt = 0
234 // Reload the logcat.
236 } catch (exception: Exception) {
240 // Consume the event.
244 else -> { // The home button was pushed.
245 // Do not consume the event. The system will process the home command.
246 super.onOptionsItemSelected(menuItem)
251 public override fun onSaveInstanceState(savedInstanceState: Bundle) {
252 // Run the default commands.
253 super.onSaveInstanceState(savedInstanceState)
255 // Get the scrollview Y position.
256 val scrollViewYPositionInt = logcatScrollView.scrollY
258 // Store the scrollview Y position in the bundle.
259 savedInstanceState.putInt(SCROLLVIEW_POSITION, scrollViewYPositionInt)
262 private fun getLogcat() {
264 // 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.
265 val getLogcatProcess = Runtime.getRuntime().exec("logcat -b all -v long -d")
267 // Wrap the logcat in a buffered reader.
268 val logcatBufferedReader = BufferedReader(InputStreamReader(getLogcatProcess.inputStream))
270 // Display the logcat.
271 logcatTextView.text = logcatBufferedReader.readText()
273 // Close the buffered reader.
274 logcatBufferedReader.close()
275 } catch (exception: IOException) {
279 // Update the scroll position after the text is populated.
280 logcatTextView.post {
281 // Set the scroll position.
282 logcatScrollView.scrollY = scrollViewYPositionInt
285 // Stop the swipe to refresh animation if it is displayed.
286 swipeRefreshLayout.isRefreshing = false