]> gitweb.stoutner.com Git - PrivacyCell.git/blob - app/src/main/java/com/stoutner/privacycell/activities/LogcatActivity.kt
Bump target API to 33. https://redmine.stoutner.com/issues/890
[PrivacyCell.git] / app / src / main / java / com / stoutner / privacycell / activities / LogcatActivity.kt
1 /*
2  * Copyright 2021-2022 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 kotlinx.coroutines.CoroutineScope
44 import kotlinx.coroutines.Dispatchers
45 import kotlinx.coroutines.launch
46 import kotlinx.coroutines.withContext
47
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
53
54 // Define the class constants.
55 private const val SCROLLVIEW_POSITION = "scrollview_position"
56
57 class LogcatActivity : AppCompatActivity() {
58     // Define the class variables.
59     private var scrollViewYPositionInt = 0
60
61     // Define the class views.
62     private lateinit var swipeRefreshLayout: SwipeRefreshLayout
63     private lateinit var logcatScrollView: ScrollView
64     private lateinit var logcatTextView: TextView
65
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) {
70             try {
71                 // Get the logcat as a string.
72                 val logcatString = logcatTextView.text.toString()
73
74                 // Open an output stream.
75                 val outputStream = contentResolver.openOutputStream(fileNameUri)!!
76
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))
82
83                         // Close the output stream.
84                         outputStream.close()
85                     }
86                 }
87
88                 // Get a cursor from the content resolver.
89                 val contentResolverCursor = contentResolver.query(fileNameUri, null, null, null)!!
90
91                 // Move to the first row.
92                 contentResolverCursor.moveToFirst()
93
94                 // Get the file name from the cursor.
95                 val fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
96
97                 // Close the cursor.
98                 contentResolverCursor.close()
99
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()
105             }
106         }
107     }
108
109     public override fun onCreate(savedInstanceState: Bundle?) {
110         // Run the default commands.
111         super.onCreate(savedInstanceState)
112
113         // Get a handle for the shared preferences.
114         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
115
116         // Get the bottom app bar preference.
117         val bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false)
118
119         // Set the content view.
120         if (bottomAppBar) {
121             setContentView(R.layout.logcat_bottom_appbar)
122         } else {
123             setContentView(R.layout.logcat_top_appbar)
124         }
125
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)
131
132         // Set the toolbar as the action bar.
133         setSupportActionBar(toolbar)
134
135         // Get a handle for the action bar.
136         val actionBar = supportActionBar!!
137
138         // Display the back arrow in the action bar.
139         actionBar.setDisplayHomeAsUpEnabled(true)
140
141         // Implement swipe to refresh.
142         swipeRefreshLayout.setOnRefreshListener {
143             // Get the current logcat.
144             getLogcat()
145         }
146
147         // Set the swipe refresh color scheme according to the theme.
148         swipeRefreshLayout.setColorSchemeResources(R.color.blue_text)
149
150         // Initialize a color background typed value.
151         val colorBackgroundTypedValue = TypedValue()
152
153         // Get the color background from the theme.
154         theme.resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true)
155
156         // Get the color background int from the typed value.
157         val colorBackgroundInt = colorBackgroundTypedValue.data
158
159         // Set the swipe refresh background color.
160         swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt)
161
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)
166         }
167
168         // Get the logcat.
169         getLogcat()
170     }
171
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)
175
176         // Display the menu.
177         return true
178     }
179
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
186
187                 // Save the logcat in a clip data.
188                 val logcatClipData = ClipData.newPlainText(getString(R.string.logcat), logcatTextView.text)
189
190                 // Place the clip data on the clipboard.
191                 clipboardManager.setPrimaryClip(logcatClipData)
192
193                 // Display a snackbar.
194                 Snackbar.make(logcatTextView, R.string.logcat_copied, Snackbar.LENGTH_SHORT).show()
195
196                 // Consume the event.
197                 true
198             }
199
200             R.id.save -> {  // Save was selected.
201                 // Open the file picker.
202                 saveLogcatActivityResultLauncher.launch(getString(R.string.privacy_cell_logcat_txt))
203
204                 // Consume the event.
205                 true
206             }
207
208             R.id.clear -> {  // Clear was selected.
209                 try {
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")
212
213                     // Wait for the process to finish.
214                     process.waitFor()
215
216                     // Reset the scroll view Y position int.
217                     scrollViewYPositionInt = 0
218
219                     // Reload the logcat.
220                     getLogcat()
221                 } catch (exception: Exception) {
222                     // Do nothing.
223                 }
224
225                 // Consume the event.
226                 true
227             }
228
229             else -> {  // The home button was pushed.
230                 // Do not consume the event.  The system will process the home command.
231                 super.onOptionsItemSelected(menuItem)
232             }
233         }
234     }
235
236     public override fun onSaveInstanceState(savedInstanceState: Bundle) {
237         // Run the default commands.
238         super.onSaveInstanceState(savedInstanceState)
239
240         // Get the scrollview Y position.
241         val scrollViewYPositionInt = logcatScrollView.scrollY
242
243         // Store the scrollview Y position in the bundle.
244         savedInstanceState.putInt(SCROLLVIEW_POSITION, scrollViewYPositionInt)
245     }
246
247     private fun getLogcat() {
248         try {
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")
251
252             // Wrap the logcat in a buffered reader.
253             val logcatBufferedReader = BufferedReader(InputStreamReader(getLogcatProcess.inputStream))
254
255             // Display the logcat.
256             logcatTextView.text = logcatBufferedReader.readText()
257
258             // Close the buffered reader.
259             logcatBufferedReader.close()
260         } catch (exception: IOException) {
261             // Do nothing.
262         }
263
264         // Update the scroll position after the text is populated.
265         logcatTextView.post {
266             // Set the scroll position.
267             logcatScrollView.scrollY = scrollViewYPositionInt
268         }
269
270         // Stop the swipe to refresh animation if it is displayed.
271         swipeRefreshLayout.isRefreshing = false
272     }
273 }