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.drawable.BitmapDrawable
26 import android.graphics.drawable.Drawable
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.text.DateFormat
44 import java.util.Calendar
46 // Define the class constants.
47 private const val WEBVIEW_FRAGMENT_ID = "webview_fragment_id"
49 class ViewSslCertificateDialog : DialogFragment() {
51 // `@JvmStatic` will no longer be required once all the code has transitioned to Kotlin.
53 fun displayDialog(webViewFragmentId: Long): ViewSslCertificateDialog {
54 // Create an arguments bundle.
55 val argumentsBundle = Bundle()
57 // Store the WebView fragment ID in the bundle.
58 argumentsBundle.putLong(WEBVIEW_FRAGMENT_ID, webViewFragmentId)
60 // Create a new instance of the view SSL certificate dialog.
61 val viewSslCertificateDialog = ViewSslCertificateDialog()
63 // Add the bundle to the new dialog.
64 viewSslCertificateDialog.arguments = argumentsBundle
66 // Return the new dialog.
67 return viewSslCertificateDialog
71 // `@SuppressLint("InflateParams")` removes the warning about using `null` as the parent view group when inflating the alert dialog.
72 @SuppressLint("InflateParams")
73 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
74 // Get the current position of this WebView fragment.
75 val webViewPosition = MainWebViewActivity.webViewPagerAdapter.getPositionForId(requireArguments().getLong(WEBVIEW_FRAGMENT_ID))
77 // Get the WebView tab fragment.
78 val webViewTabFragment = MainWebViewActivity.webViewPagerAdapter.getPageFragment(webViewPosition)
80 // Get the fragment view.
81 val fragmentView = webViewTabFragment.requireView()
83 // Get a handle for the current nested scroll WebView.
84 val nestedScrollWebView: NestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview)
86 // Use a builder to create the alert dialog.
87 val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
89 // Create a drawable version of the favorite icon.
90 val favoriteIconDrawable: Drawable = BitmapDrawable(resources, nestedScrollWebView.favoriteOrDefaultIcon)
93 dialogBuilder.setIcon(favoriteIconDrawable)
95 // Set the close button listener. Using `null` as the listener closes the dialog without doing anything else.
96 dialogBuilder.setNegativeButton(R.string.close, null)
98 // Get the SSL certificate.
99 val sslCertificate = nestedScrollWebView.certificate
101 // Get a handle for the shared preferences.
102 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
104 // Get the screenshot preference.
105 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
107 // Check to see if the website is encrypted.
108 if (sslCertificate == null) { // The website is not encrypted.
110 dialogBuilder.setTitle(R.string.unencrypted_website)
112 // Set the Layout. The parent view is `null` because it will be assigned by the alert dialog.
113 dialogBuilder.setView(layoutInflater.inflate(R.layout.unencrypted_website_dialog, null))
115 // Create an alert dialog from the builder.
116 val alertDialog = dialogBuilder.create()
118 // Disable screenshots if not allowed.
119 if (!allowScreenshots) {
120 // Disable screenshots.
121 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
124 // Return the alert dialog.
126 } else { // The website is encrypted.
128 dialogBuilder.setTitle(R.string.ssl_certificate)
130 // Set the layout. The parent view is `null` because it will be assigned by the alert dialog.
131 dialogBuilder.setView(layoutInflater.inflate(R.layout.view_ssl_certificate_dialog, null))
133 // Create an alert dialog from the builder.
134 val alertDialog = dialogBuilder.create()
136 // Disable screenshots if not allowed.
137 if (!allowScreenshots) {
138 // Disable screenshots.
139 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
142 // The alert dialog must be shown before items in the layout can be modified.
145 // Get handles for the text views.
146 val domainTextView = alertDialog.findViewById<TextView>(R.id.domain)!!
147 val ipAddressesTextView = alertDialog.findViewById<TextView>(R.id.ip_addresses)!!
148 val issuedToCNameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_cname)!!
149 val issuedToONameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_oname)!!
150 val issuedToUNameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_uname)!!
151 val issuedByCNameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_cname)!!
152 val issuedByONameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_oname)!!
153 val issuedByUNameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_uname)!!
154 val startDateTextView = alertDialog.findViewById<TextView>(R.id.start_date)!!
155 val endDateTextView = alertDialog.findViewById<TextView>(R.id.end_date)!!
158 val domainLabel = getString(R.string.domain_label) + " "
159 val ipAddressesLabel = getString(R.string.ip_addresses) + " "
160 val cNameLabel = getString(R.string.common_name) + " "
161 val oNameLabel = getString(R.string.organization) + " "
162 val uNameLabel = getString(R.string.organizational_unit) + " "
163 val startDateLabel = getString(R.string.start_date) + " "
164 val endDateLabel = getString(R.string.end_date) + " "
166 // Convert the URL to a URI.
167 val uri = Uri.parse(nestedScrollWebView.url)
169 // Extract the domain name from the URI.
170 val domainString = uri.host
172 // Get the strings from the SSL certificate.
173 val issuedToCName = sslCertificate.issuedTo.cName
174 val issuedToOName = sslCertificate.issuedTo.oName
175 val issuedToUName = sslCertificate.issuedTo.uName
176 val issuedByCName = sslCertificate.issuedBy.cName
177 val issuedByOName = sslCertificate.issuedBy.oName
178 val issuedByUName = sslCertificate.issuedBy.uName
179 val startDate = sslCertificate.validNotBeforeDate
180 val endDate = sslCertificate.validNotAfterDate
182 // Create spannable string builders for each text view that needs multiple colors of text.
183 val domainStringBuilder = SpannableStringBuilder(domainLabel + domainString)
184 val ipAddressesStringBuilder = SpannableStringBuilder(ipAddressesLabel + nestedScrollWebView.currentIpAddresses)
185 val issuedToCNameStringBuilder = SpannableStringBuilder(cNameLabel + issuedToCName)
186 val issuedToONameStringBuilder = SpannableStringBuilder(oNameLabel + issuedToOName)
187 val issuedToUNameStringBuilder = SpannableStringBuilder(uNameLabel + issuedToUName)
188 val issuedByCNameStringBuilder = SpannableStringBuilder(cNameLabel + issuedByCName)
189 val issuedByONameStringBuilder = SpannableStringBuilder(oNameLabel + issuedByOName)
190 val issuedByUNameStringBuilder = SpannableStringBuilder(uNameLabel + issuedByUName)
191 val startDateStringBuilder = SpannableStringBuilder(startDateLabel + DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(startDate))
192 val endDateStringBuilder = SpannableStringBuilder(endDateLabel + DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(endDate))
194 // Define the color spans.
195 val blueColorSpan: ForegroundColorSpan
196 val redColorSpan: ForegroundColorSpan
198 // Get the current theme status.
199 val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
201 // Set the color spans according to the theme. The deprecated `getColor()` must be used until the minimum API >= 23.
202 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
203 @Suppress("DEPRECATION")
204 blueColorSpan = ForegroundColorSpan(resources.getColor(R.color.blue_700))
205 @Suppress("DEPRECATION")
206 redColorSpan = ForegroundColorSpan(resources.getColor(R.color.red_a700))
208 @Suppress("DEPRECATION")
209 blueColorSpan = ForegroundColorSpan(resources.getColor(R.color.violet_700))
210 @Suppress("DEPRECATION")
211 redColorSpan = ForegroundColorSpan(resources.getColor(R.color.red_900))
214 // Format the domain string and issued to CName colors.
215 if (domainString == issuedToCName) { // The domain and issued to CName match.
216 // Set the strings to be blue.
217 domainStringBuilder.setSpan(blueColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
218 issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
219 } else if (issuedToCName.startsWith("*.")) { // The issued to CName begins with a wildcard.
220 // Remove the initial `*.`.
221 val baseCertificateDomain = issuedToCName.substring(2)
223 // Setup a copy of the domain string to test subdomains.
224 var domainStringSubdomain = domainString!!
226 // Define a domain names match variable.
227 var domainNamesMatch = false
229 // Check all the subdomains against the base certificate domain.
230 while (!domainNamesMatch && domainStringSubdomain.contains(".")) { // Stop checking if we know that the domain names match or if we run out of subdomains.
231 // Test the subdomain against the base certificate domain.
232 if (domainStringSubdomain == baseCertificateDomain) {
233 domainNamesMatch = true
236 // Strip out the lowest subdomain.
237 domainStringSubdomain = domainStringSubdomain.substring(domainStringSubdomain.indexOf(".") + 1)
240 // Format the domain and issued to CName.
241 if (domainNamesMatch) { // The domain is a subdomain of the wildcard certificate.
242 // Set the strings to be blue.
243 domainStringBuilder.setSpan(blueColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
244 issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
245 } else { // The domain is not a subdomain of the wildcard certificate.
246 // Set the string to be red.
247 domainStringBuilder.setSpan(redColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
248 issuedToCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
250 } else { // The strings do not match and issued to CName does not begin with a wildcard.
251 // Set the strings to be red.
252 domainStringBuilder.setSpan(redColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
253 issuedToCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
256 // 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.
257 ipAddressesStringBuilder.setSpan(blueColorSpan, ipAddressesLabel.length, ipAddressesStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
258 issuedToONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length, issuedToONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
259 issuedToUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length, issuedToUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
260 issuedByCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedByCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
261 issuedByONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length, issuedByONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
262 issuedByUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length, issuedByUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
264 // Get the current date.
265 val currentDate = Calendar.getInstance().time
267 // Format the start date color. `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
268 if (startDate.after(currentDate)) { // The certificate start date is in the future.
269 startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
270 } else { // The certificate start date is in the past.
271 startDateStringBuilder.setSpan(blueColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
274 // Format the end date color. `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
275 if (endDate.before(currentDate)) { // The certificate end date is in the past.
276 endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
277 } else { // The certificate end date is in the future.
278 endDateStringBuilder.setSpan(blueColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
281 // Display the strings.
282 domainTextView.text = domainStringBuilder
283 ipAddressesTextView.text = ipAddressesStringBuilder
284 issuedToCNameTextView.text = issuedToCNameStringBuilder
285 issuedToONameTextView.text = issuedToONameStringBuilder
286 issuedToUNameTextView.text = issuedToUNameStringBuilder
287 issuedByCNameTextView.text = issuedByCNameStringBuilder
288 issuedByONameTextView.text = issuedByONameStringBuilder
289 issuedByUNameTextView.text = issuedByUNameStringBuilder
290 startDateTextView.text = startDateStringBuilder
291 endDateTextView.text = endDateStringBuilder
293 // Return the alert dialog.