Add a logcat activity. https://redmine.stoutner.com/issues/768
[PrivacyCell.git] / app / src / main / java / com / stoutner / privacycell / activities / LogcatActivity.kt
1 /*
2  * Copyright © 2021 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Cell <https://www.stoutner.com/privacy-cell>.
5  *
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.
10  *
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.
15  *
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/>.
18  */
19
20 package com.stoutner.privacycell.activities
21
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
32
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
38
39 import com.google.android.material.snackbar.Snackbar
40
41 import com.stoutner.privacycell.R
42
43 import java.io.BufferedReader
44 import java.io.IOException
45 import java.io.InputStreamReader
46 import java.lang.Exception
47 import java.nio.charset.StandardCharsets
48
49 // Define the class constants.
50 private const val SCROLLVIEW_POSITION = "scrollview_position"
51
52 class LogcatActivity : AppCompatActivity() {
53     // Define the class variables.
54     private var scrollViewYPositionInt = 0
55
56     // Define the class views.
57     private lateinit var swipeRefreshLayout: SwipeRefreshLayout
58     private lateinit var logcatScrollView: ScrollView
59     private lateinit var logcatTextView: TextView
60
61     // Define the save logcat activity result launcher.
62     private val saveLogcatActivityResultLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument()) { fileNameUri: Uri? ->
63         // Only save the file if the URI is not null, which happens if the user exited the file picker by pressing back.
64         if (fileNameUri != null) {
65             try {
66                 // Get the logcat as a string.
67                 val logcatString = logcatTextView.text.toString()
68
69                 // Open an output stream.
70                 val outputStream = contentResolver.openOutputStream(fileNameUri)!!
71
72                 // Write the logcat string to the output stream.
73                 outputStream.write(logcatString.toByteArray(StandardCharsets.UTF_8))
74
75                 // Close the output stream.
76                 outputStream.close()
77
78                 // Get a cursor from the content resolver.
79                 val contentResolverCursor = contentResolver.query(fileNameUri, null, null, null)!!
80
81                 // Move to the first row.
82                 contentResolverCursor.moveToFirst()
83
84                 // Get the file name from the cursor.
85                 val fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
86
87                 // Close the cursor.
88                 contentResolverCursor.close()
89
90                 // Display a snackbar with the saved logcat information.
91                 Snackbar.make(logcatTextView, getString(R.string.logcat_saved, fileNameString), Snackbar.LENGTH_SHORT).show()
92             } catch (exception: Exception) {
93                 // Display a snackbar with the error message.
94                 Snackbar.make(logcatTextView, getString(R.string.error_saving_logcat, exception.toString()), Snackbar.LENGTH_INDEFINITE).show()
95             }
96         }
97     }
98
99     public override fun onCreate(savedInstanceState: Bundle?) {
100         // Run the default commands.
101         super.onCreate(savedInstanceState)
102
103         // Get a handle for the shared preferences.
104         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
105
106         // Get the bottom app bar preference.
107         val bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false)
108
109         // Set the content view.
110         if (bottomAppBar) {
111             setContentView(R.layout.logcat_bottom_appbar)
112         } else {
113             setContentView(R.layout.logcat_top_appbar)
114         }
115
116         // Get handles for the views.
117         val toolbar = findViewById<Toolbar>(R.id.toolbar)
118         swipeRefreshLayout = findViewById(R.id.swiperefreshlayout)
119         logcatScrollView = findViewById(R.id.scrollview)
120         logcatTextView = findViewById(R.id.logcat_textview)
121
122         // Set the toolbar as the action bar.
123         setSupportActionBar(toolbar)
124
125         // Get a handle for the action bar.
126         val actionBar = supportActionBar!!
127
128         // Display the back arrow in the action bar.
129         actionBar.setDisplayHomeAsUpEnabled(true)
130
131         // Implement swipe to refresh.
132         swipeRefreshLayout.setOnRefreshListener {
133             // Get the current logcat.
134             getLogcat()
135         }
136
137         // Set the swipe refresh color scheme according to the theme.
138         swipeRefreshLayout.setColorSchemeResources(R.color.blue_text)
139
140         // Initialize a color background typed value.
141         val colorBackgroundTypedValue = TypedValue()
142
143         // Get the color background from the theme.
144         theme.resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true)
145
146         // Get the color background int from the typed value.
147         val colorBackgroundInt = colorBackgroundTypedValue.data
148
149         // Set the swipe refresh background color.
150         swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt)
151
152         // Check to see if the activity has been restarted.
153         if (savedInstanceState != null) {
154             // Get the saved scrollview position.
155             scrollViewYPositionInt = savedInstanceState.getInt(SCROLLVIEW_POSITION)
156         }
157
158         // Get the logcat.
159         getLogcat()
160     }
161
162     override fun onCreateOptionsMenu(menu: Menu): Boolean {
163         // Inflate the menu.  This adds items to the action bar.
164         menuInflater.inflate(R.menu.logcat_options_menu, menu)
165
166         // Display the menu.
167         return true
168     }
169
170     override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
171         // Run the commands that correlate to the selected menu item.
172         return when (menuItem.itemId) {
173             R.id.copy -> {  // Copy was selected.
174                 // Get a handle for the clipboard manager.
175                 val clipboardManager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
176
177                 // Save the logcat in a clip data.
178                 val logcatClipData = ClipData.newPlainText(getString(R.string.logcat), logcatTextView.text)
179
180                 // Place the clip data on the clipboard.
181                 clipboardManager.setPrimaryClip(logcatClipData)
182
183                 // Display a snackbar.
184                 Snackbar.make(logcatTextView, R.string.logcat_copied, Snackbar.LENGTH_SHORT).show()
185
186                 // Consume the event.
187                 true
188             }
189
190             R.id.save -> {  // Save was selected.
191                 // Open the file picker.
192                 saveLogcatActivityResultLauncher.launch(getString(R.string.privacy_cell_logcat_txt))
193
194                 // Consume the event.
195                 true
196             }
197
198             R.id.clear -> {  // Clear was selected.
199                 try {
200                     // Clear the logcat.  `-c` clears the logcat.  `-b all` clears all the buffers (instead of just crash, main, and system).
201                     val process = Runtime.getRuntime().exec("logcat -b all -c")
202
203                     // Wait for the process to finish.
204                     process.waitFor()
205
206                     // Reset the scroll view Y position int.
207                     scrollViewYPositionInt = 0
208
209                     // Reload the logcat.
210                     getLogcat()
211                 } catch (exception: Exception) {
212                     // Do nothing.
213                 }
214
215                 // Consume the event.
216                 true
217             }
218
219             else -> {  // The home button was pushed.
220                 // Do not consume the event.  The system will process the home command.
221                 super.onOptionsItemSelected(menuItem)
222             }
223         }
224     }
225
226     public override fun onSaveInstanceState(savedInstanceState: Bundle) {
227         // Run the default commands.
228         super.onSaveInstanceState(savedInstanceState)
229
230         // Get the scrollview Y position.
231         val scrollViewYPositionInt = logcatScrollView.scrollY
232
233         // Store the scrollview Y position in the bundle.
234         savedInstanceState.putInt(SCROLLVIEW_POSITION, scrollViewYPositionInt)
235     }
236
237     private fun getLogcat() {
238         try {
239             // 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.
240             val getLogcatProcess = Runtime.getRuntime().exec("logcat -b all -v long -d")
241
242             // Wrap the logcat in a buffered reader.
243             val logcatBufferedReader = BufferedReader(InputStreamReader(getLogcatProcess.inputStream))
244
245             // Display the logcat.
246             logcatTextView.text = logcatBufferedReader.readText()
247
248             // Close the buffered reader.
249             logcatBufferedReader.close()
250         } catch (exception: IOException) {
251             // Do nothing.
252         }
253
254         // Update the scroll position after the text is populated.
255         logcatTextView.post {
256             // Set the scroll position.
257             logcatScrollView.scrollY = scrollViewYPositionInt
258         }
259
260         // Stop the swipe to refresh animation if it is displayed.
261         swipeRefreshLayout.isRefreshing = false
262     }
263 }