From 9a15b265860ef93cf1648c0bc30036895681f46a Mon Sep 17 00:00:00 2001 From: Soren Stoutner Date: Tue, 24 Oct 2023 12:21:22 -0700 Subject: [PATCH] Display SSL information in View Headers. https://redmine.stoutner.com/issues/706 --- .../activities/ViewHeadersActivity.kt | 74 ++++++++-- .../GetHeadersBackgroundTask.kt | 131 ++++++++++++++---- .../dialogs/ViewHeadersDetailDialog.kt | 121 ++++++++++++++++ .../viewmodelfactories/ViewHeadersFactory.kt | 9 +- .../viewmodels/HeadersViewModel.kt | 12 +- .../res/layout/view_headers_bottom_appbar.xml | 49 +++++++ .../res/layout/view_headers_top_appbar.xml | 50 +++++++ 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-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 | 11 +- 15 files changed, 425 insertions(+), 53 deletions(-) create mode 100644 app/src/main/java/com/stoutner/privacybrowser/dialogs/ViewHeadersDetailDialog.kt 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 2f0aeb0b..c8878c5f 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/ViewHeadersActivity.kt +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/ViewHeadersActivity.kt @@ -30,6 +30,7 @@ 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 @@ -46,7 +47,10 @@ 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 @@ -59,13 +63,20 @@ 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 ciphersButton: Button + private lateinit var certificateButton: Button private lateinit var requestHeadersTitleTextView: TextView private lateinit var requestHeadersTextView: TextView private lateinit var responseMessageTitleTextView: TextView @@ -122,6 +133,10 @@ class ViewHeadersActivity: AppCompatActivity(), UntrustedSslCertificateListener urlEditText = findViewById(R.id.url_edittext) val progressBar = findViewById(R.id.progress_bar) 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) requestHeadersTitleTextView = findViewById(R.id.request_headers_title_textview) requestHeadersTextView = findViewById(R.id.request_headers_textview) responseMessageTitleTextView = findViewById(R.id.response_message_title_textview) @@ -244,7 +259,7 @@ class ViewHeadersActivity: AppCompatActivity(), UntrustedSslCertificateListener 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] @@ -252,16 +267,22 @@ class ViewHeadersActivity: AppCompatActivity(), UntrustedSslCertificateListener // Create a headers observer. headersViewModel.observeHeaders().observe(this) { headersStringArray: Array -> // 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 } @@ -364,9 +385,31 @@ class ViewHeadersActivity: AppCompatActivity(), UntrustedSslCertificateListener 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 + ciphersButton.visibility = View.GONE + certificateButton.visibility = View.GONE requestHeadersTitleTextView.visibility = View.GONE requestHeadersTextView.visibility = View.GONE responseMessageTitleTextView.visibility = View.GONE @@ -376,7 +419,22 @@ class ViewHeadersActivity: AppCompatActivity(), UntrustedSslCertificateListener 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 + ciphersButton.visibility = View.GONE + certificateButton.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 + } + + // Show the other views. requestHeadersTitleTextView.visibility = View.VISIBLE requestHeadersTextView.visibility = View.VISIBLE responseMessageTitleTextView.visibility = View.VISIBLE diff --git a/app/src/main/java/com/stoutner/privacybrowser/backgroundtasks/GetHeadersBackgroundTask.kt b/app/src/main/java/com/stoutner/privacybrowser/backgroundtasks/GetHeadersBackgroundTask.kt index 030ce4c6..634e4f65 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/backgroundtasks/GetHeadersBackgroundTask.kt +++ b/app/src/main/java/com/stoutner/privacybrowser/backgroundtasks/GetHeadersBackgroundTask.kt @@ -20,6 +20,7 @@ package com.stoutner.privacybrowser.backgroundtasks import android.annotation.SuppressLint +import android.app.Application import android.content.ContentResolver import android.graphics.Typeface import android.net.Uri @@ -28,6 +29,7 @@ import android.text.Spanned import android.text.style.StyleSpan import android.webkit.CookieManager +import com.stoutner.privacybrowser.R import com.stoutner.privacybrowser.viewmodels.HeadersViewModel import java.io.BufferedInputStream @@ -53,15 +55,23 @@ import javax.net.ssl.X509TrustManager class GetHeadersBackgroundTask { - fun acquire(urlString: String, userAgent: String, localeString: String, proxy: Proxy, contentResolver: ContentResolver, headersViewModel: HeadersViewModel, ignoreSslErrors: Boolean): + fun acquire(application: Application, urlString: String, userAgent: String, localeString: String, proxy: Proxy, contentResolver: ContentResolver, headersViewModel: HeadersViewModel, ignoreSslErrors: Boolean): Array { // Initialize the spannable string builders. + val sslInformationBuilder = SpannableStringBuilder() + val appliedCipherBuilder = SpannableStringBuilder() + val availableCiphersBuilder = SpannableStringBuilder() + val sslCertificateBuilder = SpannableStringBuilder() val requestHeadersBuilder = SpannableStringBuilder() val responseMessageBuilder = SpannableStringBuilder() val responseHeadersBuilder = SpannableStringBuilder() val responseBodyBuilder = SpannableStringBuilder() + // Get the colon string. + val colonString = application.getString(R.string.colon) + val newLineString = System.getProperty("line.separator") + if (urlString.startsWith("content://")) { // This is a content URL. // Attempt to read the content data. Return an error if this fails. try { @@ -78,11 +88,11 @@ class GetHeadersBackgroundTask { for (i in 0 until contentCursor.columnCount) { // Add a new line if this is not the first entry. if (i > 0) - responseHeadersBuilder.append(System.getProperty("line.separator")) + responseHeadersBuilder.append(newLineString) // Add each header to the string builder. responseHeadersBuilder.append(contentCursor.getColumnName(i), StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - responseHeadersBuilder.append(": ") + responseHeadersBuilder.append(colonString) responseHeadersBuilder.append(contentCursor.getString(i)) } @@ -121,7 +131,7 @@ class GetHeadersBackgroundTask { // Add the `Host` header to the string builder and format the text. requestHeadersBuilder.append("Host", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - requestHeadersBuilder.append(": ") + requestHeadersBuilder.append(colonString) requestHeadersBuilder.append(url.host) @@ -129,27 +139,29 @@ class GetHeadersBackgroundTask { httpUrlConnection.setRequestProperty("Connection", "keep-alive") // Add the `Connection` header to the string builder and format the text. - requestHeadersBuilder.append(System.getProperty("line.separator")) + requestHeadersBuilder.append(newLineString) requestHeadersBuilder.append("Connection", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - requestHeadersBuilder.append(": keep-alive") + requestHeadersBuilder.append(colonString) + requestHeadersBuilder.append("keep-alive") // Set the `Upgrade-Insecure-Requests` header property. httpUrlConnection.setRequestProperty("Upgrade-Insecure-Requests", "1") // Add the `Upgrade-Insecure-Requests` header to the string builder and format the text. - requestHeadersBuilder.append(System.getProperty("line.separator")) + requestHeadersBuilder.append(newLineString) requestHeadersBuilder.append("Upgrade-Insecure-Requests", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - requestHeadersBuilder.append(": 1") + requestHeadersBuilder.append(colonString) + requestHeadersBuilder.append("1") // Set the `User-Agent` header property. httpUrlConnection.setRequestProperty("User-Agent", userAgent) // Add the `User-Agent` header to the string builder and format the text. - requestHeadersBuilder.append(System.getProperty("line.separator")) + requestHeadersBuilder.append(newLineString) requestHeadersBuilder.append("User-Agent", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - requestHeadersBuilder.append(": ") + requestHeadersBuilder.append(colonString) requestHeadersBuilder.append(userAgent) @@ -157,36 +169,39 @@ class GetHeadersBackgroundTask { httpUrlConnection.setRequestProperty("Sec-Fetch-Site", "none") // Add the `Sec-Fetch-Site` header to the string builder and format the text. - requestHeadersBuilder.append(System.getProperty("line.separator")) + requestHeadersBuilder.append(newLineString) requestHeadersBuilder.append("Sec-Fetch-Site", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - requestHeadersBuilder.append(": none") + requestHeadersBuilder.append(colonString) + requestHeadersBuilder.append("none") // Set the `Sec-Fetch-Mode` header property. httpUrlConnection.setRequestProperty("Sec-Fetch-Mode", "navigate") // Add the `Sec-Fetch-Mode` header to the string builder and format the text. - requestHeadersBuilder.append(System.getProperty("line.separator")) + requestHeadersBuilder.append(newLineString) requestHeadersBuilder.append("Sec-Fetch-Mode", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - requestHeadersBuilder.append(": navigate") + requestHeadersBuilder.append(colonString) + requestHeadersBuilder.append("navigate") // Set the `Sec-Fetch-User` header property. httpUrlConnection.setRequestProperty("Sec-Fetch-User", "?1") // Add the `Sec-Fetch-User` header to the string builder and format the text. - requestHeadersBuilder.append(System.getProperty("line.separator")) + requestHeadersBuilder.append(newLineString) requestHeadersBuilder.append("Sec-Fetch-User", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - requestHeadersBuilder.append(": ?1") + requestHeadersBuilder.append(colonString) + requestHeadersBuilder.append("?1") // Set the `Accept` header property. httpUrlConnection.setRequestProperty("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3") // Add the `Accept` header to the string builder and format the text. - requestHeadersBuilder.append(System.getProperty("line.separator")) + requestHeadersBuilder.append(newLineString) requestHeadersBuilder.append("Accept", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - requestHeadersBuilder.append(": ") + requestHeadersBuilder.append(colonString) requestHeadersBuilder.append("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3") @@ -194,9 +209,9 @@ class GetHeadersBackgroundTask { httpUrlConnection.setRequestProperty("Accept-Language", localeString) // Add the `Accept-Language` header to the string builder and format the text. - requestHeadersBuilder.append(System.getProperty("line.separator")) + requestHeadersBuilder.append(newLineString) requestHeadersBuilder.append("Accept-Language", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - requestHeadersBuilder.append(": ") + requestHeadersBuilder.append(colonString) requestHeadersBuilder.append(localeString) @@ -209,18 +224,19 @@ class GetHeadersBackgroundTask { httpUrlConnection.setRequestProperty("Cookie", cookiesString) // Add the cookie header to the string builder and format the text. - requestHeadersBuilder.append(System.getProperty("line.separator")) + requestHeadersBuilder.append(newLineString) requestHeadersBuilder.append("Cookie", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - requestHeadersBuilder.append(": ") + requestHeadersBuilder.append(colonString) requestHeadersBuilder.append(cookiesString) } // `HttpUrlConnection` sets `Accept-Encoding` to be `gzip` by default. If the property is manually set, than `HttpUrlConnection` does not process the decoding. // Add the `Accept-Encoding` header to the string builder and format the text. - requestHeadersBuilder.append(System.getProperty("line.separator")) + requestHeadersBuilder.append(newLineString) requestHeadersBuilder.append("Accept-Encoding", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - requestHeadersBuilder.append(": gzip") + requestHeadersBuilder.append(colonString) + requestHeadersBuilder.append("gzip") // Ignore SSL errors if requested. if (ignoreSslErrors) { @@ -268,9 +284,68 @@ class GetHeadersBackgroundTask { // Get the response code, which causes the connection to the server to be made. val responseCode = httpUrlConnection.responseCode + // Try to populate the SSL certificate information. + try { + // Get the applied cipher suite string. + val appliedCipherString = (httpUrlConnection as HttpsURLConnection).cipherSuite + + // Populate the applied cipher builder, returned separately. + appliedCipherBuilder.append(appliedCipherString) + + // Append the applied cipher suite to the SSL information builder. + sslInformationBuilder.append(application.getString(R.string.applied_cipher), StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + sslInformationBuilder.append(colonString) + sslInformationBuilder.append(appliedCipherString) + sslInformationBuilder.append(newLineString) + + // Append the peer principal to the SSL information builder. + sslInformationBuilder.append(application.getString(R.string.peer_principal), StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + sslInformationBuilder.append(colonString) + sslInformationBuilder.append(httpUrlConnection.peerPrincipal.toString()) + sslInformationBuilder.append(newLineString) + + // Get the server certificate. + val serverCertificate = httpUrlConnection.serverCertificates[0] + + // Append the certificate type to the SSL information builder. + sslInformationBuilder.append(application.getString(R.string.certificate_type), StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + sslInformationBuilder.append(colonString) + sslInformationBuilder.append(serverCertificate.type) + sslInformationBuilder.append(newLineString) + + // Append the certificate hash code to the SSL information builder. + sslInformationBuilder.append(application.getString(R.string.certificate_hash_code), StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + sslInformationBuilder.append(colonString) + sslInformationBuilder.append(serverCertificate.hashCode().toString()) + + // Get the available cipher suites string array. + val availableCipherSuitesStringArray = httpUrlConnection.sslSocketFactory.defaultCipherSuites + + // Get the available cipher suites string array size. + val availableCipherSuitesStringArraySize = availableCipherSuitesStringArray.size + + // Populate the available cipher suites, returned separately. + for (i in 0 until availableCipherSuitesStringArraySize) { + // Append a new line if a cipher is already populated. + if (i > 0) + availableCiphersBuilder.append(newLineString) + + // Get the current cipher suite. + val currentCipherSuite = availableCipherSuitesStringArray[i] + + // Append the current cipher to the list. + availableCiphersBuilder.append(currentCipherSuite) + } + + // Populate the SSL certificate, returned separately. + sslCertificateBuilder.append(serverCertificate.toString()) + } catch (exception: Exception) { + // Do nothing. + } + // Populate the response message string builder. responseMessageBuilder.append(responseCode.toString(), StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - responseMessageBuilder.append(": ") + responseMessageBuilder.append(colonString) responseMessageBuilder.append(httpUrlConnection.responseMessage) // Initialize the iteration variable. @@ -280,11 +355,11 @@ class GetHeadersBackgroundTask { while (httpUrlConnection.getHeaderField(i) != null) { // Add a new line if there is already information in the string builder. if (i > 0) - responseHeadersBuilder.append(System.getProperty("line.separator")) + responseHeadersBuilder.append(newLineString) // Add the header to the string builder and format the text. responseHeadersBuilder.append(httpUrlConnection.getHeaderFieldKey(i), StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - responseHeadersBuilder.append(": ") + responseHeadersBuilder.append(colonString) responseHeadersBuilder.append(httpUrlConnection.getHeaderField(i)) // Increment the iteration variable. @@ -331,6 +406,6 @@ class GetHeadersBackgroundTask { } // Return the spannable string builders. - return arrayOf(requestHeadersBuilder, responseMessageBuilder, responseHeadersBuilder, responseBodyBuilder) + return arrayOf(sslInformationBuilder, appliedCipherBuilder, availableCiphersBuilder, sslCertificateBuilder, requestHeadersBuilder, responseMessageBuilder, responseHeadersBuilder, responseBodyBuilder) } } diff --git a/app/src/main/java/com/stoutner/privacybrowser/dialogs/ViewHeadersDetailDialog.kt b/app/src/main/java/com/stoutner/privacybrowser/dialogs/ViewHeadersDetailDialog.kt new file mode 100644 index 00000000..11d2e75e --- /dev/null +++ b/app/src/main/java/com/stoutner/privacybrowser/dialogs/ViewHeadersDetailDialog.kt @@ -0,0 +1,121 @@ +/* + * Copyright 2023 Soren Stoutner . + * + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Privacy Browser Android 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 Browser Android. If not, see . + */ + +package com.stoutner.privacybrowser.dialogs + +import android.app.Dialog +import android.graphics.Typeface +import android.os.Bundle +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.style.StyleSpan +import android.view.WindowManager + +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import androidx.preference.PreferenceManager + +import com.stoutner.privacybrowser.R + +// Define the public class constants. +const val AVAILABLE_CIPHERS = 0 +const val SSL_CERTIFICATE = 1 + +// Define the private class constants. +private const val DIALOG_TYPE = "A" +private const val MESSAGE = "B" +private const val APPLIED_CIPHER_STRING = "C" + +class ViewHeadersDetailDialog : DialogFragment() { + companion object { + fun displayDialog(dialogType: Int, message: String, appliedCipherString: String = ""): ViewHeadersDetailDialog { + // Create an arguments bundle. + val argumentsBundle = Bundle() + + // Store the SSL error message components in the bundle. + argumentsBundle.putInt(DIALOG_TYPE, dialogType) + argumentsBundle.putString(MESSAGE, message) + argumentsBundle.putString(APPLIED_CIPHER_STRING, appliedCipherString) + + // Create a new instance of the SSL certificate error dialog. + val thisHeadersSslCertificateDialog = ViewHeadersDetailDialog() + + // Add the arguments bundle to the new dialog. + thisHeadersSslCertificateDialog.arguments = argumentsBundle + + // Return the new dialog. + return thisHeadersSslCertificateDialog + } + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + // Get the arguments from the bundle. + val dialogType = requireArguments().getInt(DIALOG_TYPE) + val message = requireArguments().getString(MESSAGE)!! + val appliedCipherString = requireArguments().getString(APPLIED_CIPHER_STRING)!! + + // Use a builder to create the alert dialog. + val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog) + + // Set the icon according to the theme. + dialogBuilder.setIcon(R.drawable.ssl_certificate) + + // Set the title and message according to the type. + if (dialogType == AVAILABLE_CIPHERS) { // A cipher suite dialog is displayed. + // Set the title + dialogBuilder.setTitle(R.string.available_ciphers) + + // Create a message spannable string builder with the applied cipher bolded. + val messageSpannableStringBuilder = SpannableStringBuilder(message) + + // Get the applied cipher index. + val appliedCipherIndex = message.indexOf(appliedCipherString) + + // Set the applied cipher to be bold. + messageSpannableStringBuilder.setSpan(StyleSpan(Typeface.BOLD), appliedCipherIndex, appliedCipherIndex + appliedCipherString.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + + // Set the message. + dialogBuilder.setMessage(messageSpannableStringBuilder) + } else { // An SSL certificate dialog is displayed. + // Set the title and message. + dialogBuilder.setTitle(R.string.ssl_certificate) + dialogBuilder.setMessage(message) + } + + // Set the close button listener. Using `null` as the listener closes the dialog without doing anything else. + dialogBuilder.setNegativeButton(R.string.close, null) + + // Create an alert dialog from the alert dialog builder. + val alertDialog = dialogBuilder.create() + + // Get a handle for the shared preferences. + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) + + // Get the screenshot preference. + val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false) + + // Disable screenshots if not allowed. + if (!allowScreenshots) { + alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE) + } + + // Return the alert dialog. + return alertDialog + } +} diff --git a/app/src/main/java/com/stoutner/privacybrowser/viewmodelfactories/ViewHeadersFactory.kt b/app/src/main/java/com/stoutner/privacybrowser/viewmodelfactories/ViewHeadersFactory.kt index 83f0a365..12155721 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/viewmodelfactories/ViewHeadersFactory.kt +++ b/app/src/main/java/com/stoutner/privacybrowser/viewmodelfactories/ViewHeadersFactory.kt @@ -19,6 +19,7 @@ package com.stoutner.privacybrowser.viewmodelfactories +import android.app.Application import android.content.ContentResolver import androidx.lifecycle.ViewModel @@ -27,12 +28,12 @@ import androidx.lifecycle.ViewModelProvider import java.net.Proxy import java.util.concurrent.ExecutorService -class ViewHeadersFactory (private val urlString: String, private val userAgent: String, private val localeString: String, private val proxy: Proxy, private val contentResolver: ContentResolver, - private val executorService: ExecutorService): ViewModelProvider.Factory { +class ViewHeadersFactory (private val application: Application, private val urlString: String, private val userAgent: String, private val localeString: String, private val proxy: Proxy, + private val contentResolver: ContentResolver, private val executorService: ExecutorService): ViewModelProvider.Factory { // Override the create function in order to add the provided arguments. override fun create(modelClass: Class): T { // Return a new instance of the model class with the provided arguments. - return modelClass.getConstructor(String::class.java, String::class.java, String::class.java, Proxy::class.java, ContentResolver::class.java, ExecutorService::class.java) - .newInstance(urlString, userAgent, localeString, proxy, contentResolver, executorService) + return modelClass.getConstructor(Application::class.java, String::class.java, String::class.java, String::class.java, Proxy::class.java, ContentResolver::class.java, ExecutorService::class.java) + .newInstance(application, urlString, userAgent, localeString, proxy, contentResolver, executorService) } } diff --git a/app/src/main/java/com/stoutner/privacybrowser/viewmodels/HeadersViewModel.kt b/app/src/main/java/com/stoutner/privacybrowser/viewmodels/HeadersViewModel.kt index ccfe79ab..3fff3263 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/viewmodels/HeadersViewModel.kt +++ b/app/src/main/java/com/stoutner/privacybrowser/viewmodels/HeadersViewModel.kt @@ -19,20 +19,22 @@ package com.stoutner.privacybrowser.viewmodels +import android.app.Application import android.content.ContentResolver import android.text.SpannableStringBuilder +import androidx.lifecycle.AndroidViewModel + import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel import com.stoutner.privacybrowser.backgroundtasks.GetHeadersBackgroundTask import java.net.Proxy import java.util.concurrent.ExecutorService -class HeadersViewModel(private val urlString: String, private val userAgent: String, private val localeString: String, private val proxy: Proxy, private val contentResolver: ContentResolver, - private val executorService: ExecutorService): ViewModel() { +class HeadersViewModel(application: Application, private val urlString: String, private val userAgent: String, private val localeString: String, private val proxy: Proxy, private val contentResolver: ContentResolver, + private val executorService: ExecutorService): AndroidViewModel(application) { // Initialize the mutable live data variables. private val mutableLiveDataSourceStringArray = MutableLiveData>() private val mutableLiveDataErrorString = MutableLiveData() @@ -43,7 +45,7 @@ class HeadersViewModel(private val urlString: String, private val userAgent: Str val getSourceBackgroundTask = GetHeadersBackgroundTask() // Get the headers. - executorService.execute { mutableLiveDataSourceStringArray.postValue(getSourceBackgroundTask.acquire(urlString, userAgent, localeString, proxy, contentResolver, this, + executorService.execute { mutableLiveDataSourceStringArray.postValue(getSourceBackgroundTask.acquire(application, urlString, userAgent, localeString, proxy, contentResolver, this, false)) } } @@ -74,7 +76,7 @@ class HeadersViewModel(private val urlString: String, private val userAgent: Str val getSourceBackgroundTask = GetHeadersBackgroundTask() // Get the headers. - executorService.execute { mutableLiveDataSourceStringArray.postValue(getSourceBackgroundTask.acquire(urlString, userAgent, localeString, proxy, contentResolver, this, + executorService.execute { mutableLiveDataSourceStringArray.postValue(getSourceBackgroundTask.acquire(getApplication(), urlString, userAgent, localeString, proxy, contentResolver, this, ignoreSslErrors)) } } } diff --git a/app/src/main/res/layout/view_headers_bottom_appbar.xml b/app/src/main/res/layout/view_headers_bottom_appbar.xml index 0e05cafd..0b896c71 100644 --- a/app/src/main/res/layout/view_headers_bottom_appbar.xml +++ b/app/src/main/res/layout/view_headers_bottom_appbar.xml @@ -54,6 +54,55 @@ android:orientation="vertical" android:layout_margin="10dp" > + + + + + + + + +