+/*
+ * Copyright © 2021 Soren Stoutner <soren@stoutner.com>.
+ *
+ * This file is part of Privacy Cell <https://www.stoutner.com/privacy-cell>.
+ *
+ * Privacy Cell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Privacy Cell is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Privacy Cell. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.stoutner.privacycell.activities
+
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.net.Uri
+import android.os.Bundle
+import android.provider.OpenableColumns
+import android.util.TypedValue
+import android.view.Menu
+import android.view.MenuItem
+import android.widget.ScrollView
+import android.widget.TextView
+
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.widget.Toolbar
+import androidx.preference.PreferenceManager
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+
+import com.google.android.material.snackbar.Snackbar
+
+import com.stoutner.privacycell.R
+
+import java.io.BufferedReader
+import java.io.IOException
+import java.io.InputStreamReader
+import java.lang.Exception
+import java.nio.charset.StandardCharsets
+
+// Define the class constants.
+private const val SCROLLVIEW_POSITION = "scrollview_position"
+
+class LogcatActivity : AppCompatActivity() {
+ // Define the class variables.
+ private var scrollViewYPositionInt = 0
+
+ // Define the class views.
+ private lateinit var swipeRefreshLayout: SwipeRefreshLayout
+ private lateinit var logcatScrollView: ScrollView
+ private lateinit var logcatTextView: TextView
+
+ // Define the save logcat activity result launcher.
+ private val saveLogcatActivityResultLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument()) { fileNameUri: Uri? ->
+ // Only save the file if the URI is not null, which happens if the user exited the file picker by pressing back.
+ if (fileNameUri != null) {
+ try {
+ // Get the logcat as a string.
+ val logcatString = logcatTextView.text.toString()
+
+ // Open an output stream.
+ val outputStream = contentResolver.openOutputStream(fileNameUri)!!
+
+ // Write the logcat string to the output stream.
+ outputStream.write(logcatString.toByteArray(StandardCharsets.UTF_8))
+
+ // Close the output stream.
+ outputStream.close()
+
+ // Get a cursor from the content resolver.
+ val contentResolverCursor = contentResolver.query(fileNameUri, null, null, null)!!
+
+ // Move to the first row.
+ contentResolverCursor.moveToFirst()
+
+ // Get the file name from the cursor.
+ val fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
+
+ // Close the cursor.
+ contentResolverCursor.close()
+
+ // Display a snackbar with the saved logcat information.
+ Snackbar.make(logcatTextView, getString(R.string.logcat_saved, fileNameString), Snackbar.LENGTH_SHORT).show()
+ } catch (exception: Exception) {
+ // Display a snackbar with the error message.
+ Snackbar.make(logcatTextView, getString(R.string.error_saving_logcat, exception.toString()), Snackbar.LENGTH_INDEFINITE).show()
+ }
+ }
+ }
+
+ public override fun onCreate(savedInstanceState: Bundle?) {
+ // Run the default commands.
+ super.onCreate(savedInstanceState)
+
+ // Get a handle for the shared preferences.
+ val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
+
+ // Get the bottom app bar preference.
+ val bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false)
+
+ // Set the content view.
+ if (bottomAppBar) {
+ setContentView(R.layout.logcat_bottom_appbar)
+ } else {
+ setContentView(R.layout.logcat_top_appbar)
+ }
+
+ // Get handles for the views.
+ val toolbar = findViewById<Toolbar>(R.id.toolbar)
+ swipeRefreshLayout = findViewById(R.id.swiperefreshlayout)
+ logcatScrollView = findViewById(R.id.scrollview)
+ logcatTextView = findViewById(R.id.logcat_textview)
+
+ // Set the toolbar as the action bar.
+ setSupportActionBar(toolbar)
+
+ // Get a handle for the action bar.
+ val actionBar = supportActionBar!!
+
+ // Display the back arrow in the action bar.
+ actionBar.setDisplayHomeAsUpEnabled(true)
+
+ // Implement swipe to refresh.
+ swipeRefreshLayout.setOnRefreshListener {
+ // Get the current logcat.
+ getLogcat()
+ }
+
+ // Set the swipe refresh color scheme according to the theme.
+ swipeRefreshLayout.setColorSchemeResources(R.color.blue_text)
+
+ // Initialize a color background typed value.
+ val colorBackgroundTypedValue = TypedValue()
+
+ // Get the color background from the theme.
+ theme.resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true)
+
+ // Get the color background int from the typed value.
+ val colorBackgroundInt = colorBackgroundTypedValue.data
+
+ // Set the swipe refresh background color.
+ swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt)
+
+ // Check to see if the activity has been restarted.
+ if (savedInstanceState != null) {
+ // Get the saved scrollview position.
+ scrollViewYPositionInt = savedInstanceState.getInt(SCROLLVIEW_POSITION)
+ }
+
+ // Get the logcat.
+ getLogcat()
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ // Inflate the menu. This adds items to the action bar.
+ menuInflater.inflate(R.menu.logcat_options_menu, menu)
+
+ // Display the menu.
+ return true
+ }
+
+ override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
+ // Run the commands that correlate to the selected menu item.
+ return when (menuItem.itemId) {
+ R.id.copy -> { // Copy was selected.
+ // Get a handle for the clipboard manager.
+ val clipboardManager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
+
+ // Save the logcat in a clip data.
+ val logcatClipData = ClipData.newPlainText(getString(R.string.logcat), logcatTextView.text)
+
+ // Place the clip data on the clipboard.
+ clipboardManager.setPrimaryClip(logcatClipData)
+
+ // Display a snackbar.
+ Snackbar.make(logcatTextView, R.string.logcat_copied, Snackbar.LENGTH_SHORT).show()
+
+ // Consume the event.
+ true
+ }
+
+ R.id.save -> { // Save was selected.
+ // Open the file picker.
+ saveLogcatActivityResultLauncher.launch(getString(R.string.privacy_cell_logcat_txt))
+
+ // Consume the event.
+ true
+ }
+
+ R.id.clear -> { // Clear was selected.
+ try {
+ // Clear the logcat. `-c` clears the logcat. `-b all` clears all the buffers (instead of just crash, main, and system).
+ val process = Runtime.getRuntime().exec("logcat -b all -c")
+
+ // Wait for the process to finish.
+ process.waitFor()
+
+ // Reset the scroll view Y position int.
+ scrollViewYPositionInt = 0
+
+ // Reload the logcat.
+ getLogcat()
+ } catch (exception: Exception) {
+ // Do nothing.
+ }
+
+ // Consume the event.
+ true
+ }
+
+ else -> { // The home button was pushed.
+ // Do not consume the event. The system will process the home command.
+ super.onOptionsItemSelected(menuItem)
+ }
+ }
+ }
+
+ public override fun onSaveInstanceState(savedInstanceState: Bundle) {
+ // Run the default commands.
+ super.onSaveInstanceState(savedInstanceState)
+
+ // Get the scrollview Y position.
+ val scrollViewYPositionInt = logcatScrollView.scrollY
+
+ // Store the scrollview Y position in the bundle.
+ savedInstanceState.putInt(SCROLLVIEW_POSITION, scrollViewYPositionInt)
+ }
+
+ private fun getLogcat() {
+ try {
+ // 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.
+ val getLogcatProcess = Runtime.getRuntime().exec("logcat -b all -v long -d")
+
+ // Wrap the logcat in a buffered reader.
+ val logcatBufferedReader = BufferedReader(InputStreamReader(getLogcatProcess.inputStream))
+
+ // Display the logcat.
+ logcatTextView.text = logcatBufferedReader.readText()
+
+ // Close the buffered reader.
+ logcatBufferedReader.close()
+ } catch (exception: IOException) {
+ // Do nothing.
+ }
+
+ // Update the scroll position after the text is populated.
+ logcatTextView.post {
+ // Set the scroll position.
+ logcatScrollView.scrollY = scrollViewYPositionInt
+ }
+
+ // Stop the swipe to refresh animation if it is displayed.
+ swipeRefreshLayout.isRefreshing = false
+ }
+}
\ No newline at end of file