2 * Copyright 2016-2023 Soren Stoutner <soren@stoutner.com>.
4 * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
6 * Privacy Browser Android is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
11 * Privacy Browser Android is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with Privacy Browser Android. If not, see <http://www.gnu.org/licenses/>.
20 package com.stoutner.privacybrowser.dialogs
22 import android.app.Dialog
23 import android.graphics.Bitmap
24 import android.graphics.BitmapFactory
25 import android.graphics.drawable.BitmapDrawable
26 import android.net.Uri
27 import android.os.Bundle
28 import android.text.SpannableStringBuilder
29 import android.text.Spanned
30 import android.text.style.ForegroundColorSpan
31 import android.view.WindowManager
32 import android.widget.TextView
34 import androidx.appcompat.app.AlertDialog
35 import androidx.fragment.app.DialogFragment
36 import androidx.preference.PreferenceManager
38 import com.stoutner.privacybrowser.R
39 import com.stoutner.privacybrowser.activities.MainWebViewActivity
40 import com.stoutner.privacybrowser.views.NestedScrollWebView
42 import java.io.ByteArrayOutputStream
44 import java.text.DateFormat
45 import java.util.Calendar
48 // Define the private class constants.
49 private const val DOMAIN = "A"
50 private const val END_DATE = "B"
51 private const val FAVORITE_ICON_BYTE_ARRAY = "C"
52 private const val HAS_SSL_CERTIFICATE = "D"
53 private const val IP_ADDRESSES = "E"
54 private const val ISSUED_BY_CNAME = "F"
55 private const val ISSUED_BY_ONAME = "G"
56 private const val ISSUED_BY_UNAME = "H"
57 private const val ISSUED_TO_CNAME = "I"
58 private const val ISSUED_TO_ONAME = "J"
59 private const val ISSUED_TO_UNAME = "K"
60 private const val START_DATE = "L"
61 private const val WEBVIEW_FRAGMENT_ID = "M"
62 private const val URL = "N"
64 class ViewSslCertificateDialog : DialogFragment() {
66 fun displayDialog(webViewFragmentId: Long, favoriteIconBitmap: Bitmap): ViewSslCertificateDialog {
67 // Create an arguments bundle.
68 val argumentsBundle = Bundle()
70 // Create a favorite icon byte array output stream.
71 val favoriteIconByteArrayOutputStream = ByteArrayOutputStream()
73 // Convert the bitmap to a PNG and place it in the byte array output stream. `0` is for lossless compression (the only option for a PNG).
74 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream)
76 // Convert the favorite icon byte array output stream to a byte array.
77 val favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray()
79 // Store the arguments in the bundle.
80 argumentsBundle.putLong(WEBVIEW_FRAGMENT_ID, webViewFragmentId)
81 argumentsBundle.putByteArray(FAVORITE_ICON_BYTE_ARRAY, favoriteIconByteArray)
83 // Create a new instance of the view SSL certificate dialog.
84 val viewSslCertificateDialog = ViewSslCertificateDialog()
86 // Add the bundle to the new dialog.
87 viewSslCertificateDialog.arguments = argumentsBundle
89 // Return the new dialog.
90 return viewSslCertificateDialog
94 // Define the class variables.
95 private var hasSslCertificate: Boolean = false
97 // Declare the class variables.
98 private lateinit var domainString: String
99 private lateinit var endDate: Date
100 private lateinit var ipAddresses: String
101 private lateinit var issuedByCName: String
102 private lateinit var issuedByOName: String
103 private lateinit var issuedByUName: String
104 private lateinit var issuedToCName: String
105 private lateinit var issuedToOName: String
106 private lateinit var issuedToUName: String
107 private lateinit var nestedScrollWebView: NestedScrollWebView
108 private lateinit var startDate: Date
109 private lateinit var urlString: String
111 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
112 // Use a builder to create the alert dialog.
113 val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
115 // Populate the class variables.
116 if (savedInstanceState == null) { // The dialog is starting for the first time.
117 // Get the current position of this WebView fragment.
118 val webViewPosition = MainWebViewActivity.webViewStateAdapter!!.getPositionForId(requireArguments().getLong(WEBVIEW_FRAGMENT_ID))
120 // Get the WebView tab fragment.
121 val webViewTabFragment = MainWebViewActivity.webViewStateAdapter!!.getPageFragment(webViewPosition)
123 // Get the fragment view.
124 val fragmentView = webViewTabFragment.requireView()
126 // Get a handle for the current nested scroll WebView.
127 nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview)
129 // Get the SSL certificate.
130 val sslCertificate = nestedScrollWebView.certificate
132 // Store the status of the SSL certificate.
133 hasSslCertificate = sslCertificate != null
135 // Store the URL string.
136 urlString = nestedScrollWebView.currentUrl
138 // Populate the certificate class variables if the webpage has an SSL certificate.
139 if (hasSslCertificate) {
140 // Convert the URL to a URI.
141 val uri = Uri.parse(nestedScrollWebView.currentUrl)
143 // Extract the domain name from the URI.
144 domainString = uri.host!!
146 // Get the ip addresses from the nested scroll WebView.
147 ipAddresses = nestedScrollWebView.currentIpAddresses
149 // Get the strings from the SSL certificate.
150 issuedToCName = sslCertificate!!.issuedTo.cName
151 issuedToOName = sslCertificate.issuedTo.oName
152 issuedToUName = sslCertificate.issuedTo.uName
153 issuedByCName = sslCertificate.issuedBy.cName
154 issuedByOName = sslCertificate.issuedBy.oName
155 issuedByUName = sslCertificate.issuedBy.uName
156 startDate = sslCertificate.validNotBeforeDate
157 endDate = sslCertificate.validNotAfterDate
159 } else { // The dialog has been restarted.
160 // Get the data from the saved instance state.
161 hasSslCertificate = savedInstanceState.getBoolean(HAS_SSL_CERTIFICATE)
162 urlString = savedInstanceState.getString(URL)!!
164 // Populate the certificate class variables if the webpage has an SSL certificate.
165 if (hasSslCertificate) {
166 // Populate the certificate class variables from the saved instance state.
167 domainString = savedInstanceState.getString(DOMAIN)!!
168 ipAddresses = savedInstanceState.getString(IP_ADDRESSES)!!
169 issuedToCName = savedInstanceState.getString(ISSUED_TO_CNAME)!!
170 issuedToOName = savedInstanceState.getString(ISSUED_TO_ONAME)!!
171 issuedToUName = savedInstanceState.getString(ISSUED_TO_UNAME)!!
172 issuedByCName = savedInstanceState.getString(ISSUED_BY_CNAME)!!
173 issuedByOName = savedInstanceState.getString(ISSUED_BY_ONAME)!!
174 issuedByUName = savedInstanceState.getString(ISSUED_BY_UNAME)!!
175 startDate = Date(savedInstanceState.getLong(START_DATE))
176 endDate = Date(savedInstanceState.getLong(END_DATE))
180 // Get the favorite icon byte array from the arguments.
181 val favoriteIconByteArray = requireArguments().getByteArray(FAVORITE_ICON_BYTE_ARRAY)!!
183 // Convert the favorite icon byte array to a bitmap.
184 val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
186 // Create a drawable version of the favorite icon.
187 val favoriteIconDrawable = BitmapDrawable(resources, favoriteIconBitmap)
190 dialogBuilder.setIcon(favoriteIconDrawable)
192 // Set the close button listener. Using `null` as the listener closes the dialog without doing anything else.
193 dialogBuilder.setNegativeButton(R.string.close, null)
195 // Get a handle for the shared preferences.
196 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
198 // Get the screenshot preference.
199 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
201 // Check to see if the website is encrypted.
202 if (hasSslCertificate) { // The website is encrypted.
204 dialogBuilder.setTitle(R.string.ssl_certificate)
207 dialogBuilder.setView(R.layout.view_ssl_certificate_dialog)
209 // Create an alert dialog from the builder.
210 val alertDialog = dialogBuilder.create()
212 // Disable screenshots if not allowed.
213 if (!allowScreenshots) {
214 // Disable screenshots.
215 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
218 // The alert dialog must be shown before items in the layout can be modified.
221 // Get handles for the text views.
222 val domainTextView = alertDialog.findViewById<TextView>(R.id.domain)!!
223 val ipAddressesTextView = alertDialog.findViewById<TextView>(R.id.ip_addresses)!!
224 val issuedToCNameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_cname)!!
225 val issuedToONameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_oname)!!
226 val issuedToUNameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_uname)!!
227 val issuedByCNameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_cname)!!
228 val issuedByONameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_oname)!!
229 val issuedByUNameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_uname)!!
230 val startDateTextView = alertDialog.findViewById<TextView>(R.id.start_date)!!
231 val endDateTextView = alertDialog.findViewById<TextView>(R.id.end_date)!!
234 val domainLabel = getString(R.string.domain_label)
235 val ipAddressesLabel = getString(R.string.ip_addresses)
236 val cNameLabel = getString(R.string.common_name)
237 val oNameLabel = getString(R.string.organization)
238 val uNameLabel = getString(R.string.organizational_unit)
239 val startDateLabel = getString(R.string.start_date)
240 val endDateLabel = getString(R.string.end_date)
242 // Create spannable string builders for each text view that needs multiple colors of text.
243 val domainStringBuilder = SpannableStringBuilder(domainLabel + domainString)
244 val ipAddressesStringBuilder = SpannableStringBuilder(ipAddressesLabel + ipAddresses)
245 val issuedToCNameStringBuilder = SpannableStringBuilder(cNameLabel + issuedToCName)
246 val issuedToONameStringBuilder = SpannableStringBuilder(oNameLabel + issuedToOName)
247 val issuedToUNameStringBuilder = SpannableStringBuilder(uNameLabel + issuedToUName)
248 val issuedByCNameStringBuilder = SpannableStringBuilder(cNameLabel + issuedByCName)
249 val issuedByONameStringBuilder = SpannableStringBuilder(oNameLabel + issuedByOName)
250 val issuedByUNameStringBuilder = SpannableStringBuilder(uNameLabel + issuedByUName)
251 val startDateStringBuilder = SpannableStringBuilder(startDateLabel + DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(startDate))
252 val endDateStringBuilder = SpannableStringBuilder(endDateLabel + DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(endDate))
254 // Define the color spans.
255 val blueColorSpan = ForegroundColorSpan(requireContext().getColor(R.color.alt_blue_text))
256 val redColorSpan = ForegroundColorSpan(requireContext().getColor(R.color.red_text))
258 // Format the domain string and issued to CName colors.
259 if (domainString == issuedToCName) { // The domain and issued to CName match.
260 // Set the strings to be blue.
261 domainStringBuilder.setSpan(blueColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
262 issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
263 } else if (issuedToCName.startsWith("*.")) { // The issued to CName begins with a wildcard.
264 // Remove the initial `*.`.
265 val baseCertificateDomain = issuedToCName.substring(2)
267 // Setup a copy of the domain string to test subdomains.
268 var domainStringSubdomain = domainString
270 // Define a domain names match variable.
271 var domainNamesMatch = false
273 // Check all the subdomains against the base certificate domain.
274 while (!domainNamesMatch && domainStringSubdomain.contains(".")) { // Stop checking if we know that the domain names match or if we run out of subdomains.
275 // Test the subdomain against the base certificate domain.
276 if (domainStringSubdomain == baseCertificateDomain) {
277 domainNamesMatch = true
280 // Strip out the lowest subdomain.
281 domainStringSubdomain = domainStringSubdomain.substring(domainStringSubdomain.indexOf(".") + 1)
284 // Format the domain and issued to CName.
285 if (domainNamesMatch) { // The domain is a subdomain of the wildcard certificate.
286 // Set the strings to be blue.
287 domainStringBuilder.setSpan(blueColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
288 issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
289 } else { // The domain is not a subdomain of the wildcard certificate.
290 // Set the string to be red.
291 domainStringBuilder.setSpan(redColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
292 issuedToCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
294 } else { // The strings do not match and issued to CName does not begin with a wildcard.
295 // Set the strings to be red.
296 domainStringBuilder.setSpan(redColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
297 issuedToCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
300 // Set the IP addresses, issued to, and issued by spans to display the certificate information in blue. `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
301 ipAddressesStringBuilder.setSpan(blueColorSpan, ipAddressesLabel.length, ipAddressesStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
302 issuedToONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length, issuedToONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
303 issuedToUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length, issuedToUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
304 issuedByCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedByCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
305 issuedByONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length, issuedByONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
306 issuedByUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length, issuedByUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
308 // Get the current date.
309 val currentDate = Calendar.getInstance().time
311 // Format the start date color. `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
312 if (startDate.after(currentDate)) { // The certificate start date is in the future.
313 startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
314 } else { // The certificate start date is in the past.
315 startDateStringBuilder.setSpan(blueColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
318 // Format the end date color. `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
319 if (endDate.before(currentDate)) { // The certificate end date is in the past.
320 endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
321 } else { // The certificate end date is in the future.
322 endDateStringBuilder.setSpan(blueColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
325 // Display the strings.
326 domainTextView.text = domainStringBuilder
327 ipAddressesTextView.text = ipAddressesStringBuilder
328 issuedToCNameTextView.text = issuedToCNameStringBuilder
329 issuedToONameTextView.text = issuedToONameStringBuilder
330 issuedToUNameTextView.text = issuedToUNameStringBuilder
331 issuedByCNameTextView.text = issuedByCNameStringBuilder
332 issuedByONameTextView.text = issuedByONameStringBuilder
333 issuedByUNameTextView.text = issuedByUNameStringBuilder
334 startDateTextView.text = startDateStringBuilder
335 endDateTextView.text = endDateStringBuilder
337 // Return the alert dialog.
339 } else { // The website is not encrypted.
340 // Populate the dialog according to the URL type.
341 if (urlString.startsWith("content://")) { // A content URL is loaded.
343 dialogBuilder.setTitle(R.string.content_url)
346 dialogBuilder.setMessage(R.string.content_url_message)
347 } else { // The website is unencrypted.
349 dialogBuilder.setTitle(R.string.unencrypted_website)
352 dialogBuilder.setView(R.layout.unencrypted_website_dialog)
355 // Create an alert dialog from the builder.
356 val alertDialog = dialogBuilder.create()
358 // Disable screenshots if not allowed.
359 if (!allowScreenshots) {
360 // Disable screenshots.
361 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
364 // Return the alert dialog.
369 override fun onSaveInstanceState(savedInstanceState: Bundle) {
370 // Run the default commands.
371 super.onSaveInstanceState(savedInstanceState)
373 // Save the common class variables.
374 savedInstanceState.putBoolean(HAS_SSL_CERTIFICATE, hasSslCertificate)
375 savedInstanceState.putString(URL, urlString)
377 // Save the SSL certificate strings if they exist.
378 if (hasSslCertificate) {
379 savedInstanceState.putString(DOMAIN, domainString)
380 savedInstanceState.putString(IP_ADDRESSES, ipAddresses)
381 savedInstanceState.putString(ISSUED_TO_CNAME, issuedToCName)
382 savedInstanceState.putString(ISSUED_TO_ONAME, issuedToOName)
383 savedInstanceState.putString(ISSUED_TO_UNAME, issuedToUName)
384 savedInstanceState.putString(ISSUED_BY_CNAME, issuedByCName)
385 savedInstanceState.putString(ISSUED_BY_ONAME, issuedByOName)
386 savedInstanceState.putString(ISSUED_BY_UNAME, issuedByUName)
387 savedInstanceState.putLong(START_DATE, startDate.time)
388 savedInstanceState.putLong(END_DATE, endDate.time)