2 * Copyright © 2016-2019 Soren Stoutner <soren@stoutner.com>.
4 * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
6 * Privacy Browser is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
11 * Privacy Browser is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with Privacy Browser. If not, see <http://www.gnu.org/licenses/>.
20 package com.stoutner.privacybrowser.dialogs;
22 import android.annotation.SuppressLint;
23 import android.app.Activity;
24 import android.app.AlertDialog;
25 import android.app.Dialog;
26 import android.content.Context;
27 import android.content.DialogInterface;
28 import android.content.SharedPreferences;
29 import android.net.Uri;
30 import android.net.http.SslCertificate;
31 import android.net.http.SslError;
32 import android.os.AsyncTask;
33 import android.os.Bundle;
34 import android.preference.PreferenceManager;
35 import android.text.SpannableStringBuilder;
36 import android.text.Spanned;
37 import android.text.style.ForegroundColorSpan;
38 import android.view.LayoutInflater;
39 import android.view.WindowManager;
40 import android.widget.TextView;
42 import androidx.annotation.NonNull;
43 import androidx.fragment.app.DialogFragment; // The AndroidX dialog fragment must be used or an error is produced on API <=22.
45 import com.stoutner.privacybrowser.R;
47 import java.lang.ref.WeakReference;
48 import java.net.InetAddress;
49 import java.net.UnknownHostException;
50 import java.text.DateFormat;
51 import java.util.Date;
53 public class SslCertificateErrorDialog extends DialogFragment {
54 // `sslCertificateErrorListener` is used in `onAttach` and `onCreateDialog`.
55 private SslCertificateErrorListener sslCertificateErrorListener;
57 // The public interface is used to send information back to the parent activity.
58 public interface SslCertificateErrorListener {
59 void onSslErrorCancel();
61 void onSslErrorProceed();
64 public void onAttach(Context context) {
65 // Run the default commands.
66 super.onAttach(context);
68 // Get a handle for `SslCertificateErrorListener` from the launching context.
69 sslCertificateErrorListener = (SslCertificateErrorListener) context;
72 public static SslCertificateErrorDialog displayDialog(SslError error) {
73 // Get the various components of the SSL error message.
74 int primaryErrorIntForBundle = error.getPrimaryError();
75 String urlWithErrorForBundle = error.getUrl();
76 SslCertificate sslCertificate = error.getCertificate();
77 String issuedToCNameForBundle = sslCertificate.getIssuedTo().getCName();
78 String issuedToONameForBundle = sslCertificate.getIssuedTo().getOName();
79 String issuedToUNameForBundle = sslCertificate.getIssuedTo().getUName();
80 String issuedByCNameForBundle = sslCertificate.getIssuedBy().getCName();
81 String issuedByONameForBundle = sslCertificate.getIssuedBy().getOName();
82 String issuedByUNameForBundle = sslCertificate.getIssuedBy().getUName();
83 Date startDateForBundle = sslCertificate.getValidNotBeforeDate();
84 Date endDateForBundle = sslCertificate.getValidNotAfterDate();
86 // Store the SSL error message components in a `Bundle`.
87 Bundle argumentsBundle = new Bundle();
88 argumentsBundle.putInt("PrimaryErrorInt", primaryErrorIntForBundle);
89 argumentsBundle.putString("UrlWithError", urlWithErrorForBundle);
90 argumentsBundle.putString("IssuedToCName", issuedToCNameForBundle);
91 argumentsBundle.putString("IssuedToOName", issuedToONameForBundle);
92 argumentsBundle.putString("IssuedToUName", issuedToUNameForBundle);
93 argumentsBundle.putString("IssuedByCName", issuedByCNameForBundle);
94 argumentsBundle.putString("IssuedByOName", issuedByONameForBundle);
95 argumentsBundle.putString("IssuedByUName", issuedByUNameForBundle);
96 argumentsBundle.putString("StartDate", DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(startDateForBundle));
97 argumentsBundle.putString("EndDate", DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(endDateForBundle));
99 // Add `argumentsBundle` to this instance of `SslCertificateErrorDialog`.
100 SslCertificateErrorDialog thisSslCertificateErrorDialog = new SslCertificateErrorDialog();
101 thisSslCertificateErrorDialog.setArguments(argumentsBundle);
102 return thisSslCertificateErrorDialog;
105 // `@SuppressLing("InflateParams")` removes the warning about using `null` as the parent view group when inflating the `AlertDialog`.
106 @SuppressLint("InflateParams")
107 @SuppressWarnings("deprecation")
110 public Dialog onCreateDialog(Bundle savedInstanceState) {
111 // Remove the incorrect lint warning that `getArguments()` might be null.
112 assert getArguments() != null;
114 // Get the components of the SSL error message from the bundle.
115 int primaryErrorInt = getArguments().getInt("PrimaryErrorInt");
116 String urlWithErrors = getArguments().getString("UrlWithError");
117 String issuedToCName = getArguments().getString("IssuedToCName");
118 String issuedToOName = getArguments().getString("IssuedToOName");
119 String issuedToUName = getArguments().getString("IssuedToUName");
120 String issuedByCName = getArguments().getString("IssuedByCName");
121 String issuedByOName = getArguments().getString("IssuedByOName");
122 String issuedByUName = getArguments().getString("IssuedByUName");
123 String startDate = getArguments().getString("StartDate");
124 String endDate = getArguments().getString("EndDate");
126 // Remove the incorrect lint warning that `getActivity()` might be null.
127 assert getActivity() != null;
129 // Get the activity's layout inflater.
130 LayoutInflater layoutInflater = getActivity().getLayoutInflater();
132 // Use an alert dialog builder to create the alert dialog.
133 AlertDialog.Builder dialogBuilder;
135 // Get a handle for the shared preferences.
136 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext());
138 // Get the screenshot and theme preferences.
139 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
140 boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
142 // Set the style and icon according to the theme.
145 dialogBuilder = new AlertDialog.Builder(getActivity(), R.style.PrivacyBrowserAlertDialogDark);
148 dialogBuilder.setIcon(R.drawable.ssl_certificate_enabled_dark);
151 dialogBuilder = new AlertDialog.Builder(getActivity(), R.style.PrivacyBrowserAlertDialogLight);
154 dialogBuilder.setIcon(R.drawable.ssl_certificate_enabled_light);
158 dialogBuilder.setTitle(R.string.ssl_certificate_error);
160 // Set the view. The parent view is `null` because it will be assigned by `AlertDialog`.
161 dialogBuilder.setView(layoutInflater.inflate(R.layout.ssl_certificate_error, null));
163 // Set a listener on the negative button.
164 dialogBuilder.setNegativeButton(R.string.cancel, (DialogInterface dialog, int which) -> sslCertificateErrorListener.onSslErrorCancel());
166 // Set a listener on the positive button.
167 dialogBuilder.setPositiveButton(R.string.proceed, (DialogInterface dialog, int which) -> sslCertificateErrorListener.onSslErrorProceed());
170 // Create an alert dialog from the alert dialog builder.
171 AlertDialog alertDialog = dialogBuilder.create();
173 // Disable screenshots if not allowed.
174 if (!allowScreenshots) {
175 // Remove the warning below that `getWindow()` might be null.
176 assert alertDialog.getWindow() != null;
178 // Disable screenshots.
179 alertDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
182 // Get a URI for the URL with errors.
183 Uri uriWithErrors = Uri.parse(urlWithErrors);
185 // Get the IP addresses for the URI.
186 new GetIpAddresses(getActivity(), alertDialog).execute(uriWithErrors.getHost());
188 // The alert dialog must be shown before the contents can be modified.
191 // Get handles for the `TextViews`
192 TextView primaryErrorTextView = alertDialog.findViewById(R.id.primary_error);
193 TextView urlTextView = alertDialog.findViewById(R.id.url);
194 TextView issuedToCNameTextView = alertDialog.findViewById(R.id.issued_to_cname);
195 TextView issuedToONameTextView = alertDialog.findViewById(R.id.issued_to_oname);
196 TextView issuedToUNameTextView = alertDialog.findViewById(R.id.issued_to_uname);
197 TextView issuedByTextView = alertDialog.findViewById(R.id.issued_by_textview);
198 TextView issuedByCNameTextView = alertDialog.findViewById(R.id.issued_by_cname);
199 TextView issuedByONameTextView = alertDialog.findViewById(R.id.issued_by_oname);
200 TextView issuedByUNameTextView = alertDialog.findViewById(R.id.issued_by_uname);
201 TextView validDatesTextView = alertDialog.findViewById(R.id.valid_dates_textview);
202 TextView startDateTextView = alertDialog.findViewById(R.id.start_date);
203 TextView endDateTextView = alertDialog.findViewById(R.id.end_date);
205 // Setup the common strings.
206 String urlLabel = getString(R.string.url_label) + " ";
207 String cNameLabel = getString(R.string.common_name) + " ";
208 String oNameLabel = getString(R.string.organization) + " ";
209 String uNameLabel = getString(R.string.organizational_unit) + " ";
210 String startDateLabel = getString(R.string.start_date) + " ";
211 String endDateLabel = getString(R.string.end_date) + " ";
213 // Create a spannable string builder for each text view that needs multiple colors of text.
214 SpannableStringBuilder urlStringBuilder = new SpannableStringBuilder(urlLabel + urlWithErrors);
215 SpannableStringBuilder issuedToCNameStringBuilder = new SpannableStringBuilder(cNameLabel + issuedToCName);
216 SpannableStringBuilder issuedToONameStringBuilder = new SpannableStringBuilder(oNameLabel + issuedToOName);
217 SpannableStringBuilder issuedToUNameStringBuilder = new SpannableStringBuilder(uNameLabel + issuedToUName);
218 SpannableStringBuilder issuedByCNameStringBuilder = new SpannableStringBuilder(cNameLabel + issuedByCName);
219 SpannableStringBuilder issuedByONameStringBuilder = new SpannableStringBuilder(oNameLabel + issuedByOName);
220 SpannableStringBuilder issuedByUNameStringBuilder = new SpannableStringBuilder(uNameLabel + issuedByUName);
221 SpannableStringBuilder startDateStringBuilder = new SpannableStringBuilder(startDateLabel + startDate);
222 SpannableStringBuilder endDateStringBuilder = new SpannableStringBuilder((endDateLabel + endDate));
224 // Create a red foreground color span. The deprecated `getResources().getColor` must be used until the minimum API >= 23.
225 @SuppressWarnings("deprecation") ForegroundColorSpan redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_a700));
227 // Create a blue `ForegroundColorSpan`.
228 ForegroundColorSpan blueColorSpan;
230 // Set a blue color span according to the theme. The deprecated `getResources().getColor` must be used until the minimum API >= 23.
232 //noinspection deprecation
233 blueColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.blue_400));
235 //noinspection deprecation
236 blueColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.blue_700));
239 // Setup the spans to display the certificate information in blue. `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
240 urlStringBuilder.setSpan(blueColorSpan, urlLabel.length(), urlStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
241 issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length(), issuedToCNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
242 issuedToONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length(), issuedToONameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
243 issuedToUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length(), issuedToUNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
244 issuedByCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length(), issuedByCNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
245 issuedByONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length(), issuedByONameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
246 issuedByUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length(), issuedByUNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
247 startDateStringBuilder.setSpan(blueColorSpan, startDateLabel.length(), startDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
248 endDateStringBuilder.setSpan(blueColorSpan, endDateLabel.length(), endDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
250 // Initialize `primaryErrorString`.
251 String primaryErrorString = "";
253 // Highlight the primary error in red and store the primary error string in `primaryErrorString`.
254 switch (primaryErrorInt) {
255 case SslError.SSL_IDMISMATCH:
256 // Change the URL span colors to red.
257 urlStringBuilder.setSpan(redColorSpan, urlLabel.length(), urlStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
258 issuedToCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length(), issuedToCNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
260 // Store the primary error string.
261 primaryErrorString = getString(R.string.cn_mismatch);
264 case SslError.SSL_UNTRUSTED:
265 // Change the issued by text view text to red. The deprecated `getResources().getColor` must be used until the minimum API >= 23.
266 issuedByTextView.setTextColor(getResources().getColor(R.color.red_a700));
268 // Change the issued by span color to red.
269 issuedByCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length(), issuedByCNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
270 issuedByONameStringBuilder.setSpan(redColorSpan, oNameLabel.length(), issuedByONameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
271 issuedByUNameStringBuilder.setSpan(redColorSpan, uNameLabel.length(), issuedByUNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
273 // Store the primary error string.
274 primaryErrorString = getString(R.string.untrusted);
277 case SslError.SSL_DATE_INVALID:
278 // Change the valid dates text view text to red. The deprecated `getResources().getColor` must be used until the minimum API >= 23.
279 validDatesTextView.setTextColor(getResources().getColor(R.color.red_a700));
281 // Change the date span colors to red.
282 startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length(), startDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
283 endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length(), endDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
285 // Store the primary error string.
286 primaryErrorString = getString(R.string.invalid_date);
289 case SslError.SSL_NOTYETVALID:
290 // Change the start date span color to red.
291 startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length(), startDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
293 // Store the primary error string.
294 primaryErrorString = getString(R.string.future_certificate);
297 case SslError.SSL_EXPIRED:
298 // Change the end date span color to red.
299 endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length(), endDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
301 // Store the primary error string.
302 primaryErrorString = getString(R.string.expired_certificate);
305 case SslError.SSL_INVALID:
306 // Store the primary error string.
307 primaryErrorString = getString(R.string.invalid_certificate);
312 // Display the strings.
313 primaryErrorTextView.setText(primaryErrorString);
314 urlTextView.setText(urlStringBuilder);
315 issuedToCNameTextView.setText(issuedToCNameStringBuilder);
316 issuedToONameTextView.setText(issuedToONameStringBuilder);
317 issuedToUNameTextView.setText(issuedToUNameStringBuilder);
318 issuedByCNameTextView.setText(issuedByCNameStringBuilder);
319 issuedByONameTextView.setText(issuedByONameStringBuilder);
320 issuedByUNameTextView.setText(issuedByUNameStringBuilder);
321 startDateTextView.setText(startDateStringBuilder);
322 endDateTextView.setText(endDateStringBuilder);
324 // `onCreateDialog` requires the return of an alert dialog.
329 // This must run asynchronously because it involves a network request. `String` declares the parameters. `Void` does not declare progress units. `SpannableStringBuilder` contains the results.
330 private static class GetIpAddresses extends AsyncTask<String, Void, SpannableStringBuilder> {
331 // The weak references are used to determine if the activity or the alert dialog have disappeared while the AsyncTask is running.
332 private WeakReference<Activity> activityWeakReference;
333 private WeakReference<AlertDialog> alertDialogWeakReference;
335 GetIpAddresses(Activity activity, AlertDialog alertDialog) {
336 // Populate the weak references.
337 activityWeakReference = new WeakReference<>(activity);
338 alertDialogWeakReference = new WeakReference<>(alertDialog);
342 protected SpannableStringBuilder doInBackground(String... domainName) {
343 // Get handles for the activity and the alert dialog.
344 Activity activity = activityWeakReference.get();
345 AlertDialog alertDialog = alertDialogWeakReference.get();
347 // Abort if the activity or the dialog is gone.
348 if ((activity == null) || (activity.isFinishing()) || (alertDialog == null)) {
349 return new SpannableStringBuilder();
352 // Initialize an IP address string builder.
353 StringBuilder ipAddresses = new StringBuilder();
355 // Get an array with the IP addresses for the host.
357 // Get an array with all the IP addresses for the domain.
358 InetAddress[] inetAddressesArray = InetAddress.getAllByName(domainName[0]);
360 // Add each IP address to the string builder.
361 for (InetAddress inetAddress : inetAddressesArray) {
362 if (ipAddresses.length() == 0) { // This is the first IP address.
363 // Add the IP Address to the string builder.
364 ipAddresses.append(inetAddress.getHostAddress());
365 } else { // This is not the first IP address.
366 // Add a line break to the string builder first.
367 ipAddresses.append("\n");
369 // Add the IP address to the string builder.
370 ipAddresses.append(inetAddress.getHostAddress());
373 } catch (UnknownHostException exception) {
378 String ipAddressesLabel = activity.getString(R.string.ip_addresses) + " ";
380 // Create a spannable string builder.
381 SpannableStringBuilder ipAddressesStringBuilder = new SpannableStringBuilder(ipAddressesLabel + ipAddresses);
383 // Get a handle for the shared preferences.
384 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity.getApplicationContext());
386 // Get the screenshot and theme preferences.
387 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
389 // Create a blue foreground color span.
390 ForegroundColorSpan blueColorSpan;
392 // Set the blue color span according to the theme. The deprecated `getColor()` must be used until the minimum API >= 23.
394 //noinspection deprecation
395 blueColorSpan = new ForegroundColorSpan(activity.getResources().getColor(R.color.blue_400));
397 //noinspection deprecation
398 blueColorSpan = new ForegroundColorSpan(activity.getResources().getColor(R.color.blue_700));
401 // Set the string builder to display the certificate information in blue. `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
402 ipAddressesStringBuilder.setSpan(blueColorSpan, ipAddressesLabel.length(), ipAddressesStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
404 // Return the formatted string.
405 return ipAddressesStringBuilder;
408 // `onPostExecute()` operates on the UI thread.
410 protected void onPostExecute(SpannableStringBuilder ipAddresses) {
411 // Get handles for the activity and the alert dialog.
412 Activity activity = activityWeakReference.get();
413 AlertDialog alertDialog = alertDialogWeakReference.get();
415 // Abort if the activity or the alert dialog is gone.
416 if ((activity == null) || (activity.isFinishing()) || (alertDialog == null)) {
420 // Get a handle for the IP addresses text view.
421 TextView ipAddressesTextView = alertDialog.findViewById(R.id.ip_addresses);
423 // Populate the IP addresses text view.
424 ipAddressesTextView.setText(ipAddresses);