2 * Copyright © 2016-2021 Soren Stoutner <soren@stoutner.com>.
4 * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
6 * Privacy Browser 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 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. If not, see <http://www.gnu.org/licenses/>.
20 package com.stoutner.privacybrowser.dialogs
22 import android.annotation.SuppressLint
23 import android.app.Dialog
24 import android.content.res.Configuration
25 import android.graphics.Bitmap
26 import android.graphics.BitmapFactory
27 import android.graphics.drawable.BitmapDrawable
28 import android.graphics.drawable.Drawable
29 import android.net.Uri
30 import android.os.Bundle
31 import android.text.SpannableStringBuilder
32 import android.text.Spanned
33 import android.text.style.ForegroundColorSpan
34 import android.view.WindowManager
35 import android.widget.TextView
37 import androidx.appcompat.app.AlertDialog
38 import androidx.fragment.app.DialogFragment
39 import androidx.preference.PreferenceManager
41 import com.stoutner.privacybrowser.R
42 import com.stoutner.privacybrowser.activities.MainWebViewActivity
43 import com.stoutner.privacybrowser.views.NestedScrollWebView
44 import java.io.ByteArrayOutputStream
46 import java.text.DateFormat
47 import java.util.Calendar
50 // Define the class constants.
51 private const val WEBVIEW_FRAGMENT_ID = "webview_fragment_id"
52 private const val HAS_SSL_CERTIFICATE = "has_ssl_certificate"
53 private const val FAVORITE_ICON = "favorite_icon"
54 private const val DOMAIN = "domain"
55 private const val IP_ADDRESSES = "ip_addresses"
56 private const val ISSUED_TO_CNAME = "issued_to_cname"
57 private const val ISSUED_TO_ONAME = "issued_to_oname"
58 private const val ISSUED_TO_UNAME = "issued_to_uname"
59 private const val ISSUED_BY_CNAME = "issued_by_cname"
60 private const val ISSUED_BY_ONAME = "issued_by_oname"
61 private const val ISSUED_BY_UNAME = "issued_by_uname"
62 private const val START_DATE = "start_date"
63 private const val END_DATE = "end_date"
65 class ViewSslCertificateDialog : DialogFragment() {
66 // Define the class variables.
67 private var hasSslCertificate: Boolean = false
69 // Declare the class variables.
70 private lateinit var favoriteIconDrawable: Drawable
71 private lateinit var domainString: String
72 private lateinit var ipAddresses: String
73 private lateinit var issuedToCName: String
74 private lateinit var issuedToOName: String
75 private lateinit var issuedToUName: String
76 private lateinit var issuedByCName: String
77 private lateinit var issuedByOName: String
78 private lateinit var issuedByUName: String
79 private lateinit var startDate: Date
80 private lateinit var endDate: Date
82 // Declare the class views.
83 private lateinit var nestedScrollWebView: NestedScrollWebView
86 // `@JvmStatic` will no longer be required once all the code has transitioned to Kotlin.
88 fun displayDialog(webViewFragmentId: Long): ViewSslCertificateDialog {
89 // Create an arguments bundle.
90 val argumentsBundle = Bundle()
92 // Store the WebView fragment ID in the bundle.
93 argumentsBundle.putLong(WEBVIEW_FRAGMENT_ID, webViewFragmentId)
95 // Create a new instance of the view SSL certificate dialog.
96 val viewSslCertificateDialog = ViewSslCertificateDialog()
98 // Add the bundle to the new dialog.
99 viewSslCertificateDialog.arguments = argumentsBundle
101 // Return the new dialog.
102 return viewSslCertificateDialog
106 // `@SuppressLint("InflateParams")` removes the warning about using `null` as the parent view group when inflating the alert dialog.
107 @SuppressLint("InflateParams")
108 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
109 // Use a builder to create the alert dialog.
110 val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
112 // Populate the class variables.
113 if (savedInstanceState == null) { // The dialog is starting for the first time.
114 // Get the current position of this WebView fragment.
115 val webViewPosition = MainWebViewActivity.webViewPagerAdapter.getPositionForId(requireArguments().getLong(WEBVIEW_FRAGMENT_ID))
117 // Get the WebView tab fragment.
118 val webViewTabFragment = MainWebViewActivity.webViewPagerAdapter.getPageFragment(webViewPosition)
120 // Get the fragment view.
121 val fragmentView = webViewTabFragment.requireView()
123 // Get a handle for the current nested scroll WebView.
124 nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview)
126 // Get the SSL certificate.
127 val sslCertificate = nestedScrollWebView.certificate
129 // Store the status of the SSL certificate.
130 hasSslCertificate = sslCertificate != null
132 // Create a drawable version of the favorite icon.
133 favoriteIconDrawable = BitmapDrawable(resources, nestedScrollWebView.favoriteOrDefaultIcon)
135 // Populate the certificate class variables if the webpage has an SSL certificate.
136 if (hasSslCertificate) {
137 // Convert the URL to a URI.
138 val uri = Uri.parse(nestedScrollWebView.url)
140 // Extract the domain name from the URI.
141 domainString = uri.host!!
143 // Get the ip addresses from the nested scroll WebView.
144 ipAddresses = nestedScrollWebView.currentIpAddresses
146 // Get the strings from the SSL certificate.
147 issuedToCName = sslCertificate!!.issuedTo.cName
148 issuedToOName = sslCertificate.issuedTo.oName
149 issuedToUName = sslCertificate.issuedTo.uName
150 issuedByCName = sslCertificate.issuedBy.cName
151 issuedByOName = sslCertificate.issuedBy.oName
152 issuedByUName = sslCertificate.issuedBy.uName
153 startDate = sslCertificate.validNotBeforeDate
154 endDate = sslCertificate.validNotAfterDate
156 } else { // The dialog has been restarted.
157 // Get the data from the saved instance state.
158 hasSslCertificate = savedInstanceState.getBoolean(HAS_SSL_CERTIFICATE)
159 val favoriteIconByteArray = savedInstanceState.getByteArray(FAVORITE_ICON)!!
161 // Convert the favorite icon byte array to a bitmap.
162 val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
164 // Create a drawable version of the favorite icon.
165 favoriteIconDrawable = BitmapDrawable(resources, favoriteIconBitmap)
167 // Populate the certificate class variables if the webpage has an SSL certificate.
168 if (hasSslCertificate) {
169 // Populate the certificate class variables from the saved instance state.
170 domainString = savedInstanceState.getString(DOMAIN)!!
171 ipAddresses = savedInstanceState.getString(IP_ADDRESSES)!!
172 issuedToCName = savedInstanceState.getString(ISSUED_TO_CNAME)!!
173 issuedToOName = savedInstanceState.getString(ISSUED_TO_ONAME)!!
174 issuedToUName = savedInstanceState.getString(ISSUED_TO_UNAME)!!
175 issuedByCName = savedInstanceState.getString(ISSUED_BY_CNAME)!!
176 issuedByOName = savedInstanceState.getString(ISSUED_BY_ONAME)!!
177 issuedByUName = savedInstanceState.getString(ISSUED_BY_UNAME)!!
178 startDate = Date(savedInstanceState.getLong(START_DATE))
179 endDate = Date(savedInstanceState.getLong(END_DATE))
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(context)
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)
200 // Set the layout. The parent view is `null` because it will be assigned by the alert dialog.
201 dialogBuilder.setView(layoutInflater.inflate(R.layout.view_ssl_certificate_dialog, null))
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
250 val redColorSpan: ForegroundColorSpan
252 // Get the current theme status.
253 val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
255 // Set the color spans according to the theme. The deprecated `getColor()` must be used until the minimum API >= 23.
256 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
257 @Suppress("DEPRECATION")
258 blueColorSpan = ForegroundColorSpan(resources.getColor(R.color.blue_700))
259 @Suppress("DEPRECATION")
260 redColorSpan = ForegroundColorSpan(resources.getColor(R.color.red_a700))
262 @Suppress("DEPRECATION")
263 blueColorSpan = ForegroundColorSpan(resources.getColor(R.color.violet_700))
264 @Suppress("DEPRECATION")
265 redColorSpan = ForegroundColorSpan(resources.getColor(R.color.red_900))
268 // Format the domain string and issued to CName colors.
269 if (domainString == issuedToCName) { // The domain and issued to CName match.
270 // Set the strings to be blue.
271 domainStringBuilder.setSpan(blueColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
272 issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
273 } else if (issuedToCName.startsWith("*.")) { // The issued to CName begins with a wildcard.
274 // Remove the initial `*.`.
275 val baseCertificateDomain = issuedToCName.substring(2)
277 // Setup a copy of the domain string to test subdomains.
278 var domainStringSubdomain = domainString
280 // Define a domain names match variable.
281 var domainNamesMatch = false
283 // Check all the subdomains against the base certificate domain.
284 while (!domainNamesMatch && domainStringSubdomain.contains(".")) { // Stop checking if we know that the domain names match or if we run out of subdomains.
285 // Test the subdomain against the base certificate domain.
286 if (domainStringSubdomain == baseCertificateDomain) {
287 domainNamesMatch = true
290 // Strip out the lowest subdomain.
291 domainStringSubdomain = domainStringSubdomain.substring(domainStringSubdomain.indexOf(".") + 1)
294 // Format the domain and issued to CName.
295 if (domainNamesMatch) { // The domain is a subdomain of the wildcard certificate.
296 // Set the strings to be blue.
297 domainStringBuilder.setSpan(blueColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
298 issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
299 } else { // The domain is not a subdomain of the wildcard certificate.
300 // Set the string to be red.
301 domainStringBuilder.setSpan(redColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
302 issuedToCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
304 } else { // The strings do not match and issued to CName does not begin with a wildcard.
305 // Set the strings 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)
310 // 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.
311 ipAddressesStringBuilder.setSpan(blueColorSpan, ipAddressesLabel.length, ipAddressesStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
312 issuedToONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length, issuedToONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
313 issuedToUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length, issuedToUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
314 issuedByCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedByCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
315 issuedByONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length, issuedByONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
316 issuedByUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length, issuedByUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
318 // Get the current date.
319 val currentDate = Calendar.getInstance().time
321 // Format the start date color. `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
322 if (startDate.after(currentDate)) { // The certificate start date is in the future.
323 startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
324 } else { // The certificate start date is in the past.
325 startDateStringBuilder.setSpan(blueColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
328 // Format the end date color. `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
329 if (endDate.before(currentDate)) { // The certificate end date is in the past.
330 endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
331 } else { // The certificate end date is in the future.
332 endDateStringBuilder.setSpan(blueColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
335 // Display the strings.
336 domainTextView.text = domainStringBuilder
337 ipAddressesTextView.text = ipAddressesStringBuilder
338 issuedToCNameTextView.text = issuedToCNameStringBuilder
339 issuedToONameTextView.text = issuedToONameStringBuilder
340 issuedToUNameTextView.text = issuedToUNameStringBuilder
341 issuedByCNameTextView.text = issuedByCNameStringBuilder
342 issuedByONameTextView.text = issuedByONameStringBuilder
343 issuedByUNameTextView.text = issuedByUNameStringBuilder
344 startDateTextView.text = startDateStringBuilder
345 endDateTextView.text = endDateStringBuilder
347 // Return the alert dialog.
349 } else { // The website is not encrypted.
351 dialogBuilder.setTitle(R.string.unencrypted_website)
353 // Set the Layout. The parent view is `null` because it will be assigned by the alert dialog.
354 dialogBuilder.setView(layoutInflater.inflate(R.layout.unencrypted_website_dialog, null))
356 // Create an alert dialog from the builder.
357 val alertDialog = dialogBuilder.create()
359 // Disable screenshots if not allowed.
360 if (!allowScreenshots) {
361 // Disable screenshots.
362 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
365 // Return the alert dialog.
370 override fun onSaveInstanceState(savedInstanceState: Bundle) {
371 // Run the default commands.
372 super.onSaveInstanceState(savedInstanceState)
374 // Get the favorite icon bitmap drawable.
375 val favoriteIconBitmapDrawable = favoriteIconDrawable as BitmapDrawable
377 // Get the favorite icon bitmap.
378 val favoriteIconBitmap = favoriteIconBitmapDrawable.bitmap
380 // Create a favorite icon byte array output stream.
381 val favoriteIconByteArrayOutputStream = ByteArrayOutputStream()
383 // 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).
384 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream)
386 // Convert the favorite icon byte array output stream to a byte array.
387 val favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray()
389 // Save the common class variables.
390 savedInstanceState.putBoolean(HAS_SSL_CERTIFICATE, hasSslCertificate)
391 savedInstanceState.putByteArray(FAVORITE_ICON, favoriteIconByteArray)
393 // Save the SSL certificate strings if they exist.
394 if (hasSslCertificate) {
395 savedInstanceState.putString(DOMAIN, domainString)
396 savedInstanceState.putString(IP_ADDRESSES, ipAddresses)
397 savedInstanceState.putString(ISSUED_TO_CNAME, issuedToCName)
398 savedInstanceState.putString(ISSUED_TO_ONAME, issuedToOName)
399 savedInstanceState.putString(ISSUED_TO_UNAME, issuedToUName)
400 savedInstanceState.putString(ISSUED_BY_CNAME, issuedByCName)
401 savedInstanceState.putString(ISSUED_BY_ONAME, issuedByOName)
402 savedInstanceState.putString(ISSUED_BY_UNAME, issuedByUName)
403 savedInstanceState.putLong(START_DATE, startDate.time)
404 savedInstanceState.putLong(END_DATE, endDate.time)