X-Git-Url: https://gitweb.stoutner.com/?a=blobdiff_plain;f=app%2Fsrc%2Fmain%2Fjava%2Fcom%2Fstoutner%2Fprivacybrowser%2Factivities%2FViewHeadersActivity.kt;h=8e72b596b9cea383b7d6a0b509b71e03030e4060;hb=HEAD;hp=c8878c5fa772a4e53b824155ad4eb6ccbdfe8a3a;hpb=9a15b265860ef93cf1648c0bc30036895681f46a;p=PrivacyBrowserAndroid.git 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 c8878c5f..8e72b596 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/ViewHeadersActivity.kt +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/ViewHeadersActivity.kt @@ -1,7 +1,7 @@ /* - * Copyright 2017-2023 Soren Stoutner . + * Copyright 2017-2024 Soren Stoutner . * - * This file is part of Privacy Browser Android . + * This file is part of 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 @@ -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 @@ -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(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(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(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) - - // 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 = 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.