]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/activities/LogcatActivity.kt
Bump the minimum API to 26. https://redmine.stoutner.com/issues/1163
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / activities / LogcatActivity.kt
1 /*
2  * Copyright 2019-2024 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
42 import com.stoutner.privacybrowser.BuildConfig
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                 // Get a cursor from the content resolver.
91                 val contentResolverCursor = contentResolver.query(fileUri, null, null, null)!!
92
93                 // Move to the fist row.
94                 contentResolverCursor.moveToFirst()
95
96                 // Get the file name from the cursor.
97                 val fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
98
99                 // Close the cursor.
100                 contentResolverCursor.close()
101
102                 // Display a snackbar with the saved logcat information.
103                 Snackbar.make(logcatTextView, getString(R.string.saved, fileNameString), Snackbar.LENGTH_SHORT).show()
104             } catch (exception: Exception) {
105                 // Display a snackbar with the error message.
106                 Snackbar.make(logcatTextView, getString(R.string.error_saving_logcat, exception.toString()), Snackbar.LENGTH_INDEFINITE).show()
107             }
108         }
109     }
110
111     public override fun onCreate(savedInstanceState: Bundle?) {
112         // Get a handle for the shared preferences.
113         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
114
115         // Get the preferences.
116         val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
117         val bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false)
118
119         // Disable screenshots if not allowed.
120         if (!allowScreenshots) {
121             window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
122         }
123
124         // Run the default commands.
125         super.onCreate(savedInstanceState)
126
127         // Set the content view.
128         if (bottomAppBar) {
129             setContentView(R.layout.logcat_bottom_appbar)
130         } else {
131             setContentView(R.layout.logcat_top_appbar)
132         }
133
134         // Get handles for the views.
135         val toolbar = findViewById<Toolbar>(R.id.toolbar)
136         swipeRefreshLayout = findViewById(R.id.swiperefreshlayout)
137         logcatScrollView = findViewById(R.id.scrollview)
138         logcatTextView = findViewById(R.id.logcat_textview)
139
140         // Set the toolbar as the action bar.
141         setSupportActionBar(toolbar)
142
143         // Get a handle for the action bar.
144         val actionBar = supportActionBar!!
145
146         // Display the back arrow in the action bar.
147         actionBar.setDisplayHomeAsUpEnabled(true)
148
149         // Implement swipe to refresh.
150         swipeRefreshLayout.setOnRefreshListener {
151             // Get the current logcat.
152             getLogcat()
153         }
154
155         // Set the swipe refresh color scheme according to the theme.
156         swipeRefreshLayout.setColorSchemeResources(R.color.blue_text)
157
158         // Initialize a color background typed value.
159         val colorBackgroundTypedValue = TypedValue()
160
161         // Get the color background from the theme.
162         theme.resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true)
163
164         // Get the color background int from the typed value.
165         val colorBackgroundInt = colorBackgroundTypedValue.data
166
167         // Set the swipe refresh background color.
168         swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt)
169
170         // Check to see if the activity has been restarted.
171         if (savedInstanceState != null) {
172             // Get the saved scrollview position.
173             scrollViewYPositionInt = savedInstanceState.getInt(SCROLLVIEW_POSITION)
174         }
175
176         // Get the logcat.
177         getLogcat()
178     }
179
180     override fun onCreateOptionsMenu(menu: Menu): Boolean {
181         // Inflate the menu.  This adds items to the action bar.
182         menuInflater.inflate(R.menu.logcat_options_menu, menu)
183
184         // Display the menu.
185         return true
186     }
187
188     override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
189         // Run the commands that correlate to the selected menu item.
190         return when (menuItem.itemId) {
191             R.id.copy -> {  // Copy was selected.
192                 // Get a handle for the clipboard manager.
193                 val clipboardManager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
194
195                 // Save the logcat in a clip data.
196                 val logcatClipData = ClipData.newPlainText(getString(R.string.logcat), logcatTextView.text)
197
198                 // Place the clip data on the clipboard.
199                 clipboardManager.setPrimaryClip(logcatClipData)
200
201                 // Display a snackbar if the API <= 32 (Android 12L).  Beginning in Android 13 the OS displays a notification that covers up the snackbar.
202                 if (Build.VERSION.SDK_INT <= 32)
203                     Snackbar.make(logcatTextView, R.string.logcat_copied, Snackbar.LENGTH_SHORT).show()
204
205                 // Consume the event.
206                 true
207             }
208
209             R.id.save -> {  // Save was selected.
210                 // Open the file picker.
211                 saveLogcatActivityResultLauncher.launch(getString(R.string.privacy_browser_logcat_txt, BuildConfig.VERSION_NAME))
212
213                 // Consume the event.
214                 true
215             }
216
217             R.id.clear -> {  // Clear was selected.
218                 try {
219                     // Clear the logcat.  `-c` clears the logcat.  `-b all` clears all the buffers (instead of just crash, main, and system).
220                     val process = Runtime.getRuntime().exec("logcat -b all -c")
221
222                     // Wait for the process to finish.
223                     process.waitFor()
224
225                     // Reset the scroll view Y position int.
226                     scrollViewYPositionInt = 0
227
228                     // Reload the logcat.
229                     getLogcat()
230                 } catch (exception: Exception) {
231                     // Do nothing.
232                 }
233
234                 // Consume the event.
235                 true
236             }
237
238             else -> {  // The home button was pushed.
239                 // Do not consume the event.  The system will process the home command.
240                 super.onOptionsItemSelected(menuItem)
241             }
242         }
243     }
244
245     public override fun onSaveInstanceState(savedInstanceState: Bundle) {
246         // Run the default commands.
247         super.onSaveInstanceState(savedInstanceState)
248
249         // Get the scrollview Y position.
250         val scrollViewYPositionInt = logcatScrollView.scrollY
251
252         // Store the scrollview Y position in the bundle.
253         savedInstanceState.putInt(SCROLLVIEW_POSITION, scrollViewYPositionInt)
254     }
255
256     private fun getLogcat() {
257         try {
258             // 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.
259             val getLogcatProcess = Runtime.getRuntime().exec("logcat -b all -v long -d")
260
261             // Wrap the logcat in a buffered reader.
262             val logcatBufferedReader = BufferedReader(InputStreamReader(getLogcatProcess.inputStream))
263
264             // Display the logcat.
265             logcatTextView.text = logcatBufferedReader.readText()
266
267             // Close the buffered reader.
268             logcatBufferedReader.close()
269         } catch (exception: IOException) {
270             // Do nothing.
271         }
272
273         // Update the scroll position after the text is populated.
274         logcatTextView.post {
275             // Set the scroll position.
276             logcatScrollView.scrollY = scrollViewYPositionInt
277         }
278
279         // Stop the swipe to refresh animation if it is displayed.
280         swipeRefreshLayout.isRefreshing = false
281     }
282 }