2 * Copyright © 2016-2022 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.content.res.Configuration
24 import android.graphics.Bitmap
25 import android.graphics.BitmapFactory
26 import android.graphics.drawable.BitmapDrawable
27 import android.net.Uri
28 import android.os.Bundle
29 import android.text.SpannableStringBuilder
30 import android.text.Spanned
31 import android.text.style.ForegroundColorSpan
32 import android.view.WindowManager
33 import android.widget.TextView
35 import androidx.appcompat.app.AlertDialog
36 import androidx.fragment.app.DialogFragment
37 import androidx.preference.PreferenceManager
39 import com.stoutner.privacybrowser.R
40 import com.stoutner.privacybrowser.activities.MainWebViewActivity
41 import com.stoutner.privacybrowser.views.NestedScrollWebView
43 import java.io.ByteArrayOutputStream
45 import java.text.DateFormat
46 import java.util.Calendar
49 // Define the class constants.
50 private const val WEBVIEW_FRAGMENT_ID = "webview_fragment_id"
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 DOMAIN = "domain"
54 private const val IP_ADDRESSES = "ip_addresses"
55 private const val ISSUED_TO_CNAME = "issued_to_cname"
56 private const val ISSUED_TO_ONAME = "issued_to_oname"
57 private const val ISSUED_TO_UNAME = "issued_to_uname"
58 private const val ISSUED_BY_CNAME = "issued_by_cname"
59 private const val ISSUED_BY_ONAME = "issued_by_oname"
60 private const val ISSUED_BY_UNAME = "issued_by_uname"
61 private const val START_DATE = "start_date"
62 private const val END_DATE = "end_date"
64 class ViewSslCertificateDialog : DialogFragment() {
65 // Define the class variables.
66 private var hasSslCertificate: Boolean = false
68 // Declare the class variables.
69 private lateinit var domainString: String
70 private lateinit var ipAddresses: String
71 private lateinit var issuedToCName: String
72 private lateinit var issuedToOName: String
73 private lateinit var issuedToUName: String
74 private lateinit var issuedByCName: String
75 private lateinit var issuedByOName: String
76 private lateinit var issuedByUName: String
77 private lateinit var startDate: Date
78 private lateinit var endDate: Date
80 // Declare the class views.
81 private lateinit var nestedScrollWebView: NestedScrollWebView
84 // `@JvmStatic` will no longer be required once all the code has transitioned to Kotlin.
86 fun displayDialog(webViewFragmentId: Long, favoriteIconBitmap: Bitmap): ViewSslCertificateDialog {
87 // Create an arguments bundle.
88 val argumentsBundle = Bundle()
90 // Create a favorite icon byte array output stream.
91 val favoriteIconByteArrayOutputStream = ByteArrayOutputStream()
93 // 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).
94 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream)
96 // Convert the favorite icon byte array output stream to a byte array.
97 val favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray()
99 // Store the arguments in the bundle.
100 argumentsBundle.putLong(WEBVIEW_FRAGMENT_ID, webViewFragmentId)
101 argumentsBundle.putByteArray(FAVORITE_ICON_BYTE_ARRAY, favoriteIconByteArray)
103 // Create a new instance of the view SSL certificate dialog.
104 val viewSslCertificateDialog = ViewSslCertificateDialog()
106 // Add the bundle to the new dialog.
107 viewSslCertificateDialog.arguments = argumentsBundle
109 // Return the new dialog.
110 return viewSslCertificateDialog
114 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
115 // Use a builder to create the alert dialog.
116 val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
118 // Populate the class variables.
119 if (savedInstanceState == null) { // The dialog is starting for the first time.
120 // Get the current position of this WebView fragment.
121 val webViewPosition = MainWebViewActivity.webViewPagerAdapter.getPositionForId(requireArguments().getLong(WEBVIEW_FRAGMENT_ID))
123 // Get the WebView tab fragment.
124 val webViewTabFragment = MainWebViewActivity.webViewPagerAdapter.getPageFragment(webViewPosition)
126 // Get the fragment view.
127 val fragmentView = webViewTabFragment.requireView()
129 // Get a handle for the current nested scroll WebView.
130 nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview)
132 // Get the SSL certificate.
133 val sslCertificate = nestedScrollWebView.certificate
135 // Store the status of the SSL certificate.
136 hasSslCertificate = sslCertificate != null
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.url)
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)
163 // Populate the certificate class variables if the webpage has an SSL certificate.
164 if (hasSslCertificate) {
165 // Populate the certificate class variables from the saved instance state.
166 domainString = savedInstanceState.getString(DOMAIN)!!
167 ipAddresses = savedInstanceState.getString(IP_ADDRESSES)!!
168 issuedToCName = savedInstanceState.getString(ISSUED_TO_CNAME)!!
169 issuedToOName = savedInstanceState.getString(ISSUED_TO_ONAME)!!
170 issuedToUName = savedInstanceState.getString(ISSUED_TO_UNAME)!!
171 issuedByCName = savedInstanceState.getString(ISSUED_BY_CNAME)!!
172 issuedByOName = savedInstanceState.getString(ISSUED_BY_ONAME)!!
173 issuedByUName = savedInstanceState.getString(ISSUED_BY_UNAME)!!
174 startDate = Date(savedInstanceState.getLong(START_DATE))
175 endDate = Date(savedInstanceState.getLong(END_DATE))
179 // Get the favorite icon byte array from the arguments.
180 val favoriteIconByteArray = requireArguments().getByteArray(FAVORITE_ICON_BYTE_ARRAY)!!
182 // Convert the favorite icon byte array to a bitmap.
183 val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
185 // Create a drawable version of the favorite icon.
186 val favoriteIconDrawable = BitmapDrawable(resources, favoriteIconBitmap)
189 dialogBuilder.setIcon(favoriteIconDrawable)
191 // Set the close button listener. Using `null` as the listener closes the dialog without doing anything else.
192 dialogBuilder.setNegativeButton(R.string.close, null)
194 // Get a handle for the shared preferences.
195 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
197 // Get the screenshot preference.
198 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
200 // Check to see if the website is encrypted.
201 if (hasSslCertificate) { // The website is encrypted.
203 dialogBuilder.setTitle(R.string.ssl_certificate)
206 dialogBuilder.setView(R.layout.view_ssl_certificate_dialog)
208 // Create an alert dialog from the builder.
209 val alertDialog = dialogBuilder.create()
211 // Disable screenshots if not allowed.
212 if (!allowScreenshots) {
213 // Disable screenshots.
214 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
217 // The alert dialog must be shown before items in the layout can be modified.
220 // Get handles for the text views.
221 val domainTextView = alertDialog.findViewById<TextView>(R.id.domain)!!
222 val ipAddressesTextView = alertDialog.findViewById<TextView>(R.id.ip_addresses)!!
223 val issuedToCNameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_cname)!!
224 val issuedToONameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_oname)!!
225 val issuedToUNameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_uname)!!
226 val issuedByCNameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_cname)!!
227 val issuedByONameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_oname)!!
228 val issuedByUNameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_uname)!!
229 val startDateTextView = alertDialog.findViewById<TextView>(R.id.start_date)!!
230 val endDateTextView = alertDialog.findViewById<TextView>(R.id.end_date)!!
233 val domainLabel = getString(R.string.domain_label) + " "
234 val ipAddressesLabel = getString(R.string.ip_addresses) + " "
235 val cNameLabel = getString(R.string.common_name) + " "
236 val oNameLabel = getString(R.string.organization) + " "
237 val uNameLabel = getString(R.string.organizational_unit) + " "
238 val startDateLabel = getString(R.string.start_date) + " "
239 val endDateLabel = getString(R.string.end_date) + " "
241 // Create spannable string builders for each text view that needs multiple colors of text.
242 val domainStringBuilder = SpannableStringBuilder(domainLabel + domainString)
243 val ipAddressesStringBuilder = SpannableStringBuilder(ipAddressesLabel + ipAddresses)
244 val issuedToCNameStringBuilder = SpannableStringBuilder(cNameLabel + issuedToCName)
245 val issuedToONameStringBuilder = SpannableStringBuilder(oNameLabel + issuedToOName)
246 val issuedToUNameStringBuilder = SpannableStringBuilder(uNameLabel + issuedToUName)
247 val issuedByCNameStringBuilder = SpannableStringBuilder(cNameLabel + issuedByCName)
248 val issuedByONameStringBuilder = SpannableStringBuilder(oNameLabel + issuedByOName)
249 val issuedByUNameStringBuilder = SpannableStringBuilder(uNameLabel + issuedByUName)
250 val startDateStringBuilder = SpannableStringBuilder(startDateLabel + DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(startDate))
251 val endDateStringBuilder = SpannableStringBuilder(endDateLabel + DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(endDate))
253 // Define the color spans.
254 val blueColorSpan: ForegroundColorSpan
255 val redColorSpan: ForegroundColorSpan
257 // Get the current theme status.
258 val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
260 // Set the color spans according to the theme. The deprecated `getColor()` must be used until the minimum API >= 23.
261 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
262 @Suppress("DEPRECATION")
263 blueColorSpan = ForegroundColorSpan(resources.getColor(R.color.blue_700))
264 @Suppress("DEPRECATION")
265 redColorSpan = ForegroundColorSpan(resources.getColor(R.color.red_a700))
267 @Suppress("DEPRECATION")
268 blueColorSpan = ForegroundColorSpan(resources.getColor(R.color.violet_700))
269 @Suppress("DEPRECATION")
270 redColorSpan = ForegroundColorSpan(resources.getColor(R.color.red_900))
273 // Format the domain string and issued to CName colors.
274 if (domainString == issuedToCName) { // The domain and issued to CName match.
275 // Set the strings to be blue.
276 domainStringBuilder.setSpan(blueColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
277 issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
278 } else if (issuedToCName.startsWith("*.")) { // The issued to CName begins with a wildcard.
279 // Remove the initial `*.`.
280 val baseCertificateDomain = issuedToCName.substring(2)
282 // Setup a copy of the domain string to test subdomains.
283 var domainStringSubdomain = domainString
285 // Define a domain names match variable.
286 var domainNamesMatch = false
288 // Check all the subdomains against the base certificate domain.
289 while (!domainNamesMatch && domainStringSubdomain.contains(".")) { // Stop checking if we know that the domain names match or if we run out of subdomains.
290 // Test the subdomain against the base certificate domain.
291 if (domainStringSubdomain == baseCertificateDomain) {
292 domainNamesMatch = true
295 // Strip out the lowest subdomain.
296 domainStringSubdomain = domainStringSubdomain.substring(domainStringSubdomain.indexOf(".") + 1)
299 // Format the domain and issued to CName.
300 if (domainNamesMatch) { // The domain is a subdomain of the wildcard certificate.
301 // Set the strings to be blue.
302 domainStringBuilder.setSpan(blueColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
303 issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
304 } else { // The domain is not a subdomain of the wildcard certificate.
305 // Set the string to be red.
306 domainStringBuilder.setSpan(redColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
307 issuedToCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
309 } else { // The strings do not match and issued to CName does not begin with a wildcard.
310 // Set the strings to be red.
311 domainStringBuilder.setSpan(redColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
312 issuedToCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
315 // 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.
316 ipAddressesStringBuilder.setSpan(blueColorSpan, ipAddressesLabel.length, ipAddressesStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
317 issuedToONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length, issuedToONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
318 issuedToUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length, issuedToUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
319 issuedByCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedByCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
320 issuedByONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length, issuedByONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
321 issuedByUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length, issuedByUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
323 // Get the current date.
324 val currentDate = Calendar.getInstance().time
326 // Format the start date color. `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
327 if (startDate.after(currentDate)) { // The certificate start date is in the future.
328 startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
329 } else { // The certificate start date is in the past.
330 startDateStringBuilder.setSpan(blueColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
333 // Format the end date color. `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
334 if (endDate.before(currentDate)) { // The certificate end date is in the past.
335 endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
336 } else { // The certificate end date is in the future.
337 endDateStringBuilder.setSpan(blueColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
340 // Display the strings.
341 domainTextView.text = domainStringBuilder
342 ipAddressesTextView.text = ipAddressesStringBuilder
343 issuedToCNameTextView.text = issuedToCNameStringBuilder
344 issuedToONameTextView.text = issuedToONameStringBuilder
345 issuedToUNameTextView.text = issuedToUNameStringBuilder
346 issuedByCNameTextView.text = issuedByCNameStringBuilder
347 issuedByONameTextView.text = issuedByONameStringBuilder
348 issuedByUNameTextView.text = issuedByUNameStringBuilder
349 startDateTextView.text = startDateStringBuilder
350 endDateTextView.text = endDateStringBuilder
352 // Return the alert dialog.
354 } else { // The website is not encrypted.
356 dialogBuilder.setTitle(R.string.unencrypted_website)
359 dialogBuilder.setView(R.layout.unencrypted_website_dialog)
361 // Create an alert dialog from the builder.
362 val alertDialog = dialogBuilder.create()
364 // Disable screenshots if not allowed.
365 if (!allowScreenshots) {
366 // Disable screenshots.
367 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
370 // Return the alert dialog.
375 override fun onSaveInstanceState(savedInstanceState: Bundle) {
376 // Run the default commands.
377 super.onSaveInstanceState(savedInstanceState)
379 // Save the common class variables.
380 savedInstanceState.putBoolean(HAS_SSL_CERTIFICATE, hasSslCertificate)
382 // Save the SSL certificate strings if they exist.
383 if (hasSslCertificate) {
384 savedInstanceState.putString(DOMAIN, domainString)
385 savedInstanceState.putString(IP_ADDRESSES, ipAddresses)
386 savedInstanceState.putString(ISSUED_TO_CNAME, issuedToCName)
387 savedInstanceState.putString(ISSUED_TO_ONAME, issuedToOName)
388 savedInstanceState.putString(ISSUED_TO_UNAME, issuedToUName)
389 savedInstanceState.putString(ISSUED_BY_CNAME, issuedByCName)
390 savedInstanceState.putString(ISSUED_BY_ONAME, issuedByOName)
391 savedInstanceState.putString(ISSUED_BY_UNAME, issuedByUName)
392 savedInstanceState.putLong(START_DATE, startDate.time)
393 savedInstanceState.putLong(END_DATE, endDate.time)