]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blobdiff - app/src/main/java/com/stoutner/privacybrowser/activities/ViewHeadersActivity.kt
First wrong button text in View Headers in night theme. https://redmine.stoutner...
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / activities / ViewHeadersActivity.kt
index c8878c5fa772a4e53b824155ad4eb6ccbdfe8a3a..8e72b596b9cea383b7d6a0b509b71e03030e4060 100644 (file)
@@ -1,7 +1,7 @@
 /*
- * Copyright 2017-2023 Soren Stoutner <soren@stoutner.com>.
+ * Copyright 2017-2024 Soren Stoutner <soren@stoutner.com>.
  *
- * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
+ * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android/>.
  *
  * Privacy Browser Android is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
 
 package com.stoutner.privacybrowser.activities
 
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
 import android.os.Bundle
+import android.provider.OpenableColumns
 import android.text.SpannableStringBuilder
 import android.text.style.ForegroundColorSpan
 import android.util.TypedValue
@@ -30,16 +37,15 @@ import android.view.View
 import android.view.View.OnFocusChangeListener
 import android.view.WindowManager
 import android.view.inputmethod.InputMethodManager
-import android.widget.Button
 import android.widget.EditText
 import android.widget.ProgressBar
 import android.widget.TextView
 
+import androidx.activity.result.contract.ActivityResultContracts
 import androidx.appcompat.app.ActionBar
 import androidx.appcompat.app.AppCompatActivity
 import androidx.appcompat.widget.Toolbar
-import androidx.core.app.NavUtils
-import androidx.fragment.app.DialogFragment
+import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.lifecycle.ViewModelProvider
 import androidx.preference.PreferenceManager
 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
@@ -58,6 +64,14 @@ import com.stoutner.privacybrowser.helpers.UrlHelper
 import com.stoutner.privacybrowser.viewmodelfactories.ViewHeadersFactory
 import com.stoutner.privacybrowser.viewmodels.HeadersViewModel
 
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+import java.lang.Exception
+import java.nio.charset.StandardCharsets
+
 // Define the public constants.
 const val USER_AGENT = "user_agent"
 
@@ -75,14 +89,58 @@ class ViewHeadersActivity: AppCompatActivity(), UntrustedSslCertificateListener
     private lateinit var urlEditText: EditText
     private lateinit var sslInformationTitleTextView: TextView
     private lateinit var sslInformationTextView: TextView
-    private lateinit var ciphersButton: Button
-    private lateinit var certificateButton: Button
+    private lateinit var sslButtonsConstraintLayout: ConstraintLayout
     private lateinit var requestHeadersTitleTextView: TextView
     private lateinit var requestHeadersTextView: TextView
     private lateinit var responseMessageTitleTextView: TextView
     private lateinit var responseMessageTextView: TextView
     private lateinit var responseHeadersTitleTextView: TextView
+    private lateinit var responseHeadersTextView: TextView
     private lateinit var responseBodyTitleTextView: TextView
+    private lateinit var responseBodyTextView: TextView
+
+    // Define the save text activity result launcher.  It must be defined before `onCreate()` is run or the app will crash.
+    private val saveTextActivityResultLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { fileUri ->
+        // Only save the file if the URI is not null, which happens if the user exited the file picker by pressing back.
+        if (fileUri != null) {
+            // Get a cursor from the content resolver.
+            val contentResolverCursor = contentResolver.query(fileUri, null, null, null)!!
+
+            // Move to the first row.
+            contentResolverCursor.moveToFirst()
+
+            // Get the file name from the cursor.
+            val fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
+
+            // Close the cursor.
+            contentResolverCursor.close()
+
+            try {
+                // Get the about version string.
+                val headersString = getHeadersString()
+
+                // Open an output stream.
+                val outputStream = contentResolver.openOutputStream(fileUri)!!
+
+                // Save the headers using a coroutine with Dispatchers.IO.
+                CoroutineScope(Dispatchers.Main).launch {
+                    withContext(Dispatchers.IO) {
+                        // Write the headers string to the output stream.
+                        outputStream.write(headersString.toByteArray(StandardCharsets.UTF_8))
+
+                        // Close the output stream.
+                        outputStream.close()
+                    }
+                }
+
+                // Display a snackbar with the saved logcat information.
+                Snackbar.make(urlEditText, getString(R.string.saved, fileNameString), Snackbar.LENGTH_SHORT).show()
+            } catch (exception: Exception) {
+                // Display a snackbar with the error message.
+                Snackbar.make(urlEditText, getString(R.string.error_saving_file, fileNameString, exception.toString()), Snackbar.LENGTH_INDEFINITE).show()
+            }
+        }
+    }
 
     override fun onCreate(savedInstanceState: Bundle?) {
         // Get a handle for the shared preferences.
@@ -93,9 +151,8 @@ class ViewHeadersActivity: AppCompatActivity(), UntrustedSslCertificateListener
         val bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false)
 
         // Disable screenshots if not allowed.
-        if (!allowScreenshots) {
+        if (!allowScreenshots)
             window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
-        }
 
         // Run the default commands.
         super.onCreate(savedInstanceState)
@@ -108,11 +165,10 @@ class ViewHeadersActivity: AppCompatActivity(), UntrustedSslCertificateListener
         val userAgent = intent.getStringExtra(USER_AGENT)!!
 
         // Set the content view.
-        if (bottomAppBar) {
+        if (bottomAppBar)
             setContentView(R.layout.view_headers_bottom_appbar)
-        } else {
+        else
             setContentView(R.layout.view_headers_top_appbar)
-        }
 
         // Get a handle for the toolbar.
         val toolbar = findViewById<Toolbar>(R.id.toolbar)
@@ -127,7 +183,7 @@ class ViewHeadersActivity: AppCompatActivity(), UntrustedSslCertificateListener
         actionBar.setCustomView(R.layout.view_headers_appbar_custom_view)
 
         // Instruct the action bar to display a custom layout.
-        actionBar.displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM
+        actionBar.displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM or ActionBar.DISPLAY_HOME_AS_UP
 
         // Get handles for the views.
         urlEditText = findViewById(R.id.url_edittext)
@@ -135,38 +191,33 @@ class ViewHeadersActivity: AppCompatActivity(), UntrustedSslCertificateListener
         val swipeRefreshLayout = findViewById<SwipeRefreshLayout>(R.id.swiperefreshlayout)
         sslInformationTitleTextView = findViewById(R.id.ssl_information_title_textview)
         sslInformationTextView = findViewById(R.id.ssl_information_textview)
-        ciphersButton = findViewById(R.id.ciphers_button)
-        certificateButton = findViewById(R.id.certificate_button)
+        sslButtonsConstraintLayout = findViewById(R.id.ssl_buttons_constraintlayout)
         requestHeadersTitleTextView = findViewById(R.id.request_headers_title_textview)
         requestHeadersTextView = findViewById(R.id.request_headers_textview)
         responseMessageTitleTextView = findViewById(R.id.response_message_title_textview)
         responseMessageTextView = findViewById(R.id.response_message_textview)
         responseHeadersTitleTextView = findViewById(R.id.response_headers_title_textview)
-        val responseHeadersTextView = findViewById<TextView>(R.id.response_headers_textview)
+        responseHeadersTextView = findViewById(R.id.response_headers_textview)
         responseBodyTitleTextView = findViewById(R.id.response_body_title_textview)
-        val responseBodyTextView = findViewById<TextView>(R.id.response_body_textview)
-
-        // Populate the URL text box.
-        urlEditText.setText(currentUrl)
+        responseBodyTextView = findViewById(R.id.response_body_textview)
 
         // Initialize the gray foreground color spans for highlighting the URLs.
         initialGrayColorSpan = ForegroundColorSpan(getColor(R.color.gray_500))
         finalGrayColorSpan = ForegroundColorSpan(getColor(R.color.gray_500))
         redColorSpan = ForegroundColorSpan(getColor(R.color.red_text))
 
-        // Apply text highlighting to the URL.
-        UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
-
         // Get a handle for the input method manager, which is used to hide the keyboard.
         val inputMethodManager = (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
 
         // Remove the formatting from the URL when the user is editing the text.
         urlEditText.onFocusChangeListener = OnFocusChangeListener { _: View?, hasFocus: Boolean ->
             if (hasFocus) {  // The user is editing the URL text box.
-                // Remove the highlighting.
-                urlEditText.text.removeSpan(redColorSpan)
-                urlEditText.text.removeSpan(initialGrayColorSpan)
-                urlEditText.text.removeSpan(finalGrayColorSpan)
+                // Get the foreground color spans.
+                val foregroundColorSpans: Array<ForegroundColorSpan> = urlEditText.text.getSpans(0, urlEditText.text.length, ForegroundColorSpan::class.java)
+
+                // Remove each foreground color span that highlights the text.
+                for (foregroundColorSpan in foregroundColorSpans)
+                    urlEditText.text.removeSpan(foregroundColorSpan)
             } else {  // The user has stopped editing the URL text box.
                 // Hide the soft keyboard.
                 inputMethodManager.hideSoftInputFromWindow(urlEditText.windowToken, 0)
@@ -174,11 +225,20 @@ class ViewHeadersActivity: AppCompatActivity(), UntrustedSslCertificateListener
                 // Move to the beginning of the string.
                 urlEditText.setSelection(0)
 
+                // Store the URL text in the intent, so update layout uses the new text if the app is restarted.
+                intent.putExtra(CURRENT_URL, urlEditText.text.toString())
+
                 // Reapply the highlighting.
                 UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
             }
         }
 
+        // Populate the URL text box.
+        urlEditText.setText(currentUrl)
+
+        // Apply the initial text highlighting to the URL.
+        UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
+
         // Set the refresh color scheme according to the theme.
         swipeRefreshLayout.setColorSchemeResources(R.color.blue_text)
 
@@ -364,20 +424,130 @@ class ViewHeadersActivity: AppCompatActivity(), UntrustedSslCertificateListener
     }
 
     override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
-        // Instantiate the about dialog fragment.
-        val aboutDialogFragment: DialogFragment = AboutViewHeadersDialog()
+        // Run the commands that correlate to the selected menu item.
+        when (menuItem.itemId) {
+            R.id.copy_headers -> {  // Copy the headers.
+                // Get the headers string.
+                val headersString = getHeadersString()
 
-        // Show the about alert dialog.
-        aboutDialogFragment.show(supportFragmentManager, getString(R.string.about))
+                // Get a handle for the clipboard manager.
+                val clipboardManager = (getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager)
 
-        // Consume the event.
-        return true
+                // Place the headers string in a clip data.
+                val headersClipData = ClipData.newPlainText(getString(R.string.view_headers), headersString)
+
+                // Place the clip data on the clipboard.
+                clipboardManager.setPrimaryClip(headersClipData)
+
+                // Display a snackbar if the API <= 32 (Android 12L).  Beginning in Android 13 the OS displays a notification that covers up the snackbar.
+                if (Build.VERSION.SDK_INT <= 32)
+                    Snackbar.make(urlEditText, R.string.headers_copied, Snackbar.LENGTH_SHORT).show()
+
+                // Consume the event.
+                return true
+            }
+
+            R.id.share_headers -> {  // Share the headers.
+                // Get the headers string.
+                val headersString = getHeadersString()
+
+                // Create a share intent.
+                val shareIntent = Intent(Intent.ACTION_SEND)
+
+                // Add the headers string to the intent.
+                shareIntent.putExtra(Intent.EXTRA_TEXT, headersString)
+
+                // Set the MIME type.
+                shareIntent.type = "text/plain"
+
+                // Set the intent to open in a new task.
+                shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+
+                // Make it so.
+                startActivity(Intent.createChooser(shareIntent, getString(R.string.share)))
+
+                // Consume the event.
+                return true
+            }
+
+            R.id.save_headers -> {  // Save the headers as a text file.
+                // Get the current URL.
+                val currentUrlString = urlEditText.text.toString()
+
+                // Get a URI for the current URL.
+                val currentUri = Uri.parse(currentUrlString)
+
+                // Get the current domain name.
+                val currentDomainName = currentUri.host
+
+                // Open the file picker.
+                saveTextActivityResultLauncher.launch(getString(R.string.headers_txt, currentDomainName))
+
+                // Consume the event.
+                return true
+            }
+
+            R.id.about_view_headers -> {  // Display the about dialog.
+                // Instantiate the about dialog fragment.
+                val aboutDialogFragment = AboutViewHeadersDialog()
+
+                // Show the about alert dialog.
+                aboutDialogFragment.show(supportFragmentManager, getString(R.string.about))
+
+                // Consume the event.
+                return true
+            }
+
+            else -> {  // The home button was selected.
+                // Do not consume the event.  The system will process the home command.
+                return super.onOptionsItemSelected(menuItem)
+            }
+        }
     }
 
-    // This method must be named `goBack()` and must have a View argument to match the default back arrow in the app bar.
-    fun goBack(@Suppress("UNUSED_PARAMETER") view: View) {
-        // Go home.
-        NavUtils.navigateUpFromSameTask(this)
+    private fun getHeadersString(): String {
+        // Initialize a headers string builder.
+        val headersStringBuilder = StringBuilder()
+
+        // Populate the SSL information if it is visible (an HTTPS URL is loaded).
+        if (sslInformationTitleTextView.visibility == View.VISIBLE) {
+            headersStringBuilder.append(sslInformationTitleTextView.text)
+            headersStringBuilder.append("\n")
+            headersStringBuilder.append(sslInformationTextView.text)
+            headersStringBuilder.append("\n\n")
+            headersStringBuilder.append(getString(R.string.available_ciphers))
+            headersStringBuilder.append("\n")
+            headersStringBuilder.append(availableCiphersString)
+            headersStringBuilder.append("\n\n")
+            headersStringBuilder.append(getString(R.string.ssl_certificate))
+            headersStringBuilder.append("\n")
+            headersStringBuilder.append(sslCertificateString)
+            headersStringBuilder.append("\n")  // Only a single new line is needed after the certificate as it already ends in one.
+        }
+
+        // Populate the request information if it is visible (an HTTP URL is loaded).
+        if (requestHeadersTitleTextView.visibility == View.VISIBLE) {
+            headersStringBuilder.append(requestHeadersTitleTextView.text)
+            headersStringBuilder.append("\n")
+            headersStringBuilder.append(requestHeadersTextView.text)
+            headersStringBuilder.append("\n\n")
+            headersStringBuilder.append(responseMessageTitleTextView.text)
+            headersStringBuilder.append("\n")
+            headersStringBuilder.append(responseMessageTextView.text)
+            headersStringBuilder.append("\n\n")
+        }
+
+        // Populate the response information, which is visible for both HTTP and content URLs.
+        headersStringBuilder.append(responseHeadersTitleTextView.text)
+        headersStringBuilder.append("\n")
+        headersStringBuilder.append(responseHeadersTextView.text)
+        headersStringBuilder.append("\n\n")
+        headersStringBuilder.append(responseBodyTitleTextView.text)
+        headersStringBuilder.append("\n")
+        headersStringBuilder.append(responseBodyTextView.text)
+
+        // Return the string.
+        return headersStringBuilder.toString()
     }
 
     override fun loadAnyway() {
@@ -408,8 +578,7 @@ class ViewHeadersActivity: AppCompatActivity(), UntrustedSslCertificateListener
             // Hide the unused views.
             sslInformationTitleTextView.visibility = View.GONE
             sslInformationTextView.visibility = View.GONE
-            ciphersButton.visibility = View.GONE
-            certificateButton.visibility = View.GONE
+            sslButtonsConstraintLayout.visibility = View.GONE
             requestHeadersTitleTextView.visibility = View.GONE
             requestHeadersTextView.visibility = View.GONE
             responseMessageTitleTextView.visibility = View.GONE
@@ -424,14 +593,12 @@ class ViewHeadersActivity: AppCompatActivity(), UntrustedSslCertificateListener
                 // Hide the SSL information views.
                 sslInformationTitleTextView.visibility = View.GONE
                 sslInformationTextView.visibility = View.GONE
-                ciphersButton.visibility = View.GONE
-                certificateButton.visibility = View.GONE
+                sslButtonsConstraintLayout.visibility = View.GONE
             } else {  // This is not an HTTP URL.
                 // Show the SSL information views.
                 sslInformationTitleTextView.visibility = View.VISIBLE
                 sslInformationTextView.visibility = View.VISIBLE
-                ciphersButton.visibility = View.VISIBLE
-                certificateButton.visibility = View.VISIBLE
+                sslButtonsConstraintLayout.visibility = View.VISIBLE
             }
 
             // Show the other views.