Fix a crash if the View SSL Certificate is displayed when the app restarts. https...
[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.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
36
37 import androidx.appcompat.app.AlertDialog
38 import androidx.fragment.app.DialogFragment
39 import androidx.preference.PreferenceManager
40
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
45
46 import java.text.DateFormat
47 import java.util.Calendar
48 import java.util.Date
49
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"
64
65 class ViewSslCertificateDialog : DialogFragment() {
66     // Define the class variables.
67     private var hasSslCertificate: Boolean = false
68
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
81
82     // Declare the class views.
83     private lateinit var nestedScrollWebView: NestedScrollWebView
84
85     companion object {
86         // `@JvmStatic` will no longer be required once all the code has transitioned to Kotlin.
87         @JvmStatic
88         fun displayDialog(webViewFragmentId: Long): ViewSslCertificateDialog {
89             // Create an arguments bundle.
90             val argumentsBundle = Bundle()
91
92             // Store the WebView fragment ID in the bundle.
93             argumentsBundle.putLong(WEBVIEW_FRAGMENT_ID, webViewFragmentId)
94
95             // Create a new instance of the view SSL certificate dialog.
96             val viewSslCertificateDialog = ViewSslCertificateDialog()
97
98             // Add the bundle to the new dialog.
99             viewSslCertificateDialog.arguments = argumentsBundle
100
101             // Return the new dialog.
102             return viewSslCertificateDialog
103         }
104     }
105
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)
111
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))
116
117             // Get the WebView tab fragment.
118             val webViewTabFragment = MainWebViewActivity.webViewPagerAdapter.getPageFragment(webViewPosition)
119
120             // Get the fragment view.
121             val fragmentView = webViewTabFragment.requireView()
122
123             // Get a handle for the current nested scroll WebView.
124             nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview)
125
126             // Get the SSL certificate.
127             val sslCertificate = nestedScrollWebView.certificate
128
129             // Store the status of the SSL certificate.
130             hasSslCertificate = sslCertificate != null
131
132             // Create a drawable version of the favorite icon.
133             favoriteIconDrawable = BitmapDrawable(resources, nestedScrollWebView.favoriteOrDefaultIcon)
134
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)
139
140                 // Extract the domain name from the URI.
141                 domainString = uri.host!!
142
143                 // Get the ip addresses from the nested scroll WebView.
144                 ipAddresses = nestedScrollWebView.currentIpAddresses
145
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
155             }
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)!!
160
161             // Convert the favorite icon byte array to a bitmap.
162             val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
163
164             // Create a drawable version of the favorite icon.
165             favoriteIconDrawable = BitmapDrawable(resources, favoriteIconBitmap)
166
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))
180             }
181         }
182
183         // Set the icon.
184         dialogBuilder.setIcon(favoriteIconDrawable)
185
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)
188
189         // Get a handle for the shared preferences.
190         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
191
192         // Get the screenshot preference.
193         val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
194
195         // Check to see if the website is encrypted.
196         if (hasSslCertificate) {  // The website is encrypted.
197             // Set the title.
198             dialogBuilder.setTitle(R.string.ssl_certificate)
199
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))
202
203             // Create an alert dialog from the builder.
204             val alertDialog = dialogBuilder.create()
205
206             // Disable screenshots if not allowed.
207             if (!allowScreenshots) {
208                 // Disable screenshots.
209                 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
210             }
211
212             // The alert dialog must be shown before items in the layout can be modified.
213             alertDialog.show()
214
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)!!
226
227             // Setup the labels.
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) + "  "
235
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))
247
248             // Define the color spans.
249             val blueColorSpan: ForegroundColorSpan
250             val redColorSpan: ForegroundColorSpan
251
252             // Get the current theme status.
253             val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
254
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))
261             } else {
262                 @Suppress("DEPRECATION")
263                 blueColorSpan = ForegroundColorSpan(resources.getColor(R.color.violet_700))
264                 @Suppress("DEPRECATION")
265                 redColorSpan = ForegroundColorSpan(resources.getColor(R.color.red_900))
266             }
267
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)
276
277                 // Setup a copy of the domain string to test subdomains.
278                 var domainStringSubdomain = domainString
279
280                 // Define a domain names match variable.
281                 var domainNamesMatch = false
282
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
288                     }
289
290                     // Strip out the lowest subdomain.
291                     domainStringSubdomain = domainStringSubdomain.substring(domainStringSubdomain.indexOf(".") + 1)
292                 }
293
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)
303                 }
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)
308             }
309
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)
317
318             // Get the current date.
319             val currentDate = Calendar.getInstance().time
320
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)
326             }
327
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)
333             }
334
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
346
347             // Return the alert dialog.
348             return alertDialog
349         } else {  // The website is not encrypted.
350             // Set the title.
351             dialogBuilder.setTitle(R.string.unencrypted_website)
352
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))
355
356             // Create an alert dialog from the builder.
357             val alertDialog = dialogBuilder.create()
358
359             // Disable screenshots if not allowed.
360             if (!allowScreenshots) {
361                 // Disable screenshots.
362                 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
363             }
364
365             // Return the alert dialog.
366             return alertDialog
367         }
368     }
369
370     override fun onSaveInstanceState(savedInstanceState: Bundle) {
371         // Run the default commands.
372         super.onSaveInstanceState(savedInstanceState)
373
374         // Get the favorite icon bitmap drawable.
375         val favoriteIconBitmapDrawable = favoriteIconDrawable as BitmapDrawable
376
377         // Get the favorite icon bitmap.
378         val favoriteIconBitmap = favoriteIconBitmapDrawable.bitmap
379
380         // Create a favorite icon byte array output stream.
381         val favoriteIconByteArrayOutputStream = ByteArrayOutputStream()
382
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)
385
386         // Convert the favorite icon byte array output stream to a byte array.
387         val favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray()
388
389         // Save the common class variables.
390         savedInstanceState.putBoolean(HAS_SSL_CERTIFICATE, hasSslCertificate)
391         savedInstanceState.putByteArray(FAVORITE_ICON, favoriteIconByteArray)
392
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)
405         }
406     }
407 }