]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blobdiff - app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java
Only show the pinned mismatch dialog when there is a mismatch. https://redmine.stout...
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / activities / MainWebViewActivity.java
index 96098947696d915603fa7e53d69d4b4de556a812..65528ebf25d8b6cf222df1599154254e0f10d732 100644 (file)
@@ -47,6 +47,7 @@ import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.net.http.SslCertificate;
 import android.net.http.SslError;
+import android.os.AsyncTask;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Environment;
@@ -60,6 +61,7 @@ import android.support.design.widget.FloatingActionButton;
 import android.support.design.widget.NavigationView;
 import android.support.design.widget.Snackbar;
 import android.support.v4.app.ActivityCompat;
+import android.support.v4.app.FragmentManager;
 import android.support.v4.content.ContextCompat;
 // `ShortcutInfoCompat`, `ShortcutManagerCompat`, and `IconCompat` can be switched to the non-compat version once API >= 26.
 import android.support.v4.content.pm.ShortcutInfoCompat;
@@ -123,7 +125,7 @@ import com.stoutner.privacybrowser.dialogs.DownloadLocationPermissionDialog;
 import com.stoutner.privacybrowser.dialogs.EditBookmarkDialog;
 import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDialog;
 import com.stoutner.privacybrowser.dialogs.HttpAuthenticationDialog;
-import com.stoutner.privacybrowser.dialogs.PinnedSslCertificateMismatchDialog;
+import com.stoutner.privacybrowser.dialogs.PinnedMismatchDialog;
 import com.stoutner.privacybrowser.dialogs.UrlHistoryDialog;
 import com.stoutner.privacybrowser.dialogs.ViewSslCertificateDialog;
 import com.stoutner.privacybrowser.helpers.AdHelper;
@@ -139,10 +141,13 @@ import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
+import java.lang.ref.WeakReference;
+import java.net.InetAddress;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.net.URLDecoder;
 import java.net.URLEncoder;
+import java.net.UnknownHostException;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.HashMap;
@@ -155,7 +160,7 @@ import java.util.Set;
 public class MainWebViewActivity extends AppCompatActivity implements CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener,
         CreateHomeScreenShortcutDialog.CreateHomeScreenShortcutListener, DownloadFileDialog.DownloadFileListener, DownloadImageDialog.DownloadImageListener,
         DownloadLocationPermissionDialog.DownloadLocationPermissionDialogListener, EditBookmarkDialog.EditBookmarkListener, EditBookmarkFolderDialog.EditBookmarkFolderListener,
-        HttpAuthenticationDialog.HttpAuthenticationListener, NavigationView.OnNavigationItemSelectedListener, PinnedSslCertificateMismatchDialog.PinnedSslCertificateMismatchListener,
+        HttpAuthenticationDialog.HttpAuthenticationListener, NavigationView.OnNavigationItemSelectedListener, PinnedMismatchDialog.PinnedMismatchListener,
         SslCertificateErrorDialog.SslCertificateErrorListener, UrlHistoryDialog.UrlHistoryListener {
 
     // `darkTheme` is public static so it can be accessed from everywhere.
@@ -169,14 +174,21 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     // `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onCreateHomeScreenShortcutCreate()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `applyDomainSettings()`.
     public static Bitmap favoriteIconBitmap;
 
-    // `formattedUrlString` is public static so it can be accessed from `BookmarksActivity`, `CreateBookmarkDialog`, and `AddDomainDialog`.
+    // `favoriteIconDefaultBitmap` public static so it can be accessed from `PinnedMismatchDialog`.  It is also used in `onCreate()` and `applyDomainSettings`.
+    public static Bitmap favoriteIconDefaultBitmap;
+
+    // `formattedUrlString` is public static so it can be accessed from `AddDomainDialog`, `BookmarksActivity`, `DomainSettingsFragment`, `CreateBookmarkDialog`, and `PinnedMismatchDialog`.
     // It is also used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onCreateHomeScreenShortcutCreate()`, `loadUrlFromTextBox()`, and `applyProxyThroughOrbot()`.
     public static String formattedUrlString;
 
-    // `sslCertificate` is public static so it can be accessed from `DomainsActivity`, `DomainsListFragment`, `DomainSettingsFragment`, `PinnedSslCertificateMismatchDialog`,
-    // and `ViewSslCertificateDialog`.  It is also used in `onCreate()`.
+    // `sslCertificate` is public static so it can be accessed from `DomainsActivity`, `DomainsListFragment`, `DomainSettingsFragment`, `PinnedMismatchDialog`, and `ViewSslCertificateDialog`.
+    // It is also used in `onCreate()` and `checkPinnedMismatch()`.
     public static SslCertificate sslCertificate;
 
+    // `currentHostIpAddresses` is public static so it can be accessed from `DomainSettingsFragment` and `ViewSslCertificateDialog`.
+    // It is also used in `onCreate()` and `GetHostIpAddresses()`.
+    public static String currentHostIpAddresses;
+
     // `orbotStatus` is public static so it can be accessed from `OrbotProxyHelper`.  It is also used in `onCreate()`, `onResume()`, and `applyProxyThroughOrbot()`.
     public static String orbotStatus;
 
@@ -257,18 +269,19 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     // `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
     public static String currentBookmarksFolder;
 
-    // `domainSettingsDatabaseId` is public static so it can be accessed from `PinnedSslCertificateMismatchDialog`.  It is also used in `onCreate()`, `onOptionsItemSelected()`, and `applyDomainSettings()`.
+    // `domainSettingsDatabaseId` is public static so it can be accessed from `PinnedMismatchDialog`.  It is also used in `onCreate()`, `onOptionsItemSelected()`, and `applyDomainSettings()`.
     public static int domainSettingsDatabaseId;
 
-    // The pinned domain SSL Certificate variables are public static so they can be accessed from `PinnedSslCertificateMismatchDialog`.  They are also used in `onCreate()` and `applyDomainSettings()`.
-    public static String pinnedDomainSslIssuedToCNameString;
-    public static String pinnedDomainSslIssuedToONameString;
-    public static String pinnedDomainSslIssuedToUNameString;
-    public static String pinnedDomainSslIssuedByCNameString;
-    public static String pinnedDomainSslIssuedByONameString;
-    public static String pinnedDomainSslIssuedByUNameString;
-    public static Date pinnedDomainSslStartDate;
-    public static Date pinnedDomainSslEndDate;
+    // The pinned variables are public static so they can be accessed from `PinnedMismatchDialog`.  They are also used in `onCreate()`, `applyDomainSettings()`, and `checkPinnedMismatch()`.
+    public static String pinnedSslIssuedToCName;
+    public static String pinnedSslIssuedToOName;
+    public static String pinnedSslIssuedToUName;
+    public static String pinnedSslIssuedByCName;
+    public static String pinnedSslIssuedByOName;
+    public static String pinnedSslIssuedByUName;
+    public static Date pinnedSslStartDate;
+    public static Date pinnedSslEndDate;
+    public static String pinnedHostIpAddresses;
 
     // The user agent constants are public static so they can be accessed from `SettingsFragment`, `DomainsActivity`, and `DomainSettingsFragment`.
     public final static int UNRECOGNIZED_USER_AGENT = -1;
@@ -279,15 +292,33 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     public final static int DOMAINS_CUSTOM_USER_AGENT = 13;
 
 
+
+    // `urlIsLoading` is used in `onCreate()`, `onCreateOptionsMenu()`, `loadUrl()`, `applyDomainSettings()`, and `GetHostIpAddresses`.
+    private static boolean urlIsLoading;
+
+    // `gettingIpAddresses` is used in `onCreate() and `GetHostIpAddresses`.
+    private static boolean gettingIpAddresses;
+
+    // `pinnedDomainSslCertificate` is used in `onCreate()`, `applyDomainSettings()`, and `checkPinnedMismatch()`.
+    private static boolean pinnedSslCertificate;
+
+    // `pinnedIpAddress` is used in `applyDomainSettings()` and `checkPinnedMismatch()`.
+    private static boolean pinnedIpAddresses;
+
+    // `ignorePinnedDomainInformation` is used in `onSslMismatchProceed()`, `applyDomainSettings()`, and `checkPinnedMismatch()`.
+    private static boolean ignorePinnedDomainInformation;
+
+    // `supportFragmentManager` is used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onCreateContextMenu()`, `onRequestPermissionResult()`, `viewSslCertificate()`,
+    // `applyAppSettings()`, and `checkPinnedMismatch()`.
+    private static FragmentManager supportFragmentManager;
+
+
     // `appBar` is used in `onCreate()`, `onOptionsItemSelected()`, `closeFindOnPage()`, `applyAppSettings()`, and `applyProxyThroughOrbot()`.
     private ActionBar appBar;
 
     // `navigatingHistory` is used in `onCreate()`, `onNavigationItemSelected()`, `onSslMismatchBack()`, and `applyDomainSettings()`.
     private boolean navigatingHistory;
 
-    // `favoriteIconDefaultBitmap` is used in `onCreate()` and `applyDomainSettings`.
-    private Bitmap favoriteIconDefaultBitmap;
-
     // `drawerLayout` is used in `onCreate()`, `onNewIntent()`, `onBackPressed()`, and `onRestart()`.
     private DrawerLayout drawerLayout;
 
@@ -404,9 +435,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     // `currentDomainName` is used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onAddDomain()`, and `applyDomainSettings()`.
     private String currentDomainName;
 
-    // `ignorePinnedSslCertificateForDomain` is used in `onCreate()`, `onSslMismatchProceed()`, and `applyDomainSettings()`.
-    private boolean ignorePinnedSslCertificate;
-
     // `orbotStatusBroadcastReceiver` is used in `onCreate()` and `onDestroy()`.
     private BroadcastReceiver orbotStatusBroadcastReceiver;
 
@@ -465,12 +493,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     // `mainWebViewRelativeLayout` is used in `onCreate()` and `onNavigationItemSelected()`.
     private RelativeLayout mainWebViewRelativeLayout;
 
-    // `urlIsLoading` is used in `onCreate()`, `onCreateOptionsMenu()`, `loadUrl()`, and `applyDomainSettings()`.
-    private boolean urlIsLoading;
-
-    // `pinnedDomainSslCertificate` is used in `onCreate()` and `applyDomainSettings()`.
-    private boolean pinnedDomainSslCertificate;
-
     // `bookmarksDatabaseHelper` is used in `onCreate()`, `onDestroy`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`,
     // and `loadBookmarksFolder()`.
     private BookmarksDatabaseHelper bookmarksDatabaseHelper;
@@ -541,8 +563,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Set the content view.
         setContentView(R.layout.main_drawerlayout);
 
-        // Get a handle for the resources.
+        // Get a handle for the resources and the support fragment manager.
         Resources resources = getResources();
+        supportFragmentManager = getSupportFragmentManager();
 
         // Get a handle for `inputMethodManager`.
         inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
@@ -672,14 +695,14 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         createBookmarkFolderFab.setOnClickListener(v -> {
             // Show the `CreateBookmarkFolderDialog` `AlertDialog` and name the instance `@string/create_folder`.
             AppCompatDialogFragment createBookmarkFolderDialog = new CreateBookmarkFolderDialog();
-            createBookmarkFolderDialog.show(getSupportFragmentManager(), resources.getString(R.string.create_folder));
+            createBookmarkFolderDialog.show(supportFragmentManager, resources.getString(R.string.create_folder));
         });
 
         // Set the create new bookmark FAB to display an alert dialog.
         createBookmarkFab.setOnClickListener(view -> {
             // Show the `CreateBookmarkDialog` `AlertDialog` and name the instance `@string/create_bookmark`.
             AppCompatDialogFragment createBookmarkDialog = new CreateBookmarkDialog();
-            createBookmarkDialog.show(getSupportFragmentManager(), resources.getString(R.string.create_bookmark));
+            createBookmarkDialog.show(supportFragmentManager, resources.getString(R.string.create_bookmark));
         });
 
         // Create a double-tap listener to toggle full-screen mode.
@@ -899,11 +922,11 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
                 // Show the edit bookmark folder `AlertDialog` and name the instance `@string/edit_folder`.
                 AppCompatDialogFragment editFolderDialog = EditBookmarkFolderDialog.folderDatabaseId(databaseId);
-                editFolderDialog.show(getSupportFragmentManager(), resources.getString(R.string.edit_folder));
+                editFolderDialog.show(supportFragmentManager, resources.getString(R.string.edit_folder));
             } else {
                 // Show the edit bookmark `AlertDialog` and name the instance `@string/edit_bookmark`.
                 AppCompatDialogFragment editBookmarkDialog = EditBookmarkDialog.bookmarkDatabaseId(databaseId);
-                editBookmarkDialog.show(getSupportFragmentManager(), resources.getString(R.string.edit_bookmark));
+                editBookmarkDialog.show(supportFragmentManager, resources.getString(R.string.edit_bookmark));
             }
 
             // Consume the event.
@@ -1211,7 +1234,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     AppCompatDialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(url, contentDisposition, contentLength);
 
                     // Show the download file alert dialog.
-                    downloadFileDialogFragment.show(getSupportFragmentManager(), getString(R.string.download));
+                    downloadFileDialogFragment.show(supportFragmentManager, getString(R.string.download));
                 }
             }
         });
@@ -1589,12 +1612,19 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
                 // Display the HTTP authentication dialog.
                 AppCompatDialogFragment httpAuthenticationDialogFragment = HttpAuthenticationDialog.displayDialog(host, realm);
-                httpAuthenticationDialogFragment.show(getSupportFragmentManager(), getString(R.string.http_authentication));
+                httpAuthenticationDialogFragment.show(supportFragmentManager, getString(R.string.http_authentication));
             }
 
             // Update the URL in urlTextBox when the page starts to load.
             @Override
             public void onPageStarted(WebView view, String url, Bitmap favicon) {
+                // Set `urlIsLoading` to `true`, so that redirects while loading do not trigger changes in the user agent, which forces another reload of the existing page.
+                // This is also used to determine when to check for pinned mismatches.
+                urlIsLoading = true;
+
+                // Reset the list of host IP addresses.
+                currentHostIpAddresses = "";
+
                 // Reset the list of resource requests.
                 resourceRequests.clear();
 
@@ -1616,7 +1646,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 inputMethodManager.hideSoftInputFromWindow(mainWebView.getWindowToken(), 0);
 
                 // Check to see if Privacy Browser is waiting on Orbot.
-                if (!waitingForOrbot) {  // We are not waiting on Orbot, so we need to process the URL.
+                if (!waitingForOrbot) {  // Process the URL.
                     // The formatted URL string must be updated at the beginning of the load, so that if the user toggles JavaScript during the load the new website is reloaded.
                     formattedUrlString = url;
 
@@ -1626,6 +1656,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     // Apply text highlighting to `urlTextBox`.
                     highlightUrlText();
 
+                    // Get a URI for the current URL.
+                    Uri currentUri = Uri.parse(formattedUrlString);
+
+                    // Get the IP addresses for the host.
+                    new GetHostIpAddresses(activity).execute(currentUri.getHost());
+
                     // Apply any custom domain settings if the URL was loaded by navigating history.
                     if (navigatingHistory) {
                         // Apply the domain settings.
@@ -1640,9 +1676,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                         }
                     }
 
-                    // Set `urlIsLoading` to `true`, so that redirects while loading do not trigger changes in the user agent, which forces another reload of the existing page.
-                    urlIsLoading = true;
-
                     // Replace Refresh with Stop if the menu item has been created.  (The WebView typically begins loading before the menu items are instantiated.)
                     if (refreshMenuItem != null) {
                         // Set the title.
@@ -1689,8 +1722,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     }
                 }
 
-                // Reset `urlIsLoading`, which is used to prevent reloads on redirect if the user agent changes.
-                urlIsLoading = false;
+
 
                 // Clear the cache and history if Incognito Mode is enabled.
                 if (incognitoModeEnabled) {
@@ -1713,7 +1745,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     }
                 }
 
-                // Update `urlTextBox` and apply domain settings if not waiting on Orbot.
+                // Update the URL text box and apply domain settings if not waiting on Orbot.
                 if (!waitingForOrbot) {
                     // Check to see if `WebView` has set `url` to be `about:blank`.
                     if (url.equals("about:blank")) {  // `WebView` is blank, so `formattedUrlString` should be `""` and `urlTextBox` should display a hint.
@@ -1744,70 +1776,17 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                         }
                     }
 
-                    // Store the SSL certificate so it can be accessed from `ViewSslCertificateDialog` and `PinnedSslCertificateMismatchDialog`.
+                    // Store the SSL certificate so it can be accessed from `ViewSslCertificateDialog` and `PinnedMismatchDialog`.
                     sslCertificate = mainWebView.getCertificate();
 
-                    // Check the current website SSL certificate against the pinned SSL certificate if there is a pinned SSL certificate the user has not chosen to ignore it for this session.
-                    // Also ignore if changes in the user agent causes an error while navigating history.
-                    if (pinnedDomainSslCertificate && !ignorePinnedSslCertificate && navigatingHistory) {
-                        // Initialize the current SSL certificate variables.
-                        String currentWebsiteIssuedToCName = "";
-                        String currentWebsiteIssuedToOName = "";
-                        String currentWebsiteIssuedToUName = "";
-                        String currentWebsiteIssuedByCName = "";
-                        String currentWebsiteIssuedByOName = "";
-                        String currentWebsiteIssuedByUName = "";
-                        Date currentWebsiteSslStartDate = null;
-                        Date currentWebsiteSslEndDate = null;
-
-
-                        // Extract the individual pieces of information from the current website SSL certificate if it is not null.
-                        if (sslCertificate != null) {
-                            currentWebsiteIssuedToCName = sslCertificate.getIssuedTo().getCName();
-                            currentWebsiteIssuedToOName = sslCertificate.getIssuedTo().getOName();
-                            currentWebsiteIssuedToUName = sslCertificate.getIssuedTo().getUName();
-                            currentWebsiteIssuedByCName = sslCertificate.getIssuedBy().getCName();
-                            currentWebsiteIssuedByOName = sslCertificate.getIssuedBy().getOName();
-                            currentWebsiteIssuedByUName = sslCertificate.getIssuedBy().getUName();
-                            currentWebsiteSslStartDate = sslCertificate.getValidNotBeforeDate();
-                            currentWebsiteSslEndDate = sslCertificate.getValidNotAfterDate();
-                        }
-
-                        // Initialize `String` variables to store the SSL certificate dates.  `Strings` are needed to compare the values below, which doesn't work with `Dates` if they are `null`.
-                        String currentWebsiteSslStartDateString = "";
-                        String currentWebsiteSslEndDateString = "";
-                        String pinnedDomainSslStartDateString = "";
-                        String pinnedDomainSslEndDateString = "";
-
-                        // Convert the `Dates` to `Strings` if they are not `null`.
-                        if (currentWebsiteSslStartDate != null) {
-                            currentWebsiteSslStartDateString = currentWebsiteSslStartDate.toString();
-                        }
-
-                        if (currentWebsiteSslEndDate != null) {
-                            currentWebsiteSslEndDateString = currentWebsiteSslEndDate.toString();
-                        }
-
-                        if (pinnedDomainSslStartDate != null) {
-                            pinnedDomainSslStartDateString = pinnedDomainSslStartDate.toString();
-                        }
-
-                        if (pinnedDomainSslEndDate != null) {
-                            pinnedDomainSslEndDateString = pinnedDomainSslEndDate.toString();
-                        }
-
-                        // Check to see if the pinned SSL certificate matches the current website certificate.
-                        if (!currentWebsiteIssuedToCName.equals(pinnedDomainSslIssuedToCNameString) || !currentWebsiteIssuedToOName.equals(pinnedDomainSslIssuedToONameString) ||
-                                !currentWebsiteIssuedToUName.equals(pinnedDomainSslIssuedToUNameString) || !currentWebsiteIssuedByCName.equals(pinnedDomainSslIssuedByCNameString) ||
-                                !currentWebsiteIssuedByOName.equals(pinnedDomainSslIssuedByONameString) || !currentWebsiteIssuedByUName.equals(pinnedDomainSslIssuedByUNameString) ||
-                                !currentWebsiteSslStartDateString.equals(pinnedDomainSslStartDateString) || !currentWebsiteSslEndDateString.equals(pinnedDomainSslEndDateString)) {
-                            // The pinned SSL certificate doesn't match the current domain certificate.
-                            //Display the pinned SSL certificate mismatch `AlertDialog`.
-                            AppCompatDialogFragment pinnedSslCertificateMismatchDialogFragment = new PinnedSslCertificateMismatchDialog();
-                            pinnedSslCertificateMismatchDialogFragment.show(getSupportFragmentManager(), getString(R.string.ssl_certificate_mismatch));
-                        }
+                    // Check the current website information against any pinned domain information if the current IP addresses have been loaded.
+                    if (!gettingIpAddresses) {
+                        checkPinnedMismatch();
                     }
                 }
+
+                // Reset `urlIsLoading`, which is used to prevent reloads on redirect if the user agent changes.  It is also used to determine when to check for pinned mismatches.
+                urlIsLoading = false;
             }
 
             // Handle SSL Certificate errors.
@@ -1827,13 +1806,13 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 Date currentWebsiteSslEndDate = currentWebsiteSslCertificate.getValidNotAfterDate();
 
                 // Proceed to the website if the current SSL website certificate matches the pinned domain certificate.
-                if (pinnedDomainSslCertificate &&
-                        currentWebsiteIssuedToCName.equals(pinnedDomainSslIssuedToCNameString) && currentWebsiteIssuedToOName.equals(pinnedDomainSslIssuedToONameString) &&
-                        currentWebsiteIssuedToUName.equals(pinnedDomainSslIssuedToUNameString) && currentWebsiteIssuedByCName.equals(pinnedDomainSslIssuedByCNameString) &&
-                        currentWebsiteIssuedByOName.equals(pinnedDomainSslIssuedByONameString) && currentWebsiteIssuedByUName.equals(pinnedDomainSslIssuedByUNameString) &&
-                        currentWebsiteSslStartDate.equals(pinnedDomainSslStartDate) && currentWebsiteSslEndDate.equals(pinnedDomainSslEndDate)) {
-                    // An SSL certificate is pinned and matches the current domain certificate.
-                    // Proceed to the website without displaying an error.
+                if (pinnedSslCertificate &&
+                        currentWebsiteIssuedToCName.equals(pinnedSslIssuedToCName) && currentWebsiteIssuedToOName.equals(pinnedSslIssuedToOName) &&
+                        currentWebsiteIssuedToUName.equals(pinnedSslIssuedToUName) && currentWebsiteIssuedByCName.equals(pinnedSslIssuedByCName) &&
+                        currentWebsiteIssuedByOName.equals(pinnedSslIssuedByOName) && currentWebsiteIssuedByUName.equals(pinnedSslIssuedByUName) &&
+                        currentWebsiteSslStartDate.equals(pinnedSslStartDate) && currentWebsiteSslEndDate.equals(pinnedSslEndDate)) {
+
+                    // An SSL certificate is pinned and matches the current domain certificate.  Proceed to the website without displaying an error.
                     handler.proceed();
                 } else {  // Either there isn't a pinned SSL certificate or it doesn't match the current website certificate.
                     // Store `handler` so it can be accesses from `onSslErrorCancel()` and `onSslErrorProceed()`.
@@ -1841,7 +1820,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
                     // Display the SSL error `AlertDialog`.
                     AppCompatDialogFragment sslCertificateErrorDialogFragment = SslCertificateErrorDialog.displayDialog(error);
-                    sslCertificateErrorDialogFragment.show(getSupportFragmentManager(), getString(R.string.ssl_certificate_error));
+                    sslCertificateErrorDialogFragment.show(supportFragmentManager, getString(R.string.ssl_certificate_error));
                 }
             }
         });
@@ -2849,20 +2828,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 mainWebView.reload();
                 return true;
 
-            case R.id.print:
-                // Get a `PrintManager` instance.
-                PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);
-
-                // Convert `mainWebView` to `printDocumentAdapter`.
-                PrintDocumentAdapter printDocumentAdapter = mainWebView.createPrintDocumentAdapter();
-
-                // Remove the lint error below that `printManager` might be `null`.
-                assert printManager != null;
-
-                // Print the document.  The print attributes are `null`.
-                printManager.print(getString(R.string.privacy_browser_web_page), printDocumentAdapter, null);
-                return true;
-
             case R.id.find_on_page:
                 // Hide the URL app bar.
                 supportAppBar.setVisibility(View.GONE);
@@ -2881,14 +2846,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 }, 200);
                 return true;
 
-            case R.id.add_to_homescreen:
-                // Show the alert dialog.
-                AppCompatDialogFragment createHomeScreenShortcutDialogFragment = new CreateHomeScreenShortcutDialog();
-                createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getString(R.string.create_shortcut));
-
-                //Everything else will be handled by the alert dialog and the associated listener below.
-                return true;
-
             case R.id.view_source:
                 // Launch the View Source activity.
                 Intent viewSourceIntent = new Intent(this, ViewSourceActivity.class);
@@ -2908,32 +2865,34 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 startActivity(Intent.createChooser(shareIntent, getString(R.string.share_url)));
                 return true;
 
-            case R.id.open_with_app:
-                // Create the open with intent with `ACTION_VIEW`.
-                Intent openWithAppIntent = new Intent(Intent.ACTION_VIEW);
+            case R.id.print:
+                // Get a `PrintManager` instance.
+                PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);
+
+                // Convert `mainWebView` to `printDocumentAdapter`.
+                PrintDocumentAdapter printDocumentAdapter = mainWebView.createPrintDocumentAdapter();
 
-                // Set the URI but not the MIME type.  This should open all available apps.
-                openWithAppIntent.setData(Uri.parse(formattedUrlString));
+                // Remove the lint error below that `printManager` might be `null`.
+                assert printManager != null;
 
-                // Flag the intent to open in a new task.
-                openWithAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+                // Print the document.  The print attributes are `null`.
+                printManager.print(getString(R.string.privacy_browser_web_page), printDocumentAdapter, null);
+                return true;
 
-                // Show the chooser.
-                startActivity(Intent.createChooser(openWithAppIntent, getString(R.string.open_with)));
+            case R.id.open_with_app:
+                openWithApp(formattedUrlString);
                 return true;
 
             case R.id.open_with_browser:
-                // Create the open with intent with `ACTION_VIEW`.
-                Intent openWithBrowserIntent = new Intent(Intent.ACTION_VIEW);
-
-                // Set the URI and the MIME type.  `"text/html"` should load browser options.
-                openWithBrowserIntent.setDataAndType(Uri.parse(formattedUrlString), "text/html");
+                openWithBrowser(formattedUrlString);
+                return true;
 
-                // Flag the intent to open in a new task.
-                openWithBrowserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            case R.id.add_to_homescreen:
+                // Show the alert dialog.
+                AppCompatDialogFragment createHomeScreenShortcutDialogFragment = new CreateHomeScreenShortcutDialog();
+                createHomeScreenShortcutDialogFragment.show(supportFragmentManager, getString(R.string.create_shortcut));
 
-                // Show the chooser.
-                startActivity(Intent.createChooser(openWithBrowserIntent, getString(R.string.open_with)));
+                //Everything else will be handled by the alert dialog and the associated listener below.
                 return true;
 
             case R.id.proxy_through_orbot:
@@ -3009,7 +2968,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
                 // Show the `UrlHistoryDialog` `AlertDialog` and name this instance `R.string.history`.  `this` is the `Context`.
                 AppCompatDialogFragment urlHistoryDialogFragment = UrlHistoryDialog.loadBackForwardList(this, webBackForwardList);
-                urlHistoryDialogFragment.show(getSupportFragmentManager(), getString(R.string.history));
+                urlHistoryDialogFragment.show(supportFragmentManager, getString(R.string.history));
                 break;
 
             case R.id.requests:
@@ -3301,13 +3260,25 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                             AppCompatDialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(linkUrl, "none", -1);
 
                             // Show the download file alert dialog.
-                            downloadFileDialogFragment.show(getSupportFragmentManager(), getString(R.string.download));
+                            downloadFileDialogFragment.show(supportFragmentManager, getString(R.string.download));
                         }
                     }
                     return false;
                 });
 
-                // Add a `Cancel` entry, which by default closes the `ContextMenu`.
+                // Add an Open with App entry.
+                menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
+                    openWithApp(linkUrl);
+                    return false;
+                });
+
+                // Add an Open with Browser entry.
+                menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
+                    openWithBrowser(linkUrl);
+                    return false;
+                });
+
+                // Add a Cancel entry, which by default closes the context menu.
                 menu.add(R.string.cancel);
                 break;
 
@@ -3318,7 +3289,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Set the target URL as the title of the `ContextMenu`.
                 menu.setHeaderTitle(linkUrl);
 
-                // Add a `Write Email` entry.
+                // Add a Write Email entry.
                 menu.add(R.string.write_email).setOnMenuItemClickListener(item -> {
                     // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
                     Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
@@ -3334,7 +3305,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     return false;
                 });
 
-                // Add a `Copy Email Address` entry.
+                // Add a Copy Email Address entry.
                 menu.add(R.string.copy_email_address).setOnMenuItemClickListener(item -> {
                     // Save the email address in a `ClipData`.
                     ClipData srcEmailTypeClipData = ClipData.newPlainText(getString(R.string.email_address), linkUrl);
@@ -3356,13 +3327,13 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Set the image URL as the title of the `ContextMenu`.
                 menu.setHeaderTitle(imageUrl);
 
-                // Add a `View Image` entry.
+                // Add a View Image entry.
                 menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
                     loadUrl(imageUrl);
                     return false;
                 });
 
-                // Add a `Download Image` entry.
+                // Add a Download Image entry.
                 menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
                     // Check if the download should be processed by an external app.
                     if (downloadWithExternalApp) {  // Download with an external app.
@@ -3389,13 +3360,13 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                             AppCompatDialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
 
                             // Show the download image alert dialog.
-                            downloadImageDialogFragment.show(getSupportFragmentManager(), getString(R.string.download));
+                            downloadImageDialogFragment.show(supportFragmentManager, getString(R.string.download));
                         }
                     }
                     return false;
                 });
 
-                // Add a `Copy URL` entry.
+                // Add a Copy URL entry.
                 menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> {
                     // Save the image URL in a `ClipData`.
                     ClipData srcImageTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
@@ -3405,6 +3376,18 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     return false;
                 });
 
+                // Add an Open with App entry.
+                menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
+                    openWithApp(imageUrl);
+                    return false;
+                });
+
+                // Add an Open with Browser entry.
+                menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
+                    openWithBrowser(imageUrl);
+                    return false;
+                });
+
                 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
                 menu.add(R.string.cancel);
                 break;
@@ -3451,7 +3434,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                             AppCompatDialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
 
                             // Show the download image alert dialog.
-                            downloadImageDialogFragment.show(getSupportFragmentManager(), getString(R.string.download));
+                            downloadImageDialogFragment.show(supportFragmentManager, getString(R.string.download));
                         }
                     }
                     return false;
@@ -3467,6 +3450,18 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     return false;
                 });
 
+                // Add an Open with App entry.
+                menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
+                    openWithApp(imageUrl);
+                    return false;
+                });
+
+                // Add an Open with Browser entry.
+                menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
+                    openWithBrowser(imageUrl);
+                    return false;
+                });
+
                 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
                 menu.add(R.string.cancel);
                 break;
@@ -3598,9 +3593,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
                 // On API 23, displaying the fragment must be delayed or the app will crash.
                 if (Build.VERSION.SDK_INT == 23) {
-                    new Handler().postDelayed(() -> downloadFileDialogFragment.show(getSupportFragmentManager(), getString(R.string.download)), 500);
+                    new Handler().postDelayed(() -> downloadFileDialogFragment.show(supportFragmentManager, getString(R.string.download)), 500);
                 } else {
-                    downloadFileDialogFragment.show(getSupportFragmentManager(), getString(R.string.download));
+                    downloadFileDialogFragment.show(supportFragmentManager, getString(R.string.download));
                 }
 
                 // Reset the download variables.
@@ -3615,9 +3610,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
                 // On API 23, displaying the fragment must be delayed or the app will crash.
                 if (Build.VERSION.SDK_INT == 23) {
-                    new Handler().postDelayed(() -> downloadImageDialogFragment.show(getSupportFragmentManager(), getString(R.string.download)), 500);
+                    new Handler().postDelayed(() -> downloadImageDialogFragment.show(supportFragmentManager, getString(R.string.download)), 500);
                 } else {
-                    downloadImageDialogFragment.show(getSupportFragmentManager(), getString(R.string.download));
+                    downloadImageDialogFragment.show(supportFragmentManager, getString(R.string.download));
                 }
 
                 // Reset the image URL variable.
@@ -3856,7 +3851,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     }
 
     @Override
-    public void onSslMismatchBack() {
+    public void onPinnedMismatchBack() {
         if (mainWebView.canGoBack()) {  // There is a back page in the history.
             // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
             formattedUrlString = "";
@@ -3873,9 +3868,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     }
 
     @Override
-    public void onSslMismatchProceed() {
-        // Do not check the pinned SSL certificate for this domain again until the domain changes.
-        ignorePinnedSslCertificate = true;
+    public void onPinnedMismatchProceed() {
+        // Do not check the pinned information for this domain again until the domain changes.
+        ignorePinnedDomainInformation = true;
     }
 
     @Override
@@ -4170,8 +4165,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             // Set the new `hostname` as the `currentDomainName`.
             currentDomainName = hostName;
 
-            // Reset `ignorePinnedSslCertificate`.
-            ignorePinnedSslCertificate = false;
+            // Reset the ignoring of pinned domain information.
+            ignorePinnedDomainInformation = false;
 
             // Reset the favorite icon if specified.
             if (resetFavoriteIcon) {
@@ -4263,13 +4258,15 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 int swipeToRefreshInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SWIPE_TO_REFRESH));
                 int nightModeInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.NIGHT_MODE));
                 int displayWebpageImagesInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.DISPLAY_IMAGES));
-                pinnedDomainSslCertificate = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)) == 1);
-                pinnedDomainSslIssuedToCNameString = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME));
-                pinnedDomainSslIssuedToONameString = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION));
-                pinnedDomainSslIssuedToUNameString = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT));
-                pinnedDomainSslIssuedByCNameString = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME));
-                pinnedDomainSslIssuedByONameString = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION));
-                pinnedDomainSslIssuedByUNameString = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT));
+                pinnedSslCertificate = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)) == 1);
+                pinnedSslIssuedToCName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME));
+                pinnedSslIssuedToOName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION));
+                pinnedSslIssuedToUName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT));
+                pinnedSslIssuedByCName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME));
+                pinnedSslIssuedByOName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION));
+                pinnedSslIssuedByUName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT));
+                pinnedIpAddresses = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_IP_ADDRESSES)) == 1);
+                pinnedHostIpAddresses = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.IP_ADDRESSES));
 
                 // Set `nightMode` according to `nightModeInt`.  If `nightModeInt` is `DomainsDatabaseHelper.NIGHT_MODE_SYSTEM_DEFAULT` the current setting from `sharedPreferences` will be used.
                 switch (nightModeInt) {
@@ -4292,16 +4289,16 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
                 // Set the pinned SSL certificate start date to `null` if the saved date `long` is 0.
                 if (currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)) == 0) {
-                    pinnedDomainSslStartDate = null;
+                    pinnedSslStartDate = null;
                 } else {
-                    pinnedDomainSslStartDate = new Date(currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)));
+                    pinnedSslStartDate = new Date(currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)));
                 }
 
                 // Set the pinned SSL certificate end date to `null` if the saved date `long` is 0.
                 if (currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)) == 0) {
-                    pinnedDomainSslEndDate = null;
+                    pinnedSslEndDate = null;
                 } else {
-                    pinnedDomainSslEndDate = new Date(currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)));
+                    pinnedSslEndDate = new Date(currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)));
                 }
 
                 // Close `currentHostDomainSettingsCursor`.
@@ -4454,17 +4451,19 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     mainWebView.getSettings().setSaveFormData(saveFormDataEnabled);
                 }
 
-                // Reset the pinned SSL certificate information.
+                // Reset the pinned variables.
                 domainSettingsDatabaseId = -1;
-                pinnedDomainSslCertificate = false;
-                pinnedDomainSslIssuedToCNameString = "";
-                pinnedDomainSslIssuedToONameString = "";
-                pinnedDomainSslIssuedToUNameString = "";
-                pinnedDomainSslIssuedByCNameString = "";
-                pinnedDomainSslIssuedByONameString = "";
-                pinnedDomainSslIssuedByUNameString = "";
-                pinnedDomainSslStartDate = null;
-                pinnedDomainSslEndDate = null;
+                pinnedSslCertificate = false;
+                pinnedSslIssuedToCName = "";
+                pinnedSslIssuedToOName = "";
+                pinnedSslIssuedToUName = "";
+                pinnedSslIssuedByCName = "";
+                pinnedSslIssuedByOName = "";
+                pinnedSslIssuedByUName = "";
+                pinnedSslStartDate = null;
+                pinnedSslEndDate = null;
+                pinnedIpAddresses = false;
+                pinnedHostIpAddresses = "";
 
                 // Set third-party cookies status if API >= 21.
                 if (Build.VERSION.SDK_INT >= 21) {
@@ -4509,7 +4508,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Set the loading of webpage images.
                 mainWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
 
-                // Set a transparent background on `urlTextBox`.  We have to use the deprecated `.getDrawable()` until the minimum API >= 21.
+                // Set a transparent background on `urlTextBox`.  The deprecated `.getDrawable()` must be used until the minimum API >= 21.
                 urlAppBarRelativeLayout.setBackgroundDrawable(getResources().getDrawable(R.color.transparent));
             }
 
@@ -4692,59 +4691,62 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     }
 
     private void highlightUrlText() {
-        // Get the URL string.
-        String urlString = urlTextBox.getText().toString();
-
-        // Highlight the URL according to the protocol.
-        if (urlString.startsWith("file://")) {  // This is a file URL.
-            // De-emphasize only the protocol.
-            urlTextBox.getText().setSpan(initialGrayColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-        } else if (urlString.startsWith("content://")) {
-            // De-emphasize only the protocol.
-            urlTextBox.getText().setSpan(initialGrayColorSpan, 0, 10, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-        } else {  // This is a web URL.
-            // Get the index of the `/` immediately after the domain name.
-            int endOfDomainName = urlString.indexOf("/", (urlString.indexOf("//") + 2));
-
-            // Create a base URL string.
-            String baseUrl;
-
-            // Get the base URL.
-            if (endOfDomainName > 0) {  // There is at least one character after the base URL.
+        // Only highlight the URL text if the box is not currently selected.
+        if (!urlTextBox.hasFocus()) {
+            // Get the URL string.
+            String urlString = urlTextBox.getText().toString();
+
+            // Highlight the URL according to the protocol.
+            if (urlString.startsWith("file://")) {  // This is a file URL.
+                // De-emphasize only the protocol.
+                urlTextBox.getText().setSpan(initialGrayColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+            } else if (urlString.startsWith("content://")) {
+                // De-emphasize only the protocol.
+                urlTextBox.getText().setSpan(initialGrayColorSpan, 0, 10, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+            } else {  // This is a web URL.
+                // Get the index of the `/` immediately after the domain name.
+                int endOfDomainName = urlString.indexOf("/", (urlString.indexOf("//") + 2));
+
+                // Create a base URL string.
+                String baseUrl;
+
                 // Get the base URL.
-                baseUrl = urlString.substring(0, endOfDomainName);
-            } else {  // There are no characters after the base URL.
-                // Set the base URL to be the entire URL string.
-                baseUrl = urlString;
-            }
+                if (endOfDomainName > 0) {  // There is at least one character after the base URL.
+                    // Get the base URL.
+                    baseUrl = urlString.substring(0, endOfDomainName);
+                } else {  // There are no characters after the base URL.
+                    // Set the base URL to be the entire URL string.
+                    baseUrl = urlString;
+                }
 
-            // Get the index of the last `.` in the domain.
-            int lastDotIndex = baseUrl.lastIndexOf(".");
+                // Get the index of the last `.` in the domain.
+                int lastDotIndex = baseUrl.lastIndexOf(".");
 
-            // Get the index of the penultimate `.` in the domain.
-            int penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1);
+                // Get the index of the penultimate `.` in the domain.
+                int penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1);
 
-            // Markup the beginning of the URL.
-            if (urlString.startsWith("http://")) {  // Highlight the protocol of connections that are not encrypted.
-                urlTextBox.getText().setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+                // Markup the beginning of the URL.
+                if (urlString.startsWith("http://")) {  // Highlight the protocol of connections that are not encrypted.
+                    urlTextBox.getText().setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
 
-                // De-emphasize subdomains.
-                if (penultimateDotIndex > 0) {  // There is more than one subdomain in the domain name.
-                    urlTextBox.getText().setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-                }
-            } else if (urlString.startsWith("https://")) {  // De-emphasize the protocol of connections that are encrypted.
-                if (penultimateDotIndex > 0) {  // There is more than one subdomain in the domain name.
-                    // De-emphasize the protocol and the additional subdomains.
-                    urlTextBox.getText().setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-                } else {  // There is only one subdomain in the domain name.
-                    // De-emphasize only the protocol.
-                    urlTextBox.getText().setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+                    // De-emphasize subdomains.
+                    if (penultimateDotIndex > 0) {  // There is more than one subdomain in the domain name.
+                        urlTextBox.getText().setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+                    }
+                } else if (urlString.startsWith("https://")) {  // De-emphasize the protocol of connections that are encrypted.
+                    if (penultimateDotIndex > 0) {  // There is more than one subdomain in the domain name.
+                        // De-emphasize the protocol and the additional subdomains.
+                        urlTextBox.getText().setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+                    } else {  // There is only one subdomain in the domain name.
+                        // De-emphasize only the protocol.
+                        urlTextBox.getText().setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+                    }
                 }
-            }
 
-            // De-emphasize the text after the domain name.
-            if (endOfDomainName > 0) {
-                urlTextBox.getText().setSpan(finalGrayColorSpan, endOfDomainName, urlString.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+                // De-emphasize the text after the domain name.
+                if (endOfDomainName > 0) {
+                    urlTextBox.getText().setSpan(finalGrayColorSpan, endOfDomainName, urlString.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+                }
             }
         }
     }
@@ -4799,4 +4801,185 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             bookmarksTitleTextView.setText(currentBookmarksFolder);
         }
     }
+
+    private void openWithApp(String url) {
+        // Create the open with intent with `ACTION_VIEW`.
+        Intent openWithAppIntent = new Intent(Intent.ACTION_VIEW);
+
+        // Set the URI but not the MIME type.  This should open all available apps.
+        openWithAppIntent.setData(Uri.parse(url));
+
+        // Flag the intent to open in a new task.
+        openWithAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+        // Show the chooser.
+        startActivity(openWithAppIntent);
+    }
+
+    private void openWithBrowser(String url) {
+        // Create the open with intent with `ACTION_VIEW`.
+        Intent openWithBrowserIntent = new Intent(Intent.ACTION_VIEW);
+
+        // Set the URI and the MIME type.  `"text/html"` should load browser options.
+        openWithBrowserIntent.setDataAndType(Uri.parse(url), "text/html");
+
+        // Flag the intent to open in a new task.
+        openWithBrowserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+        // Show the chooser.
+        startActivity(openWithBrowserIntent);
+    }
+
+    private static void checkPinnedMismatch() {
+        if ((pinnedSslCertificate || pinnedIpAddresses) && !ignorePinnedDomainInformation) {
+            // Initialize the current SSL certificate variables.
+            String currentWebsiteIssuedToCName = "";
+            String currentWebsiteIssuedToOName = "";
+            String currentWebsiteIssuedToUName = "";
+            String currentWebsiteIssuedByCName = "";
+            String currentWebsiteIssuedByOName = "";
+            String currentWebsiteIssuedByUName = "";
+            Date currentWebsiteSslStartDate = null;
+            Date currentWebsiteSslEndDate = null;
+
+
+            // Extract the individual pieces of information from the current website SSL certificate if it is not null.
+            if (sslCertificate != null) {
+                currentWebsiteIssuedToCName = sslCertificate.getIssuedTo().getCName();
+                currentWebsiteIssuedToOName = sslCertificate.getIssuedTo().getOName();
+                currentWebsiteIssuedToUName = sslCertificate.getIssuedTo().getUName();
+                currentWebsiteIssuedByCName = sslCertificate.getIssuedBy().getCName();
+                currentWebsiteIssuedByOName = sslCertificate.getIssuedBy().getOName();
+                currentWebsiteIssuedByUName = sslCertificate.getIssuedBy().getUName();
+                currentWebsiteSslStartDate = sslCertificate.getValidNotBeforeDate();
+                currentWebsiteSslEndDate = sslCertificate.getValidNotAfterDate();
+            }
+
+            // Initialize string variables to store the SSL certificate dates.  Strings are needed to compare the values below, which doesn't work with `Dates` if they are `null`.
+            String currentWebsiteSslStartDateString = "";
+            String currentWebsiteSslEndDateString = "";
+            String pinnedSslStartDateString = "";
+            String pinnedSslEndDateString = "";
+
+            // Convert the `Dates` to `Strings` if they are not `null`.
+            if (currentWebsiteSslStartDate != null) {
+                currentWebsiteSslStartDateString = currentWebsiteSslStartDate.toString();
+            }
+
+            if (currentWebsiteSslEndDate != null) {
+                currentWebsiteSslEndDateString = currentWebsiteSslEndDate.toString();
+            }
+
+            if (pinnedSslStartDate != null) {
+                pinnedSslStartDateString = pinnedSslStartDate.toString();
+            }
+
+            if (pinnedSslEndDate != null) {
+                pinnedSslEndDateString = pinnedSslEndDate.toString();
+            }
+
+            // Check to see if the pinned information matches the current information.
+            if ((pinnedIpAddresses && !currentHostIpAddresses.equals(pinnedHostIpAddresses)) || (pinnedSslCertificate && (!currentWebsiteIssuedToCName.equals(pinnedSslIssuedToCName) ||
+                    !currentWebsiteIssuedToOName.equals(pinnedSslIssuedToOName) || !currentWebsiteIssuedToUName.equals(pinnedSslIssuedToUName) ||
+                    !currentWebsiteIssuedByCName.equals(pinnedSslIssuedByCName) || !currentWebsiteIssuedByOName.equals(pinnedSslIssuedByOName) ||
+                    !currentWebsiteIssuedByUName.equals(pinnedSslIssuedByUName) || !currentWebsiteSslStartDateString.equals(pinnedSslStartDateString) ||
+                    !currentWebsiteSslEndDateString.equals(pinnedSslEndDateString)))) {
+
+                // Get a handle for the pinned mismatch alert dialog.
+                AppCompatDialogFragment pinnedMismatchDialogFragment = PinnedMismatchDialog.displayDialog(pinnedSslCertificate, pinnedIpAddresses);
+
+                // Show the pinned mismatch alert dialog.
+                pinnedMismatchDialogFragment.show(supportFragmentManager, "Pinned Mismatch");
+            }
+        }
+    }
+
+    // This must run asynchronously because it involves a network request.  `String` declares the parameters.  `Void` does not declare progress units.  `String` contains the results.
+    private static class GetHostIpAddresses extends AsyncTask<String, Void, String> {
+        // The weak references are used to determine if the activity have disappeared while the AsyncTask is running.
+        private final WeakReference<Activity> activityWeakReference;
+
+        GetHostIpAddresses(Activity activity) {
+            // Populate the weak references.
+            activityWeakReference = new WeakReference<>(activity);
+        }
+
+        // `onPreExecute()` operates on the UI thread.
+        @Override
+        protected void onPreExecute() {
+            // Get a handle for the activity.
+            Activity activity = activityWeakReference.get();
+
+            // Abort if the activity is gone.
+            if ((activity == null) || activity.isFinishing()) {
+                return;
+            }
+
+            // Set the getting IP addresses tracker.
+            gettingIpAddresses = true;
+        }
+
+
+        @Override
+        protected String doInBackground(String... domainName) {
+            // Get a handle for the activity.
+            Activity activity = activityWeakReference.get();
+
+            // Abort if the activity is gone.
+            if ((activity == null) || activity.isFinishing()) {
+                // Return an empty spannable string builder.
+                return "";
+            }
+
+            // Initialize an IP address string builder.
+            StringBuilder ipAddresses = new StringBuilder();
+
+            // Get an array with the IP addresses for the host.
+            try {
+                // Get an array with all the IP addresses for the domain.
+                InetAddress[] inetAddressesArray = InetAddress.getAllByName(domainName[0]);
+
+                // Add each IP address to the string builder.
+                for (InetAddress inetAddress : inetAddressesArray) {
+                    if (ipAddresses.length() == 0) {  // This is the first IP address.
+                        // Add the IP address to the string builder.
+                        ipAddresses.append(inetAddress.getHostAddress());
+                    } else {  // This is not the first IP address.
+                        // Add a line break to the string builder first.
+                        ipAddresses.append("\n");
+
+                        // Add the IP address to the string builder.
+                        ipAddresses.append(inetAddress.getHostAddress());
+                    }
+                }
+            } catch (UnknownHostException exception) {
+                // Do nothing.
+            }
+
+            // Return the string.
+            return ipAddresses.toString();
+        }
+
+        // `onPostExecute()` operates on the UI thread.
+        @Override
+        protected void onPostExecute(String ipAddresses) {
+            // Get a handle for the activity.
+            Activity activity = activityWeakReference.get();
+
+            // Abort if the activity is gone.
+            if ((activity == null) || activity.isFinishing()) {
+                return;
+            }
+
+            // Store the IP addresses.
+            currentHostIpAddresses = ipAddresses;
+
+            if (!urlIsLoading) {
+                checkPinnedMismatch();
+            }
+
+            // Reset the getting IP addresses tracker.
+            gettingIpAddresses = false;
+        }
+    }
 }
\ No newline at end of file