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