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 = "domain"
50 private const val END_DATE = "end_date"
51 private const val FAVORITE_ICON_BYTE_ARRAY = "favorite_icon_byte_array"
52 private const val HAS_SSL_CERTIFICATE = "has_ssl_certificate"
53 private const val IP_ADDRESSES = "ip_addresses"
54 private const val ISSUED_BY_CNAME = "issued_by_cname"
55 private const val ISSUED_BY_ONAME = "issued_by_oname"
56 private const val ISSUED_BY_UNAME = "issued_by_uname"
57 private const val ISSUED_TO_CNAME = "issued_to_cname"
58 private const val ISSUED_TO_ONAME = "issued_to_oname"
59 private const val ISSUED_TO_UNAME = "issued_to_uname"
60 private const val START_DATE = "start_date"
61 private const val WEBVIEW_FRAGMENT_ID = "webview_fragment_id"
63 class ViewSslCertificateDialog : DialogFragment() {
65 fun displayDialog(webViewFragmentId: Long, favoriteIconBitmap: Bitmap): ViewSslCertificateDialog {
66 // Create an arguments bundle.
67 val argumentsBundle = Bundle()
69 // Create a favorite icon byte array output stream.
70 val favoriteIconByteArrayOutputStream = ByteArrayOutputStream()
72 // 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).
73 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream)
75 // Convert the favorite icon byte array output stream to a byte array.
76 val favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray()
78 // Store the arguments in the bundle.
79 argumentsBundle.putLong(WEBVIEW_FRAGMENT_ID, webViewFragmentId)
80 argumentsBundle.putByteArray(FAVORITE_ICON_BYTE_ARRAY, favoriteIconByteArray)
82 // Create a new instance of the view SSL certificate dialog.
83 val viewSslCertificateDialog = ViewSslCertificateDialog()
85 // Add the bundle to the new dialog.
86 viewSslCertificateDialog.arguments = argumentsBundle
88 // Return the new dialog.
89 return viewSslCertificateDialog
93 // Define the class variables.
94 private var hasSslCertificate: Boolean = false
96 // Declare the class variables.
97 private lateinit var domainString: String
98 private lateinit var endDate: Date
99 private lateinit var ipAddresses: String
100 private lateinit var issuedByCName: String
101 private lateinit var issuedByOName: String
102 private lateinit var issuedByUName: String
103 private lateinit var issuedToCName: String
104 private lateinit var issuedToOName: String
105 private lateinit var issuedToUName: String
106 private lateinit var nestedScrollWebView: NestedScrollWebView
107 private lateinit var startDate: Date
109 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
110 // Use a builder to create the alert dialog.
111 val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
113 // Populate the class variables.
114 if (savedInstanceState == null) { // The dialog is starting for the first time.
115 // Get the current position of this WebView fragment.
116 val webViewPosition = MainWebViewActivity.webViewStateAdapter!!.getPositionForId(requireArguments().getLong(WEBVIEW_FRAGMENT_ID))
118 // Get the WebView tab fragment.
119 val webViewTabFragment = MainWebViewActivity.webViewStateAdapter!!.getPageFragment(webViewPosition)
121 // Get the fragment view.
122 val fragmentView = webViewTabFragment.requireView()
124 // Get a handle for the current nested scroll WebView.
125 nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview)
127 // Get the SSL certificate.
128 val sslCertificate = nestedScrollWebView.certificate
130 // Store the status of the SSL certificate.
131 hasSslCertificate = sslCertificate != null
133 // Populate the certificate class variables if the webpage has an SSL certificate.
134 if (hasSslCertificate) {
135 // Convert the URL to a URI.
136 val uri = Uri.parse(nestedScrollWebView.url)
138 // Extract the domain name from the URI.
139 domainString = uri.host!!
141 // Get the ip addresses from the nested scroll WebView.
142 ipAddresses = nestedScrollWebView.currentIpAddresses
144 // Get the strings from the SSL certificate.
145 issuedToCName = sslCertificate!!.issuedTo.cName
146 issuedToOName = sslCertificate.issuedTo.oName
147 issuedToUName = sslCertificate.issuedTo.uName
148 issuedByCName = sslCertificate.issuedBy.cName
149 issuedByOName = sslCertificate.issuedBy.oName
150 issuedByUName = sslCertificate.issuedBy.uName
151 startDate = sslCertificate.validNotBeforeDate
152 endDate = sslCertificate.validNotAfterDate
154 } else { // The dialog has been restarted.
155 // Get the data from the saved instance state.
156 hasSslCertificate = savedInstanceState.getBoolean(HAS_SSL_CERTIFICATE)
158 // Populate the certificate class variables if the webpage has an SSL certificate.
159 if (hasSslCertificate) {
160 // Populate the certificate class variables from the saved instance state.
161 domainString = savedInstanceState.getString(DOMAIN)!!
162 ipAddresses = savedInstanceState.getString(IP_ADDRESSES)!!
163 issuedToCName = savedInstanceState.getString(ISSUED_TO_CNAME)!!
164 issuedToOName = savedInstanceState.getString(ISSUED_TO_ONAME)!!
165 issuedToUName = savedInstanceState.getString(ISSUED_TO_UNAME)!!
166 issuedByCName = savedInstanceState.getString(ISSUED_BY_CNAME)!!
167 issuedByOName = savedInstanceState.getString(ISSUED_BY_ONAME)!!
168 issuedByUName = savedInstanceState.getString(ISSUED_BY_UNAME)!!
169 startDate = Date(savedInstanceState.getLong(START_DATE))
170 endDate = Date(savedInstanceState.getLong(END_DATE))
174 // Get the favorite icon byte array from the arguments.
175 val favoriteIconByteArray = requireArguments().getByteArray(FAVORITE_ICON_BYTE_ARRAY)!!
177 // Convert the favorite icon byte array to a bitmap.
178 val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
180 // Create a drawable version of the favorite icon.
181 val favoriteIconDrawable = BitmapDrawable(resources, favoriteIconBitmap)
184 dialogBuilder.setIcon(favoriteIconDrawable)
186 // Set the close button listener. Using `null` as the listener closes the dialog without doing anything else.
187 dialogBuilder.setNegativeButton(R.string.close, null)
189 // Get a handle for the shared preferences.
190 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
192 // Get the screenshot preference.
193 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
195 // Check to see if the website is encrypted.
196 if (hasSslCertificate) { // The website is encrypted.
198 dialogBuilder.setTitle(R.string.ssl_certificate)
201 dialogBuilder.setView(R.layout.view_ssl_certificate_dialog)
203 // Create an alert dialog from the builder.
204 val alertDialog = dialogBuilder.create()
206 // Disable screenshots if not allowed.
207 if (!allowScreenshots) {
208 // Disable screenshots.
209 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
212 // The alert dialog must be shown before items in the layout can be modified.
215 // Get handles for the text views.
216 val domainTextView = alertDialog.findViewById<TextView>(R.id.domain)!!
217 val ipAddressesTextView = alertDialog.findViewById<TextView>(R.id.ip_addresses)!!
218 val issuedToCNameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_cname)!!
219 val issuedToONameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_oname)!!
220 val issuedToUNameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_uname)!!
221 val issuedByCNameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_cname)!!
222 val issuedByONameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_oname)!!
223 val issuedByUNameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_uname)!!
224 val startDateTextView = alertDialog.findViewById<TextView>(R.id.start_date)!!
225 val endDateTextView = alertDialog.findViewById<TextView>(R.id.end_date)!!
228 val domainLabel = getString(R.string.domain_label)
229 val ipAddressesLabel = getString(R.string.ip_addresses)
230 val cNameLabel = getString(R.string.common_name)
231 val oNameLabel = getString(R.string.organization)
232 val uNameLabel = getString(R.string.organizational_unit)
233 val startDateLabel = getString(R.string.start_date)
234 val endDateLabel = getString(R.string.end_date)
236 // Create spannable string builders for each text view that needs multiple colors of text.
237 val domainStringBuilder = SpannableStringBuilder(domainLabel + domainString)
238 val ipAddressesStringBuilder = SpannableStringBuilder(ipAddressesLabel + ipAddresses)
239 val issuedToCNameStringBuilder = SpannableStringBuilder(cNameLabel + issuedToCName)
240 val issuedToONameStringBuilder = SpannableStringBuilder(oNameLabel + issuedToOName)
241 val issuedToUNameStringBuilder = SpannableStringBuilder(uNameLabel + issuedToUName)
242 val issuedByCNameStringBuilder = SpannableStringBuilder(cNameLabel + issuedByCName)
243 val issuedByONameStringBuilder = SpannableStringBuilder(oNameLabel + issuedByOName)
244 val issuedByUNameStringBuilder = SpannableStringBuilder(uNameLabel + issuedByUName)
245 val startDateStringBuilder = SpannableStringBuilder(startDateLabel + DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(startDate))
246 val endDateStringBuilder = SpannableStringBuilder(endDateLabel + DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(endDate))
248 // Define the color spans.
249 val blueColorSpan = ForegroundColorSpan(requireContext().getColor(R.color.alt_blue_text))
250 val redColorSpan = ForegroundColorSpan(requireContext().getColor(R.color.red_text))
252 // Format the domain string and issued to CName colors.
253 if (domainString == issuedToCName) { // The domain and issued to CName match.
254 // Set the strings to be blue.
255 domainStringBuilder.setSpan(blueColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
256 issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
257 } else if (issuedToCName.startsWith("*.")) { // The issued to CName begins with a wildcard.
258 // Remove the initial `*.`.
259 val baseCertificateDomain = issuedToCName.substring(2)
261 // Setup a copy of the domain string to test subdomains.
262 var domainStringSubdomain = domainString
264 // Define a domain names match variable.
265 var domainNamesMatch = false
267 // Check all the subdomains against the base certificate domain.
268 while (!domainNamesMatch && domainStringSubdomain.contains(".")) { // Stop checking if we know that the domain names match or if we run out of subdomains.
269 // Test the subdomain against the base certificate domain.
270 if (domainStringSubdomain == baseCertificateDomain) {
271 domainNamesMatch = true
274 // Strip out the lowest subdomain.
275 domainStringSubdomain = domainStringSubdomain.substring(domainStringSubdomain.indexOf(".") + 1)
278 // Format the domain and issued to CName.
279 if (domainNamesMatch) { // The domain is a subdomain of the wildcard certificate.
280 // Set the strings to be blue.
281 domainStringBuilder.setSpan(blueColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
282 issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
283 } else { // The domain is not a subdomain of the wildcard certificate.
284 // Set the string to be red.
285 domainStringBuilder.setSpan(redColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
286 issuedToCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
288 } else { // The strings do not match and issued to CName does not begin with a wildcard.
289 // Set the strings to be red.
290 domainStringBuilder.setSpan(redColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
291 issuedToCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
294 // 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.
295 ipAddressesStringBuilder.setSpan(blueColorSpan, ipAddressesLabel.length, ipAddressesStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
296 issuedToONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length, issuedToONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
297 issuedToUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length, issuedToUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
298 issuedByCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedByCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
299 issuedByONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length, issuedByONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
300 issuedByUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length, issuedByUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
302 // Get the current date.
303 val currentDate = Calendar.getInstance().time
305 // Format the start date color. `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
306 if (startDate.after(currentDate)) { // The certificate start date is in the future.
307 startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
308 } else { // The certificate start date is in the past.
309 startDateStringBuilder.setSpan(blueColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
312 // Format the end date color. `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
313 if (endDate.before(currentDate)) { // The certificate end date is in the past.
314 endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
315 } else { // The certificate end date is in the future.
316 endDateStringBuilder.setSpan(blueColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
319 // Display the strings.
320 domainTextView.text = domainStringBuilder
321 ipAddressesTextView.text = ipAddressesStringBuilder
322 issuedToCNameTextView.text = issuedToCNameStringBuilder
323 issuedToONameTextView.text = issuedToONameStringBuilder
324 issuedToUNameTextView.text = issuedToUNameStringBuilder
325 issuedByCNameTextView.text = issuedByCNameStringBuilder
326 issuedByONameTextView.text = issuedByONameStringBuilder
327 issuedByUNameTextView.text = issuedByUNameStringBuilder
328 startDateTextView.text = startDateStringBuilder
329 endDateTextView.text = endDateStringBuilder
331 // Return the alert dialog.
333 } else { // The website is not encrypted.
335 dialogBuilder.setTitle(R.string.unencrypted_website)
338 dialogBuilder.setView(R.layout.unencrypted_website_dialog)
340 // Create an alert dialog from the builder.
341 val alertDialog = dialogBuilder.create()
343 // Disable screenshots if not allowed.
344 if (!allowScreenshots) {
345 // Disable screenshots.
346 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
349 // Return the alert dialog.
354 override fun onSaveInstanceState(savedInstanceState: Bundle) {
355 // Run the default commands.
356 super.onSaveInstanceState(savedInstanceState)
358 // Save the common class variables.
359 savedInstanceState.putBoolean(HAS_SSL_CERTIFICATE, hasSslCertificate)
361 // Save the SSL certificate strings if they exist.
362 if (hasSslCertificate) {
363 savedInstanceState.putString(DOMAIN, domainString)
364 savedInstanceState.putString(IP_ADDRESSES, ipAddresses)
365 savedInstanceState.putString(ISSUED_TO_CNAME, issuedToCName)
366 savedInstanceState.putString(ISSUED_TO_ONAME, issuedToOName)
367 savedInstanceState.putString(ISSUED_TO_UNAME, issuedToUName)
368 savedInstanceState.putString(ISSUED_BY_CNAME, issuedByCName)
369 savedInstanceState.putString(ISSUED_BY_ONAME, issuedByOName)
370 savedInstanceState.putString(ISSUED_BY_UNAME, issuedByUName)
371 savedInstanceState.putLong(START_DATE, startDate.time)
372 savedInstanceState.putLong(END_DATE, endDate.time)