2 * Copyright © 2019-2021 Soren Stoutner <soren@stoutner.com>.
4 * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
6 * Privacy Browser 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 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. 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.content.Intent
25 import android.net.Uri
26 import android.os.Build
27 import android.os.Bundle
28 import android.provider.OpenableColumns
29 import android.util.TypedValue
30 import android.view.Menu
31 import android.view.MenuItem
32 import android.view.WindowManager
33 import android.widget.EditText
34 import android.widget.TextView
35 import android.widget.ScrollView
37 import androidx.appcompat.app.AppCompatActivity
38 import androidx.appcompat.widget.Toolbar
39 import androidx.fragment.app.DialogFragment
40 import androidx.preference.PreferenceManager
41 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
43 import com.google.android.material.snackbar.Snackbar
45 import com.stoutner.privacybrowser.R
46 import com.stoutner.privacybrowser.dialogs.SaveDialog
48 import java.io.BufferedReader
49 import java.io.IOException
50 import java.io.InputStreamReader
51 import java.lang.Exception
52 import java.nio.charset.StandardCharsets
54 // Define the class constants.
55 private const val SCROLLVIEW_POSITION = "scrollview_position"
57 class LogcatActivity : AppCompatActivity(), SaveDialog.SaveListener {
58 // Define the class variables.
59 private var scrollViewYPositionInt = 0
61 // Define the class views.
62 private lateinit var swipeRefreshLayout: SwipeRefreshLayout
63 private lateinit var logcatScrollView: ScrollView
64 private lateinit var logcatTextView: TextView
66 public override fun onCreate(savedInstanceState: Bundle?) {
67 // Get a handle for the shared preferences.
68 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
70 // Get the preferences.
71 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
72 val bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false)
74 // Disable screenshots if not allowed.
75 if (!allowScreenshots) {
76 window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
80 setTheme(R.style.PrivacyBrowser)
82 // Run the default commands.
83 super.onCreate(savedInstanceState)
85 // Set the content view.
87 setContentView(R.layout.logcat_bottom_appbar)
89 setContentView(R.layout.logcat_top_appbar)
92 // Get handles for the views.
93 val toolbar = findViewById<Toolbar>(R.id.toolbar)
94 swipeRefreshLayout = findViewById(R.id.swiperefreshlayout)
95 logcatScrollView = findViewById(R.id.scrollview)
96 logcatTextView = findViewById(R.id.logcat_textview)
98 // Set the toolbar as the action bar.
99 setSupportActionBar(toolbar)
101 // Get a handle for the action bar.
102 val actionBar = supportActionBar!!
104 // Display the back arrow in the action bar.
105 actionBar.setDisplayHomeAsUpEnabled(true)
107 // Implement swipe to refresh.
108 swipeRefreshLayout.setOnRefreshListener {
109 // Get the current logcat.
113 // Set the swipe refresh color scheme according to the theme.
114 swipeRefreshLayout.setColorSchemeResources(R.color.blue_text)
116 // Initialize a color background typed value.
117 val colorBackgroundTypedValue = TypedValue()
119 // Get the color background from the theme.
120 theme.resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true)
122 // Get the color background int from the typed value.
123 val colorBackgroundInt = colorBackgroundTypedValue.data
125 // Set the swipe refresh background color.
126 swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt)
128 // Check to see if the activity has been restarted.
129 if (savedInstanceState != null) {
130 // Get the saved scrollview position.
131 scrollViewYPositionInt = savedInstanceState.getInt(SCROLLVIEW_POSITION)
138 override fun onCreateOptionsMenu(menu: Menu): Boolean {
139 // Inflate the menu. This adds items to the action bar.
140 menuInflater.inflate(R.menu.logcat_options_menu, menu)
146 override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
147 // Run the commands that correlate to the selected menu item.
148 return when (menuItem.itemId) {
149 R.id.copy -> { // Copy was selected.
150 // Get a handle for the clipboard manager.
151 val clipboardManager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
153 // Save the logcat in a clip data.
154 val logcatClipData = ClipData.newPlainText(getString(R.string.logcat), logcatTextView.text)
156 // Place the clip data on the clipboard.
157 clipboardManager.setPrimaryClip(logcatClipData)
159 // Display a snackbar.
160 Snackbar.make(logcatTextView, R.string.logcat_copied, Snackbar.LENGTH_SHORT).show()
162 // Consume the event.
166 R.id.save -> { // Save was selected.
167 // Instantiate the save alert dialog.
168 val saveDialogFragment: DialogFragment = SaveDialog.save(SaveDialog.SAVE_LOGCAT)
170 // Show the save alert dialog.
171 saveDialogFragment.show(supportFragmentManager, getString(R.string.save_logcat))
173 // Consume the event.
177 R.id.clear -> { // Clear was selected.
179 // Clear the logcat. `-c` clears the logcat. `-b all` clears all the buffers (instead of just crash, main, and system).
180 val process = Runtime.getRuntime().exec("logcat -b all -c")
182 // Wait for the process to finish.
185 // Reset the scroll view Y position int.
186 scrollViewYPositionInt = 0
188 // Reload the logcat.
190 } catch (exception: Exception) {
194 // Consume the event.
198 else -> { // The home button was pushed.
199 // Do not consume the event. The system will process the home command.
200 super.onOptionsItemSelected(menuItem)
205 public override fun onSaveInstanceState(savedInstanceState: Bundle) {
206 // Run the default commands.
207 super.onSaveInstanceState(savedInstanceState)
209 // Get the scrollview Y position.
210 val scrollViewYPositionInt = logcatScrollView.scrollY
212 // Store the scrollview Y position in the bundle.
213 savedInstanceState.putInt(SCROLLVIEW_POSITION, scrollViewYPositionInt)
216 private fun getLogcat() {
218 // 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.
219 val getLogcatProcess = Runtime.getRuntime().exec("logcat -b all -v long -d")
221 // Wrap the logcat in a buffered reader.
222 val logcatBufferedReader = BufferedReader(InputStreamReader(getLogcatProcess.inputStream))
224 // Display the logcat.
225 logcatTextView.text = logcatBufferedReader.readText()
227 // Close the buffered reader.
228 logcatBufferedReader.close()
229 } catch (exception: IOException) {
233 // Update the scroll position after the text is populated.
234 logcatTextView.post {
235 // Set the scroll position.
236 logcatScrollView.scrollY = scrollViewYPositionInt
239 // Stop the swipe to refresh animation if it is displayed.
240 swipeRefreshLayout.isRefreshing = false
243 // The activity result is called after browsing for a file in the save alert dialog.
244 public override fun onActivityResult(requestCode: Int, resultCode: Int, returnedIntent: Intent?) {
245 // Run the default commands.
246 super.onActivityResult(requestCode, resultCode, returnedIntent)
248 // Only do something if the user didn't press back from the file picker.
249 if (resultCode == RESULT_OK) {
250 // Get a handle for the save dialog fragment.
251 val saveDialogFragment = supportFragmentManager.findFragmentByTag(getString(R.string.save_logcat)) as DialogFragment?
253 // Only update the file name if the dialog still exists.
254 if (saveDialogFragment != null) {
255 // Get a handle for the save dialog.
256 val saveDialog = saveDialogFragment.dialog!!
258 // Get a handle for the file name edit text.
259 val fileNameEditText = saveDialog.findViewById<EditText>(R.id.file_name_edittext)
261 // Get the file name URI from the intent.
262 val fileNameUri = returnedIntent!!.data
264 // Get the file name string from the URI.
265 val fileNameString = fileNameUri.toString()
267 // Set the file name text.
268 fileNameEditText.setText(fileNameString)
270 // Move the cursor to the end of the file name edit text.
271 fileNameEditText.setSelection(fileNameString.length)
276 override fun onSave(saveType: Int, dialogFragment: DialogFragment) {
277 // Get a handle for the dialog.
278 val dialog = dialogFragment.dialog!!
280 // Get a handle for the file name edit text.
281 val fileNameEditText = dialog.findViewById<EditText>(R.id.file_name_edittext)
283 // Get the file path string.
284 var fileNameString = fileNameEditText.text.toString()
287 // Get the logcat as a string.
288 val logcatString = logcatTextView.text.toString()
290 // Open an output stream.
291 val outputStream = contentResolver.openOutputStream(Uri.parse(fileNameString))!!
293 // Write the logcat string to the output stream.
294 outputStream.write(logcatString.toByteArray(StandardCharsets.UTF_8))
296 // Close the output stream.
299 // Get the actual file name if the API >= 26.
300 if (Build.VERSION.SDK_INT >= 26) {
301 // Get a cursor from the content resolver.
302 val contentResolverCursor = contentResolver.query(Uri.parse(fileNameString), null, null, null)!!
304 // Move to the first row.
305 contentResolverCursor.moveToFirst()
307 // Get the file name from the cursor.
308 fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
311 contentResolverCursor.close()
314 // Display a snackbar with the saved logcat information.
315 Snackbar.make(logcatTextView, getString(R.string.file_saved) + " " + fileNameString, Snackbar.LENGTH_SHORT).show()
316 } catch (exception: Exception) {
317 // Display a snackbar with the error message.
318 Snackbar.make(logcatTextView, getString(R.string.error_saving_file) + " " + exception.toString(), Snackbar.LENGTH_INDEFINITE).show()