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