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
45 import java.io.ByteArrayOutputStream
47 import java.text.DateFormat
48 import java.util.Calendar
51 // Define the class constants.
52 private const val WEBVIEW_FRAGMENT_ID = "webview_fragment_id"
53 private const val HAS_SSL_CERTIFICATE = "has_ssl_certificate"
54 private const val FAVORITE_ICON = "favorite_icon"
55 private const val DOMAIN = "domain"
56 private const val IP_ADDRESSES = "ip_addresses"
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 ISSUED_BY_CNAME = "issued_by_cname"
61 private const val ISSUED_BY_ONAME = "issued_by_oname"
62 private const val ISSUED_BY_UNAME = "issued_by_uname"
63 private const val START_DATE = "start_date"
64 private const val END_DATE = "end_date"
66 class ViewSslCertificateDialog : DialogFragment() {
67 // Define the class variables.
68 private var hasSslCertificate: Boolean = false
70 // Declare the class variables.
71 private lateinit var favoriteIconDrawable: Drawable
72 private lateinit var domainString: String
73 private lateinit var ipAddresses: String
74 private lateinit var issuedToCName: String
75 private lateinit var issuedToOName: String
76 private lateinit var issuedToUName: String
77 private lateinit var issuedByCName: String
78 private lateinit var issuedByOName: String
79 private lateinit var issuedByUName: String
80 private lateinit var startDate: Date
81 private lateinit var endDate: Date
83 // Declare the class views.
84 private lateinit var nestedScrollWebView: NestedScrollWebView
87 // `@JvmStatic` will no longer be required once all the code has transitioned to Kotlin.
89 fun displayDialog(webViewFragmentId: Long): ViewSslCertificateDialog {
90 // Create an arguments bundle.
91 val argumentsBundle = Bundle()
93 // Store the WebView fragment ID in the bundle.
94 argumentsBundle.putLong(WEBVIEW_FRAGMENT_ID, webViewFragmentId)
96 // Create a new instance of the view SSL certificate dialog.
97 val viewSslCertificateDialog = ViewSslCertificateDialog()
99 // Add the bundle to the new dialog.
100 viewSslCertificateDialog.arguments = argumentsBundle
102 // Return the new dialog.
103 return viewSslCertificateDialog
107 // `@SuppressLint("InflateParams")` removes the warning about using `null` as the parent view group when inflating the alert dialog.
108 @SuppressLint("InflateParams")
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.webViewPagerAdapter.getPositionForId(requireArguments().getLong(WEBVIEW_FRAGMENT_ID))
118 // Get the WebView tab fragment.
119 val webViewTabFragment = MainWebViewActivity.webViewPagerAdapter.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 // Create a drawable version of the favorite icon.
134 favoriteIconDrawable = BitmapDrawable(resources, nestedScrollWebView.favoriteOrDefaultIcon)
136 // Populate the certificate class variables if the webpage has an SSL certificate.
137 if (hasSslCertificate) {
138 // Convert the URL to a URI.
139 val uri = Uri.parse(nestedScrollWebView.url)
141 // Extract the domain name from the URI.
142 domainString = uri.host!!
144 // Get the ip addresses from the nested scroll WebView.
145 ipAddresses = nestedScrollWebView.currentIpAddresses
147 // Get the strings from the SSL certificate.
148 issuedToCName = sslCertificate!!.issuedTo.cName
149 issuedToOName = sslCertificate.issuedTo.oName
150 issuedToUName = sslCertificate.issuedTo.uName
151 issuedByCName = sslCertificate.issuedBy.cName
152 issuedByOName = sslCertificate.issuedBy.oName
153 issuedByUName = sslCertificate.issuedBy.uName
154 startDate = sslCertificate.validNotBeforeDate
155 endDate = sslCertificate.validNotAfterDate
157 } else { // The dialog has been restarted.
158 // Get the data from the saved instance state.
159 hasSslCertificate = savedInstanceState.getBoolean(HAS_SSL_CERTIFICATE)
160 val favoriteIconByteArray = savedInstanceState.getByteArray(FAVORITE_ICON)!!
162 // Convert the favorite icon byte array to a bitmap.
163 val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
165 // Create a drawable version of the favorite icon.
166 favoriteIconDrawable = BitmapDrawable(resources, favoriteIconBitmap)
168 // Populate the certificate class variables if the webpage has an SSL certificate.
169 if (hasSslCertificate) {
170 // Populate the certificate class variables from the saved instance state.
171 domainString = savedInstanceState.getString(DOMAIN)!!
172 ipAddresses = savedInstanceState.getString(IP_ADDRESSES)!!
173 issuedToCName = savedInstanceState.getString(ISSUED_TO_CNAME)!!
174 issuedToOName = savedInstanceState.getString(ISSUED_TO_ONAME)!!
175 issuedToUName = savedInstanceState.getString(ISSUED_TO_UNAME)!!
176 issuedByCName = savedInstanceState.getString(ISSUED_BY_CNAME)!!
177 issuedByOName = savedInstanceState.getString(ISSUED_BY_ONAME)!!
178 issuedByUName = savedInstanceState.getString(ISSUED_BY_UNAME)!!
179 startDate = Date(savedInstanceState.getLong(START_DATE))
180 endDate = Date(savedInstanceState.getLong(END_DATE))
185 dialogBuilder.setIcon(favoriteIconDrawable)
187 // Set the close button listener. Using `null` as the listener closes the dialog without doing anything else.
188 dialogBuilder.setNegativeButton(R.string.close, null)
190 // Get a handle for the shared preferences.
191 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
193 // Get the screenshot preference.
194 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
196 // Check to see if the website is encrypted.
197 if (hasSslCertificate) { // The website is encrypted.
199 dialogBuilder.setTitle(R.string.ssl_certificate)
201 // Set the layout. The parent view is `null` because it will be assigned by the alert dialog.
202 dialogBuilder.setView(layoutInflater.inflate(R.layout.view_ssl_certificate_dialog, null))
204 // Create an alert dialog from the builder.
205 val alertDialog = dialogBuilder.create()
207 // Disable screenshots if not allowed.
208 if (!allowScreenshots) {
209 // Disable screenshots.
210 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
213 // The alert dialog must be shown before items in the layout can be modified.
216 // Get handles for the text views.
217 val domainTextView = alertDialog.findViewById<TextView>(R.id.domain)!!
218 val ipAddressesTextView = alertDialog.findViewById<TextView>(R.id.ip_addresses)!!
219 val issuedToCNameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_cname)!!
220 val issuedToONameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_oname)!!
221 val issuedToUNameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_uname)!!
222 val issuedByCNameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_cname)!!
223 val issuedByONameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_oname)!!
224 val issuedByUNameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_uname)!!
225 val startDateTextView = alertDialog.findViewById<TextView>(R.id.start_date)!!
226 val endDateTextView = alertDialog.findViewById<TextView>(R.id.end_date)!!
229 val domainLabel = getString(R.string.domain_label) + " "
230 val ipAddressesLabel = getString(R.string.ip_addresses) + " "
231 val cNameLabel = getString(R.string.common_name) + " "
232 val oNameLabel = getString(R.string.organization) + " "
233 val uNameLabel = getString(R.string.organizational_unit) + " "
234 val startDateLabel = getString(R.string.start_date) + " "
235 val endDateLabel = getString(R.string.end_date) + " "
237 // Create spannable string builders for each text view that needs multiple colors of text.
238 val domainStringBuilder = SpannableStringBuilder(domainLabel + domainString)
239 val ipAddressesStringBuilder = SpannableStringBuilder(ipAddressesLabel + ipAddresses)
240 val issuedToCNameStringBuilder = SpannableStringBuilder(cNameLabel + issuedToCName)
241 val issuedToONameStringBuilder = SpannableStringBuilder(oNameLabel + issuedToOName)
242 val issuedToUNameStringBuilder = SpannableStringBuilder(uNameLabel + issuedToUName)
243 val issuedByCNameStringBuilder = SpannableStringBuilder(cNameLabel + issuedByCName)
244 val issuedByONameStringBuilder = SpannableStringBuilder(oNameLabel + issuedByOName)
245 val issuedByUNameStringBuilder = SpannableStringBuilder(uNameLabel + issuedByUName)
246 val startDateStringBuilder = SpannableStringBuilder(startDateLabel + DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(startDate))
247 val endDateStringBuilder = SpannableStringBuilder(endDateLabel + DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(endDate))
249 // Define the color spans.
250 val blueColorSpan: ForegroundColorSpan
251 val redColorSpan: ForegroundColorSpan
253 // Get the current theme status.
254 val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
256 // Set the color spans according to the theme. The deprecated `getColor()` must be used until the minimum API >= 23.
257 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
258 @Suppress("DEPRECATION")
259 blueColorSpan = ForegroundColorSpan(resources.getColor(R.color.blue_700))
260 @Suppress("DEPRECATION")
261 redColorSpan = ForegroundColorSpan(resources.getColor(R.color.red_a700))
263 @Suppress("DEPRECATION")
264 blueColorSpan = ForegroundColorSpan(resources.getColor(R.color.violet_700))
265 @Suppress("DEPRECATION")
266 redColorSpan = ForegroundColorSpan(resources.getColor(R.color.red_900))
269 // Format the domain string and issued to CName colors.
270 if (domainString == issuedToCName) { // The domain and issued to CName match.
271 // Set the strings to be blue.
272 domainStringBuilder.setSpan(blueColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
273 issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
274 } else if (issuedToCName.startsWith("*.")) { // The issued to CName begins with a wildcard.
275 // Remove the initial `*.`.
276 val baseCertificateDomain = issuedToCName.substring(2)
278 // Setup a copy of the domain string to test subdomains.
279 var domainStringSubdomain = domainString
281 // Define a domain names match variable.
282 var domainNamesMatch = false
284 // Check all the subdomains against the base certificate domain.
285 while (!domainNamesMatch && domainStringSubdomain.contains(".")) { // Stop checking if we know that the domain names match or if we run out of subdomains.
286 // Test the subdomain against the base certificate domain.
287 if (domainStringSubdomain == baseCertificateDomain) {
288 domainNamesMatch = true
291 // Strip out the lowest subdomain.
292 domainStringSubdomain = domainStringSubdomain.substring(domainStringSubdomain.indexOf(".") + 1)
295 // Format the domain and issued to CName.
296 if (domainNamesMatch) { // The domain is a subdomain of the wildcard certificate.
297 // Set the strings to be blue.
298 domainStringBuilder.setSpan(blueColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
299 issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
300 } else { // The domain is not a subdomain of the wildcard certificate.
301 // Set the string to be red.
302 domainStringBuilder.setSpan(redColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
303 issuedToCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
305 } else { // The strings do not match and issued to CName does not begin with a wildcard.
306 // Set the strings to be red.
307 domainStringBuilder.setSpan(redColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
308 issuedToCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
311 // 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.
312 ipAddressesStringBuilder.setSpan(blueColorSpan, ipAddressesLabel.length, ipAddressesStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
313 issuedToONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length, issuedToONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
314 issuedToUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length, issuedToUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
315 issuedByCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedByCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
316 issuedByONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length, issuedByONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
317 issuedByUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length, issuedByUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
319 // Get the current date.
320 val currentDate = Calendar.getInstance().time
322 // Format the start date color. `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
323 if (startDate.after(currentDate)) { // The certificate start date is in the future.
324 startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
325 } else { // The certificate start date is in the past.
326 startDateStringBuilder.setSpan(blueColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
329 // Format the end date color. `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
330 if (endDate.before(currentDate)) { // The certificate end date is in the past.
331 endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
332 } else { // The certificate end date is in the future.
333 endDateStringBuilder.setSpan(blueColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
336 // Display the strings.
337 domainTextView.text = domainStringBuilder
338 ipAddressesTextView.text = ipAddressesStringBuilder
339 issuedToCNameTextView.text = issuedToCNameStringBuilder
340 issuedToONameTextView.text = issuedToONameStringBuilder
341 issuedToUNameTextView.text = issuedToUNameStringBuilder
342 issuedByCNameTextView.text = issuedByCNameStringBuilder
343 issuedByONameTextView.text = issuedByONameStringBuilder
344 issuedByUNameTextView.text = issuedByUNameStringBuilder
345 startDateTextView.text = startDateStringBuilder
346 endDateTextView.text = endDateStringBuilder
348 // Return the alert dialog.
350 } else { // The website is not encrypted.
352 dialogBuilder.setTitle(R.string.unencrypted_website)
354 // Set the Layout. The parent view is `null` because it will be assigned by the alert dialog.
355 dialogBuilder.setView(layoutInflater.inflate(R.layout.unencrypted_website_dialog, null))
357 // Create an alert dialog from the builder.
358 val alertDialog = dialogBuilder.create()
360 // Disable screenshots if not allowed.
361 if (!allowScreenshots) {
362 // Disable screenshots.
363 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
366 // Return the alert dialog.
371 override fun onSaveInstanceState(savedInstanceState: Bundle) {
372 // Run the default commands.
373 super.onSaveInstanceState(savedInstanceState)
375 // Get the favorite icon bitmap drawable.
376 val favoriteIconBitmapDrawable = favoriteIconDrawable as BitmapDrawable
378 // Get the favorite icon bitmap.
379 val favoriteIconBitmap = favoriteIconBitmapDrawable.bitmap
381 // Create a favorite icon byte array output stream.
382 val favoriteIconByteArrayOutputStream = ByteArrayOutputStream()
384 // 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).
385 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream)
387 // Convert the favorite icon byte array output stream to a byte array.
388 val favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray()
390 // Save the common class variables.
391 savedInstanceState.putBoolean(HAS_SSL_CERTIFICATE, hasSslCertificate)
392 savedInstanceState.putByteArray(FAVORITE_ICON, favoriteIconByteArray)
394 // Save the SSL certificate strings if they exist.
395 if (hasSslCertificate) {
396 savedInstanceState.putString(DOMAIN, domainString)
397 savedInstanceState.putString(IP_ADDRESSES, ipAddresses)
398 savedInstanceState.putString(ISSUED_TO_CNAME, issuedToCName)
399 savedInstanceState.putString(ISSUED_TO_ONAME, issuedToOName)
400 savedInstanceState.putString(ISSUED_TO_UNAME, issuedToUName)
401 savedInstanceState.putString(ISSUED_BY_CNAME, issuedByCName)
402 savedInstanceState.putString(ISSUED_BY_ONAME, issuedByOName)
403 savedInstanceState.putString(ISSUED_BY_UNAME, issuedByUName)
404 savedInstanceState.putLong(START_DATE, startDate.time)
405 savedInstanceState.putLong(END_DATE, endDate.time)