]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blobdiff - app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java
Add Russian strings translation. Partial EasyList Implementation.
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / activities / MainWebViewActivity.java
index c6f04a0fab47119a7ca812ea8b6af1c2a257e63c..cfe93f2740163d27a76435965151891ecfe69404 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2015-2017 Soren Stoutner <soren@stoutner.com>.
+ * Copyright © 2015-2018 Soren Stoutner <soren@stoutner.com>.
  *
  * Download cookie code contributed 2017 Hendrik Knackstedt.  Copyright assigned to Soren Stoutner <soren@stoutner.com>.
  *
@@ -22,6 +22,7 @@
 package com.stoutner.privacybrowser.activities;
 
 import android.annotation.SuppressLint;
+import android.app.Activity;
 import android.app.DialogFragment;
 import android.app.DownloadManager;
 import android.content.BroadcastReceiver;
@@ -54,6 +55,10 @@ import android.support.design.widget.NavigationView;
 import android.support.design.widget.Snackbar;
 import android.support.v4.app.ActivityCompat;
 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;
+import android.support.v4.content.pm.ShortcutManagerCompat;
+import android.support.v4.graphics.drawable.IconCompat;
 import android.support.v4.view.GravityCompat;
 import android.support.v4.widget.DrawerLayout;
 import android.support.v4.widget.SwipeRefreshLayout;
@@ -66,6 +71,7 @@ import android.text.Editable;
 import android.text.Spanned;
 import android.text.TextWatcher;
 import android.text.style.ForegroundColorSpan;
+import android.util.Log;
 import android.util.Patterns;
 import android.view.ContextMenu;
 import android.view.GestureDetector;
@@ -101,6 +107,7 @@ import android.widget.TextView;
 import com.stoutner.privacybrowser.BannerAd;
 import com.stoutner.privacybrowser.BuildConfig;
 import com.stoutner.privacybrowser.R;
+import com.stoutner.privacybrowser.dialogs.AddDomainDialog;
 import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog;
 import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog;
 import com.stoutner.privacybrowser.dialogs.CreateHomeScreenShortcutDialog;
@@ -131,29 +138,36 @@ import java.net.URLEncoder;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.regex.Pattern;
 
 // We need to use AppCompatActivity from android.support.v7.app.AppCompatActivity to have access to the SupportActionBar until the minimum API is >= 21.
-public class MainWebViewActivity extends AppCompatActivity implements CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener, CreateHomeScreenShortcutDialog.CreateHomeScreenSchortcutListener,
-        DownloadFileDialog.DownloadFileListener, DownloadImageDialog.DownloadImageListener, EditBookmarkDialog.EditBookmarkListener, EditBookmarkFolderDialog.EditBookmarkFolderListener, HttpAuthenticationDialog.HttpAuthenticationListener,
-        NavigationView.OnNavigationItemSelectedListener, PinnedSslCertificateMismatchDialog.PinnedSslCertificateMismatchListener, SslCertificateErrorDialog.SslCertificateErrorListener, UrlHistoryDialog.UrlHistoryListener {
-
-    // `darkTheme` is public static so it can be accessed from `AboutActivity`, `GuideActivity`, `AddDomainDialog`, `SettingsActivity`, `DomainsActivity`, `DomainsListFragment`, `BookmarksActivity`, `BookmarksDatabaseViewActivity`,
-    // `CreateBookmarkDialog`, `CreateBookmarkFolderDialog`, `DownloadFileDialog`, `DownloadImageDialog`, `EditBookmarkDialog`, `EditBookmarkFolderDialog`, `EditBookmarkDatabaseViewDialog`, `HttpAuthenticationDialog`, `MoveToFolderDialog`,
-    // `SslCertificateErrorDialog`, `UrlHistoryDialog`, `ViewSslCertificateDialog`, `CreateHomeScreenShortcutDialog`, and `OrbotProxyHelper`. It is also used in `onCreate()`, `applyAppSettings()`, `applyDomainSettings()`, and `updatePrivacyIcons()`.
+public class MainWebViewActivity extends AppCompatActivity implements AddDomainDialog.AddDomainListener, CreateBookmarkDialog.CreateBookmarkListener,
+        CreateBookmarkFolderDialog.CreateBookmarkFolderListener, CreateHomeScreenShortcutDialog.CreateHomeScreenSchortcutListener, DownloadFileDialog.DownloadFileListener,
+        DownloadImageDialog.DownloadImageListener, EditBookmarkDialog.EditBookmarkListener, EditBookmarkFolderDialog.EditBookmarkFolderListener, HttpAuthenticationDialog.HttpAuthenticationListener,
+        NavigationView.OnNavigationItemSelectedListener, PinnedSslCertificateMismatchDialog.PinnedSslCertificateMismatchListener, SslCertificateErrorDialog.SslCertificateErrorListener,
+        UrlHistoryDialog.UrlHistoryListener {
+
+    // `darkTheme` is public static so it can be accessed from `AboutActivity`, `GuideActivity`, `AddDomainDialog`, `SettingsActivity`, `DomainsActivity`, `DomainsListFragment`, `BookmarksActivity`,
+    // `BookmarksDatabaseViewActivity`, `CreateBookmarkDialog`, `CreateBookmarkFolderDialog`, `DownloadFileDialog`, `DownloadImageDialog`, `EditBookmarkDialog`, `EditBookmarkFolderDialog`,
+    // `EditBookmarkDatabaseViewDialog`, `HttpAuthenticationDialog`, `MoveToFolderDialog`, `SslCertificateErrorDialog`, `UrlHistoryDialog`, `ViewSslCertificateDialog`, `CreateHomeScreenShortcutDialog`,
+    //  and `OrbotProxyHelper`. It is also used in `onCreate()`, `applyAppSettings()`, `applyDomainSettings()`, and `updatePrivacyIcons()`.
     public static boolean darkTheme;
 
-    // `favoriteIconBitmap` is public static so it can be accessed from `CreateHomeScreenShortcutDialog`, `BookmarksActivity`, `BookmarksDatabaseViewActivity`, `CreateBookmarkDialog`, `CreateBookmarkFolderDialog`, `EditBookmarkDialog`,
-    // `EditBookmarkFolderDialog`, `EditBookmarkDatabaseViewDialog`, and `ViewSslCertificateDialog`.  It is also used in `onCreate()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onCreateHomeScreenShortcutCreate()`, `onSaveEditBookmark()`,
-    // `onSaveEditBookmarkFolder()`, and `applyDomainSettings()`.
+    // `favoriteIconBitmap` is public static so it can be accessed from `CreateHomeScreenShortcutDialog`, `BookmarksActivity`, `BookmarksDatabaseViewActivity`, `CreateBookmarkDialog`,
+    // `CreateBookmarkFolderDialog`, `EditBookmarkDialog`, `EditBookmarkFolderDialog`, `EditBookmarkDatabaseViewDialog`, and `ViewSslCertificateDialog`.  It is also used in `onCreate()`,
+    // `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`.
     // It is also used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onCreateHomeScreenShortcutCreate()`, and `loadUrlFromTextBox()`.
     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`, `PinnedSslCertificateMismatchDialog`,
+    // and `ViewSslCertificateDialog`.  It is also used in `onCreate()`.
     public static SslCertificate sslCertificate;
 
     // `orbotStatus` is public static so it can be accessed from `OrbotProxyHelper`.  It is also used in `onCreate()`.
@@ -162,6 +176,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     // `webViewTitle` is public static so it can be accessed from `CreateBookmarkDialog` and `CreateHomeScreenShortcutDialog`.  It is also used in `onCreate()`.
     public static String webViewTitle;
 
+    // `appliedUserAgentString` is public static so it can be accessed from `ViewSourceActivity`.  It is also used in `applyDomainSettings()`.
+    public static String appliedUserAgentString;
+
     // `displayWebpageImagesBoolean` is public static so it can be accessed from `DomainSettingsFragment`.  It is also used in `applyAppSettings()` and `applyDomainSettings()`.
     public static boolean displayWebpageImagesBoolean;
 
@@ -174,12 +191,17 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     // `restartFromBookmarksActivity` is public static so it can be accessed from `BookmarksActivity`.  It is also used in `onRestart()`.
     public static boolean restartFromBookmarksActivity;
 
-    // `currentBookmarksFolder` is public static so it can be accessed from `BookmarksActivity`.  It is also used in `onCreate()`, `onBackPressed()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and
-    // `loadBookmarksFolder()`.
+    // `easyListVersion` is public static so it can be accessed from `AboutTabFragment`.  It is also used in `onCreate()`.
+    public static String easyListVersion;
+
+    // `currentBookmarksFolder` is public static so it can be accessed from `BookmarksActivity`.  It is also used in `onCreate()`, `onBackPressed()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`,
+    // `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
     public static String currentBookmarksFolder;
 
-    // The pinned domain SSL Certificate variables are public static so they can be accessed from `PinnedSslCertificateMismatchDialog`.  They are also used in `onCreate()` and `applyDomainSettings()`.
+    // `domainSettingsDatabaseId` is public static so it can be accessed from `PinnedSslCertificateMismatchDialog`.  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;
@@ -205,8 +227,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     // `rootCoordinatorLayout` is used in `onCreate()` and `applyAppSettings()`.
     private CoordinatorLayout rootCoordinatorLayout;
 
-    // `mainWebView` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`, `findNextOnPage()`, `closeFindOnPage()`, `loadUrlFromTextBox()`
-    // `onSslMismatchBack()`, and `setDisplayWebpageImages()`.
+    // `mainWebView` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`,
+    // `findNextOnPage()`, `closeFindOnPage()`, `loadUrlFromTextBox()`, `onSslMismatchBack()`, and `setDisplayWebpageImages()`.
     private WebView mainWebView;
 
     // `fullScreenVideoFrameLayout` is used in `onCreate()` and `onConfigurationChanged()`.
@@ -275,7 +297,10 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     // `translucentNavigationBarOnFullscreen` is used in `onCreate()` and `applyAppSettings()`.
     private boolean translucentNavigationBarOnFullscreen;
 
-    // `currentDomainName` is used in `onCreate()`, `onNavigationItemSelected()`, `onSslMismatchProceed()`, and `applyDomainSettings()`.
+    // `reapplyDomainSettingsOnRestart` is used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, and `onAddDomain()`, .
+    private boolean reapplyDomainSettingsOnRestart;
+
+    // `currentDomainName` is used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onAddDomain()`, and `applyDomainSettings()`.
     private String currentDomainName;
 
     // `ignorePinnedSslCertificateForDomain` is used in `onCreate()`, `onSslMismatchProceed()`, and `applyDomainSettings()`.
@@ -284,7 +309,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     // `waitingForOrbot` is used in `onCreate()` and `applyAppSettings()`.
     private boolean waitingForOrbot;
 
-    // `domainSettingsApplied` is used in `applyDomainSettings()` and `setDisplayWebpageImages()`.
+    // `domainSettingsApplied` is used in `prepareOptionsMenu()`, `applyDomainSettings()`, and `setDisplayWebpageImages()`.
     private boolean domainSettingsApplied;
 
     // `displayWebpageImagesInt` is used in `applyDomainSettings()` and `setDisplayWebpageImages()`.
@@ -317,13 +342,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     // `urlTextBox` is used in `onCreate()`, `onOptionsItemSelected()`, `loadUrlFromTextBox()`, `loadUrl()`, and `highlightUrlText()`.
     private EditText urlTextBox;
 
-    // `redColorSpan` is used in `onCreate()` and `highlightUrlText()`.
+    // The color spans are used in `onCreate()` and `highlightUrlText()`.
     private ForegroundColorSpan redColorSpan;
-
-    // `initialGrayColorSpan` is sued in `onCreate()` and `highlightUrlText()`.
     private ForegroundColorSpan initialGrayColorSpan;
-
-    // `finalGrayColorSpam` is used in `onCreate()` and `highlightUrlText()`.
     private ForegroundColorSpan finalGrayColorSpan;
 
     // `adView` is used in `onCreate()` and `onConfigurationChanged()`.
@@ -387,6 +408,745 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Run the default commands.
         super.onCreate(savedInstanceState);
 
+        Log.i("BlockLists", "Begin populating block lists.");
+
+        // Initialize the block lists.
+        List<String> mainWhiteList = new LinkedList<>();
+        List<String[]> multiEntryWhiteList = new LinkedList<>();
+        List<String> mainBlockList = new LinkedList<>();
+        List<String> initialBlockList = new LinkedList<>();
+        List<String> finalBlockList = new LinkedList<>();
+        List<String[]> multiEntryBlockList = new LinkedList<>();
+        List<String[]> multiEntryInitialBlockList = new LinkedList<>();
+        List<String[]> multiEntryFinalBlockList = new LinkedList<>();
+        List<String[]> domainBlockList = new LinkedList<>();
+        List<String[]> domainInitialBlockList = new LinkedList<>();
+        List<String[]> domainFinalBlockList = new LinkedList<>();
+        List<String[]> domainMultiEntryBlockList = new LinkedList<>();
+        List<String[]> domainRegularExpressionBlockList = new LinkedList<>();
+        List<String> regularExpressionBlockList = new LinkedList<>();
+
+        // Populate the block lists.
+        try {
+            // Load `easylist.txt` into a `BufferedReader`.
+            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(getAssets().open("easylist.txt")));
+
+            // Create a string for storing the block list entries.
+            String blockListEntry;
+
+            // Parse EasyList.
+            while ((blockListEntry = bufferedReader.readLine()) != null) {
+                //noinspection StatementWithEmptyBody
+                if (blockListEntry.contains("##") || blockListEntry.contains("#?#") || blockListEntry.contains("#@#") || blockListEntry.startsWith("[")) {
+                    // Entries that contain `##`, `#?#`, and `#@#` are for hiding elements in the main page's HTML.  Entries that start with `[` describe the AdBlock compatibility level.
+
+                    // Do nothing.  Privacy Browser does not currently use these entries.
+
+                    //Log.i("BlackLists", "Not added: " + adBlockerEntry);
+                } else if (blockListEntry.startsWith("!")){  //  Entries that begin with `!` are comments.
+                    if (blockListEntry.startsWith("! Version:")) {
+                        // Store the EasyList version number.
+                        easyListVersion = blockListEntry.substring(11);
+                    }
+
+                    //Log.i("BlackLists", "Not added: " + adBlockerEntry);
+                } else if (blockListEntry.startsWith("@@")) {  // Entries that begin with `@@` are excludes (whitelists).
+                    // mainWhiteList.add(blockListEntry.substring(2));
+
+                    //Log.i("BlockLists", "Main white list added: " + blockListEntry.substring(2, blockListEntry.length()));
+                } else if (blockListEntry.endsWith("|")){  // Entries that end with `|` match against the end of the URL.
+                    // Strip out the final "|"
+                    blockListEntry = blockListEntry.substring(0, blockListEntry.length() - 1);
+
+                    // Strip out any initial `||`.  They are redundant in this case because the block list entry is being matched against the end of the URL.
+                    if (blockListEntry.startsWith("||")) {
+                        blockListEntry = blockListEntry.substring(2);
+                    }
+
+                    if (blockListEntry.contains("*")) {
+                        int wildcardIndex = blockListEntry.indexOf("*");
+
+                        String firstEntry = blockListEntry.substring(0, wildcardIndex);
+                        String secondEntry = blockListEntry.substring(wildcardIndex + 1);
+
+                        if (firstEntry.endsWith("^")) {
+                            String firstEntryBase = firstEntry.substring(0, firstEntry.length() - 1);
+
+                            String firstEntry1 = firstEntryBase + ":";
+                            String firstEntry2 = firstEntryBase + "/";
+                            String firstEntry3 = firstEntryBase + "?";
+                            String firstEntry4 = firstEntryBase + "=";
+                            String firstEntry5 = firstEntryBase + "&";
+
+                            String[] doubleEntry1 = {firstEntry1, secondEntry};
+                            String[] doubleEntry2 = {firstEntry2, secondEntry};
+                            String[] doubleEntry3 = {firstEntry3, secondEntry};
+                            String[] doubleEntry4 = {firstEntry4, secondEntry};
+                            String[] doubleEntry5 = {firstEntry5, secondEntry};
+
+                            multiEntryFinalBlockList.add(doubleEntry1);
+                            multiEntryFinalBlockList.add(doubleEntry2);
+                            multiEntryFinalBlockList.add(doubleEntry3);
+                            multiEntryFinalBlockList.add(doubleEntry4);
+                            multiEntryFinalBlockList.add(doubleEntry5);
+
+                            //Log.i("BlockLists", "Multi entry final block list added: " + firstEntry1 + " , " + secondEntry);
+                            //Log.i("BlockLists", "Multi entry final block list added: " + firstEntry2 + " , " + secondEntry);
+                            //Log.i("BlockLists", "Multi entry final block list added: " + firstEntry3 + " , " + secondEntry);
+                            //Log.i("BlockLists", "Multi entry final block list added: " + firstEntry4 + " , " + secondEntry);
+                            //Log.i("BlockLists", "Multi entry final block list added: " + firstEntry5 + " , " + secondEntry);
+                        } else {
+                            String[] doubleEntry = {firstEntry, secondEntry};
+
+                            multiEntryFinalBlockList.add(doubleEntry);
+
+                            //Log.i("BlockLists", "Multi entry final block list added: " + firstEntry + " , " + secondEntry);
+                        }
+                    } else {
+                        finalBlockList.add(blockListEntry);
+
+                        //Log.i("BlockLists", "Final block list added: " + blockListEntry);
+                    }
+                } else if (blockListEntry.contains("$")) {  // Entries that contain `$` use filter options.
+                    // Strip out any initial `||`.  These will be treated like any other entry.
+                    if (blockListEntry.startsWith("||")) {
+                        blockListEntry = blockListEntry.substring(2);
+                    }
+
+                    if (blockListEntry.contains("third-party")) {
+                        // Log.i("BlockLists", "Not added: " + blockListEntry);
+                    } else if (blockListEntry.substring(blockListEntry.indexOf("$")).contains("domain")) {
+                        if (blockListEntry.contains("~")) {  // Whitelist.
+
+                        } else {
+                            // Separate the filters.
+                            String entry = blockListEntry.substring(0, blockListEntry.indexOf("$"));
+                            String filters = blockListEntry.substring(blockListEntry.indexOf("$") + 1);
+                            String domains = filters.substring(filters.indexOf("domain=") + 7);
+
+                            // Only process the block list item if the entry is not null.  Some lines in EasyList begin with `$websocket`, which create a null entry.
+                            if (!entry.equals("")) {
+                                do {
+                                    String domain;
+
+                                    if (domains.contains("|")) {
+                                        // Get the first domain.
+                                        domain = domains.substring(0, domains.indexOf("|"));
+
+                                        // Remove the first domain from the list.
+                                        domains = domains.substring(domains.indexOf("|") + 1);
+                                    } else {
+                                        domain = domains;
+                                    }
+
+                                    if (entry.contains("*")) {
+                                        int wildcardIndex = entry.indexOf("*");
+
+                                        String firstEntry = entry.substring(0, wildcardIndex);
+                                        String secondEntry = entry.substring(wildcardIndex + 1);
+
+                                        if (firstEntry.endsWith("^")) {
+                                            String firstEntryBase = firstEntry.substring(0, firstEntry.length() - 1);
+
+                                            String firstEntry1 = firstEntryBase + ":";
+                                            String firstEntry2 = firstEntryBase + "/";
+                                            String firstEntry3 = firstEntryBase + "?";
+                                            String firstEntry4 = firstEntryBase + "=";
+                                            String firstEntry5 = firstEntryBase + "&";
+
+                                            String[] domainDoubleEntry1 = {domain, firstEntry1, secondEntry};
+                                            String[] domainDoubleEntry2 = {domain, firstEntry2, secondEntry};
+                                            String[] domainDoubleEntry3 = {domain, firstEntry3, secondEntry};
+                                            String[] domainDoubleEntry4 = {domain, firstEntry4, secondEntry};
+                                            String[] domainDoubleEntry5 = {domain, firstEntry5, secondEntry};
+
+                                            domainMultiEntryBlockList.add(domainDoubleEntry1);
+                                            domainMultiEntryBlockList.add(domainDoubleEntry2);
+                                            domainMultiEntryBlockList.add(domainDoubleEntry3);
+                                            domainMultiEntryBlockList.add(domainDoubleEntry4);
+                                            domainMultiEntryBlockList.add(domainDoubleEntry5);
+
+                                            //Log.i("BlockLists", "Domain ^ double entry block list added: " + domain + " , " + firstEntry1 + " , " + secondEntry);
+                                            //Log.i("BlockLists", "Domain ^ double entry block list added: " + domain + " , " + firstEntry2 + " , " + secondEntry);
+                                            //Log.i("BlockLists", "Domain ^ double entry block list added: " + domain + " , " + firstEntry3 + " , " + secondEntry);
+                                            //Log.i("BlockLists", "Domain ^ double entry block list added: " + domain + " , " + firstEntry4 + " , " + secondEntry);
+                                            //Log.i("BlockLists", "Domain ^ double entry block list added: " + domain + " , " + firstEntry5 + " , " + secondEntry);
+                                        } else {
+                                            String[] domainDoubleEntry = {domain, firstEntry, secondEntry};
+
+                                            domainMultiEntryBlockList.add(domainDoubleEntry);
+
+                                            //Log.i("BlockLists", "Domain double entry block list added: " + domain + " , " + firstEntry + " , " + secondEntry);
+                                        }
+                                    } else if (entry.endsWith("^")) {
+                                        String entryBase = entry.substring(0, entry.length() - 1);
+
+                                        String entry1 = entryBase + ":";
+                                        String entry2 = entryBase + "/";
+                                        String entry3 = entryBase + "?";
+                                        String entry4 = entryBase + "=";
+                                        String entry5 = entryBase + "&";
+
+                                        String[] domainEntry1 = {domain, entry1};
+                                        String[] domainEntry2 = {domain, entry2};
+                                        String[] domainEntry3 = {domain, entry3};
+                                        String[] domainEntry4 = {domain, entry4};
+                                        String[] domainEntry5 = {domain, entry5};
+
+                                        domainBlockList.add(domainEntry1);
+                                        domainBlockList.add(domainEntry2);
+                                        domainBlockList.add(domainEntry3);
+                                        domainBlockList.add(domainEntry4);
+                                        domainBlockList.add(domainEntry5);
+
+                                        //Log.i("BlockLists", "Domain ^ block list added: " + domain + " , " + entry1);
+                                        //Log.i("BlockLists", "Domain ^ block list added: " + domain + " , " + entry2);
+                                        //Log.i("BlockLists", "Domain ^ block list added: " + domain + " , " + entry3);
+                                        //Log.i("BlockLists", "Domain ^ block list added: " + domain + " , " + entry4);
+                                        //Log.i("BlockLists", "Domain ^ block list added: " + domain + " , " + entry5);
+                                    } else if (entry.startsWith("^")) {
+                                        String entryBase = entry.substring(1);
+
+                                        String entry1 = ":" + entryBase;
+                                        String entry2 = ":" + entryBase;
+                                        String entry3 = ":" + entryBase;
+                                        String entry4 = ":" + entryBase;
+                                        String entry5 = ":" + entryBase;
+
+                                        String[] domainEntry1 = {domain, entry1};
+                                        String[] domainEntry2 = {domain, entry2};
+                                        String[] domainEntry3 = {domain, entry3};
+                                        String[] domainEntry4 = {domain, entry4};
+                                        String[] domainEntry5 = {domain, entry5};
+
+                                        domainBlockList.add(domainEntry1);
+                                        domainBlockList.add(domainEntry2);
+                                        domainBlockList.add(domainEntry3);
+                                        domainBlockList.add(domainEntry4);
+                                        domainBlockList.add(domainEntry5);
+
+                                        //Log.i("BlockLists", "Domain ^ block list added: " + domain + " , " + entry1);
+                                        //Log.i("BlockLists", "Domain ^ block list added: " + domain + " , " + entry2);
+                                        //Log.i("BlockLists", "Domain ^ block list added: " + domain + " , " + entry3);
+                                        //Log.i("BlockLists", "Domain ^ block list added: " + domain + " , " + entry4);
+                                        //Log.i("BlockLists", "Domain ^ block list added: " + domain + " , " + entry5);
+                                    } else if (entry.startsWith("|")) {
+                                        // Remove the initial `|`;
+                                        String entryBase = entry.substring(1);
+
+                                        //noinspection StatementWithEmptyBody
+                                        if (entryBase.equals("http://") || entryBase.equals("https://")) {
+                                            // Do nothing.  These entries will entirely block the website.
+                                            // Often the original entry blocks `$script` but Privacy Browser does not currently differentiate between scripts and other entries.
+                                        } else {
+                                            String[] domainEntry = {domain, entryBase};
+
+                                            domainInitialBlockList.add(domainEntry);
+
+                                            //Log.i("BlockLists", "Domain initial block list added: " + domain + " , " + entryBase);
+                                        }
+                                    } else if (entry.endsWith("|")) {
+                                        // Remove the final `|`.
+                                        String entryBase = entry.substring(0, entry.length() - 1);
+
+                                        String[] domainEntry = {domain, entryBase};
+
+                                        domainFinalBlockList.add(domainEntry);
+
+                                        Log.i("BlockLists", "Domain final block list added: " + domain + " , " + entryBase);
+                                    } else if (entry.contains("\\")) {
+                                        String[] domainEntry = {domain, entry};
+
+                                        domainRegularExpressionBlockList.add(domainEntry);
+
+                                        // Log.i("BlockLists", "Domain regular expression block list added: " + domain + " , " + entry);
+                                    } else {
+                                        String[] domainEntry = {domain, entry};
+
+                                        domainBlockList.add(domainEntry);
+
+                                        //Log.i("BlockLists", "Domain block list added: " + domain + " , " + entry);
+                                    }
+                                } while (domains.contains("|"));
+                            }
+                        }
+                    } else if (blockListEntry.contains("~")) {  // Whitelist entries.
+                        // Remove the filter options.
+                        blockListEntry = blockListEntry.substring(0, blockListEntry.indexOf("$"));
+
+                        // Strip any trailing `*`.
+                        if (blockListEntry.endsWith("*")) {
+                            blockListEntry = blockListEntry.substring(0, blockListEntry.length() -1);
+                        }
+
+                        if (blockListEntry.contains("*")) {
+                            int wildcardIndex = blockListEntry.indexOf("*");
+
+                            String firstEntry = blockListEntry.substring(0, wildcardIndex);
+                            String secondEntry = blockListEntry.substring(wildcardIndex + 1);
+
+                            if (firstEntry.endsWith("^")) {
+                                String firstEntryBase = firstEntry.substring(0, firstEntry.length() - 1);
+
+                                String firstEntry1 = firstEntryBase + ":";
+                                String firstEntry2 = firstEntryBase + "/";
+                                String firstEntry3 = firstEntryBase + "?";
+                                String firstEntry4 = firstEntryBase + "=";
+                                String firstEntry5 = firstEntryBase + "&";
+
+                                String[] doubleEntry1 = {firstEntry1, secondEntry};
+                                String[] doubleEntry2 = {firstEntry2, secondEntry};
+                                String[] doubleEntry3 = {firstEntry3, secondEntry};
+                                String[] doubleEntry4 = {firstEntry4, secondEntry};
+                                String[] doubleEntry5 = {firstEntry5, secondEntry};
+
+                                multiEntryWhiteList.add(doubleEntry1);
+                                multiEntryWhiteList.add(doubleEntry2);
+                                multiEntryWhiteList.add(doubleEntry3);
+                                multiEntryWhiteList.add(doubleEntry4);
+                                multiEntryWhiteList.add(doubleEntry5);
+
+                                //Log.i("BlockLists", "Multi entry white list added: " + firstEntry1 + " , " + secondEntry);
+                                //Log.i("BlockLists", "Multi entry white list added: " + firstEntry2 + " , " + secondEntry);
+                                //Log.i("BlockLists", "Multi entry white list added: " + firstEntry3 + " , " + secondEntry);
+                                //Log.i("BlockLists", "Multi entry white list added: " + firstEntry4 + " , " + secondEntry);
+                                //Log.i("BlockLists", "Multi entry white list added: " + firstEntry5 + " , " + secondEntry);
+                            } else {
+                                String[] doubleEntry = {firstEntry, secondEntry};
+
+                                multiEntryWhiteList.add(doubleEntry);
+
+                                //Log.i("BlockLists", "Multi entry white list added: " + firstEntry + " , " + secondEntry);
+                            }
+                        } else {
+                            if (blockListEntry.endsWith("^")) {
+                                String blockListEntryBase = blockListEntry.substring(0, blockListEntry.length() - 1);
+
+                                String blockListEntry1 = blockListEntryBase + ":";
+                                String blockListEntry2 = blockListEntryBase + "/";
+                                String blockListEntry3 = blockListEntryBase + "?";
+                                String blockListEntry4 = blockListEntryBase + "=";
+                                String blockListEntry5 = blockListEntryBase + "&";
+
+                                mainWhiteList.add(blockListEntry1);
+                                mainWhiteList.add(blockListEntry2);
+                                mainWhiteList.add(blockListEntry3);
+                                mainWhiteList.add(blockListEntry4);
+                                mainWhiteList.add(blockListEntry5);
+
+                                // Log.i("BlockLists", "Main white list added: " + blockListEntry1);
+                                // Log.i("BlockLists", "Main white list added: " + blockListEntry2);
+                                // Log.i("BlockLists", "Main white list added: " + blockListEntry3);
+                                // Log.i("BlockLists", "Main white list added: " + blockListEntry4);
+                                // Log.i("BlockLists", "Main white list added: " + blockListEntry5);
+                            } else {
+                                mainWhiteList.add(blockListEntry);
+
+                                // Log.i("BlockLists", "Main white list added: " + blockListEntry);
+                            }
+                        }
+                    } else if (blockListEntry.contains("\\")) {  // Regular expressions.
+                        // Remove the filter options.
+                        blockListEntry = blockListEntry.substring(0, blockListEntry.indexOf("$"));
+
+                        regularExpressionBlockList.add(blockListEntry);
+
+                        //Log.i("BlockLists", "Regular expression list added: " + blockListEntry);
+                    } else {
+                        // Remove the filter options.
+                        blockListEntry = blockListEntry.substring(0, blockListEntry.indexOf("$"));
+
+                        // Strip any trailing `*` or `^`.  Many of these entries have `^$`, which seem redundant for the purposes of Privacy Browser.
+                        if (blockListEntry.endsWith("*") || blockListEntry.endsWith("^")) {
+                            blockListEntry = blockListEntry.substring(0, blockListEntry.length() - 1);
+                        }
+
+                        if (blockListEntry.contains("*")) {  // Use a multi entry list.
+                            int wildcardIndex = blockListEntry.indexOf("*");
+
+                            String firstEntry = blockListEntry.substring(0, wildcardIndex);
+                            String secondEntry = blockListEntry.substring(wildcardIndex + 1);
+
+                            // Remove `.*` if it appears at the beginning of the second entry.
+                            if (secondEntry.startsWith(".*")) {
+                                secondEntry = secondEntry.substring(2);
+                            }
+
+                            // Create a third entry if required.
+                            if (secondEntry.contains("*")) {
+                                wildcardIndex = secondEntry.indexOf("*");
+
+                                String thirdEntry = secondEntry.substring(wildcardIndex + 1);
+                                secondEntry = secondEntry.substring(0, wildcardIndex);
+
+                                if (firstEntry.endsWith("^")) {
+                                    String firstEntryBase = firstEntry.substring(0, firstEntry.length() - 1);
+
+                                    String firstEntry1 = firstEntryBase + ":";
+                                    String firstEntry2 = firstEntryBase + "/";
+                                    String firstEntry3 = firstEntryBase + "?";
+                                    String firstEntry4 = firstEntryBase + "=";
+                                    String firstEntry5 = firstEntryBase + "&";
+
+                                    if (thirdEntry.endsWith("|")) {
+                                        thirdEntry = thirdEntry.substring(0, thirdEntry.length() - 1);
+
+                                        String[] tripleEntry1 = {firstEntry1, secondEntry, thirdEntry};
+                                        String[] tripleEntry2 = {firstEntry2, secondEntry, thirdEntry};
+                                        String[] tripleEntry3 = {firstEntry3, secondEntry, thirdEntry};
+                                        String[] tripleEntry4 = {firstEntry4, secondEntry, thirdEntry};
+                                        String[] tripleEntry5 = {firstEntry5, secondEntry, thirdEntry};
+
+                                        multiEntryFinalBlockList.add(tripleEntry1);
+                                        multiEntryFinalBlockList.add(tripleEntry2);
+                                        multiEntryFinalBlockList.add(tripleEntry3);
+                                        multiEntryFinalBlockList.add(tripleEntry4);
+                                        multiEntryFinalBlockList.add(tripleEntry5);
+
+                                        //Log.i("BlockLists", "Multi entry final tripple block list added: " + firstEntry1 + " , " + secondEntry + " , " + thirdEntry);
+                                        //Log.i("BlockLists", "Multi entry final tripple block list added: " + firstEntry2 + " , " + secondEntry + " , " + thirdEntry);
+                                        //Log.i("BlockLists", "Multi entry final tripple block list added: " + firstEntry3 + " , " + secondEntry + " , " + thirdEntry);
+                                        //Log.i("BlockLists", "Multi entry final tripple block list added: " + firstEntry4 + " , " + secondEntry + " , " + thirdEntry);
+                                        //Log.i("BlockLists", "Multi entry final tripple block list added: " + firstEntry5 + " , " + secondEntry + " , " + thirdEntry);
+                                    } else {
+                                        String[] tripleEntry1 = {firstEntry1, secondEntry, thirdEntry};
+                                        String[] tripleEntry2 = {firstEntry2, secondEntry, thirdEntry};
+                                        String[] tripleEntry3 = {firstEntry3, secondEntry, thirdEntry};
+                                        String[] tripleEntry4 = {firstEntry4, secondEntry, thirdEntry};
+                                        String[] tripleEntry5 = {firstEntry5, secondEntry, thirdEntry};
+
+                                        multiEntryBlockList.add(tripleEntry1);
+                                        multiEntryBlockList.add(tripleEntry2);
+                                        multiEntryBlockList.add(tripleEntry3);
+                                        multiEntryBlockList.add(tripleEntry4);
+                                        multiEntryBlockList.add(tripleEntry5);
+
+                                        //Log.i("BlockLists", "Multi entry tripple block list added: " + firstEntry1 + " , " + secondEntry + " , " + thirdEntry);
+                                        //Log.i("BlockLists", "Multi entry tripple block list added: " + firstEntry2 + " , " + secondEntry + " , " + thirdEntry);
+                                        //Log.i("BlockLists", "Multi entry tripple block list added: " + firstEntry3 + " , " + secondEntry + " , " + thirdEntry);
+                                        //Log.i("BlockLists", "Multi entry tripple block list added: " + firstEntry4 + " , " + secondEntry + " , " + thirdEntry);
+                                        //Log.i("BlockLists", "Multi entry tripple block list added: " + firstEntry5 + " , " + secondEntry + " , " + thirdEntry);
+                                    }
+                                } else {
+                                    String[] tripleEntry = {firstEntry, secondEntry, thirdEntry};
+
+                                    multiEntryBlockList.add(tripleEntry);
+
+                                    // Log.i("BlockLists", "Multi entry tripple block list added: " + firstEntry + " , " + secondEntry + " , " + thirdEntry);
+                                }
+                            } else {  // This is a double entry.
+                                if (firstEntry.startsWith("|")) {
+                                    String[] doubleEntry = {firstEntry.substring(1), secondEntry};
+
+                                    multiEntryInitialBlockList.add(doubleEntry);
+
+                                    //Log.i("BlockLists", "Multi entry initial block list added: " + firstEntry.substring(1) + " , " + secondEntry);
+                                } else if (firstEntry.startsWith("^")) {
+                                    String firstEntryBase = firstEntry.substring(1);
+
+                                    String firstEntry1 = ":" + firstEntryBase;
+                                    String firstEntry2 = "/" + firstEntryBase;
+                                    String firstEntry3 = "?" + firstEntryBase;
+                                    String firstEntry4 = "=" + firstEntryBase;
+                                    String firstEntry5 = "&" + firstEntryBase;
+
+                                    String[] doubleEntry1 = {firstEntry1, secondEntry};
+                                    String[] doubleEntry2 = {firstEntry2, secondEntry};
+                                    String[] doubleEntry3 = {firstEntry3, secondEntry};
+                                    String[] doubleEntry4 = {firstEntry4, secondEntry};
+                                    String[] doubleEntry5 = {firstEntry5, secondEntry};
+
+                                    multiEntryBlockList.add(doubleEntry1);
+                                    multiEntryBlockList.add(doubleEntry2);
+                                    multiEntryBlockList.add(doubleEntry3);
+                                    multiEntryBlockList.add(doubleEntry4);
+                                    multiEntryBlockList.add(doubleEntry5);
+
+                                    //Log.i("BlockLists", "Multi entry ^ block list added: " + firstEntry1 + " , " + secondEntry);
+                                    //Log.i("BlockLists", "Multi entry ^ block list added: " + firstEntry2 + " , " + secondEntry);
+                                    //Log.i("BlockLists", "Multi entry ^ block list added: " + firstEntry3 + " , " + secondEntry);
+                                    //Log.i("BlockLists", "Multi entry ^ block list added: " + firstEntry4 + " , " + secondEntry);
+                                    //Log.i("BlockLists", "Multi entry ^ block list added: " + firstEntry5 + " , " + secondEntry);
+                                } else if (firstEntry.endsWith("^")) {
+                                    String firstEntryBase = firstEntry.substring(0, firstEntry.length() - 1);
+
+                                    String firstEntry1 = firstEntryBase + ":";
+                                    String firstEntry2 = firstEntryBase + "/";
+                                    String firstEntry3 = firstEntryBase + "?";
+                                    String firstEntry4 = firstEntryBase + "=";
+                                    String firstEntry5 = firstEntryBase + "&";
+
+                                    String[] doubleEntry1 = {firstEntry1, secondEntry};
+                                    String[] doubleEntry2 = {firstEntry2, secondEntry};
+                                    String[] doubleEntry3 = {firstEntry3, secondEntry};
+                                    String[] doubleEntry4 = {firstEntry4, secondEntry};
+                                    String[] doubleEntry5 = {firstEntry5, secondEntry};
+
+                                    multiEntryBlockList.add(doubleEntry1);
+                                    multiEntryBlockList.add(doubleEntry2);
+                                    multiEntryBlockList.add(doubleEntry3);
+                                    multiEntryBlockList.add(doubleEntry4);
+                                    multiEntryBlockList.add(doubleEntry5);
+
+                                    //Log.i("BlockLists", "Multi entry ^ block list added: " + firstEntry1 + " , " + secondEntry);
+                                    //Log.i("BlockLists", "Multi entry ^ block list added: " + firstEntry2 + " , " + secondEntry);
+                                    //Log.i("BlockLists", "Multi entry ^ block list added: " + firstEntry3 + " , " + secondEntry);
+                                    //Log.i("BlockLists", "Multi entry ^ block list added: " + firstEntry4 + " , " + secondEntry);
+                                    //Log.i("BlockLists", "Multi entry ^ block list added: " + firstEntry5 + " , " + secondEntry);
+                                } else if (secondEntry.startsWith("^")) {
+                                    String secondEntryBase = secondEntry.substring(1);
+
+                                    String secondEntry1 = ":" + secondEntryBase;
+                                    String secondEntry2 = "/" + secondEntryBase;
+                                    String secondEntry3 = "?" + secondEntryBase;
+                                    String secondEntry4 = "=" + secondEntryBase;
+                                    String secondEntry5 = "&" + secondEntryBase;
+
+                                    String[] doubleEntry1 = {firstEntry, secondEntry1};
+                                    String[] doubleEntry2 = {firstEntry, secondEntry2};
+                                    String[] doubleEntry3 = {firstEntry, secondEntry3};
+                                    String[] doubleEntry4 = {firstEntry, secondEntry4};
+                                    String[] doubleEntry5 = {firstEntry, secondEntry5};
+
+                                    multiEntryBlockList.add(doubleEntry1);
+                                    multiEntryBlockList.add(doubleEntry2);
+                                    multiEntryBlockList.add(doubleEntry3);
+                                    multiEntryBlockList.add(doubleEntry4);
+                                    multiEntryBlockList.add(doubleEntry5);
+
+                                    //Log.i("BlockLists", "Multi entry ^ block list added: " + firstEntry + " , " + secondEntry1);
+                                    //Log.i("BlockLists", "Multi entry ^ block list added: " + firstEntry + " , " + secondEntry2);
+                                    //Log.i("BlockLists", "Multi entry ^ block list added: " + firstEntry + " , " + secondEntry3);
+                                    //Log.i("BlockLists", "Multi entry ^ block list added: " + firstEntry + " , " + secondEntry4);
+                                    //Log.i("BlockLists", "Multi entry ^ block list added: " + firstEntry + " , " + secondEntry5);
+                                } else if (secondEntry.endsWith("^")) {
+                                    String secondEntryBase = secondEntry.substring(0, secondEntry.length() - 1);
+
+                                    String secondEntry1 = secondEntryBase + ":";
+                                    String secondEntry2 = secondEntryBase + "/";
+                                    String secondEntry3 = secondEntryBase + "?";
+                                    String secondEntry4 = secondEntryBase + "=";
+                                    String secondEntry5 = secondEntryBase + "&";
+
+                                    String[] doubleEntry1 = {firstEntry, secondEntry1};
+                                    String[] doubleEntry2 = {firstEntry, secondEntry2};
+                                    String[] doubleEntry3 = {firstEntry, secondEntry3};
+                                    String[] doubleEntry4 = {firstEntry, secondEntry4};
+                                    String[] doubleEntry5 = {firstEntry, secondEntry5};
+
+                                    String[] doubleEntryFinal = {firstEntry, secondEntryBase};
+
+                                    multiEntryBlockList.add(doubleEntry1);
+                                    multiEntryBlockList.add(doubleEntry2);
+                                    multiEntryBlockList.add(doubleEntry3);
+                                    multiEntryBlockList.add(doubleEntry4);
+                                    multiEntryBlockList.add(doubleEntry5);
+
+                                    multiEntryFinalBlockList.add(doubleEntryFinal);
+
+                                    //Log.i("BlockLists", "Multi entry ^ block list added: " + firstEntry + " , " + secondEntry1);
+                                    //Log.i("BlockLists", "Multi entry ^ block list added: " + firstEntry + " , " + secondEntry2);
+                                    //Log.i("BlockLists", "Multi entry ^ block list added: " + firstEntry + " , " + secondEntry3);
+                                    //Log.i("BlockLists", "Multi entry ^ block list added: " + firstEntry + " , " + secondEntry4);
+                                    //Log.i("BlockLists", "Multi entry ^ block list added: " + firstEntry + " , " + secondEntry5);
+
+                                    //Log.i("BlockLists", "Multi entry final block list added: " + firstEntry + " , " + secondEntryBase);
+                                } else {
+                                    String[] doubleEntry = {firstEntry, secondEntry};
+
+                                    multiEntryBlockList.add(doubleEntry);
+
+                                    //Log.i("BlockLists", "Multi entry block list added: " + firstEntry + " , " + secondEntry);
+                                }
+                            }
+                        } else if (blockListEntry.startsWith("|")) {  // Populate the initial block list.
+                            // Strip the initial `|`.
+                            blockListEntry = blockListEntry.substring(1);
+
+                            // Populate the initial block list.
+                            initialBlockList.add(blockListEntry);
+
+                            //Log.i("BlockLists", "Initial block list added: " + blockListEntry);
+                        } else {  // Populate the main block list.
+                            mainBlockList.add(blockListEntry);
+
+                            //Log.i("BlockLists", "Main block list added: " + blockListEntry);
+                        }
+                    }
+                } else {  // Populate the standard lists.
+                    // Strip out any initial `||`.  These will be treated like any other entry.
+                    if (blockListEntry.startsWith("||")) {
+                        blockListEntry = blockListEntry.substring(2);
+                    }
+
+                    // Strip out any initial `*`.
+                    if (blockListEntry.startsWith("*")) {
+                        blockListEntry = blockListEntry.substring(1);
+                    }
+
+                    // Strip out any trailing `*`.
+                    if (blockListEntry.endsWith("*")) {
+                        blockListEntry = blockListEntry.substring(0, blockListEntry.length() - 1);
+                    }
+
+                    if (blockListEntry.contains("*")) {  // Entries that contain a `*` in the middle have to be treated specially.
+                        int wildcardIndex = blockListEntry.indexOf("*");
+
+                        String firstEntry = blockListEntry.substring(0, wildcardIndex);
+                        String secondEntry = blockListEntry.substring(wildcardIndex + 1);
+
+                        // Remove `.*` if it appears at the beginning of the second entry.
+                        if (secondEntry.startsWith(".*")) {
+                            secondEntry = secondEntry.substring(2);
+                        }
+
+                        // Create a third entry if required.
+                        if (secondEntry.contains("*")) {
+                            wildcardIndex = secondEntry.indexOf("*");
+
+                            String thirdEntry = secondEntry.substring(wildcardIndex + 1);
+                            secondEntry = secondEntry.substring(0, wildcardIndex);
+
+                            String[] tripleEntry = {firstEntry, secondEntry, thirdEntry};
+
+                            multiEntryBlockList.add(tripleEntry);
+
+                            //Log.i("BlockLists", "Multi entry tripple block list added: " + firstEntry + " , " + secondEntry + " , " + thirdEntry);
+                        } else {
+                            if (firstEntry.startsWith("|")) {
+                                String[] doubleEntry = {firstEntry.substring(1), secondEntry};
+
+                                multiEntryInitialBlockList.add(doubleEntry);
+
+                                //Log.i("BlockLists", "Multi entry initial block list added: " + firstEntry.substring(1) + " , " + secondEntry);
+                            } else if (firstEntry.startsWith("^")) {
+                                String firstEntryBase = firstEntry.substring(1);
+
+                                String firstEntry1 = ":" + firstEntryBase;
+                                String firstEntry2 = "/" + firstEntryBase;
+                                String firstEntry3 = "?" + firstEntryBase;
+                                String firstEntry4 = "=" + firstEntryBase;
+                                String firstEntry5 = "&" + firstEntryBase;
+
+                                String[] doubleEntry1 = {firstEntry1, secondEntry};
+                                String[] doubleEntry2 = {firstEntry2, secondEntry};
+                                String[] doubleEntry3 = {firstEntry3, secondEntry};
+                                String[] doubleEntry4 = {firstEntry4, secondEntry};
+                                String[] doubleEntry5 = {firstEntry5, secondEntry};
+
+                                multiEntryBlockList.add(doubleEntry1);
+                                multiEntryBlockList.add(doubleEntry2);
+                                multiEntryBlockList.add(doubleEntry3);
+                                multiEntryBlockList.add(doubleEntry4);
+                                multiEntryBlockList.add(doubleEntry5);
+
+                                //Log.i("BlockLists", "Multi entry ^ block list added: " + firstEntry1 + " , " + secondEntry);
+                                //Log.i("BlockLists", "Multi entry ^ block list added: " + firstEntry2 + " , " + secondEntry);
+                                //Log.i("BlockLists", "Multi entry ^ block list added: " + firstEntry3 + " , " + secondEntry);
+                                //Log.i("BlockLists", "Multi entry ^ block list added: " + firstEntry4 + " , " + secondEntry);
+                                //Log.i("BlockLists", "Multi entry ^ block list added: " + firstEntry5 + " , " + secondEntry);
+                            } else if (secondEntry.startsWith("^")) {
+                                String secondEntryBase = secondEntry.substring(1);
+
+                                String secondEntry1 = ":" + secondEntryBase;
+                                String secondEntry2 = "/" + secondEntryBase;
+                                String secondEntry3 = "?" + secondEntryBase;
+                                String secondEntry4 = "=" + secondEntryBase;
+                                String secondEntry5 = "&" + secondEntryBase;
+
+                                String[] doubleEntry1 = {firstEntry, secondEntry1};
+                                String[] doubleEntry2 = {firstEntry, secondEntry2};
+                                String[] doubleEntry3 = {firstEntry, secondEntry3};
+                                String[] doubleEntry4 = {firstEntry, secondEntry4};
+                                String[] doubleEntry5 = {firstEntry, secondEntry5};
+
+                                multiEntryBlockList.add(doubleEntry1);
+                                multiEntryBlockList.add(doubleEntry2);
+                                multiEntryBlockList.add(doubleEntry3);
+                                multiEntryBlockList.add(doubleEntry4);
+                                multiEntryBlockList.add(doubleEntry5);
+
+                                //Log.i("BlockLists", "Multi entry ^ block list added: " + firstEntry + " , " + secondEntry1);
+                                //Log.i("BlockLists", "Multi entry ^ block list added: " + firstEntry + " , " + secondEntry2);
+                                //Log.i("BlockLists", "Multi entry ^ block list added: " + firstEntry + " , " + secondEntry3);
+                                //Log.i("BlockLists", "Multi entry ^ block list added: " + firstEntry + " , " + secondEntry4);
+                                //Log.i("BlockLists", "Multi entry ^ block list added: " + firstEntry + " , " + secondEntry5);
+                            } else if (secondEntry.endsWith("^")) {
+                                String secondEntryBase = secondEntry.substring(0, secondEntry.length() - 1);
+
+                                String secondEntry1 = secondEntryBase + ":";
+                                String secondEntry2 = secondEntryBase + "/";
+                                String secondEntry3 = secondEntryBase + "?";
+                                String secondEntry4 = secondEntryBase + "=";
+                                String secondEntry5 = secondEntryBase + "&";
+
+                                String[] doubleEntry1 = {firstEntry, secondEntry1};
+                                String[] doubleEntry2 = {firstEntry, secondEntry2};
+                                String[] doubleEntry3 = {firstEntry, secondEntry3};
+                                String[] doubleEntry4 = {firstEntry, secondEntry4};
+                                String[] doubleEntry5 = {firstEntry, secondEntry5};
+
+                                String[] doubleEntryFinal = {firstEntry, secondEntryBase};
+
+                                multiEntryBlockList.add(doubleEntry1);
+                                multiEntryBlockList.add(doubleEntry2);
+                                multiEntryBlockList.add(doubleEntry3);
+                                multiEntryBlockList.add(doubleEntry4);
+                                multiEntryBlockList.add(doubleEntry5);
+
+                                multiEntryFinalBlockList.add(doubleEntryFinal);
+
+                                //Log.i("BlockLists", "Multi entry ^ block list added: " + firstEntry + " , " + secondEntry1);
+                                //Log.i("BlockLists", "Multi entry ^ block list added: " + firstEntry + " , " + secondEntry2);
+                                //Log.i("BlockLists", "Multi entry ^ block list added: " + firstEntry + " , " + secondEntry3);
+                                //Log.i("BlockLists", "Multi entry ^ block list added: " + firstEntry + " , " + secondEntry4);
+                                //Log.i("BlockLists", "Multi entry ^ block list added: " + firstEntry + " , " + secondEntry5);
+
+                                //Log.i("BlockLists", "Multi entry final block list added: " + firstEntry + " , " + secondEntryBase);
+                            } else {
+                                String[] doubleEntry = {firstEntry, secondEntry};
+
+                                multiEntryBlockList.add(doubleEntry);
+
+                                //Log.i("BlockLists", "Multi entry block list added: " + firstEntry + " , " + secondEntry);
+                            }
+                        }
+                    } else {
+                        if (blockListEntry.endsWith("^")) {  // `^` matches against `:`, `/`, `?`, `=`, `&`, and the end of the URL.
+                            // Add all the variations to the main block list.
+                            mainBlockList.add(blockListEntry.substring(0, blockListEntry.length() - 1) + ":");
+                            mainBlockList.add(blockListEntry.substring(0, blockListEntry.length() - 1) + "/");
+                            mainBlockList.add(blockListEntry.substring(0, blockListEntry.length() - 1) + "?");
+                            mainBlockList.add(blockListEntry.substring(0, blockListEntry.length() - 1) + "=");
+                            mainBlockList.add(blockListEntry.substring(0, blockListEntry.length() - 1) + "&");
+
+                            // Add the base block entry to the final block list.
+                            finalBlockList.add(blockListEntry.substring(0, blockListEntry.length() - 1));
+
+                            //Log.i("BlockLists", "Main block list added: " + blockListEntry.substring(0, blockListEntry.length() - 1) + ":");
+                            //Log.i("BlockLists", "Main block list added: " + blockListEntry.substring(0, blockListEntry.length() - 1) + "/");
+                            //Log.i("BlockLists", "Main block list added: " + blockListEntry.substring(0, blockListEntry.length() - 1) + "?");
+                            //Log.i("BlockLists", "Main block list added: " + blockListEntry.substring(0, blockListEntry.length() - 1) + "=");
+                            //Log.i("BlockLists", "Main block list added: " + blockListEntry.substring(0, blockListEntry.length() - 1) + "&");
+                            //Log.i("BlockLists", "Final block list added: " + blockListEntry.substring(0, blockListEntry.length() - 1));
+                        } else {  // This is a basic entry.
+                            // Add the modified block list entry to the main block list.
+                            mainBlockList.add(blockListEntry);
+
+                            //Log.i("BlockLists", "Main block list added: " + blockListEntry);
+                        }
+                    }
+                }
+            }
+
+            // Close `bufferedReader`.
+            bufferedReader.close();
+        } catch (IOException e) {
+            // The asset exists, so the `IOException` will never be thrown.
+        }
+
+        Log.i("BlockLists", "Finish populating block lists");
+
         // Set the content view.
         setContentView(R.layout.main_drawerlayout);
 
@@ -405,16 +1165,16 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         appBar.setCustomView(R.layout.url_app_bar);
         appBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
 
-        // Initialize the `ForegroundColorSpans` and `StyleSpan` for highlighting `urlTextBox`.  We have to use the deprecated `getColor()` until API >= 23.
+        // Initialize the foreground color spans for highlighting the URLs.  We have to use the deprecated `getColor()` until API >= 23.
         redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_a700));
         initialGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
         finalGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
 
         // Get a handle for `urlTextBox`.
-        urlTextBox = appBar.getCustomView().findViewById(R.id.url_edittext);
+        urlTextBox = findViewById(R.id.url_edittext);
 
         // Remove the formatting from `urlTextBar` when the user is editing the text.
-        urlTextBox.setOnFocusChangeListener((v, hasFocus) -> {
+        urlTextBox.setOnFocusChangeListener((View v, boolean hasFocus) -> {
             if (hasFocus) {  // The user is editing `urlTextBox`.
                 // Remove the highlighting.
                 urlTextBox.getText().removeSpan(redColorSpan);
@@ -426,8 +1186,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             }
         });
 
-        // Set the `Go` button on the keyboard to load the URL in `urlTextBox`.
-        urlTextBox.setOnKeyListener((v, keyCode, event) -> {
+        // Set the go button on the keyboard to load the URL in `urlTextBox`.
+        urlTextBox.setOnKeyListener((View v, int keyCode, KeyEvent event) -> {
             // If the event is a key-down event on the `enter` button, load the URL.
             if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {
                 // Load the URL into the mainWebView and consume the event.
@@ -566,7 +1326,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                             // Set `rootCoordinatorLayout` to fit under the status and navigation bars.
                             rootCoordinatorLayout.setFitsSystemWindows(false);
 
-                            if (translucentNavigationBarOnFullscreen) {  // There is an Android Support Library bug that causes a scrim to print on the right side of the `Drawer Layout` when the navigation bar is displayed on the right of the screen.
+                            // There is an Android Support Library bug that causes a scrim to print on the right side of the `Drawer Layout` when the navigation bar is displayed on the right of the screen.
+                            if (translucentNavigationBarOnFullscreen) {
                                 // Set the navigation bar to be translucent.
                                 getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
                             }
@@ -753,15 +1514,15 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // The `DrawerListener` allows us to update the Navigation Menu.
         drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() {
             @Override
-            public void onDrawerSlide(View drawerView, float slideOffset) {
+            public void onDrawerSlide(@NonNull View drawerView, float slideOffset) {
             }
 
             @Override
-            public void onDrawerOpened(View drawerView) {
+            public void onDrawerOpened(@NonNull View drawerView) {
             }
 
             @Override
-            public void onDrawerClosed(View drawerView) {
+            public void onDrawerClosed(@NonNull View drawerView) {
             }
 
             @Override
@@ -784,36 +1545,14 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // drawerToggle creates the hamburger icon at the start of the AppBar.
         drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, supportAppBar, R.string.open_navigation_drawer, R.string.close_navigation_drawer);
 
-        // Initialize `adServerSet`.
-        final Set<String> adServersSet = new HashSet<>();
-
-        // Load the list of ad servers into memory.
-        try {
-            // Load `pgl.yoyo.org_adservers.txt` into a `BufferedReader`.
-            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(getAssets().open("pgl.yoyo.org_adservers.txt")));
-
-            // Create a string for storing each ad server.
-            String adServer;
-
-            // Populate `adServersSet`.
-            while ((adServer = bufferedReader.readLine()) != null) {
-                adServersSet.add(adServer);
-            }
-
-            // Close `bufferedReader`.
-            bufferedReader.close();
-        } catch (IOException ioException) {
-            // We're pretty sure the asset exists, so we don't need to worry about the `IOException` ever being thrown.
-        }
-
         mainWebView.setWebViewClient(new WebViewClient() {
             // `shouldOverrideUrlLoading` makes this `WebView` the default handler for URLs inside the app, so that links are not kicked out to other apps.
             // We have to use the deprecated `shouldOverrideUrlLoading` until API >= 24.
             @SuppressWarnings("deprecation")
             @Override
             public boolean shouldOverrideUrlLoading(WebView view, String url) {
-                if (url.startsWith("mailto:")) {  // Load the URL in an external email program because it begins with `mailto:`.
-                    // We use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
+                if (url.startsWith("mailto:")) {  // Load the email address in an external email program.
+                    // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
                     Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
 
                     // Parse the url and set it as the data for the `Intent`.
@@ -825,6 +1564,21 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     // Make it so.
                     startActivity(emailIntent);
 
+                    // Returning `true` indicates the application is handling the URL.
+                    return true;
+                } else if (url.startsWith("tel:")) {  // Load the phone number in the dialer.
+                    // `ACTION_DIAL` open the dialer and loads the phone number, but waits for the user to place the call.
+                    Intent dialIntent = new Intent(Intent.ACTION_DIAL);
+
+                    // Add the phone number to the intent.
+                    dialIntent.setData(Uri.parse(url));
+
+                    // `FLAG_ACTIVITY_NEW_TASK` opens the dialer in a new task instead as part of Privacy Browser.
+                    dialIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+                    // Make it so.
+                    startActivity(dialIntent);
+
                     // Returning `true` indicates the application is handling the URL.
                     return true;
                 } else {  // Load the URL in Privacy Browser.
@@ -836,37 +1590,164 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 }
             }
 
-            // Block ads.  We have to use the deprecated `shouldInterceptRequest` until minimum API >= 21.
+            // Check requests against the block lists.  The deprecated `shouldInterceptRequest` must be used until minimum API >= 21.
             @SuppressWarnings("deprecation")
             @Override
             public WebResourceResponse shouldInterceptRequest(WebView view, String url){
-                if (adBlockerEnabled) {  // Block ads.
-                    // Extract the host from `url`.
-                    Uri requestUri = Uri.parse(url);
-                    String requestHost = requestUri.getHost();
-
-                    // Initialize a variable to track if this is an ad server.
-                    boolean requestHostIsAdServer = false;
-
-                    // Check all the subdomains of `requestHost` if it is not `null` against the ad server database.
-                    if (requestHost != null) {
-                        while (requestHost.contains(".") && !requestHostIsAdServer) {  // Stop checking if we run out of `.` or if we already know that `requestHostIsAdServer` is `true`.
-                            if (adServersSet.contains(requestHost)) {
-                                requestHostIsAdServer = true;
+                if (adBlockerEnabled) {  // Check the block lists.
+                    Log.i("BlockLists", "Begin check for " + url);
+
+                    Uri uri = Uri.parse(url);
+                    String domain = uri.getHost();
+
+                    for (String whiteListEntry : mainWhiteList) {
+                        if (url.contains(whiteListEntry)) {
+                            Log.i("BlockLists", "Request allowed by main white list: " + whiteListEntry + " | " + url);
+
+                            // `Return null` loads the requested resource.
+                            return null;
+                        }
+                    }
+
+                    for (String[] whiteListEntry : multiEntryWhiteList) {
+                        if (whiteListEntry.length == 2) {  // There are two entries.
+                            if (url.contains(whiteListEntry[0]) && url.contains(whiteListEntry[1])) {
+                                Log.i("BlockLists", "Request allowed by multi entry white list:  " + whiteListEntry[0] + " , " + whiteListEntry[1] + " | " + url);
+
+                                // `Return null` loads the requested resource.
+                                return null;
+                            }
+                        } else {  // There are three entries.
+                            if (url.contains(whiteListEntry[0]) && url.contains(whiteListEntry[1]) && url.contains(whiteListEntry[2])) {
+                                Log.i("BlockLists", "Request allowed by multi entry white list:  " + whiteListEntry[0] + " , " + whiteListEntry[1] + " , " + whiteListEntry[2] + " | " + url);
+
+                                // `Return null` loads the requested resource.
+                                return null;
+                            }
+                        }
+                    }
+
+                    for (String blockListEntry : mainBlockList) {
+                        if (url.contains(blockListEntry)) {
+                            Log.i("BlockLists", "Request blocked by main block list: " + blockListEntry + " | " + url);
+
+                            // Return an empty `WebResourceResponse`.
+                            return new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes()));
+                        }
+                    }
+
+                    for (String blockListEntry : initialBlockList) {
+                        if (url.startsWith(blockListEntry)) {
+                            Log.i("BlockLists", "Request blocked by initial block list: " + blockListEntry + " | " + url);
+
+                            // Return an empty `WebResourceResponse`.
+                            return new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes()));
+                        }
+                    }
+
+                    for (String blockListEntry : finalBlockList) {
+                        if (url.endsWith(blockListEntry)) {
+                            Log.i("BlockLists", "Request blocked by final block list: " + blockListEntry + " | " + url);
+
+                            // Return an empty `WebResourceResponse`.
+                            return new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes()));
+                        }
+                    }
+
+                    for (String[] blockListEntry : multiEntryBlockList) {
+                        if (blockListEntry.length == 2) {  // There are two entries.
+                            if (url.contains(blockListEntry[0]) && url.contains(blockListEntry[1])) {
+                                Log.i("BlockLists", "Request blocked by multi entry block list: " + blockListEntry[0] + " , " + blockListEntry[1] + " | " + url);
+
+                                // Return an empty `WebResourceResponse`.
+                                return new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes()));
                             }
+                        } else {  // There are three entries.
+                            if (url.contains(blockListEntry[0]) && url.contains(blockListEntry[1]) && url.contains(blockListEntry[2])) {
+                                Log.i("BlockLists", "Request blocked by multi entry block list: " + blockListEntry[0] + " , " + blockListEntry[1] + " , " + blockListEntry[2] + " | " + url);
 
-                            // Strip out the lowest subdomain of `requestHost`.
-                            requestHost = requestHost.substring(requestHost.indexOf(".") + 1);
+                                // Return an empty `WebResourceResponse`.
+                                return new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes()));
+                            }
                         }
                     }
 
-                    if (requestHostIsAdServer) {  // It is an ad server.
-                        // Return an empty `WebResourceResponse`.
-                        return new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes()));
-                    } else {  // It is not an ad server.
-                        // `return null` loads the requested resource.
-                        return null;
+                    for (String[] blockListEntry : multiEntryInitialBlockList) {
+                        if (url.startsWith(blockListEntry[0]) && url.contains(blockListEntry[1])) {
+                            Log.i("BlockLists", "Request blocked by multi entry initial block list: " + blockListEntry[0] + " , " + blockListEntry[1] + " | " + url);
+
+                            // Return an empty `WebResourceResponse`.
+                            return new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes()));
+                        }
                     }
+
+                    for (String[] blockListEntry : multiEntryFinalBlockList) {
+                        if (url.contains(blockListEntry[0]) && url.endsWith(blockListEntry[1])) {
+                            Log.i("BlockLists", "Request blocked by multi entry final block list: " + blockListEntry[0] + " , " + blockListEntry[1] + " | " + url);
+
+                            // Return an empty `WebResourceResponse`.
+                            return new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes()));
+                        }
+                    }
+
+                    for (String[] blockListEntry : domainBlockList) {
+                        if (domain.endsWith(blockListEntry[0]) && url.contains(blockListEntry[1])) {
+                            Log.i("BlockLists", "Request blocked by domain block list: " + blockListEntry[0] + " , " + blockListEntry[1] + " | " + url);
+
+                            // Return an empty `WebResourceResponse`.
+                            return new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes()));
+                        }
+                    }
+
+                    for (String[] blockListEntry : domainInitialBlockList) {
+                        if (domain.endsWith(blockListEntry[0]) && url.startsWith(blockListEntry[1])) {
+                            Log.i("BlockLists", "Request blocked by domain initial block list: " + blockListEntry[0] + " , " + blockListEntry[1] + " | " + url);
+
+                            // Return an empty `WebResourceResponse`.
+                            return new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes()));
+                        }
+                    }
+
+                    for (String[] blockListEntry : domainFinalBlockList) {
+                        if (domain.endsWith(blockListEntry[0]) && url.endsWith(blockListEntry[2])) {
+                            Log.i("BlockLists", "Request blocked by domain final block list: " + blockListEntry[0] + " , " + blockListEntry[1] + " | " + url);
+
+                            // Return an empty `WebResourceResponse`.
+                            return new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes()));
+                        }
+                    }
+
+                    for (String[] blockListEntry : domainMultiEntryBlockList) {
+                        if (domain.endsWith(blockListEntry[0]) && url.contains(blockListEntry[1]) && url.contains(blockListEntry[2])) {
+                            Log.i("BlockLists", "Request blocked by domain multi entry block list: " + blockListEntry[0] + " , " + blockListEntry[1] + " , " + blockListEntry[2] + " | " + url);
+
+                            // Return an empty `WebResourceResponse`.
+                            return new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes()));
+                        }
+                    }
+
+                    for (String[] blockListEntry : domainRegularExpressionBlockList) {
+                        if (domain.endsWith(blockListEntry[0]) && Pattern.matches(blockListEntry[1], url)) {
+                            Log.i("BlockLists", "Request blocked by domain regular expression block list: " + blockListEntry[0] + " , " + blockListEntry[1] + " | " + url);
+
+                            // Return an empty `WebResourceResponse`.
+                            return new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes()));
+                        }
+                    }
+
+                    for (String blockListEntry : regularExpressionBlockList) {
+                        if (Pattern.matches(blockListEntry, url)) {
+                            Log.i("BlockLists", "Request blocked by regular expression block list: " + blockListEntry + " | " + url);
+
+                            // Return an empty `WebResourceResponse`.
+                            return new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes()));
+                        }
+                    }
+
+                    Log.i("BlockLists", "End check for " + url);
+
+                    // `return null` loads the requested resource.
+                    return null;
                 } else {  // Ad blocking is disabled.
                     // `return null` loads the requested resource.
                     return null;
@@ -1025,9 +1906,11 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                         }
 
                         // 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.
+                        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));
@@ -1054,9 +1937,11 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
                 // 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.
+                        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.
                     handler.proceed();
                 } else {  // Either there isn't a pinned SSL certificate or it doesn't match the current website certificate.
@@ -1079,13 +1964,13 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             public void onProgressChanged(WebView view, int progress) {
                 // Inject the night mode CSS if night mode is enabled.
                 if (nightMode) {
-                    // `background-color: #212121` sets the background to be dark gray.  `color: #BDBDBD` sets the text color to be light gray.  `box-shadow: none` removes a lower underline on links used by WordPress.
-                    // `text-decoration: none` removes all text underlines.  `text-shadow: none` removes text shadows, which usually have a hard coded color.  `border: none` removes all borders, which can also be used to underline text.
+                    // `background-color: #212121` sets the background to be dark gray.  `color: #BDBDBD` sets the text color to be light gray.  `box-shadow: none` removes a lower underline on links
+                    // used by WordPress.  `text-decoration: none` removes all text underlines.  `text-shadow: none` removes text shadows, which usually have a hard coded color.
+                    // `border: none` removes all borders, which can also be used to underline text.
                     // `a {color: #1565C0}` sets links to be a dark blue.  `!important` takes precedent over any existing sub-settings.
-                    mainWebView.evaluateJavascript("(function() {var parent = document.getElementsByTagName('head').item(0); var style = document.createElement('style'); style.type = 'text/css'; style.innerHTML = '" +
-                            "* {background-color: #212121 !important; color: #BDBDBD !important; box-shadow: none !important; text-decoration: none !important; text-shadow: none !important; border: none !important;}" +
-                            "a {color: #1565C0 !important;}" +
-                            "'; parent.appendChild(style)})()", value -> {
+                    mainWebView.evaluateJavascript("(function() {var parent = document.getElementsByTagName('head').item(0); var style = document.createElement('style'); style.type = 'text/css'; " +
+                            "style.innerHTML = '* {background-color: #212121 !important; color: #BDBDBD !important; box-shadow: none !important; text-decoration: none !important;" +
+                            "text-shadow: none !important; border: none !important;} a {color: #1565C0 !important;}'; parent.appendChild(style)})()", value -> {
                                 // Initialize a `Handler` to display `mainWebView`.
                                 Handler displayWebViewHandler = new Handler();
 
@@ -1102,7 +1987,10 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                             });
                 }
 
+                // Update the progress bar.
                 progressBar.setProgress(progress);
+
+                // Set the visibility of the progress bar.
                 if (progress < 100) {
                     // Show the progress bar.
                     progressBar.setVisibility(View.VISIBLE);
@@ -1111,7 +1999,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     progressBar.setVisibility(View.GONE);
 
                     // Display `mainWebView` if night mode is disabled.
-                    // Because of a race condition between `applyDomainSettings` and `onPageStarted`, when night mode is set by domain settings the `WebView` may be hidden even if night mode is not currently enabled.
+                    // Because of a race condition between `applyDomainSettings` and `onPageStarted`, when night mode is set by domain settings the `WebView` may be hidden even if night mode is not
+                    // currently enabled.
                     if (!nightMode) {
                         mainWebView.setVisibility(View.VISIBLE);
                     }
@@ -1196,7 +2085,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         registerForContextMenu(mainWebView);
 
         // Allow the downloading of files.
-        mainWebView.setDownloadListener((url, userAgent, contentDisposition, mimetype, contentLength) -> {
+        mainWebView.setDownloadListener((String url, String userAgent, String contentDisposition, String mimetype, long contentLength) -> {
             // Show the `DownloadFileDialog` `AlertDialog` and name this instance `@string/download`.
             AppCompatDialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(url, contentDisposition, contentLength);
             downloadFileDialogFragment.show(getSupportFragmentManager(), getString(R.string.download));
@@ -1241,7 +2130,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         privacyBrowserRuntime = Runtime.getRuntime();
 
         // Store the application's private data directory.
-        privateDataDirectoryString = getApplicationInfo().dataDir;  // `dataDir` will vary, but will be something like `/data/user/0/com.stoutner.privacybrowser.standard`, which links to `/data/data/com.stoutner.privacybrowser.standard`.
+        privateDataDirectoryString = getApplicationInfo().dataDir;
+        // `dataDir` will vary, but will be something like `/data/user/0/com.stoutner.privacybrowser.standard`, which links to `/data/data/com.stoutner.privacybrowser.standard`.
 
         // Initialize `inFullScreenBrowsingMode`, which is always false at this point because Privacy Browser never starts in full screen browsing mode.
         inFullScreenBrowsingMode = false;
@@ -1260,9 +2150,10 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Initialize `webViewTitle`.
         webViewTitle = getString(R.string.no_title);
 
-        // Initialize `favoriteIconBitmap`.  We have to use `ContextCompat` until API >= 21.
+        // Initialize `favoriteIconBitmap`.  `ContextCompat` must be used until API >= 21.
         Drawable favoriteIconDrawable = ContextCompat.getDrawable(getApplicationContext(), R.drawable.world);
         BitmapDrawable favoriteIconBitmapDrawable = (BitmapDrawable) favoriteIconDrawable;
+        assert favoriteIconBitmapDrawable != null;
         favoriteIconDefaultBitmap = favoriteIconBitmapDrawable.getBitmap();
 
         // If the favorite icon is null, load the default.
@@ -1304,11 +2195,21 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
     @Override
     public void onRestart() {
+        // Run the default commands.
         super.onRestart();
 
         // Apply the app settings, which may have been changed in `SettingsActivity`.
         applyAppSettings();
 
+        // Apply the domain settings if returning from the Domains Activity.
+        if (reapplyDomainSettingsOnRestart) {
+            // Reset `reapplyDomainSettingsOnRestart`.
+            reapplyDomainSettingsOnRestart = false;
+
+            // Reapply the domain settings.
+            applyDomainSettings(formattedUrlString);
+        }
+
         // Update the privacy icon.  `true` runs `invalidateOptionsMenu` as the last step.
         updatePrivacyIcons(true);
 
@@ -1349,6 +2250,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     // `onResume()` runs after `onStart()`, which runs after `onCreate()` and `onRestart()`.
     @Override
     public void onResume() {
+        // Run the default commands.
         super.onResume();
 
         // Resume JavaScript (if enabled).
@@ -1419,10 +2321,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     @Override
     public boolean onPrepareOptionsMenu(Menu menu) {
         // Get handles for the menu items.
+        MenuItem addOrEditDomain = menu.findItem(R.id.add_or_edit_domain);
         MenuItem toggleFirstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies);
         MenuItem toggleThirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies);
         MenuItem toggleDomStorageMenuItem = menu.findItem(R.id.toggle_dom_storage);
         MenuItem toggleSaveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data);
+        MenuItem clearDataMenuItem = menu.findItem(R.id.clear_data);
         MenuItem clearCookiesMenuItem = menu.findItem(R.id.clear_cookies);
         MenuItem clearDOMStorageMenuItem = menu.findItem(R.id.clear_dom_storage);
         MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data);
@@ -1430,6 +2334,13 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         MenuItem displayImagesMenuItem = menu.findItem(R.id.display_images);
         MenuItem refreshMenuItem = menu.findItem(R.id.refresh);
 
+        // Set the text for the domain menu item.
+        if (domainSettingsApplied) {
+            addOrEditDomain.setTitle(R.string.edit_domain_settings);
+        } else {
+            addOrEditDomain.setTitle(R.string.add_domain_settings);
+        }
+
         // Set the status of the menu item checkboxes.
         toggleFirstPartyCookiesMenuItem.setChecked(firstPartyCookiesEnabled);
         toggleThirdPartyCookiesMenuItem.setChecked(thirdPartyCookiesEnabled);
@@ -1467,6 +2378,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(this);
         clearFormDataMenuItem.setEnabled(mainWebViewDatabase.hasFormData());
 
+        // Enable `Clear Data` if any of the submenu items are enabled.
+        clearDataMenuItem.setEnabled(clearCookiesMenuItem.isEnabled() || clearDOMStorageMenuItem.isEnabled() || clearFormDataMenuItem.isEnabled());
+
         // Initialize font size variables.
         int fontSize = mainWebView.getSettings().getTextZoom();
         String fontSizeTitle;
@@ -1530,7 +2444,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Run all the other default commands.
         super.onPrepareOptionsMenu(menu);
 
-        // `return true` displays the menu.
+        // Display the menu.
         return true;
     }
 
@@ -1540,10 +2454,32 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     // removeAllCookies is deprecated, but it is required for API < 21.
     @SuppressWarnings("deprecation")
     public boolean onOptionsItemSelected(MenuItem menuItem) {
+        // Get the selected menu item ID.
         int menuItemId = menuItem.getItemId();
 
         // Set the commands that relate to the menu entries.
         switch (menuItemId) {
+            case R.id.add_or_edit_domain:
+                if (domainSettingsApplied) {  // Edit the current domain settings.
+                    // Reapply the domain settings on returning to `MainWebViewActivity`.
+                    reapplyDomainSettingsOnRestart = true;
+                    currentDomainName = "";
+
+                    // Create an intent to launch the domains activity.
+                    Intent domainsIntent = new Intent(this, DomainsActivity.class);
+
+                    // Put extra information instructing the domains activity to directly load the current domain.
+                    domainsIntent.putExtra("LoadDomain", domainSettingsDatabaseId);
+
+                    // Make it so.
+                    startActivity(domainsIntent);
+                } else {  // Add a new domain.
+                    // Show the add domain `AlertDialog`.
+                    AppCompatDialogFragment addDomainDialog = new AddDomainDialog();
+                    addDomainDialog.show(getSupportFragmentManager(), getResources().getString(R.string.add_domain));
+                }
+                return true;
+
             case R.id.toggle_javascript:
                 // Switch the status of javaScriptEnabled.
                 javaScriptEnabled = !javaScriptEnabled;
@@ -1713,9 +2649,16 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                                         WebStorage webStorage = WebStorage.getInstance();
                                         webStorage.deleteAllData();
 
-                                        // Manually remove `IndexedDB` if it exists.
+                                        // Manually delete the DOM storage files and directories.
                                         try {
+                                            // A `String[]` must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
+                                            privacyBrowserRuntime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
+
+                                            // Multiple commands must be used because `Runtime.exec()` does not like `*`.
                                             privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
+                                            privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
+                                            privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
+                                            privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
                                         } catch (IOException e) {
                                             // Do nothing if an error is thrown.
                                         }
@@ -1815,7 +2758,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Show the Find on Page `RelativeLayout`.
                 findOnPageLinearLayout.setVisibility(View.VISIBLE);
 
-                // Display the keyboard.  We have to wait 200 ms before running the command to work around a bug in Android.  http://stackoverflow.com/questions/5520085/android-show-softkeyboard-with-showsoftinput-is-not-working
+                // Display the keyboard.  We have to wait 200 ms before running the command to work around a bug in Android.
+                // http://stackoverflow.com/questions/5520085/android-show-softkeyboard-with-showsoftinput-is-not-working
                 findOnPageEditText.postDelayed(() -> {
                     // Set the focus on `findOnPageEditText`.
                     findOnPageEditText.requestFocus();
@@ -1839,6 +2783,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 printManager.print(getString(R.string.privacy_browser_web_page), printDocumentAdapter, null);
                 return true;
 
+            case R.id.view_source:
+                // Launch the Vew Source activity.
+                Intent viewSourceIntent = new Intent(this, ViewSourceActivity.class);
+                startActivity(viewSourceIntent);
+                return true;
+
             case R.id.add_to_homescreen:
                 // Show the `CreateHomeScreenShortcutDialog` `AlertDialog` and name this instance `R.string.create_shortcut`.
                 AppCompatDialogFragment createHomeScreenShortcutDialogFragment = new CreateHomeScreenShortcutDialog();
@@ -1908,7 +2858,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 break;
 
             case R.id.domains:
-                // Reset `currentDomainName` so that domain settings are reapplied after returning to `MainWebViewActivity`.
+                // Reapply the domain settings on returning to `MainWebViewActivity`.
+                reapplyDomainSettingsOnRestart = true;
                 currentDomainName = "";
 
                 // Launch `DomainsActivity`.
@@ -1917,7 +2868,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 break;
 
             case R.id.settings:
-                // Reset `currentDomainName` so that domain settings are reapplied after returning to `MainWebViewActivity`.
+                // Reapply the domain settings on returning to `MainWebViewActivity`.
+                reapplyDomainSettingsOnRestart = true;
                 currentDomainName = "";
 
                 // Launch `SettingsActivity`.
@@ -1970,10 +2922,10 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
                     // Manually delete the DOM storage files and directories, as `WebStorage` sometimes will not flush its changes to disk before `System.exit(0)` is run.
                     try {
-                        // We have to use a `String[]` because the directory contains a space and `Runtime.exec` will not escape the string correctly otherwise.
+                        // A `String[]` must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
                         privacyBrowserRuntime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
 
-                        // We have to use multiple commands because `Runtime.exec()` does not like `*`.
+                        // Multiple commands must be used because `Runtime.exec()` does not like `*`.
                         privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
                         privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
                         privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
@@ -2007,8 +2959,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     try {
                         // Delete the main cache directory.
                         privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/cache");
-
-                        // Delete the secondary `Service Worker` cache directory.  We have to use a `String[]` because the directory contains a space and `Runtime.exec` will not escape the string correctly otherwise.
+                        // Delete the secondary `Service Worker` cache directory.
+                        // We have to use a `String[]` because the directory contains a space and `Runtime.exec` will not escape the string correctly otherwise.
                         privacyBrowserRuntime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
                     } catch (IOException e) {
                         // Do nothing if an error is thrown.
@@ -2081,7 +3033,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             adView = findViewById(R.id.adview);
         }
 
-        // `invalidateOptionsMenu` should recalculate the number of action buttons from the menu to display on the app bar, but it doesn't because of the this bug:  https://code.google.com/p/android/issues/detail?id=20493#c8
+        // `invalidateOptionsMenu` should recalculate the number of action buttons from the menu to display on the app bar, but it doesn't because of the this bug:
+        // https://code.google.com/p/android/issues/detail?id=20493#c8
         // ActivityCompat.invalidateOptionsMenu(this);
     }
 
@@ -2181,7 +3134,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 });
 
                 // Add a `Download Image` entry.
-                menu.add(R.string.download_image).setOnMenuItemClickListener(item -> {
+                menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
                     // Show the `DownloadImageDialog` `AlertDialog` and name this instance `@string/download`.
                     AppCompatDialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
                     downloadImageDialogFragment.show(getSupportFragmentManager(), getString(R.string.download));
@@ -2218,7 +3171,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 });
 
                 // Add a `Download Image` entry.
-                menu.add(R.string.download_image).setOnMenuItemClickListener(item -> {
+                menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
                     // Show the `DownloadImageDialog` `AlertDialog` and name this instance `@string/download`.
                     AppCompatDialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
                     downloadImageDialogFragment.show(getSupportFragmentManager(), getString(R.string.download));
@@ -2241,6 +3194,33 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         }
     }
 
+    @Override
+    public void onAddDomain(AppCompatDialogFragment dialogFragment) {
+        // Reapply the domain settings on returning to `MainWebViewActivity`.
+        reapplyDomainSettingsOnRestart = true;
+        currentDomainName = "";
+
+        // Get the new domain name `String` from `dialogFragment`.
+        EditText domainNameEditText = dialogFragment.getDialog().findViewById(R.id.domain_name_edittext);
+        String domainNameString = domainNameEditText.getText().toString();
+
+        // Initialize the database handler.  `this` specifies the context.  The two `nulls` do not specify the database name or a `CursorFactory`.
+        // The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
+        DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
+
+        // Create the domain and store the database ID in `currentDomainDatabaseId`.
+        int newDomainDatabaseId = domainsDatabaseHelper.addDomain(domainNameString);
+
+        // Create an intent to launch the domains activity.
+        Intent domainsIntent = new Intent(this, DomainsActivity.class);
+
+        // Put extra information instructing the domains activity to directly load the current domain.
+        domainsIntent.putExtra("LoadDomain", newDomainDatabaseId);
+
+        // Make it so.
+        startActivity(domainsIntent);
+    }
+
     @Override
     public void onCreateBookmark(AppCompatDialogFragment dialogFragment) {
         // Get the `EditTexts` from the `dialogFragment`.
@@ -2319,21 +3299,28 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
     @Override
     public void onCreateHomeScreenShortcut(AppCompatDialogFragment dialogFragment) {
-        // Get shortcutNameEditText from the alert dialog.
+        // Get the shortcut name.
         EditText shortcutNameEditText = dialogFragment.getDialog().findViewById(R.id.shortcut_name_edittext);
+        String shortcutNameString = shortcutNameEditText.getText().toString();
 
-        // Create the bookmark shortcut based on formattedUrlString.
-        Intent bookmarkShortcut = new Intent();
-        bookmarkShortcut.setAction(Intent.ACTION_VIEW);
-        bookmarkShortcut.setData(Uri.parse(formattedUrlString));
-
-        // Place the bookmark shortcut on the home screen.
-        Intent placeBookmarkShortcut = new Intent();
-        placeBookmarkShortcut.putExtra("android.intent.extra.shortcut.INTENT", bookmarkShortcut);
-        placeBookmarkShortcut.putExtra("android.intent.extra.shortcut.NAME", shortcutNameEditText.getText().toString());
-        placeBookmarkShortcut.putExtra("android.intent.extra.shortcut.ICON", favoriteIconBitmap);
-        placeBookmarkShortcut.setAction("com.android.launcher.action.INSTALL_SHORTCUT");
-        sendBroadcast(placeBookmarkShortcut);
+        // Convert the favorite icon bitmap to an `Icon`.  `IconCompat` is required until API >= 26.
+        IconCompat favoriteIcon = IconCompat.createWithBitmap(favoriteIconBitmap);
+
+        // Setup the shortcut intent.
+        Intent shortcutIntent = new Intent();
+        shortcutIntent.setAction(Intent.ACTION_VIEW);
+        shortcutIntent.setData(Uri.parse(formattedUrlString));
+
+        // Create a shortcut info builder.  The shortcut name becomes the shortcut ID.
+        ShortcutInfoCompat.Builder shortcutInfoBuilder = new ShortcutInfoCompat.Builder(this, shortcutNameString);
+
+        // Add the required fields to the shortcut info builder.
+        shortcutInfoBuilder.setIcon(favoriteIcon);
+        shortcutInfoBuilder.setIntent(shortcutIntent);
+        shortcutInfoBuilder.setShortLabel(shortcutNameString);
+
+        // Request the pin.  `ShortcutManagerCompat` can be switched to `ShortcutManager` once API >= 26.
+        ShortcutManagerCompat.requestPinShortcut(this, shortcutInfoBuilder.build(), null);
     }
 
     @Override
@@ -2712,10 +3699,10 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     }
 
     private void applyAppSettings() {
-        // Get a handle for `sharedPreferences`.  `this` references the current context.
+        // Get a handle for the shared preferences.
         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
 
-        // Store the values from `sharedPreferences` in variables.
+        // Store the values from the shared preferences in variables.
         String homepageString = sharedPreferences.getString("homepage", "https://start.duckduckgo.com");
         String torHomepageString = sharedPreferences.getString("tor_homepage", "https://3g2upl4pq6kufc4m.onion");
         String torSearchString = sharedPreferences.getString("tor_search", "https://3g2upl4pq6kufc4m.onion/html/?q=");
@@ -3036,7 +4023,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     cookieManager.setAcceptThirdPartyCookies(mainWebView, thirdPartyCookiesEnabled);
                 }
 
-                // Only set the user agent if the webpage is not currently loading.  Otherwise, changing the user agent on redirects can cause the original website to reload.  <https://redmine.stoutner.com/issues/160>
+                // Only set the user agent if the webpage is not currently loading.  Otherwise, changing the user agent on redirects can cause the original website to reload.
+                // <https://redmine.stoutner.com/issues/160>
                 if (!urlIsLoading) {
                     switch (userAgentString) {
                         case "System default user agent":
@@ -3067,6 +4055,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                             // Use the selected user agent.
                             mainWebView.getSettings().setUserAgentString(userAgentString);
                     }
+
+                    // Store the applied user agent string.
+                    appliedUserAgentString = mainWebView.getSettings().getUserAgentString();
                 }
 
                 // Set a green background on `urlTextBox` to indicate that custom domain settings are being used.  We have to use the deprecated `.getDrawable()` until the minimum API >= 21.
@@ -3112,7 +4103,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     cookieManager.setAcceptThirdPartyCookies(mainWebView, thirdPartyCookiesEnabled);
                 }
 
-                // Only set the user agent if the webpage is not currently loading.  Otherwise, changing the user agent on redirects can cause the original website to reload.  <https://redmine.stoutner.com/issues/160>
+                // Only set the user agent if the webpage is not currently loading.  Otherwise, changing the user agent on redirects can cause the original website to reload.
+                // <https://redmine.stoutner.com/issues/160>
                 if (!urlIsLoading) {
                     switch (defaultUserAgentString) {
                         case "WebView default user agent":
@@ -3129,6 +4121,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                             // Use the selected user agent.
                             mainWebView.getSettings().setUserAgentString(defaultUserAgentString);
                     }
+
+                    // Store the applied user agent string.
+                    appliedUserAgentString = mainWebView.getSettings().getUserAgentString();
                 }
 
                 // Set a transparent background on `urlTextBox`.  We have to use the deprecated `.getDrawable()` until the minimum API >= 21.
@@ -3226,18 +4221,18 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             }
         }
 
-        // `invalidateOptionsMenu` calls `onPrepareOptionsMenu()` and redraws the icons in the `AppBar`.  `this` references the current activity.
+        // `invalidateOptionsMenu` calls `onPrepareOptionsMenu()` and redraws the icons in the `AppBar`.
         if (runInvalidateOptionsMenu) {
-            ActivityCompat.invalidateOptionsMenu(this);
+            invalidateOptionsMenu();
         }
     }
 
     private void highlightUrlText() {
         String urlString = urlTextBox.getText().toString();
 
-        if (urlString.startsWith("http://")) {  // Highlight connections that are not encrypted.
+        if (urlString.startsWith("http://")) {  // Highlight the protocol of connections that are not encrypted.
             urlTextBox.getText().setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-        } else if (urlString.startsWith("https://")) {  // Highlight connections that are encrypted.
+        } else if (urlString.startsWith("https://")) {  // De-emphasize the protocol of connections that are encrypted.
             urlTextBox.getText().setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
         }