From cbeede13395a246b8a32adebbee3872031259f82 Mon Sep 17 00:00:00 2001 From: Soren Stoutner Date: Thu, 26 Oct 2023 21:12:14 -0700 Subject: [PATCH] Add options to copy, share, and save View Headers. https://redmine.stoutner.com/issues/673 --- .../activities/LogcatActivity.kt | 5 +- .../activities/ViewHeadersActivity.kt | 202 +++++++++++++++++- .../fragments/AboutVersionFragment.kt | 20 +- .../res/menu/view_headers_options_menu.xml | 26 ++- app/src/main/res/values-de/strings.xml | 3 +- app/src/main/res/values-es/strings.xml | 3 +- app/src/main/res/values-fr/strings.xml | 3 +- app/src/main/res/values-it/strings.xml | 3 +- app/src/main/res/values-pt-rBR/strings.xml | 3 +- app/src/main/res/values-ru/strings.xml | 3 +- app/src/main/res/values-tr/strings.xml | 3 +- app/src/main/res/values-zh-rCN/strings.xml | 3 +- app/src/main/res/values/strings.xml | 5 +- 13 files changed, 249 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/com/stoutner/privacybrowser/activities/LogcatActivity.kt b/app/src/main/java/com/stoutner/privacybrowser/activities/LogcatActivity.kt index 52a4075f..36751104 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/LogcatActivity.kt +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/LogcatActivity.kt @@ -204,8 +204,9 @@ class LogcatActivity : AppCompatActivity() { // Place the clip data on the clipboard. clipboardManager.setPrimaryClip(logcatClipData) - // Display a snackbar. - Snackbar.make(logcatTextView, R.string.logcat_copied, Snackbar.LENGTH_SHORT).show() + // 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(logcatTextView, R.string.logcat_copied, Snackbar.LENGTH_SHORT).show() // Consume the event. true diff --git a/app/src/main/java/com/stoutner/privacybrowser/activities/ViewHeadersActivity.kt b/app/src/main/java/com/stoutner/privacybrowser/activities/ViewHeadersActivity.kt index 145ced5d..ce86183b 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/ViewHeadersActivity.kt +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/ViewHeadersActivity.kt @@ -19,7 +19,14 @@ 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 @@ -35,11 +42,11 @@ 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.lifecycle.ViewModelProvider import androidx.preference.PreferenceManager import androidx.swiperefreshlayout.widget.SwipeRefreshLayout @@ -58,6 +65,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" @@ -82,7 +97,58 @@ class ViewHeadersActivity: AppCompatActivity(), UntrustedSslCertificateListener 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) { + // Initialize the file name string from the file URI last path segment. + var fileNameString = fileUri.lastPathSegment + + // Query the exact file name if the API >= 26. + if (Build.VERSION.SDK_INT >= 26) { + // 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. + 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. @@ -142,9 +208,9 @@ class ViewHeadersActivity: AppCompatActivity(), UntrustedSslCertificateListener 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(R.id.response_headers_textview) + responseHeadersTextView = findViewById(R.id.response_headers_textview) responseBodyTitleTextView = findViewById(R.id.response_body_title_textview) - val responseBodyTextView = findViewById(R.id.response_body_textview) + responseBodyTextView = findViewById(R.id.response_body_textview) // Initialize the gray foreground color spans for highlighting the URLs. initialGrayColorSpan = ForegroundColorSpan(getColor(R.color.gray_500)) @@ -369,22 +435,138 @@ class ViewHeadersActivity: AppCompatActivity(), UntrustedSslCertificateListener } override fun onOptionsItemSelected(menuItem: MenuItem): Boolean { - // Instantiate the about dialog fragment. - val aboutDialogFragment: DialogFragment = AboutViewHeadersDialog() + // Run the appropriate commands. + 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.version_info_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. + // Run the parents class on return. + 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. + // This method must be named `goBack()` and must have a View argument to match the default back arrow in the app bar or a crash occurs. 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() { // Load the URL anyway. headersViewModel.updateHeaders(urlEditText.text.toString(), true) diff --git a/app/src/main/java/com/stoutner/privacybrowser/fragments/AboutVersionFragment.kt b/app/src/main/java/com/stoutner/privacybrowser/fragments/AboutVersionFragment.kt index c2c90f36..03fa6f05 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/fragments/AboutVersionFragment.kt +++ b/app/src/main/java/com/stoutner/privacybrowser/fragments/AboutVersionFragment.kt @@ -628,14 +628,15 @@ class AboutVersionFragment : Fragment() { // Get a handle for the clipboard manager. val clipboardManager = (requireActivity().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager) - // Save the about version string in a clip data. + // Place the about version string in a clip data. val aboutVersionClipData = ClipData.newPlainText(getString(R.string.about), aboutVersionString) // Place the clip data on the clipboard. clipboardManager.setPrimaryClip(aboutVersionClipData) - // Display a snackbar. - Snackbar.make(aboutVersionLayout, R.string.version_info_copied, Snackbar.LENGTH_SHORT).show() + // 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(aboutVersionLayout, R.string.version_info_copied, Snackbar.LENGTH_SHORT).show() // Consume the event. return true @@ -645,20 +646,20 @@ class AboutVersionFragment : Fragment() { // Get the about version string. val aboutString = getAboutVersionString() - // Create an email intent. - val emailIntent = Intent(Intent.ACTION_SEND) + // Create a share intent. + val shareIntent = Intent(Intent.ACTION_SEND) // Add the about version string to the intent. - emailIntent.putExtra(Intent.EXTRA_TEXT, aboutString) + shareIntent.putExtra(Intent.EXTRA_TEXT, aboutString) // Set the MIME type. - emailIntent.type = "text/plain" + shareIntent.type = "text/plain" // Set the intent to open in a new task. - emailIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) // Make it so. - startActivity(Intent.createChooser(emailIntent, getString(R.string.share))) + startActivity(Intent.createChooser(shareIntent, getString(R.string.share))) // Consume the event. return true @@ -679,6 +680,7 @@ class AboutVersionFragment : Fragment() { // Consume the event. return true } + else -> { // The home button was selected. // Run the parents class on return. return super.onOptionsItemSelected(menuItem) diff --git a/app/src/main/res/menu/view_headers_options_menu.xml b/app/src/main/res/menu/view_headers_options_menu.xml index d7439959..5557ccf6 100644 --- a/app/src/main/res/menu/view_headers_options_menu.xml +++ b/app/src/main/res/menu/view_headers_options_menu.xml @@ -22,11 +22,31 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> - + + + + + + + app:showAsAction="never" /> diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 4cc9670c..5abeb8eb 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -219,7 +219,8 @@ Fehler beim Speichern der Datei %1$s:\u0020 %2$s Unbekannter Fehler - + : \u0020 Anfragekopfzeilen Status-Code diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index b69c1f5d..87feb911 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -215,7 +215,8 @@ Error al guardar %1$s:\u0020 %2$s Error desconocido - + : \u0020 Información sobre SSL Cifrado aplicado diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 79584718..e6cf67fa 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -215,7 +215,8 @@ Erreur lors de l\'enregistrement de %1$s : %2$s Erreur inconnue - + \u0020:\u0020 En-tête de la requête Message de la réponse diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 1b5de9db..3abad243 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -213,7 +213,8 @@ Error di salvataggio di %1$s:\u0020 %2$s Errore sconosciuto - + : \u0020 Informazioni SSL Cifratura Applicata diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 6c802033..01deebdd 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -208,7 +208,8 @@ Erro ao salvar %1$s:\u0020 %2$s Erro desconhecido - + : \u0020 Solicitar cabeçalhos Mensagem de Resposta diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 0a8ea2b4..293953e7 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -212,7 +212,8 @@ Ошибка сохранения %1$s:\u0020 %2$s Неизвестная ошибка - + : \u0020 Заголовки запроса Ответное сообщение diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 63cf8548..1b180023 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -179,7 +179,8 @@ Dosya adı Bilinmeyen boyut - + : \u0020 İstek Başlıkları Yanıt Mesajı diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 37f2e2bf..82c58ee3 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -213,7 +213,8 @@ 保存失败 %1$s:\u0020 %2$s 未知错误 - + : \u0020 请求头 响应 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 96aa8d54..a2310b82 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -224,7 +224,8 @@ Error saving %1$s:\u0020 %2$s Unknown error - + : \u0020 SSL Information Applied Cipher @@ -246,6 +247,8 @@ Because Android’s WebView does not expose the source information, a separate request was made using system tools to gather the information displayed in this activity. There may be some differences between this data and that used by the WebView in the main activity. This limitation will be removed in the 4.x series with the release of Privacy WebView. + Headers copied. + %1$s headers.txt Create Shortcut -- 2.43.0