2 * Copyright 2021-2022 Soren Stoutner <soren@stoutner.com>.
4 * This file is part of Privacy Cell <https://www.stoutner.com/privacy-cell>.
6 * Privacy Cell 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 Cell 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 Cell. If not, see <http://www.gnu.org/licenses/>.
20 package com.stoutner.privacycell.activities
22 import android.content.ClipData
23 import android.content.ClipboardManager
24 import android.net.Uri
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.widget.ScrollView
31 import android.widget.TextView
33 import androidx.activity.result.contract.ActivityResultContracts
34 import androidx.appcompat.app.AppCompatActivity
35 import androidx.appcompat.widget.Toolbar
36 import androidx.preference.PreferenceManager
37 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
39 import com.google.android.material.snackbar.Snackbar
41 import com.stoutner.privacycell.R
43 import kotlinx.coroutines.CoroutineScope
44 import kotlinx.coroutines.Dispatchers
45 import kotlinx.coroutines.launch
46 import kotlinx.coroutines.withContext
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() {
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 // Define the save logcat activity result launcher. It must be defined before `onCreate()` is run or the app will crash.
67 private val saveLogcatActivityResultLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { fileNameUri: Uri? ->
68 // Only save the file if the URI is not null, which happens if the user exited the file picker by pressing back.
69 if (fileNameUri != null) {
71 // Get the logcat as a string.
72 val logcatString = logcatTextView.text.toString()
74 // Open an output stream.
75 val outputStream = contentResolver.openOutputStream(fileNameUri)!!
77 // Save the logcat using a coroutine with Dispatchers.IO.
78 CoroutineScope(Dispatchers.Main).launch {
79 withContext(Dispatchers.IO) {
80 // Write the logcat string to the output stream.
81 outputStream.write(logcatString.toByteArray(StandardCharsets.UTF_8))
83 // Close the output stream.
88 // Get a cursor from the content resolver.
89 val contentResolverCursor = contentResolver.query(fileNameUri, null, null, null)!!
91 // Move to the first row.
92 contentResolverCursor.moveToFirst()
94 // Get the file name from the cursor.
95 val fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
98 contentResolverCursor.close()
100 // Display a snackbar with the saved logcat information.
101 Snackbar.make(logcatTextView, getString(R.string.logcat_saved, fileNameString), Snackbar.LENGTH_SHORT).show()
102 } catch (exception: Exception) {
103 // Display a snackbar with the error message.
104 Snackbar.make(logcatTextView, getString(R.string.error_saving_logcat, exception.toString()), Snackbar.LENGTH_INDEFINITE).show()
109 public override fun onCreate(savedInstanceState: Bundle?) {
110 // Run the default commands.
111 super.onCreate(savedInstanceState)
113 // Get a handle for the shared preferences.
114 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
116 // Get the bottom app bar preference.
117 val bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false)
119 // Set the content view.
121 setContentView(R.layout.logcat_bottom_appbar)
123 setContentView(R.layout.logcat_top_appbar)
126 // Get handles for the views.
127 val toolbar = findViewById<Toolbar>(R.id.toolbar)
128 swipeRefreshLayout = findViewById(R.id.swiperefreshlayout)
129 logcatScrollView = findViewById(R.id.scrollview)
130 logcatTextView = findViewById(R.id.logcat_textview)
132 // Set the toolbar as the action bar.
133 setSupportActionBar(toolbar)
135 // Get a handle for the action bar.
136 val actionBar = supportActionBar!!
138 // Display the back arrow in the action bar.
139 actionBar.setDisplayHomeAsUpEnabled(true)
141 // Implement swipe to refresh.
142 swipeRefreshLayout.setOnRefreshListener {
143 // Get the current logcat.
147 // Set the swipe refresh color scheme according to the theme.
148 swipeRefreshLayout.setColorSchemeResources(R.color.blue_text)
150 // Initialize a color background typed value.
151 val colorBackgroundTypedValue = TypedValue()
153 // Get the color background from the theme.
154 theme.resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true)
156 // Get the color background int from the typed value.
157 val colorBackgroundInt = colorBackgroundTypedValue.data
159 // Set the swipe refresh background color.
160 swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt)
162 // Check to see if the activity has been restarted.
163 if (savedInstanceState != null) {
164 // Get the saved scrollview position.
165 scrollViewYPositionInt = savedInstanceState.getInt(SCROLLVIEW_POSITION)
172 override fun onCreateOptionsMenu(menu: Menu): Boolean {
173 // Inflate the menu. This adds items to the action bar.
174 menuInflater.inflate(R.menu.logcat_options_menu, menu)
180 override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
181 // Run the commands that correlate to the selected menu item.
182 return when (menuItem.itemId) {
183 R.id.copy -> { // Copy was selected.
184 // Get a handle for the clipboard manager.
185 val clipboardManager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
187 // Save the logcat in a clip data.
188 val logcatClipData = ClipData.newPlainText(getString(R.string.logcat), logcatTextView.text)
190 // Place the clip data on the clipboard.
191 clipboardManager.setPrimaryClip(logcatClipData)
193 // Display a snackbar.
194 Snackbar.make(logcatTextView, R.string.logcat_copied, Snackbar.LENGTH_SHORT).show()
196 // Consume the event.
200 R.id.save -> { // Save was selected.
201 // Open the file picker.
202 saveLogcatActivityResultLauncher.launch(getString(R.string.privacy_cell_logcat_txt))
204 // Consume the event.
208 R.id.clear -> { // Clear was selected.
210 // Clear the logcat. `-c` clears the logcat. `-b all` clears all the buffers (instead of just crash, main, and system).
211 val process = Runtime.getRuntime().exec("logcat -b all -c")
213 // Wait for the process to finish.
216 // Reset the scroll view Y position int.
217 scrollViewYPositionInt = 0
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 // Get the scrollview Y position.
241 val scrollViewYPositionInt = logcatScrollView.scrollY
243 // Store the scrollview Y position in the bundle.
244 savedInstanceState.putInt(SCROLLVIEW_POSITION, scrollViewYPositionInt)
247 private fun getLogcat() {
249 // 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.
250 val getLogcatProcess = Runtime.getRuntime().exec("logcat -b all -v long -d")
252 // Wrap the logcat in a buffered reader.
253 val logcatBufferedReader = BufferedReader(InputStreamReader(getLogcatProcess.inputStream))
255 // Display the logcat.
256 logcatTextView.text = logcatBufferedReader.readText()
258 // Close the buffered reader.
259 logcatBufferedReader.close()
260 } catch (exception: IOException) {
264 // Update the scroll position after the text is populated.
265 logcatTextView.post {
266 // Set the scroll position.
267 logcatScrollView.scrollY = scrollViewYPositionInt
270 // Stop the swipe to refresh animation if it is displayed.
271 swipeRefreshLayout.isRefreshing = false