Fix a crash when opening a drawer while restarting. https://redmine.stoutner.com...
[PrivacyBrowser.git] / app / src / main / java / com / stoutner / privacybrowser / dialogs / ViewSslCertificateDialog.kt
1 /*
2  * Copyright © 2016-2021 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
5  *
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.
10  *
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.
15  *
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/>.
18  */
19
20 package com.stoutner.privacybrowser.dialogs
21
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
34
35 import androidx.appcompat.app.AlertDialog
36 import androidx.fragment.app.DialogFragment
37 import androidx.preference.PreferenceManager
38
39 import com.stoutner.privacybrowser.R
40 import com.stoutner.privacybrowser.activities.MainWebViewActivity
41 import com.stoutner.privacybrowser.views.NestedScrollWebView
42
43 import java.io.ByteArrayOutputStream
44
45 import java.text.DateFormat
46 import java.util.Calendar
47 import java.util.Date
48
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"
63
64 class ViewSslCertificateDialog : DialogFragment() {
65     // Define the class variables.
66     private var hasSslCertificate: Boolean = false
67
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
79
80     // Declare the class views.
81     private lateinit var nestedScrollWebView: NestedScrollWebView
82
83     companion object {
84         // `@JvmStatic` will no longer be required once all the code has transitioned to Kotlin.
85         @JvmStatic
86         fun displayDialog(webViewFragmentId: Long, favoriteIconBitmap: Bitmap): ViewSslCertificateDialog {
87             // Create an arguments bundle.
88             val argumentsBundle = Bundle()
89
90             // Create a favorite icon byte array output stream.
91             val favoriteIconByteArrayOutputStream = ByteArrayOutputStream()
92
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)
95
96             // Convert the favorite icon byte array output stream to a byte array.
97             val favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray()
98
99             // Store the arguments in the bundle.
100             argumentsBundle.putLong(WEBVIEW_FRAGMENT_ID, webViewFragmentId)
101             argumentsBundle.putByteArray(FAVORITE_ICON_BYTE_ARRAY, favoriteIconByteArray)
102
103             // Create a new instance of the view SSL certificate dialog.
104             val viewSslCertificateDialog = ViewSslCertificateDialog()
105
106             // Add the bundle to the new dialog.
107             viewSslCertificateDialog.arguments = argumentsBundle
108
109             // Return the new dialog.
110             return viewSslCertificateDialog
111         }
112     }
113
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)
117
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))
122
123             // Get the WebView tab fragment.
124             val webViewTabFragment = MainWebViewActivity.webViewPagerAdapter.getPageFragment(webViewPosition)
125
126             // Get the fragment view.
127             val fragmentView = webViewTabFragment.requireView()
128
129             // Get a handle for the current nested scroll WebView.
130             nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview)
131
132             // Get the SSL certificate.
133             val sslCertificate = nestedScrollWebView.certificate
134
135             // Store the status of the SSL certificate.
136             hasSslCertificate = sslCertificate != null
137
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)
142
143                 // Extract the domain name from the URI.
144                 domainString = uri.host!!
145
146                 // Get the ip addresses from the nested scroll WebView.
147                 ipAddresses = nestedScrollWebView.currentIpAddresses
148
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
158             }
159         } else {  // The dialog has been restarted.
160             // Get the data from the saved instance state.
161             hasSslCertificate = savedInstanceState.getBoolean(HAS_SSL_CERTIFICATE)
162
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))
176             }
177         }
178
179         // Get the favorite icon byte array from the arguments.
180         val favoriteIconByteArray = requireArguments().getByteArray(FAVORITE_ICON_BYTE_ARRAY)!!
181
182         // Convert the favorite icon byte array to a bitmap.
183         val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
184
185         // Create a drawable version of the favorite icon.
186         val favoriteIconDrawable = BitmapDrawable(resources, favoriteIconBitmap)
187
188         // Set the icon.
189         dialogBuilder.setIcon(favoriteIconDrawable)
190
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)
193
194         // Get a handle for the shared preferences.
195         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
196
197         // Get the screenshot preference.
198         val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
199
200         // Check to see if the website is encrypted.
201         if (hasSslCertificate) {  // The website is encrypted.
202             // Set the title.
203             dialogBuilder.setTitle(R.string.ssl_certificate)
204
205             // Set the layout.
206             dialogBuilder.setView(R.layout.view_ssl_certificate_dialog)
207
208             // Create an alert dialog from the builder.
209             val alertDialog = dialogBuilder.create()
210
211             // Disable screenshots if not allowed.
212             if (!allowScreenshots) {
213                 // Disable screenshots.
214                 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
215             }
216
217             // The alert dialog must be shown before items in the layout can be modified.
218             alertDialog.show()
219
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)!!
231
232             // Setup the labels.
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) + "  "
240
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))
252
253             // Define the color spans.
254             val blueColorSpan: ForegroundColorSpan
255             val redColorSpan: ForegroundColorSpan
256
257             // Get the current theme status.
258             val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
259
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))
266             } else {
267                 @Suppress("DEPRECATION")
268                 blueColorSpan = ForegroundColorSpan(resources.getColor(R.color.violet_700))
269                 @Suppress("DEPRECATION")
270                 redColorSpan = ForegroundColorSpan(resources.getColor(R.color.red_900))
271             }
272
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)
281
282                 // Setup a copy of the domain string to test subdomains.
283                 var domainStringSubdomain = domainString
284
285                 // Define a domain names match variable.
286                 var domainNamesMatch = false
287
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
293                     }
294
295                     // Strip out the lowest subdomain.
296                     domainStringSubdomain = domainStringSubdomain.substring(domainStringSubdomain.indexOf(".") + 1)
297                 }
298
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)
308                 }
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)
313             }
314
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)
322
323             // Get the current date.
324             val currentDate = Calendar.getInstance().time
325
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)
331             }
332
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)
338             }
339
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
351
352             // Return the alert dialog.
353             return alertDialog
354         } else {  // The website is not encrypted.
355             // Set the title.
356             dialogBuilder.setTitle(R.string.unencrypted_website)
357
358             // Set the Layout.
359             dialogBuilder.setView(R.layout.unencrypted_website_dialog)
360
361             // Create an alert dialog from the builder.
362             val alertDialog = dialogBuilder.create()
363
364             // Disable screenshots if not allowed.
365             if (!allowScreenshots) {
366                 // Disable screenshots.
367                 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
368             }
369
370             // Return the alert dialog.
371             return alertDialog
372         }
373     }
374
375     override fun onSaveInstanceState(savedInstanceState: Bundle) {
376         // Run the default commands.
377         super.onSaveInstanceState(savedInstanceState)
378
379         // Save the common class variables.
380         savedInstanceState.putBoolean(HAS_SSL_CERTIFICATE, hasSslCertificate)
381
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)
394         }
395     }
396 }