]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/activities/LogcatActivity.kt
Fix possibly blocking OutputStream calls. https://redmine.stoutner.com/issues/914
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / activities / LogcatActivity.kt
1 /*
2  * Copyright 2019-2022 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.net.Uri
25 import android.os.Build
26 import android.os.Bundle
27 import android.provider.OpenableColumns
28 import android.util.TypedValue
29 import android.view.Menu
30 import android.view.MenuItem
31 import android.view.WindowManager
32 import android.widget.TextView
33 import android.widget.ScrollView
34
35 import androidx.activity.result.contract.ActivityResultContracts
36 import androidx.appcompat.app.AppCompatActivity
37 import androidx.appcompat.widget.Toolbar
38 import androidx.preference.PreferenceManager
39 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
40
41 import com.google.android.material.snackbar.Snackbar
42 import com.stoutner.privacybrowser.BuildConfig
43
44 import com.stoutner.privacybrowser.R
45
46 import kotlinx.coroutines.CoroutineScope
47 import kotlinx.coroutines.Dispatchers
48 import kotlinx.coroutines.launch
49 import kotlinx.coroutines.withContext
50
51 import java.io.BufferedReader
52 import java.io.IOException
53 import java.io.InputStreamReader
54 import java.lang.Exception
55 import java.nio.charset.StandardCharsets
56
57 // Define the class constants.
58 private const val SCROLLVIEW_POSITION = "scrollview_position"
59
60 class LogcatActivity : AppCompatActivity() {
61     // Define the class variables.
62     private var scrollViewYPositionInt = 0
63
64     // Define the class views.
65     private lateinit var swipeRefreshLayout: SwipeRefreshLayout
66     private lateinit var logcatScrollView: ScrollView
67     private lateinit var logcatTextView: TextView
68
69     // Define the save logcat activity result launcher.  It must be defined before `onCreate()` is run or the app will crash.
70     private val saveLogcatActivityResultLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { fileUri: Uri? ->
71         // Only save the file if the URI is not null, which happens if the user exited the file picker by pressing back.
72         if (fileUri != null) {
73             try {
74                 // Get the logcat string.
75                 val logcatString = logcatTextView.text.toString()
76
77                 // Open an output stream.
78                 val outputStream = contentResolver.openOutputStream(fileUri)!!
79
80                 // Save the logcat using a coroutine with Dispatchers.IO.
81                 CoroutineScope(Dispatchers.Main).launch {
82                     withContext(Dispatchers.IO) {
83                         // Write the logcat string to the output stream.
84                         outputStream.write(logcatString.toByteArray(StandardCharsets.UTF_8))
85
86                         // Close the output stream.
87                         outputStream.close()
88                     }
89                 }
90
91                 // Initialize the file name string from the file URI last path segment.
92                 var fileNameString = fileUri.lastPathSegment
93
94                 // Query the exact file name if the API >= 26.
95                 if (Build.VERSION.SDK_INT >= 26) {
96                     // Get a cursor from the content resolver.
97                     val contentResolverCursor = contentResolver.query(fileUri, null, null, null)!!
98
99                     // Move to the fist row.
100                     contentResolverCursor.moveToFirst()
101
102                     // Get the file name from the cursor.
103                     fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
104
105                     // Close the cursor.
106                     contentResolverCursor.close()
107                 }
108
109                 // Display a snackbar with the saved logcat information.
110                 Snackbar.make(logcatTextView, getString(R.string.saved, fileNameString), Snackbar.LENGTH_SHORT).show()
111             } catch (exception: Exception) {
112                 // Display a snackbar with the error message.
113                 Snackbar.make(logcatTextView, getString(R.string.error_saving_logcat, exception.toString()), Snackbar.LENGTH_INDEFINITE).show()
114             }
115         }
116     }
117
118     public override fun onCreate(savedInstanceState: Bundle?) {
119         // Get a handle for the shared preferences.
120         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
121
122         // Get the preferences.
123         val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
124         val bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false)
125
126         // Disable screenshots if not allowed.
127         if (!allowScreenshots) {
128             window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
129         }
130
131         // Run the default commands.
132         super.onCreate(savedInstanceState)
133
134         // Set the content view.
135         if (bottomAppBar) {
136             setContentView(R.layout.logcat_bottom_appbar)
137         } else {
138             setContentView(R.layout.logcat_top_appbar)
139         }
140
141         // Get handles for the views.
142         val toolbar = findViewById<Toolbar>(R.id.toolbar)
143         swipeRefreshLayout = findViewById(R.id.swiperefreshlayout)
144         logcatScrollView = findViewById(R.id.scrollview)
145         logcatTextView = findViewById(R.id.logcat_textview)
146
147         // Set the toolbar as the action bar.
148         setSupportActionBar(toolbar)
149
150         // Get a handle for the action bar.
151         val actionBar = supportActionBar!!
152
153         // Display the back arrow in the action bar.
154         actionBar.setDisplayHomeAsUpEnabled(true)
155
156         // Implement swipe to refresh.
157         swipeRefreshLayout.setOnRefreshListener {
158             // Get the current logcat.
159             getLogcat()
160         }
161
162         // Set the swipe refresh color scheme according to the theme.
163         swipeRefreshLayout.setColorSchemeResources(R.color.blue_text)
164
165         // Initialize a color background typed value.
166         val colorBackgroundTypedValue = TypedValue()
167
168         // Get the color background from the theme.
169         theme.resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true)
170
171         // Get the color background int from the typed value.
172         val colorBackgroundInt = colorBackgroundTypedValue.data
173
174         // Set the swipe refresh background color.
175         swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt)
176
177         // Check to see if the activity has been restarted.
178         if (savedInstanceState != null) {
179             // Get the saved scrollview position.
180             scrollViewYPositionInt = savedInstanceState.getInt(SCROLLVIEW_POSITION)
181         }
182
183         // Get the logcat.
184         getLogcat()
185     }
186
187     override fun onCreateOptionsMenu(menu: Menu): Boolean {
188         // Inflate the menu.  This adds items to the action bar.
189         menuInflater.inflate(R.menu.logcat_options_menu, menu)
190
191         // Display the menu.
192         return true
193     }
194
195     override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
196         // Run the commands that correlate to the selected menu item.
197         return when (menuItem.itemId) {
198             R.id.copy -> {  // Copy was selected.
199                 // Get a handle for the clipboard manager.
200                 val clipboardManager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
201
202                 // Save the logcat in a clip data.
203                 val logcatClipData = ClipData.newPlainText(getString(R.string.logcat), logcatTextView.text)
204
205                 // Place the clip data on the clipboard.
206                 clipboardManager.setPrimaryClip(logcatClipData)
207
208                 // Display a snackbar.
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 }