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 class constants.
49 private const val WEBVIEW_FRAGMENT_ID = "webview_fragment_id"
50 private const val FAVORITE_ICON_BYTE_ARRAY = "favorite_icon_byte_array"
51 private const val HAS_SSL_CERTIFICATE = "has_ssl_certificate"
52 private const val DOMAIN = "domain"
53 private const val IP_ADDRESSES = "ip_addresses"
54 private const val ISSUED_TO_CNAME = "issued_to_cname"
55 private const val ISSUED_TO_ONAME = "issued_to_oname"
56 private const val ISSUED_TO_UNAME = "issued_to_uname"
57 private const val ISSUED_BY_CNAME = "issued_by_cname"
58 private const val ISSUED_BY_ONAME = "issued_by_oname"
59 private const val ISSUED_BY_UNAME = "issued_by_uname"
60 private const val START_DATE = "start_date"
61 private const val END_DATE = "end_date"
63 class ViewSslCertificateDialog : DialogFragment() {
64 // Define the class variables.
65 private var hasSslCertificate: Boolean = false
67 // Declare the class variables.
68 private lateinit var domainString: String
69 private lateinit var ipAddresses: String
70 private lateinit var issuedToCName: String
71 private lateinit var issuedToOName: String
72 private lateinit var issuedToUName: String
73 private lateinit var issuedByCName: String
74 private lateinit var issuedByOName: String
75 private lateinit var issuedByUName: String
76 private lateinit var startDate: Date
77 private lateinit var endDate: Date
79 // Declare the class views.
80 private lateinit var nestedScrollWebView: NestedScrollWebView
83 // `@JvmStatic` will no longer be required once all the code has transitioned to Kotlin.
85 fun displayDialog(webViewFragmentId: Long, favoriteIconBitmap: Bitmap): ViewSslCertificateDialog {
86 // Create an arguments bundle.
87 val argumentsBundle = Bundle()
89 // Create a favorite icon byte array output stream.
90 val favoriteIconByteArrayOutputStream = ByteArrayOutputStream()
92 // 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).
93 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream)
95 // Convert the favorite icon byte array output stream to a byte array.
96 val favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray()
98 // Store the arguments in the bundle.
99 argumentsBundle.putLong(WEBVIEW_FRAGMENT_ID, webViewFragmentId)
100 argumentsBundle.putByteArray(FAVORITE_ICON_BYTE_ARRAY, favoriteIconByteArray)
102 // Create a new instance of the view SSL certificate dialog.
103 val viewSslCertificateDialog = ViewSslCertificateDialog()
105 // Add the bundle to the new dialog.
106 viewSslCertificateDialog.arguments = argumentsBundle
108 // Return the new dialog.
109 return viewSslCertificateDialog
113 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
114 // Use a builder to create the alert dialog.
115 val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
117 // Populate the class variables.
118 if (savedInstanceState == null) { // The dialog is starting for the first time.
119 // Get the current position of this WebView fragment.
120 val webViewPosition = MainWebViewActivity.webViewPagerAdapter.getPositionForId(requireArguments().getLong(WEBVIEW_FRAGMENT_ID))
122 // Get the WebView tab fragment.
123 val webViewTabFragment = MainWebViewActivity.webViewPagerAdapter.getPageFragment(webViewPosition)
125 // Get the fragment view.
126 val fragmentView = webViewTabFragment.requireView()
128 // Get a handle for the current nested scroll WebView.
129 nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview)
131 // Get the SSL certificate.
132 val sslCertificate = nestedScrollWebView.certificate
134 // Store the status of the SSL certificate.
135 hasSslCertificate = sslCertificate != null
137 // Populate the certificate class variables if the webpage has an SSL certificate.
138 if (hasSslCertificate) {
139 // Convert the URL to a URI.
140 val uri = Uri.parse(nestedScrollWebView.url)
142 // Extract the domain name from the URI.
143 domainString = uri.host!!
145 // Get the ip addresses from the nested scroll WebView.
146 ipAddresses = nestedScrollWebView.currentIpAddresses
148 // Get the strings from the SSL certificate.
149 issuedToCName = sslCertificate!!.issuedTo.cName
150 issuedToOName = sslCertificate.issuedTo.oName
151 issuedToUName = sslCertificate.issuedTo.uName
152 issuedByCName = sslCertificate.issuedBy.cName
153 issuedByOName = sslCertificate.issuedBy.oName
154 issuedByUName = sslCertificate.issuedBy.uName
155 startDate = sslCertificate.validNotBeforeDate
156 endDate = sslCertificate.validNotAfterDate
158 } else { // The dialog has been restarted.
159 // Get the data from the saved instance state.
160 hasSslCertificate = savedInstanceState.getBoolean(HAS_SSL_CERTIFICATE)
162 // Populate the certificate class variables if the webpage has an SSL certificate.
163 if (hasSslCertificate) {
164 // Populate the certificate class variables from the saved instance state.
165 domainString = savedInstanceState.getString(DOMAIN)!!
166 ipAddresses = savedInstanceState.getString(IP_ADDRESSES)!!
167 issuedToCName = savedInstanceState.getString(ISSUED_TO_CNAME)!!
168 issuedToOName = savedInstanceState.getString(ISSUED_TO_ONAME)!!
169 issuedToUName = savedInstanceState.getString(ISSUED_TO_UNAME)!!
170 issuedByCName = savedInstanceState.getString(ISSUED_BY_CNAME)!!
171 issuedByOName = savedInstanceState.getString(ISSUED_BY_ONAME)!!
172 issuedByUName = savedInstanceState.getString(ISSUED_BY_UNAME)!!
173 startDate = Date(savedInstanceState.getLong(START_DATE))
174 endDate = Date(savedInstanceState.getLong(END_DATE))
178 // Get the favorite icon byte array from the arguments.
179 val favoriteIconByteArray = requireArguments().getByteArray(FAVORITE_ICON_BYTE_ARRAY)!!
181 // Convert the favorite icon byte array to a bitmap.
182 val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
184 // Create a drawable version of the favorite icon.
185 val favoriteIconDrawable = BitmapDrawable(resources, favoriteIconBitmap)
188 dialogBuilder.setIcon(favoriteIconDrawable)
190 // Set the close button listener. Using `null` as the listener closes the dialog without doing anything else.
191 dialogBuilder.setNegativeButton(R.string.close, null)
193 // Get a handle for the shared preferences.
194 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
196 // Get the screenshot preference.
197 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
199 // Check to see if the website is encrypted.
200 if (hasSslCertificate) { // The website is encrypted.
202 dialogBuilder.setTitle(R.string.ssl_certificate)
205 dialogBuilder.setView(R.layout.view_ssl_certificate_dialog)
207 // Create an alert dialog from the builder.
208 val alertDialog = dialogBuilder.create()
210 // Disable screenshots if not allowed.
211 if (!allowScreenshots) {
212 // Disable screenshots.
213 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
216 // The alert dialog must be shown before items in the layout can be modified.
219 // Get handles for the text views.
220 val domainTextView = alertDialog.findViewById<TextView>(R.id.domain)!!
221 val ipAddressesTextView = alertDialog.findViewById<TextView>(R.id.ip_addresses)!!
222 val issuedToCNameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_cname)!!
223 val issuedToONameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_oname)!!
224 val issuedToUNameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_uname)!!
225 val issuedByCNameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_cname)!!
226 val issuedByONameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_oname)!!
227 val issuedByUNameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_uname)!!
228 val startDateTextView = alertDialog.findViewById<TextView>(R.id.start_date)!!
229 val endDateTextView = alertDialog.findViewById<TextView>(R.id.end_date)!!
232 val domainLabel = getString(R.string.domain_label)
233 val ipAddressesLabel = getString(R.string.ip_addresses)
234 val cNameLabel = getString(R.string.common_name)
235 val oNameLabel = getString(R.string.organization)
236 val uNameLabel = getString(R.string.organizational_unit)
237 val startDateLabel = getString(R.string.start_date)
238 val endDateLabel = getString(R.string.end_date)
240 // Create spannable string builders for each text view that needs multiple colors of text.
241 val domainStringBuilder = SpannableStringBuilder(domainLabel + domainString)
242 val ipAddressesStringBuilder = SpannableStringBuilder(ipAddressesLabel + ipAddresses)
243 val issuedToCNameStringBuilder = SpannableStringBuilder(cNameLabel + issuedToCName)
244 val issuedToONameStringBuilder = SpannableStringBuilder(oNameLabel + issuedToOName)
245 val issuedToUNameStringBuilder = SpannableStringBuilder(uNameLabel + issuedToUName)
246 val issuedByCNameStringBuilder = SpannableStringBuilder(cNameLabel + issuedByCName)
247 val issuedByONameStringBuilder = SpannableStringBuilder(oNameLabel + issuedByOName)
248 val issuedByUNameStringBuilder = SpannableStringBuilder(uNameLabel + issuedByUName)
249 val startDateStringBuilder = SpannableStringBuilder(startDateLabel + DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(startDate))
250 val endDateStringBuilder = SpannableStringBuilder(endDateLabel + DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(endDate))
252 // Define the color spans.
253 val blueColorSpan = ForegroundColorSpan(requireContext().getColor(R.color.alt_blue_text))
254 val redColorSpan = ForegroundColorSpan(requireContext().getColor(R.color.red_text))
256 // Format the domain string and issued to CName colors.
257 if (domainString == issuedToCName) { // The domain and issued to CName match.
258 // Set the strings to be blue.
259 domainStringBuilder.setSpan(blueColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
260 issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
261 } else if (issuedToCName.startsWith("*.")) { // The issued to CName begins with a wildcard.
262 // Remove the initial `*.`.
263 val baseCertificateDomain = issuedToCName.substring(2)
265 // Setup a copy of the domain string to test subdomains.
266 var domainStringSubdomain = domainString
268 // Define a domain names match variable.
269 var domainNamesMatch = false
271 // Check all the subdomains against the base certificate domain.
272 while (!domainNamesMatch && domainStringSubdomain.contains(".")) { // Stop checking if we know that the domain names match or if we run out of subdomains.
273 // Test the subdomain against the base certificate domain.
274 if (domainStringSubdomain == baseCertificateDomain) {
275 domainNamesMatch = true
278 // Strip out the lowest subdomain.
279 domainStringSubdomain = domainStringSubdomain.substring(domainStringSubdomain.indexOf(".") + 1)
282 // Format the domain and issued to CName.
283 if (domainNamesMatch) { // The domain is a subdomain of the wildcard certificate.
284 // Set the strings to be blue.
285 domainStringBuilder.setSpan(blueColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
286 issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
287 } else { // The domain is not a subdomain of the wildcard certificate.
288 // Set the string to be red.
289 domainStringBuilder.setSpan(redColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
290 issuedToCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
292 } else { // The strings do not match and issued to CName does not begin with a wildcard.
293 // Set the strings to be red.
294 domainStringBuilder.setSpan(redColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
295 issuedToCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
298 // 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.
299 ipAddressesStringBuilder.setSpan(blueColorSpan, ipAddressesLabel.length, ipAddressesStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
300 issuedToONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length, issuedToONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
301 issuedToUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length, issuedToUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
302 issuedByCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedByCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
303 issuedByONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length, issuedByONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
304 issuedByUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length, issuedByUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
306 // Get the current date.
307 val currentDate = Calendar.getInstance().time
309 // Format the start date color. `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
310 if (startDate.after(currentDate)) { // The certificate start date is in the future.
311 startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
312 } else { // The certificate start date is in the past.
313 startDateStringBuilder.setSpan(blueColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
316 // Format the end date color. `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
317 if (endDate.before(currentDate)) { // The certificate end date is in the past.
318 endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
319 } else { // The certificate end date is in the future.
320 endDateStringBuilder.setSpan(blueColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
323 // Display the strings.
324 domainTextView.text = domainStringBuilder
325 ipAddressesTextView.text = ipAddressesStringBuilder
326 issuedToCNameTextView.text = issuedToCNameStringBuilder
327 issuedToONameTextView.text = issuedToONameStringBuilder
328 issuedToUNameTextView.text = issuedToUNameStringBuilder
329 issuedByCNameTextView.text = issuedByCNameStringBuilder
330 issuedByONameTextView.text = issuedByONameStringBuilder
331 issuedByUNameTextView.text = issuedByUNameStringBuilder
332 startDateTextView.text = startDateStringBuilder
333 endDateTextView.text = endDateStringBuilder
335 // Return the alert dialog.
337 } else { // The website is not encrypted.
339 dialogBuilder.setTitle(R.string.unencrypted_website)
342 dialogBuilder.setView(R.layout.unencrypted_website_dialog)
344 // Create an alert dialog from the builder.
345 val alertDialog = dialogBuilder.create()
347 // Disable screenshots if not allowed.
348 if (!allowScreenshots) {
349 // Disable screenshots.
350 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
353 // Return the alert dialog.
358 override fun onSaveInstanceState(savedInstanceState: Bundle) {
359 // Run the default commands.
360 super.onSaveInstanceState(savedInstanceState)
362 // Save the common class variables.
363 savedInstanceState.putBoolean(HAS_SSL_CERTIFICATE, hasSslCertificate)
365 // Save the SSL certificate strings if they exist.
366 if (hasSslCertificate) {
367 savedInstanceState.putString(DOMAIN, domainString)
368 savedInstanceState.putString(IP_ADDRESSES, ipAddresses)
369 savedInstanceState.putString(ISSUED_TO_CNAME, issuedToCName)
370 savedInstanceState.putString(ISSUED_TO_ONAME, issuedToOName)
371 savedInstanceState.putString(ISSUED_TO_UNAME, issuedToUName)
372 savedInstanceState.putString(ISSUED_BY_CNAME, issuedByCName)
373 savedInstanceState.putString(ISSUED_BY_ONAME, issuedByOName)
374 savedInstanceState.putString(ISSUED_BY_UNAME, issuedByUName)
375 savedInstanceState.putLong(START_DATE, startDate.time)
376 savedInstanceState.putLong(END_DATE, endDate.time)