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.net.Uri
29 import android.os.Bundle
30 import android.text.SpannableStringBuilder
31 import android.text.Spanned
32 import android.text.style.ForegroundColorSpan
33 import android.view.WindowManager
34 import android.widget.TextView
36 import androidx.appcompat.app.AlertDialog
37 import androidx.fragment.app.DialogFragment
38 import androidx.preference.PreferenceManager
40 import com.stoutner.privacybrowser.R
41 import com.stoutner.privacybrowser.activities.MainWebViewActivity
42 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 FAVORITE_ICON_BYTE_ARRAY = "favorite_icon_byte_array"
53 private const val HAS_SSL_CERTIFICATE = "has_ssl_certificate"
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 domainString: String
71 private lateinit var ipAddresses: String
72 private lateinit var issuedToCName: String
73 private lateinit var issuedToOName: String
74 private lateinit var issuedToUName: String
75 private lateinit var issuedByCName: String
76 private lateinit var issuedByOName: String
77 private lateinit var issuedByUName: String
78 private lateinit var startDate: Date
79 private lateinit var endDate: Date
81 // Declare the class views.
82 private lateinit var nestedScrollWebView: NestedScrollWebView
85 // `@JvmStatic` will no longer be required once all the code has transitioned to Kotlin.
87 fun displayDialog(webViewFragmentId: Long, favoriteIconBitmap: Bitmap): ViewSslCertificateDialog {
88 // Create an arguments bundle.
89 val argumentsBundle = Bundle()
91 // Create a favorite icon byte array output stream.
92 val favoriteIconByteArrayOutputStream = ByteArrayOutputStream()
94 // 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).
95 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream)
97 // Convert the favorite icon byte array output stream to a byte array.
98 val favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray()
100 // Store the arguments in the bundle.
101 argumentsBundle.putLong(WEBVIEW_FRAGMENT_ID, webViewFragmentId)
102 argumentsBundle.putByteArray(FAVORITE_ICON_BYTE_ARRAY, favoriteIconByteArray)
104 // Create a new instance of the view SSL certificate dialog.
105 val viewSslCertificateDialog = ViewSslCertificateDialog()
107 // Add the bundle to the new dialog.
108 viewSslCertificateDialog.arguments = argumentsBundle
110 // Return the new dialog.
111 return viewSslCertificateDialog
115 // `@SuppressLint("InflateParams")` removes the warning about using `null` as the parent view group when inflating the alert dialog.
116 @SuppressLint("InflateParams")
117 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
118 // Use a builder to create the alert dialog.
119 val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
121 // Populate the class variables.
122 if (savedInstanceState == null) { // The dialog is starting for the first time.
123 // Get the current position of this WebView fragment.
124 val webViewPosition = MainWebViewActivity.webViewPagerAdapter.getPositionForId(requireArguments().getLong(WEBVIEW_FRAGMENT_ID))
126 // Get the WebView tab fragment.
127 val webViewTabFragment = MainWebViewActivity.webViewPagerAdapter.getPageFragment(webViewPosition)
129 // Get the fragment view.
130 val fragmentView = webViewTabFragment.requireView()
132 // Get a handle for the current nested scroll WebView.
133 nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview)
135 // Get the SSL certificate.
136 val sslCertificate = nestedScrollWebView.certificate
138 // Store the status of the SSL certificate.
139 hasSslCertificate = sslCertificate != null
141 // Populate the certificate class variables if the webpage has an SSL certificate.
142 if (hasSslCertificate) {
143 // Convert the URL to a URI.
144 val uri = Uri.parse(nestedScrollWebView.url)
146 // Extract the domain name from the URI.
147 domainString = uri.host!!
149 // Get the ip addresses from the nested scroll WebView.
150 ipAddresses = nestedScrollWebView.currentIpAddresses
152 // Get the strings from the SSL certificate.
153 issuedToCName = sslCertificate!!.issuedTo.cName
154 issuedToOName = sslCertificate.issuedTo.oName
155 issuedToUName = sslCertificate.issuedTo.uName
156 issuedByCName = sslCertificate.issuedBy.cName
157 issuedByOName = sslCertificate.issuedBy.oName
158 issuedByUName = sslCertificate.issuedBy.uName
159 startDate = sslCertificate.validNotBeforeDate
160 endDate = sslCertificate.validNotAfterDate
162 } else { // The dialog has been restarted.
163 // Get the data from the saved instance state.
164 hasSslCertificate = savedInstanceState.getBoolean(HAS_SSL_CERTIFICATE)
166 // Populate the certificate class variables if the webpage has an SSL certificate.
167 if (hasSslCertificate) {
168 // Populate the certificate class variables from the saved instance state.
169 domainString = savedInstanceState.getString(DOMAIN)!!
170 ipAddresses = savedInstanceState.getString(IP_ADDRESSES)!!
171 issuedToCName = savedInstanceState.getString(ISSUED_TO_CNAME)!!
172 issuedToOName = savedInstanceState.getString(ISSUED_TO_ONAME)!!
173 issuedToUName = savedInstanceState.getString(ISSUED_TO_UNAME)!!
174 issuedByCName = savedInstanceState.getString(ISSUED_BY_CNAME)!!
175 issuedByOName = savedInstanceState.getString(ISSUED_BY_ONAME)!!
176 issuedByUName = savedInstanceState.getString(ISSUED_BY_UNAME)!!
177 startDate = Date(savedInstanceState.getLong(START_DATE))
178 endDate = Date(savedInstanceState.getLong(END_DATE))
182 // Get the favorite icon byte array from the arguments.
183 val favoriteIconByteArray = requireArguments().getByteArray(FAVORITE_ICON_BYTE_ARRAY)!!
185 // Convert the favorite icon byte array to a bitmap.
186 val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
188 // Create a drawable version of the favorite icon.
189 val favoriteIconDrawable = BitmapDrawable(resources, favoriteIconBitmap)
192 dialogBuilder.setIcon(favoriteIconDrawable)
194 // Set the close button listener. Using `null` as the listener closes the dialog without doing anything else.
195 dialogBuilder.setNegativeButton(R.string.close, null)
197 // Get a handle for the shared preferences.
198 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
200 // Get the screenshot preference.
201 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
203 // Check to see if the website is encrypted.
204 if (hasSslCertificate) { // The website is encrypted.
206 dialogBuilder.setTitle(R.string.ssl_certificate)
208 // Set the layout. The parent view is `null` because it will be assigned by the alert dialog.
209 dialogBuilder.setView(layoutInflater.inflate(R.layout.view_ssl_certificate_dialog, null))
211 // Create an alert dialog from the builder.
212 val alertDialog = dialogBuilder.create()
214 // Disable screenshots if not allowed.
215 if (!allowScreenshots) {
216 // Disable screenshots.
217 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
220 // The alert dialog must be shown before items in the layout can be modified.
223 // Get handles for the text views.
224 val domainTextView = alertDialog.findViewById<TextView>(R.id.domain)!!
225 val ipAddressesTextView = alertDialog.findViewById<TextView>(R.id.ip_addresses)!!
226 val issuedToCNameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_cname)!!
227 val issuedToONameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_oname)!!
228 val issuedToUNameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_uname)!!
229 val issuedByCNameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_cname)!!
230 val issuedByONameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_oname)!!
231 val issuedByUNameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_uname)!!
232 val startDateTextView = alertDialog.findViewById<TextView>(R.id.start_date)!!
233 val endDateTextView = alertDialog.findViewById<TextView>(R.id.end_date)!!
236 val domainLabel = getString(R.string.domain_label) + " "
237 val ipAddressesLabel = getString(R.string.ip_addresses) + " "
238 val cNameLabel = getString(R.string.common_name) + " "
239 val oNameLabel = getString(R.string.organization) + " "
240 val uNameLabel = getString(R.string.organizational_unit) + " "
241 val startDateLabel = getString(R.string.start_date) + " "
242 val endDateLabel = getString(R.string.end_date) + " "
244 // Create spannable string builders for each text view that needs multiple colors of text.
245 val domainStringBuilder = SpannableStringBuilder(domainLabel + domainString)
246 val ipAddressesStringBuilder = SpannableStringBuilder(ipAddressesLabel + ipAddresses)
247 val issuedToCNameStringBuilder = SpannableStringBuilder(cNameLabel + issuedToCName)
248 val issuedToONameStringBuilder = SpannableStringBuilder(oNameLabel + issuedToOName)
249 val issuedToUNameStringBuilder = SpannableStringBuilder(uNameLabel + issuedToUName)
250 val issuedByCNameStringBuilder = SpannableStringBuilder(cNameLabel + issuedByCName)
251 val issuedByONameStringBuilder = SpannableStringBuilder(oNameLabel + issuedByOName)
252 val issuedByUNameStringBuilder = SpannableStringBuilder(uNameLabel + issuedByUName)
253 val startDateStringBuilder = SpannableStringBuilder(startDateLabel + DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(startDate))
254 val endDateStringBuilder = SpannableStringBuilder(endDateLabel + DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(endDate))
256 // Define the color spans.
257 val blueColorSpan: ForegroundColorSpan
258 val redColorSpan: ForegroundColorSpan
260 // Get the current theme status.
261 val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
263 // Set the color spans according to the theme. The deprecated `getColor()` must be used until the minimum API >= 23.
264 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
265 @Suppress("DEPRECATION")
266 blueColorSpan = ForegroundColorSpan(resources.getColor(R.color.blue_700))
267 @Suppress("DEPRECATION")
268 redColorSpan = ForegroundColorSpan(resources.getColor(R.color.red_a700))
270 @Suppress("DEPRECATION")
271 blueColorSpan = ForegroundColorSpan(resources.getColor(R.color.violet_700))
272 @Suppress("DEPRECATION")
273 redColorSpan = ForegroundColorSpan(resources.getColor(R.color.red_900))
276 // Format the domain string and issued to CName colors.
277 if (domainString == issuedToCName) { // The domain and issued to CName match.
278 // Set the strings to be blue.
279 domainStringBuilder.setSpan(blueColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
280 issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
281 } else if (issuedToCName.startsWith("*.")) { // The issued to CName begins with a wildcard.
282 // Remove the initial `*.`.
283 val baseCertificateDomain = issuedToCName.substring(2)
285 // Setup a copy of the domain string to test subdomains.
286 var domainStringSubdomain = domainString
288 // Define a domain names match variable.
289 var domainNamesMatch = false
291 // Check all the subdomains against the base certificate domain.
292 while (!domainNamesMatch && domainStringSubdomain.contains(".")) { // Stop checking if we know that the domain names match or if we run out of subdomains.
293 // Test the subdomain against the base certificate domain.
294 if (domainStringSubdomain == baseCertificateDomain) {
295 domainNamesMatch = true
298 // Strip out the lowest subdomain.
299 domainStringSubdomain = domainStringSubdomain.substring(domainStringSubdomain.indexOf(".") + 1)
302 // Format the domain and issued to CName.
303 if (domainNamesMatch) { // The domain is a subdomain of the wildcard certificate.
304 // Set the strings to be blue.
305 domainStringBuilder.setSpan(blueColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
306 issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
307 } else { // The domain is not a subdomain of the wildcard certificate.
308 // Set the string to be red.
309 domainStringBuilder.setSpan(redColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
310 issuedToCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
312 } else { // The strings do not match and issued to CName does not begin with a wildcard.
313 // Set the strings to be red.
314 domainStringBuilder.setSpan(redColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
315 issuedToCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
318 // 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.
319 ipAddressesStringBuilder.setSpan(blueColorSpan, ipAddressesLabel.length, ipAddressesStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
320 issuedToONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length, issuedToONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
321 issuedToUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length, issuedToUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
322 issuedByCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedByCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
323 issuedByONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length, issuedByONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
324 issuedByUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length, issuedByUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
326 // Get the current date.
327 val currentDate = Calendar.getInstance().time
329 // Format the start date color. `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
330 if (startDate.after(currentDate)) { // The certificate start date is in the future.
331 startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
332 } else { // The certificate start date is in the past.
333 startDateStringBuilder.setSpan(blueColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
336 // Format the end date color. `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
337 if (endDate.before(currentDate)) { // The certificate end date is in the past.
338 endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
339 } else { // The certificate end date is in the future.
340 endDateStringBuilder.setSpan(blueColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
343 // Display the strings.
344 domainTextView.text = domainStringBuilder
345 ipAddressesTextView.text = ipAddressesStringBuilder
346 issuedToCNameTextView.text = issuedToCNameStringBuilder
347 issuedToONameTextView.text = issuedToONameStringBuilder
348 issuedToUNameTextView.text = issuedToUNameStringBuilder
349 issuedByCNameTextView.text = issuedByCNameStringBuilder
350 issuedByONameTextView.text = issuedByONameStringBuilder
351 issuedByUNameTextView.text = issuedByUNameStringBuilder
352 startDateTextView.text = startDateStringBuilder
353 endDateTextView.text = endDateStringBuilder
355 // Return the alert dialog.
357 } else { // The website is not encrypted.
359 dialogBuilder.setTitle(R.string.unencrypted_website)
361 // Set the Layout. The parent view is `null` because it will be assigned by the alert dialog.
362 dialogBuilder.setView(layoutInflater.inflate(R.layout.unencrypted_website_dialog, null))
364 // Create an alert dialog from the builder.
365 val alertDialog = dialogBuilder.create()
367 // Disable screenshots if not allowed.
368 if (!allowScreenshots) {
369 // Disable screenshots.
370 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
373 // Return the alert dialog.
378 override fun onSaveInstanceState(savedInstanceState: Bundle) {
379 // Run the default commands.
380 super.onSaveInstanceState(savedInstanceState)
382 // Save the common class variables.
383 savedInstanceState.putBoolean(HAS_SSL_CERTIFICATE, hasSslCertificate)
385 // Save the SSL certificate strings if they exist.
386 if (hasSslCertificate) {
387 savedInstanceState.putString(DOMAIN, domainString)
388 savedInstanceState.putString(IP_ADDRESSES, ipAddresses)
389 savedInstanceState.putString(ISSUED_TO_CNAME, issuedToCName)
390 savedInstanceState.putString(ISSUED_TO_ONAME, issuedToOName)
391 savedInstanceState.putString(ISSUED_TO_UNAME, issuedToUName)
392 savedInstanceState.putString(ISSUED_BY_CNAME, issuedByCName)
393 savedInstanceState.putString(ISSUED_BY_ONAME, issuedByOName)
394 savedInstanceState.putString(ISSUED_BY_UNAME, issuedByUName)
395 savedInstanceState.putLong(START_DATE, startDate.time)
396 savedInstanceState.putLong(END_DATE, endDate.time)