/*
- * 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>.
*
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
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.constraintlayout.widget.ConstraintLayout
import androidx.core.app.NavUtils
-import androidx.fragment.app.DialogFragment
import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.snackbar.Snackbar
import com.stoutner.privacybrowser.R
+import com.stoutner.privacybrowser.dialogs.AVAILABLE_CIPHERS
+import com.stoutner.privacybrowser.dialogs.SSL_CERTIFICATE
import com.stoutner.privacybrowser.dialogs.AboutViewHeadersDialog
+import com.stoutner.privacybrowser.dialogs.ViewHeadersDetailDialog
import com.stoutner.privacybrowser.dialogs.UntrustedSslCertificateDialog
import com.stoutner.privacybrowser.dialogs.UntrustedSslCertificateDialog.UntrustedSslCertificateListener
import com.stoutner.privacybrowser.helpers.ProxyHelper
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"
class ViewHeadersActivity: AppCompatActivity(), UntrustedSslCertificateListener {
// Declare the class variables.
+ private lateinit var appliedCipherString: String
+ private lateinit var availableCiphersString: String
private lateinit var headersViewModel: HeadersViewModel
private lateinit var initialGrayColorSpan: ForegroundColorSpan
private lateinit var finalGrayColorSpan: ForegroundColorSpan
private lateinit var redColorSpan: ForegroundColorSpan
+ private lateinit var sslCertificateString: String
// Declare the class views.
private lateinit var urlEditText: EditText
+ private lateinit var sslInformationTitleTextView: TextView
+ private lateinit var sslInformationTextView: TextView
+ 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.
urlEditText = findViewById(R.id.url_edittext)
val progressBar = findViewById<ProgressBar>(R.id.progress_bar)
val swipeRefreshLayout = findViewById<SwipeRefreshLayout>(R.id.swiperefreshlayout)
+ sslInformationTitleTextView = findViewById(R.id.ssl_information_title_textview)
+ sslInformationTextView = findViewById(R.id.ssl_information_textview)
+ 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)
// 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)
updateLayout(currentUrl)
// Instantiate the view headers factory.
- val viewHeadersFactory: ViewModelProvider.Factory = ViewHeadersFactory(currentUrl, userAgent, localesStringBuilder.toString(), proxy, contentResolver, MainWebViewActivity.executorService)
+ val viewHeadersFactory: ViewModelProvider.Factory = ViewHeadersFactory(application, currentUrl, userAgent, localesStringBuilder.toString(), proxy, contentResolver, MainWebViewActivity.executorService)
// Instantiate the headers view model.
headersViewModel = ViewModelProvider(this, viewHeadersFactory)[HeadersViewModel::class.java]
// Create a headers observer.
headersViewModel.observeHeaders().observe(this) { headersStringArray: Array<SpannableStringBuilder> ->
// Populate the text views. This can take a long time, and freezes the user interface, if the response body is particularly large.
- requestHeadersTextView.text = headersStringArray[0]
- responseMessageTextView.text = headersStringArray[1]
- responseHeadersTextView.text = headersStringArray[2]
- responseBodyTextView.text = headersStringArray[3]
+ sslInformationTextView.text = headersStringArray[0]
+ requestHeadersTextView.text = headersStringArray[4]
+ responseMessageTextView.text = headersStringArray[5]
+ responseHeadersTextView.text = headersStringArray[6]
+ responseBodyTextView.text = headersStringArray[7]
+
+ // Populate the dialog strings.
+ appliedCipherString = headersStringArray[1].toString()
+ availableCiphersString = headersStringArray[2].toString()
+ sslCertificateString = headersStringArray[3].toString()
// Hide the progress bar.
progressBar.isIndeterminate = false
progressBar.visibility = View.GONE
- //Stop the swipe to refresh indicator if it is running
+ // Stop the swipe to refresh indicator if it is running
swipeRefreshLayout.isRefreshing = false
}
}
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.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.
+ // 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)
}
+ // The view parameter cannot be removed because it is called from the layout onClick.
+ fun showCertificate(@Suppress("UNUSED_PARAMETER")view: View) {
+ // Instantiate an SSL certificate dialog.
+ val sslCertificateDialogFragment= ViewHeadersDetailDialog.displayDialog(SSL_CERTIFICATE, sslCertificateString)
+
+ // Show the dialog.
+ sslCertificateDialogFragment.show(supportFragmentManager, getString(R.string.ssl_certificate))
+ }
+
+ // The view parameter cannot be removed because it is called from the layout onClick.
+ fun showCiphers(@Suppress("UNUSED_PARAMETER")view: View) {
+ // Instantiate an SSL certificate dialog.
+ val ciphersDialogFragment= ViewHeadersDetailDialog.displayDialog(AVAILABLE_CIPHERS, availableCiphersString, appliedCipherString)
+
+ // Show the dialog.
+ ciphersDialogFragment.show(supportFragmentManager, getString(R.string.ssl_certificate))
+ }
+
private fun updateLayout(urlString: String) {
if (urlString.startsWith("content://")) { // This is a content URL.
- // Hide the unused text views.
+ // Hide the unused views.
+ sslInformationTitleTextView.visibility = View.GONE
+ sslInformationTextView.visibility = View.GONE
+ sslButtonsConstraintLayout.visibility = View.GONE
requestHeadersTitleTextView.visibility = View.GONE
requestHeadersTextView.visibility = View.GONE
responseMessageTitleTextView.visibility = View.GONE
responseHeadersTitleTextView.setText(R.string.content_metadata)
responseBodyTitleTextView.setText(R.string.content_data)
} else { // This is not a content URL.
- // Show the views.
+ // Set the status if the the SSL information views.
+ if (urlString.startsWith("http://")) { // This is an HTTP URL.
+ // Hide the SSL information views.
+ sslInformationTitleTextView.visibility = View.GONE
+ sslInformationTextView.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
+ sslButtonsConstraintLayout.visibility = View.VISIBLE
+ }
+
+ // Show the other views.
requestHeadersTitleTextView.visibility = View.VISIBLE
requestHeadersTextView.visibility = View.VISIBLE
responseMessageTitleTextView.visibility = View.VISIBLE