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