Combine drawable files. https://redmine.stoutner.com/issues/794
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / dialogs / ViewSslCertificateDialog.kt
1 /*
2  * Copyright © 2016-2022 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
5  *
6  * Privacy Browser Android 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 Android 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 Android.  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.graphics.Bitmap
24 import android.graphics.BitmapFactory
25 import android.graphics.drawable.BitmapDrawable
26 import android.net.Uri
27 import android.os.Bundle
28 import android.text.SpannableStringBuilder
29 import android.text.Spanned
30 import android.text.style.ForegroundColorSpan
31 import android.view.WindowManager
32 import android.widget.TextView
33
34 import androidx.appcompat.app.AlertDialog
35 import androidx.fragment.app.DialogFragment
36 import androidx.preference.PreferenceManager
37
38 import com.stoutner.privacybrowser.R
39 import com.stoutner.privacybrowser.activities.MainWebViewActivity
40 import com.stoutner.privacybrowser.views.NestedScrollWebView
41
42 import java.io.ByteArrayOutputStream
43
44 import java.text.DateFormat
45 import java.util.Calendar
46 import java.util.Date
47
48 // Define the class constants.
49 private const val WEBVIEW_FRAGMENT_ID = "webview_fragment_id"
50 private const val FAVORITE_ICON_BYTE_ARRAY = "favorite_icon_byte_array"
51 private const val HAS_SSL_CERTIFICATE = "has_ssl_certificate"
52 private const val DOMAIN = "domain"
53 private const val IP_ADDRESSES = "ip_addresses"
54 private const val ISSUED_TO_CNAME = "issued_to_cname"
55 private const val ISSUED_TO_ONAME = "issued_to_oname"
56 private const val ISSUED_TO_UNAME = "issued_to_uname"
57 private const val ISSUED_BY_CNAME = "issued_by_cname"
58 private const val ISSUED_BY_ONAME = "issued_by_oname"
59 private const val ISSUED_BY_UNAME = "issued_by_uname"
60 private const val START_DATE = "start_date"
61 private const val END_DATE = "end_date"
62
63 class ViewSslCertificateDialog : DialogFragment() {
64     // Define the class variables.
65     private var hasSslCertificate: Boolean = false
66
67     // Declare the class variables.
68     private lateinit var domainString: String
69     private lateinit var ipAddresses: String
70     private lateinit var issuedToCName: String
71     private lateinit var issuedToOName: String
72     private lateinit var issuedToUName: String
73     private lateinit var issuedByCName: String
74     private lateinit var issuedByOName: String
75     private lateinit var issuedByUName: String
76     private lateinit var startDate: Date
77     private lateinit var endDate: Date
78
79     // Declare the class views.
80     private lateinit var nestedScrollWebView: NestedScrollWebView
81
82     companion object {
83         // `@JvmStatic` will no longer be required once all the code has transitioned to Kotlin.
84         @JvmStatic
85         fun displayDialog(webViewFragmentId: Long, favoriteIconBitmap: Bitmap): ViewSslCertificateDialog {
86             // Create an arguments bundle.
87             val argumentsBundle = Bundle()
88
89             // Create a favorite icon byte array output stream.
90             val favoriteIconByteArrayOutputStream = ByteArrayOutputStream()
91
92             // 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).
93             favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream)
94
95             // Convert the favorite icon byte array output stream to a byte array.
96             val favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray()
97
98             // Store the arguments in the bundle.
99             argumentsBundle.putLong(WEBVIEW_FRAGMENT_ID, webViewFragmentId)
100             argumentsBundle.putByteArray(FAVORITE_ICON_BYTE_ARRAY, favoriteIconByteArray)
101
102             // Create a new instance of the view SSL certificate dialog.
103             val viewSslCertificateDialog = ViewSslCertificateDialog()
104
105             // Add the bundle to the new dialog.
106             viewSslCertificateDialog.arguments = argumentsBundle
107
108             // Return the new dialog.
109             return viewSslCertificateDialog
110         }
111     }
112
113     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
114         // Use a builder to create the alert dialog.
115         val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
116
117         // Populate the class variables.
118         if (savedInstanceState == null) {  // The dialog is starting for the first time.
119             // Get the current position of this WebView fragment.
120             val webViewPosition = MainWebViewActivity.webViewPagerAdapter.getPositionForId(requireArguments().getLong(WEBVIEW_FRAGMENT_ID))
121
122             // Get the WebView tab fragment.
123             val webViewTabFragment = MainWebViewActivity.webViewPagerAdapter.getPageFragment(webViewPosition)
124
125             // Get the fragment view.
126             val fragmentView = webViewTabFragment.requireView()
127
128             // Get a handle for the current nested scroll WebView.
129             nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview)
130
131             // Get the SSL certificate.
132             val sslCertificate = nestedScrollWebView.certificate
133
134             // Store the status of the SSL certificate.
135             hasSslCertificate = sslCertificate != null
136
137             // Populate the certificate class variables if the webpage has an SSL certificate.
138             if (hasSslCertificate) {
139                 // Convert the URL to a URI.
140                 val uri = Uri.parse(nestedScrollWebView.url)
141
142                 // Extract the domain name from the URI.
143                 domainString = uri.host!!
144
145                 // Get the ip addresses from the nested scroll WebView.
146                 ipAddresses = nestedScrollWebView.currentIpAddresses
147
148                 // Get the strings from the SSL certificate.
149                 issuedToCName = sslCertificate!!.issuedTo.cName
150                 issuedToOName = sslCertificate.issuedTo.oName
151                 issuedToUName = sslCertificate.issuedTo.uName
152                 issuedByCName = sslCertificate.issuedBy.cName
153                 issuedByOName = sslCertificate.issuedBy.oName
154                 issuedByUName = sslCertificate.issuedBy.uName
155                 startDate = sslCertificate.validNotBeforeDate
156                 endDate = sslCertificate.validNotAfterDate
157             }
158         } else {  // The dialog has been restarted.
159             // Get the data from the saved instance state.
160             hasSslCertificate = savedInstanceState.getBoolean(HAS_SSL_CERTIFICATE)
161
162             // Populate the certificate class variables if the webpage has an SSL certificate.
163             if (hasSslCertificate) {
164                 // Populate the certificate class variables from the saved instance state.
165                 domainString = savedInstanceState.getString(DOMAIN)!!
166                 ipAddresses = savedInstanceState.getString(IP_ADDRESSES)!!
167                 issuedToCName = savedInstanceState.getString(ISSUED_TO_CNAME)!!
168                 issuedToOName = savedInstanceState.getString(ISSUED_TO_ONAME)!!
169                 issuedToUName = savedInstanceState.getString(ISSUED_TO_UNAME)!!
170                 issuedByCName = savedInstanceState.getString(ISSUED_BY_CNAME)!!
171                 issuedByOName = savedInstanceState.getString(ISSUED_BY_ONAME)!!
172                 issuedByUName = savedInstanceState.getString(ISSUED_BY_UNAME)!!
173                 startDate = Date(savedInstanceState.getLong(START_DATE))
174                 endDate = Date(savedInstanceState.getLong(END_DATE))
175             }
176         }
177
178         // Get the favorite icon byte array from the arguments.
179         val favoriteIconByteArray = requireArguments().getByteArray(FAVORITE_ICON_BYTE_ARRAY)!!
180
181         // Convert the favorite icon byte array to a bitmap.
182         val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
183
184         // Create a drawable version of the favorite icon.
185         val favoriteIconDrawable = BitmapDrawable(resources, favoriteIconBitmap)
186
187         // Set the icon.
188         dialogBuilder.setIcon(favoriteIconDrawable)
189
190         // Set the close button listener.  Using `null` as the listener closes the dialog without doing anything else.
191         dialogBuilder.setNegativeButton(R.string.close, null)
192
193         // Get a handle for the shared preferences.
194         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
195
196         // Get the screenshot preference.
197         val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
198
199         // Check to see if the website is encrypted.
200         if (hasSslCertificate) {  // The website is encrypted.
201             // Set the title.
202             dialogBuilder.setTitle(R.string.ssl_certificate)
203
204             // Set the layout.
205             dialogBuilder.setView(R.layout.view_ssl_certificate_dialog)
206
207             // Create an alert dialog from the builder.
208             val alertDialog = dialogBuilder.create()
209
210             // Disable screenshots if not allowed.
211             if (!allowScreenshots) {
212                 // Disable screenshots.
213                 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
214             }
215
216             // The alert dialog must be shown before items in the layout can be modified.
217             alertDialog.show()
218
219             // Get handles for the text views.
220             val domainTextView = alertDialog.findViewById<TextView>(R.id.domain)!!
221             val ipAddressesTextView = alertDialog.findViewById<TextView>(R.id.ip_addresses)!!
222             val issuedToCNameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_cname)!!
223             val issuedToONameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_oname)!!
224             val issuedToUNameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_uname)!!
225             val issuedByCNameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_cname)!!
226             val issuedByONameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_oname)!!
227             val issuedByUNameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_uname)!!
228             val startDateTextView = alertDialog.findViewById<TextView>(R.id.start_date)!!
229             val endDateTextView = alertDialog.findViewById<TextView>(R.id.end_date)!!
230
231             // Setup the labels.
232             val domainLabel = getString(R.string.domain_label) + "  "
233             val ipAddressesLabel = getString(R.string.ip_addresses) + "  "
234             val cNameLabel = getString(R.string.common_name) + "  "
235             val oNameLabel = getString(R.string.organization) + "  "
236             val uNameLabel = getString(R.string.organizational_unit) + "  "
237             val startDateLabel = getString(R.string.start_date) + "  "
238             val endDateLabel = getString(R.string.end_date) + "  "
239
240             // Create spannable string builders for each text view that needs multiple colors of text.
241             val domainStringBuilder = SpannableStringBuilder(domainLabel + domainString)
242             val ipAddressesStringBuilder = SpannableStringBuilder(ipAddressesLabel + ipAddresses)
243             val issuedToCNameStringBuilder = SpannableStringBuilder(cNameLabel + issuedToCName)
244             val issuedToONameStringBuilder = SpannableStringBuilder(oNameLabel + issuedToOName)
245             val issuedToUNameStringBuilder = SpannableStringBuilder(uNameLabel + issuedToUName)
246             val issuedByCNameStringBuilder = SpannableStringBuilder(cNameLabel + issuedByCName)
247             val issuedByONameStringBuilder = SpannableStringBuilder(oNameLabel + issuedByOName)
248             val issuedByUNameStringBuilder = SpannableStringBuilder(uNameLabel + issuedByUName)
249             val startDateStringBuilder = SpannableStringBuilder(startDateLabel + DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(startDate))
250             val endDateStringBuilder = SpannableStringBuilder(endDateLabel + DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(endDate))
251
252             // Define the color spans.
253             val blueColorSpan = ForegroundColorSpan(requireContext().getColor(R.color.blue_text))
254             val redColorSpan = ForegroundColorSpan(requireContext().getColor(R.color.red_text))
255
256             // Format the domain string and issued to CName colors.
257             if (domainString == issuedToCName) {  // The domain and issued to CName match.
258                 // Set the strings to be blue.
259                 domainStringBuilder.setSpan(blueColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
260                 issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
261             } else if (issuedToCName.startsWith("*.")) {  // The issued to CName begins with a wildcard.
262                 // Remove the initial `*.`.
263                 val baseCertificateDomain = issuedToCName.substring(2)
264
265                 // Setup a copy of the domain string to test subdomains.
266                 var domainStringSubdomain = domainString
267
268                 // Define a domain names match variable.
269                 var domainNamesMatch = false
270
271                 // Check all the subdomains against the base certificate domain.
272                 while (!domainNamesMatch && domainStringSubdomain.contains(".")) {  // Stop checking if we know that the domain names match or if we run out of subdomains.
273                     // Test the subdomain against the base certificate domain.
274                     if (domainStringSubdomain == baseCertificateDomain) {
275                         domainNamesMatch = true
276                     }
277
278                     // Strip out the lowest subdomain.
279                     domainStringSubdomain = domainStringSubdomain.substring(domainStringSubdomain.indexOf(".") + 1)
280                 }
281
282                 // Format the domain and issued to CName.
283                 if (domainNamesMatch) {  // The domain is a subdomain of the wildcard certificate.
284                     // Set the strings to be blue.
285                     domainStringBuilder.setSpan(blueColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
286                     issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
287                 } else {  // The domain is not a subdomain of the wildcard certificate.
288                     // Set the string to be red.
289                     domainStringBuilder.setSpan(redColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
290                     issuedToCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
291                 }
292             } else {  // The strings do not match and issued to CName does not begin with a wildcard.
293                 // Set the strings to be red.
294                 domainStringBuilder.setSpan(redColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
295                 issuedToCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
296             }
297
298             // 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.
299             ipAddressesStringBuilder.setSpan(blueColorSpan, ipAddressesLabel.length, ipAddressesStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
300             issuedToONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length, issuedToONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
301             issuedToUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length, issuedToUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
302             issuedByCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedByCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
303             issuedByONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length, issuedByONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
304             issuedByUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length, issuedByUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
305
306             // Get the current date.
307             val currentDate = Calendar.getInstance().time
308
309             //  Format the start date color.  `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
310             if (startDate.after(currentDate)) {  // The certificate start date is in the future.
311                 startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
312             } else {  // The certificate start date is in the past.
313                 startDateStringBuilder.setSpan(blueColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
314             }
315
316             // Format the end date color.  `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
317             if (endDate.before(currentDate)) {  // The certificate end date is in the past.
318                 endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
319             } else {  // The certificate end date is in the future.
320                 endDateStringBuilder.setSpan(blueColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
321             }
322
323             // Display the strings.
324             domainTextView.text = domainStringBuilder
325             ipAddressesTextView.text = ipAddressesStringBuilder
326             issuedToCNameTextView.text = issuedToCNameStringBuilder
327             issuedToONameTextView.text = issuedToONameStringBuilder
328             issuedToUNameTextView.text = issuedToUNameStringBuilder
329             issuedByCNameTextView.text = issuedByCNameStringBuilder
330             issuedByONameTextView.text = issuedByONameStringBuilder
331             issuedByUNameTextView.text = issuedByUNameStringBuilder
332             startDateTextView.text = startDateStringBuilder
333             endDateTextView.text = endDateStringBuilder
334
335             // Return the alert dialog.
336             return alertDialog
337         } else {  // The website is not encrypted.
338             // Set the title.
339             dialogBuilder.setTitle(R.string.unencrypted_website)
340
341             // Set the Layout.
342             dialogBuilder.setView(R.layout.unencrypted_website_dialog)
343
344             // Create an alert dialog from the builder.
345             val alertDialog = dialogBuilder.create()
346
347             // Disable screenshots if not allowed.
348             if (!allowScreenshots) {
349                 // Disable screenshots.
350                 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
351             }
352
353             // Return the alert dialog.
354             return alertDialog
355         }
356     }
357
358     override fun onSaveInstanceState(savedInstanceState: Bundle) {
359         // Run the default commands.
360         super.onSaveInstanceState(savedInstanceState)
361
362         // Save the common class variables.
363         savedInstanceState.putBoolean(HAS_SSL_CERTIFICATE, hasSslCertificate)
364
365         // Save the SSL certificate strings if they exist.
366         if (hasSslCertificate) {
367             savedInstanceState.putString(DOMAIN, domainString)
368             savedInstanceState.putString(IP_ADDRESSES, ipAddresses)
369             savedInstanceState.putString(ISSUED_TO_CNAME, issuedToCName)
370             savedInstanceState.putString(ISSUED_TO_ONAME, issuedToOName)
371             savedInstanceState.putString(ISSUED_TO_UNAME, issuedToUName)
372             savedInstanceState.putString(ISSUED_BY_CNAME, issuedByCName)
373             savedInstanceState.putString(ISSUED_BY_ONAME, issuedByOName)
374             savedInstanceState.putString(ISSUED_BY_UNAME, issuedByUName)
375             savedInstanceState.putLong(START_DATE, startDate.time)
376             savedInstanceState.putLong(END_DATE, endDate.time)
377         }
378     }
379 }