Release 3.9.
[PrivacyBrowser.git] / app / src / main / java / com / stoutner / privacybrowser / activities / LogcatActivity.kt
1 /*
2  * Copyright © 2019-2021 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
5  *
6  * Privacy Browser 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 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.  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.content.Intent
25 import android.net.Uri
26 import android.os.Build
27 import android.os.Bundle
28 import android.provider.OpenableColumns
29 import android.util.TypedValue
30 import android.view.Menu
31 import android.view.MenuItem
32 import android.view.WindowManager
33 import android.widget.EditText
34 import android.widget.TextView
35 import android.widget.ScrollView
36
37 import androidx.appcompat.app.AppCompatActivity
38 import androidx.appcompat.widget.Toolbar
39 import androidx.fragment.app.DialogFragment
40 import androidx.preference.PreferenceManager
41 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
42
43 import com.google.android.material.snackbar.Snackbar
44
45 import com.stoutner.privacybrowser.R
46 import com.stoutner.privacybrowser.dialogs.SaveDialog
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(), SaveDialog.SaveListener {
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     public override fun onCreate(savedInstanceState: Bundle?) {
67         // Get a handle for the shared preferences.
68         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
69
70         // Get the preferences.
71         val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
72         val bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false)
73
74         // Disable screenshots if not allowed.
75         if (!allowScreenshots) {
76             window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
77         }
78
79         // Set the theme.
80         setTheme(R.style.PrivacyBrowser)
81
82         // Run the default commands.
83         super.onCreate(savedInstanceState)
84
85         // Set the content view.
86         if (bottomAppBar) {
87             setContentView(R.layout.logcat_bottom_appbar)
88         } else {
89             setContentView(R.layout.logcat_top_appbar)
90         }
91
92         // Get handles for the views.
93         val toolbar = findViewById<Toolbar>(R.id.toolbar)
94         swipeRefreshLayout = findViewById(R.id.swiperefreshlayout)
95         logcatScrollView = findViewById(R.id.scrollview)
96         logcatTextView = findViewById(R.id.logcat_textview)
97
98         // Set the toolbar as the action bar.
99         setSupportActionBar(toolbar)
100
101         // Get a handle for the action bar.
102         val actionBar = supportActionBar!!
103
104         // Display the back arrow in the action bar.
105         actionBar.setDisplayHomeAsUpEnabled(true)
106
107         // Implement swipe to refresh.
108         swipeRefreshLayout.setOnRefreshListener {
109             // Get the current logcat.
110             getLogcat()
111         }
112
113         // Set the swipe refresh color scheme according to the theme.
114         swipeRefreshLayout.setColorSchemeResources(R.color.blue_text)
115
116         // Initialize a color background typed value.
117         val colorBackgroundTypedValue = TypedValue()
118
119         // Get the color background from the theme.
120         theme.resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true)
121
122         // Get the color background int from the typed value.
123         val colorBackgroundInt = colorBackgroundTypedValue.data
124
125         // Set the swipe refresh background color.
126         swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt)
127
128         // Check to see if the activity has been restarted.
129         if (savedInstanceState != null) {
130             // Get the saved scrollview position.
131             scrollViewYPositionInt = savedInstanceState.getInt(SCROLLVIEW_POSITION)
132         }
133
134         // Get the logcat.
135         getLogcat()
136     }
137
138     override fun onCreateOptionsMenu(menu: Menu): Boolean {
139         // Inflate the menu.  This adds items to the action bar.
140         menuInflater.inflate(R.menu.logcat_options_menu, menu)
141
142         // Display the menu.
143         return true
144     }
145
146     override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
147         // Run the commands that correlate to the selected menu item.
148         return when (menuItem.itemId) {
149             R.id.copy -> {  // Copy was selected.
150                 // Get a handle for the clipboard manager.
151                 val clipboardManager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
152
153                 // Save the logcat in a clip data.
154                 val logcatClipData = ClipData.newPlainText(getString(R.string.logcat), logcatTextView.text)
155
156                 // Place the clip data on the clipboard.
157                 clipboardManager.setPrimaryClip(logcatClipData)
158
159                 // Display a snackbar.
160                 Snackbar.make(logcatTextView, R.string.logcat_copied, Snackbar.LENGTH_SHORT).show()
161
162                 // Consume the event.
163                 true
164             }
165
166             R.id.save -> {  // Save was selected.
167                 // Instantiate the save alert dialog.
168                 val saveDialogFragment: DialogFragment = SaveDialog.save(SaveDialog.SAVE_LOGCAT)
169
170                 // Show the save alert dialog.
171                 saveDialogFragment.show(supportFragmentManager, getString(R.string.save_logcat))
172
173                 // Consume the event.
174                 true
175             }
176
177             R.id.clear -> {  // Clear was selected.
178                 try {
179                     // Clear the logcat.  `-c` clears the logcat.  `-b all` clears all the buffers (instead of just crash, main, and system).
180                     val process = Runtime.getRuntime().exec("logcat -b all -c")
181
182                     // Wait for the process to finish.
183                     process.waitFor()
184
185                     // Reset the scroll view Y position int.
186                     scrollViewYPositionInt = 0
187
188                     // Reload the logcat.
189                     getLogcat()
190                 } catch (exception: Exception) {
191                     // Do nothing.
192                 }
193
194                 // Consume the event.
195                 true
196             }
197
198             else -> {  // The home button was pushed.
199                 // Do not consume the event.  The system will process the home command.
200                 super.onOptionsItemSelected(menuItem)
201             }
202         }
203     }
204
205     public override fun onSaveInstanceState(savedInstanceState: Bundle) {
206         // Run the default commands.
207         super.onSaveInstanceState(savedInstanceState)
208
209         // Get the scrollview Y position.
210         val scrollViewYPositionInt = logcatScrollView.scrollY
211
212         // Store the scrollview Y position in the bundle.
213         savedInstanceState.putInt(SCROLLVIEW_POSITION, scrollViewYPositionInt)
214     }
215
216     private fun getLogcat() {
217         try {
218             // 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.
219             val getLogcatProcess = Runtime.getRuntime().exec("logcat -b all -v long -d")
220
221             // Wrap the logcat in a buffered reader.
222             val logcatBufferedReader = BufferedReader(InputStreamReader(getLogcatProcess.inputStream))
223
224             // Display the logcat.
225             logcatTextView.text = logcatBufferedReader.readText()
226
227             // Close the buffered reader.
228             logcatBufferedReader.close()
229         } catch (exception: IOException) {
230             // Do nothing.
231         }
232
233         // Update the scroll position after the text is populated.
234         logcatTextView.post {
235             // Set the scroll position.
236             logcatScrollView.scrollY = scrollViewYPositionInt
237         }
238
239         // Stop the swipe to refresh animation if it is displayed.
240         swipeRefreshLayout.isRefreshing = false
241     }
242
243     // The activity result is called after browsing for a file in the save alert dialog.
244     public override fun onActivityResult(requestCode: Int, resultCode: Int, returnedIntent: Intent?) {
245         // Run the default commands.
246         super.onActivityResult(requestCode, resultCode, returnedIntent)
247
248         // Only do something if the user didn't press back from the file picker.
249         if (resultCode == RESULT_OK) {
250             // Get a handle for the save dialog fragment.
251             val saveDialogFragment = supportFragmentManager.findFragmentByTag(getString(R.string.save_logcat)) as DialogFragment?
252
253             // Only update the file name if the dialog still exists.
254             if (saveDialogFragment != null) {
255                 // Get a handle for the save dialog.
256                 val saveDialog = saveDialogFragment.dialog!!
257
258                 // Get a handle for the file name edit text.
259                 val fileNameEditText = saveDialog.findViewById<EditText>(R.id.file_name_edittext)
260
261                 // Get the file name URI from the intent.
262                 val fileNameUri = returnedIntent!!.data
263
264                 // Get the file name string from the URI.
265                 val fileNameString = fileNameUri.toString()
266
267                 // Set the file name text.
268                 fileNameEditText.setText(fileNameString)
269
270                 // Move the cursor to the end of the file name edit text.
271                 fileNameEditText.setSelection(fileNameString.length)
272             }
273         }
274     }
275
276     override fun onSave(saveType: Int, dialogFragment: DialogFragment) {
277         // Get a handle for the dialog.
278         val dialog = dialogFragment.dialog!!
279
280         // Get a handle for the file name edit text.
281         val fileNameEditText = dialog.findViewById<EditText>(R.id.file_name_edittext)
282
283         // Get the file path string.
284         var fileNameString = fileNameEditText.text.toString()
285
286         try {
287             // Get the logcat as a string.
288             val logcatString = logcatTextView.text.toString()
289
290             // Open an output stream.
291             val outputStream = contentResolver.openOutputStream(Uri.parse(fileNameString))!!
292
293             // Write the logcat string to the output stream.
294             outputStream.write(logcatString.toByteArray(StandardCharsets.UTF_8))
295
296             // Close the output stream.
297             outputStream.close()
298
299             // Get the actual file name if the API >= 26.
300             if (Build.VERSION.SDK_INT >= 26) {
301                 // Get a cursor from the content resolver.
302                 val contentResolverCursor = contentResolver.query(Uri.parse(fileNameString), null, null, null)!!
303
304                 // Move to the first row.
305                 contentResolverCursor.moveToFirst()
306
307                 // Get the file name from the cursor.
308                 fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
309
310                 // Close the cursor.
311                 contentResolverCursor.close()
312             }
313
314             // Display a snackbar with the saved logcat information.
315             Snackbar.make(logcatTextView, getString(R.string.file_saved) + "  " + fileNameString, Snackbar.LENGTH_SHORT).show()
316         } catch (exception: Exception) {
317             // Display a snackbar with the error message.
318             Snackbar.make(logcatTextView, getString(R.string.error_saving_file) + "  " + exception.toString(), Snackbar.LENGTH_INDEFINITE).show()
319         }
320     }
321 }