]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blobdiff - app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java
Always reload tabs after changing the proxy. https://redmine.stoutner.com/issues/516
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / activities / MainWebViewActivity.java
index 5f8d80754bbef87c1b19822b4c5855a5f47eeb16..1904673b5ff6543a0617772e62d03cc9ac828eba 100644 (file)
@@ -73,6 +73,7 @@ import android.webkit.CookieManager;
 import android.webkit.HttpAuthHandler;
 import android.webkit.SslErrorHandler;
 import android.webkit.ValueCallback;
+import android.webkit.WebBackForwardList;
 import android.webkit.WebChromeClient;
 import android.webkit.WebResourceResponse;
 import android.webkit.WebSettings;
@@ -128,12 +129,17 @@ import com.stoutner.privacybrowser.dialogs.DownloadImageDialog;
 import com.stoutner.privacybrowser.dialogs.DownloadLocationPermissionDialog;
 import com.stoutner.privacybrowser.dialogs.EditBookmarkDialog;
 import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDialog;
+import com.stoutner.privacybrowser.dialogs.FontSizeDialog;
 import com.stoutner.privacybrowser.dialogs.HttpAuthenticationDialog;
-import com.stoutner.privacybrowser.dialogs.SaveWebpageImageDialog;
+import com.stoutner.privacybrowser.dialogs.OpenDialog;
+import com.stoutner.privacybrowser.dialogs.ProxyNotInstalledDialog;
+import com.stoutner.privacybrowser.dialogs.PinnedMismatchDialog;
+import com.stoutner.privacybrowser.dialogs.SaveWebpageDialog;
 import com.stoutner.privacybrowser.dialogs.SslCertificateErrorDialog;
 import com.stoutner.privacybrowser.dialogs.StoragePermissionDialog;
 import com.stoutner.privacybrowser.dialogs.UrlHistoryDialog;
 import com.stoutner.privacybrowser.dialogs.ViewSslCertificateDialog;
+import com.stoutner.privacybrowser.dialogs.WaitingForProxyDialog;
 import com.stoutner.privacybrowser.fragments.WebViewTabFragment;
 import com.stoutner.privacybrowser.helpers.AdHelper;
 import com.stoutner.privacybrowser.helpers.BlocklistHelper;
@@ -141,7 +147,7 @@ import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper;
 import com.stoutner.privacybrowser.helpers.CheckPinnedMismatchHelper;
 import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper;
 import com.stoutner.privacybrowser.helpers.FileNameHelper;
-import com.stoutner.privacybrowser.helpers.OrbotProxyHelper;
+import com.stoutner.privacybrowser.helpers.ProxyHelper;
 import com.stoutner.privacybrowser.views.NestedScrollWebView;
 
 import java.io.ByteArrayInputStream;
@@ -159,16 +165,18 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 
 // AppCompatActivity from android.support.v7.app.AppCompatActivity must be used to have access to the SupportActionBar until the minimum API is >= 21.
 public class MainWebViewActivity extends AppCompatActivity implements CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener,
         DownloadFileDialog.DownloadFileListener, DownloadImageDialog.DownloadImageListener, DownloadLocationPermissionDialog.DownloadLocationPermissionDialogListener, EditBookmarkDialog.EditBookmarkListener,
-        EditBookmarkFolderDialog.EditBookmarkFolderListener, NavigationView.OnNavigationItemSelectedListener, PopulateBlocklists.PopulateBlocklistsListener, SaveWebpageImageDialog.SaveWebpageImageListener,
-        StoragePermissionDialog.StoragePermissionDialogListener, WebViewTabFragment.NewTabListener {
+        EditBookmarkFolderDialog.EditBookmarkFolderListener, FontSizeDialog.UpdateFontSizeListener, NavigationView.OnNavigationItemSelectedListener, OpenDialog.OpenListener,
+        PinnedMismatchDialog.PinnedMismatchListener, PopulateBlocklists.PopulateBlocklistsListener, SaveWebpageDialog.SaveWebpageListener, StoragePermissionDialog.StoragePermissionDialogListener,
+        UrlHistoryDialog.NavigateHistoryListener, WebViewTabFragment.NewTabListener {
 
-    // `orbotStatus` is public static so it can be accessed from `OrbotProxyHelper`.  It is also used in `onCreate()`, `onResume()`, and `applyProxyThroughOrbot()`.
-    public static String orbotStatus;
+    // `orbotStatus` is public static so it can be accessed from `OrbotProxyHelper`.  It is also used in `onCreate()`, `onResume()`, and `applyProxy()`.
+    public static String orbotStatus = "unknown";
 
     // The WebView pager adapter is accessed from `HttpAuthenticationDialog`, `PinnedMismatchDialog`, and `SslCertificateErrorDialog`.  It is also used in `onCreate()`, `onResume()`, and `addTab()`.
     public static WebViewPagerAdapter webViewPagerAdapter;
@@ -192,19 +200,28 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     public final static int DOMAINS_WEBVIEW_DEFAULT_USER_AGENT = 2;
     public final static int DOMAINS_CUSTOM_USER_AGENT = 13;
 
-    // Start activity for result request codes.
-    private final int FILE_UPLOAD_REQUEST_CODE = 0;
-    public final static int BROWSE_SAVE_WEBPAGE_IMAGE_REQUEST_CODE = 1;
+    // Start activity for result request codes.  The public static entries are accessed from `OpenDialog()` and `SaveWebpageDialog()`.
+    public static final int BROWSE_OPEN_REQUEST_CODE = 0;
+    public static final int BROWSE_SAVE_WEBPAGE_REQUEST_CODE = 1;
+    private final int BROWSE_FILE_UPLOAD_REQUEST_CODE = 2;
 
 
+    // The permission result request codes are used in `onCreateContextMenu()`, `onCloseDownloadLocationPermissionDialog()`, `onRequestPermissionResult()`, `onSaveWebpage()`,
+    // `onCloseStoragePermissionDialog()`, and `initializeWebView()`.
+    private final int PERMISSION_DOWNLOAD_FILE_REQUEST_CODE = 0;
+    private final int PERMISSION_DOWNLOAD_IMAGE_REQUEST_CODE = 1;
+    private final int PERMISSION_OPEN_REQUEST_CODE = 2;
+    private final int PERMISSION_SAVE_WEBPAGE_ARCHIVE_REQUEST_CODE = 3;
+    private final int PERMISSION_SAVE_WEBPAGE_IMAGE_REQUEST_CODE = 4;
+
     // The current WebView is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`,
-    // `findNextOnPage()`, `closeFindOnPage()`, `loadUrlFromTextBox()`, `onSslMismatchBack()`, `applyProxyThroughOrbot()`, and `applyDomainSettings()`.
+    // `findNextOnPage()`, `closeFindOnPage()`, `loadUrlFromTextBox()`, `onSslMismatchBack()`, `applyProxy()`, and `applyDomainSettings()`.
     private NestedScrollWebView currentWebView;
 
     // `customHeader` is used in `onCreate()`, `onOptionsItemSelected()`, `onCreateContextMenu()`, and `loadUrl()`.
     private final Map<String, String> customHeaders = new HashMap<>();
 
-    // The search URL is set in `applyProxyThroughOrbot()` and used in `onCreate()`, `onNewIntent()`, `loadURLFromTextBox()`, and `initializeWebView()`.
+    // The search URL is set in `applyAppSettings()` and used in `onNewIntent()`, `loadUrlFromTextBox()`, `initializeApp()`, and `initializeWebView()`.
     private String searchURL;
 
     // The options menu is set in `onCreateOptionsMenu()` and used in `onOptionsItemSelected()`, `updatePrivacyIcons()`, and `initializeWebView()`.
@@ -221,8 +238,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     // `webViewDefaultUserAgent` is used in `onCreate()` and `onPrepareOptionsMenu()`.
     private String webViewDefaultUserAgent;
 
-    // `proxyThroughOrbot` is used in `onRestart()`, `onOptionsItemSelected()`, `applyAppSettings()`, and `applyProxyThroughOrbot()`.
-    private boolean proxyThroughOrbot;
+    // The proxy mode is used in `onRestart()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `applyAppSettings()`, and `applyProxy()`.
+    // It will be updated in `applyAppSettings()`, but it needs to be initialized here or the first run of `onPrepareOptionsMenu()` crashes.
+    private String proxyMode = ProxyHelper.NONE;
 
     // The incognito mode is set in `applyAppSettings()` and used in `initializeWebView()`.
     private boolean incognitoModeEnabled;
@@ -252,8 +270,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     // `orbotStatusBroadcastReceiver` is used in `onCreate()` and `onDestroy()`.
     private BroadcastReceiver orbotStatusBroadcastReceiver;
 
-    // `waitingForOrbot` is used in `onCreate()`, `onResume()`, and `applyProxyThroughOrbot()`.
-    private boolean waitingForOrbot;
+    // The waiting for proxy boolean is used in `onResume()`, `initializeApp()` and `applyProxy()`.
+    private boolean waitingForProxy = false;
 
     // The action bar drawer toggle is initialized in `onCreate()` and used in `onResume()`.
     private ActionBarDrawerToggle actionBarDrawerToggle;
@@ -304,14 +322,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     // `downloadImageUrl` is used in `onCreateContextMenu()` and `onRequestPermissionResult()`.
     private String downloadImageUrl;
 
-    // The save website image file path string is used in `onSaveWebpageImage()` and `onRequestPermissionResult()`
-    private String saveWebsiteImageFilePath;
-
-    // The permission result request codes are used in `onCreateContextMenu()`, `onCloseDownloadLocationPermissionDialog()`, `onRequestPermissionResult()`, `onSaveWebpageImage()`,
-    // `onCloseStoragePermissionDialog()`, and `initializeWebView()`.
-    private final int DOWNLOAD_FILE_REQUEST_CODE = 1;
-    private final int DOWNLOAD_IMAGE_REQUEST_CODE = 2;
-    private final int SAVE_WEBPAGE_IMAGE_REQUEST_CODE = 3;
+    // The file path strings are used in `onSaveWebpageImage()` and `onRequestPermissionResult()`
+    private String openFilePath;
+    private String saveWebpageFilePath;
 
     @Override
     // Remove the warning about needing to override `performClick()` when using an `OnTouchListener` with `WebView`.
@@ -388,6 +401,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
     @Override
     protected void onNewIntent(Intent intent) {
+        // Run the default commands.
+        super.onNewIntent(intent);
+
         // Replace the intent that started the app with this one.
         setIntent(intent);
 
@@ -436,7 +452,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     addNewTab(url, true);
                 } else {  // Load the URL in the current tab.
                     // Make it so.
-                    loadUrl(url);
+                    loadUrl(currentWebView, url);
                 }
 
                 // Get a handle for the drawer layout.
@@ -460,18 +476,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Run the default commands.
         super.onRestart();
 
-        // Make sure Orbot is running if Privacy Browser is proxying through Orbot.
-        if (proxyThroughOrbot) {
-            // Request Orbot to start.  If Orbot is already running no hard will be caused by this request.
-            Intent orbotIntent = new Intent("org.torproject.android.intent.action.START");
-
-            // Send the intent to the Orbot package.
-            orbotIntent.setPackage("org.torproject.android");
-
-            // Make it so.
-            sendBroadcast(orbotIntent);
-        }
-
         // Apply the app settings if returning from the Settings activity.
         if (reapplyAppSettingsOnRestart) {
             // Reset the reapply app settings on restart tracker.
@@ -513,7 +517,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Load the URL on restart (used when loading a bookmark).
         if (loadUrlOnRestart) {
             // Load the specified URL.
-            loadUrl(urlToLoadOnRestart);
+            loadUrl(currentWebView, urlToLoadOnRestart);
 
             // Reset the load on restart tracker.
             loadUrlOnRestart = false;
@@ -544,6 +548,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Run the default commands.
         super.onResume();
 
+        // Resume any WebViews.
         for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
             // Get the WebView tab fragment.
             WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
@@ -564,16 +569,13 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             }
         }
 
-        // Display a message to the user if waiting for Orbot.
-        if (waitingForOrbot && !orbotStatus.equals("ON")) {
-            // Disable the wide view port so that the waiting for Orbot text is displayed correctly.
-            currentWebView.getSettings().setUseWideViewPort(false);
-
-            // Load a waiting page.  `null` specifies no encoding, which defaults to ASCII.
-            currentWebView.loadData("<html><body><br/><center><h1>" + getString(R.string.waiting_for_orbot) + "</h1></center></body></html>", "text/html", null);
+        // Reapply the proxy settings if the system is using a proxy.  This redisplays the appropriate alert dialog.
+        if (!proxyMode.equals(ProxyHelper.NONE)) {
+            applyProxy(false);
         }
 
-        if (displayingFullScreenVideo || inFullScreenBrowsingMode) {
+        // Reapply any system UI flags and the ad in the free flavor.
+        if (displayingFullScreenVideo || inFullScreenBrowsingMode) {  // The system is displaying a website or a video in full screen mode.
             // Get a handle for the root frame layouts.
             FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
 
@@ -588,7 +590,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
              */
             rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
                     View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
-        } else if (BuildConfig.FLAVOR.contentEquals("free")) {  // Resume the adView for the free flavor.
+        } else if (BuildConfig.FLAVOR.contentEquals("free")) {  // The system in not in full screen mode.
             // Resume the ad.
             AdHelper.resumeAd(findViewById(R.id.adview));
         }
@@ -628,7 +630,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
     @Override
     public void onDestroy() {
-        // Unregister the Orbot status broadcast receiver.
+        // Unregister the orbot status broadcast receiver.
         this.unregisterReceiver(orbotStatusBroadcastReceiver);
 
         // Close the bookmarks cursor and database.
@@ -729,12 +731,13 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         MenuItem ultraListMenuItem = menu.findItem(R.id.ultralist);
         MenuItem ultraPrivacyMenuItem = menu.findItem(R.id.ultraprivacy);
         MenuItem blockAllThirdPartyRequestsMenuItem = menu.findItem(R.id.block_all_third_party_requests);
+        MenuItem proxyMenuItem = menu.findItem(R.id.proxy);
+        MenuItem userAgentMenuItem = menu.findItem(R.id.user_agent);
         MenuItem fontSizeMenuItem = menu.findItem(R.id.font_size);
         MenuItem swipeToRefreshMenuItem = menu.findItem(R.id.swipe_to_refresh);
         MenuItem wideViewportMenuItem = menu.findItem(R.id.wide_viewport);
         MenuItem displayImagesMenuItem = menu.findItem(R.id.display_images);
         MenuItem nightModeMenuItem = menu.findItem(R.id.night_mode);
-        MenuItem proxyThroughOrbotMenuItem = menu.findItem(R.id.proxy_through_orbot);
 
         // Get a handle for the cookie manager.
         CookieManager cookieManager = CookieManager.getInstance();
@@ -796,9 +799,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             domStorageMenuItem.setEnabled(currentWebView.getSettings().getJavaScriptEnabled());
         }
 
-        // Set the status of the menu item checkboxes.
+        // Set the checked status of the first party cookies menu item.
         firstPartyCookiesMenuItem.setChecked(cookieManager.acceptCookie());
-        proxyThroughOrbotMenuItem.setChecked(proxyThroughOrbot);
 
         // Enable Clear Cookies if there are any.
         clearCookiesMenuItem.setEnabled(cookieManager.hasCookies());
@@ -810,14 +812,16 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         File localStorageDirectory = new File (privateDataDirectoryString + "/app_webview/Local Storage/");
         int localStorageDirectoryNumberOfFiles = 0;
         if (localStorageDirectory.exists()) {
-            localStorageDirectoryNumberOfFiles = localStorageDirectory.list().length;
+            // `Objects.requireNonNull` removes a lint warning that `localStorageDirectory.list` might produce a null pointed exception if it is dereferenced.
+            localStorageDirectoryNumberOfFiles = Objects.requireNonNull(localStorageDirectory.list()).length;
         }
 
         // Get a count of the number of files in the IndexedDB directory.
         File indexedDBDirectory = new File (privateDataDirectoryString + "/app_webview/IndexedDB");
         int indexedDBDirectoryNumberOfFiles = 0;
         if (indexedDBDirectory.exists()) {
-            indexedDBDirectoryNumberOfFiles = indexedDBDirectory.list().length;
+            // `Objects.requireNonNull` removes a lint warning that `indexedDBDirectory.list` might produce a null pointed exception if it is dereferenced.
+            indexedDBDirectoryNumberOfFiles = Objects.requireNonNull(indexedDBDirectory.list()).length;
         }
 
         // Enable Clear DOM Storage if there is any.
@@ -838,90 +842,124 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Disable Fanboy's Social Blocking List menu item if Fanboy's Annoyance List is checked.
         fanboysSocialBlockingListMenuItem.setEnabled(!fanboysAnnoyanceListMenuItem.isChecked());
 
+        // Set the proxy title and check the applied proxy.
+        switch (proxyMode) {
+            case ProxyHelper.NONE:
+                // Set the proxy title.
+                proxyMenuItem.setTitle(getString(R.string.proxy) + " - " + getString(R.string.proxy_none));
+
+                // Check the proxy None radio button.
+                menu.findItem(R.id.proxy_none).setChecked(true);
+                break;
+
+            case ProxyHelper.TOR:
+                // Set the proxy title.
+                proxyMenuItem.setTitle(getString(R.string.proxy) + " - " + getString(R.string.proxy_tor));
+
+                // Check the proxy Tor radio button.
+                menu.findItem(R.id.proxy_tor).setChecked(true);
+                break;
+
+            case ProxyHelper.I2P:
+                // Set the proxy title.
+                proxyMenuItem.setTitle(getString(R.string.proxy) + " - " + getString(R.string.proxy_i2p));
+
+                // Check the proxy I2P radio button.
+                menu.findItem(R.id.proxy_i2p).setChecked(true);
+                break;
+
+            case ProxyHelper.CUSTOM:
+                // Set the proxy title.
+                proxyMenuItem.setTitle(getString(R.string.proxy) + " - " + getString(R.string.proxy_custom));
+
+                // Check the proxy Custom radio button.
+                menu.findItem(R.id.proxy_custom).setChecked(true);
+                break;
+        }
+
         // Select the current user agent menu item.  A switch statement cannot be used because the user agents are not compile time constants.
         if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[0])) {  // Privacy Browser.
+            // Update the user agent menu item title.
+            userAgentMenuItem.setTitle(getString(R.string.user_agent) + " - " + getString(R.string.user_agent_privacy_browser));
+
+            // Select the Privacy Browser radio box.
             menu.findItem(R.id.user_agent_privacy_browser).setChecked(true);
         } else if (currentUserAgent.equals(webViewDefaultUserAgent)) {  // WebView Default.
+            // Update the user agent menu item title.
+            userAgentMenuItem.setTitle(getString(R.string.user_agent) + " - " + getString(R.string.user_agent_webview_default));
+
+            // Select the WebView Default radio box.
             menu.findItem(R.id.user_agent_webview_default).setChecked(true);
         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[2])) {  // Firefox on Android.
+            // Update the user agent menu item title.
+            userAgentMenuItem.setTitle(getString(R.string.user_agent) + " - " + getString(R.string.user_agent_firefox_on_android));
+
+            // Select the Firefox on Android radio box.
             menu.findItem(R.id.user_agent_firefox_on_android).setChecked(true);
         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[3])) {  // Chrome on Android.
+            // Update the user agent menu item title.
+            userAgentMenuItem.setTitle(getString(R.string.user_agent) + " - " + getString(R.string.user_agent_chrome_on_android));
+
+            // Select the Chrome on Android radio box.
             menu.findItem(R.id.user_agent_chrome_on_android).setChecked(true);
         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[4])) {  // Safari on iOS.
+            // Update the user agent menu item title.
+            userAgentMenuItem.setTitle(getString(R.string.user_agent) + " - " + getString(R.string.user_agent_safari_on_ios));
+
+            // Select the Safari on iOS radio box.
             menu.findItem(R.id.user_agent_safari_on_ios).setChecked(true);
         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[5])) {  // Firefox on Linux.
+            // Update the user agent menu item title.
+            userAgentMenuItem.setTitle(getString(R.string.user_agent) + " - " + getString(R.string.user_agent_firefox_on_linux));
+
+            // Select the Firefox on Linux radio box.
             menu.findItem(R.id.user_agent_firefox_on_linux).setChecked(true);
         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[6])) {  // Chromium on Linux.
+            // Update the user agent menu item title.
+            userAgentMenuItem.setTitle(getString(R.string.user_agent) + " - " + getString(R.string.user_agent_chromium_on_linux));
+
+            // Select the Chromium on Linux radio box.
             menu.findItem(R.id.user_agent_chromium_on_linux).setChecked(true);
         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[7])) {  // Firefox on Windows.
+            // Update the user agent menu item title.
+            userAgentMenuItem.setTitle(getString(R.string.user_agent) + " - " + getString(R.string.user_agent_firefox_on_windows));
+
+            // Select the Firefox on Windows radio box.
             menu.findItem(R.id.user_agent_firefox_on_windows).setChecked(true);
         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[8])) {  // Chrome on Windows.
+            // Update the user agent menu item title.
+            userAgentMenuItem.setTitle(getString(R.string.user_agent) + " - " + getString(R.string.user_agent_chrome_on_windows));
+
+            // Select the Chrome on Windows radio box.
             menu.findItem(R.id.user_agent_chrome_on_windows).setChecked(true);
         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[9])) {  // Edge on Windows.
+            // Update the user agent menu item title.
+            userAgentMenuItem.setTitle(getString(R.string.user_agent) + " - " + getString(R.string.user_agent_edge_on_windows));
+
+            // Select the Edge on Windows radio box.
             menu.findItem(R.id.user_agent_edge_on_windows).setChecked(true);
         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[10])) {  // Internet Explorer on Windows.
+            // Update the user agent menu item title.
+            userAgentMenuItem.setTitle(getString(R.string.user_agent) + " - " + getString(R.string.user_agent_internet_explorer_on_windows));
+
+            // Select the Internet on Windows radio box.
             menu.findItem(R.id.user_agent_internet_explorer_on_windows).setChecked(true);
         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[11])) {  // Safari on macOS.
+            // Update the user agent menu item title.
+            userAgentMenuItem.setTitle(getString(R.string.user_agent) + " - " + getString(R.string.user_agent_safari_on_macos));
+
+            // Select the Safari on macOS radio box.
             menu.findItem(R.id.user_agent_safari_on_macos).setChecked(true);
         } else {  // Custom user agent.
-            menu.findItem(R.id.user_agent_custom).setChecked(true);
-        }
-
-        // Instantiate the font size title and the selected font size menu item.
-        String fontSizeTitle;
-        MenuItem selectedFontSizeMenuItem;
-
-        // Prepare the font size title and current size menu item.
-        switch (fontSize) {
-            case 25:
-                fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.twenty_five_percent);
-                selectedFontSizeMenuItem = menu.findItem(R.id.font_size_twenty_five_percent);
-                break;
-
-            case 50:
-                fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.fifty_percent);
-                selectedFontSizeMenuItem = menu.findItem(R.id.font_size_fifty_percent);
-                break;
-
-            case 75:
-                fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.seventy_five_percent);
-                selectedFontSizeMenuItem = menu.findItem(R.id.font_size_seventy_five_percent);
-                break;
-
-            case 100:
-                fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent);
-                selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent);
-                break;
+            // Update the user agent menu item title.
+            userAgentMenuItem.setTitle(getString(R.string.user_agent) + " - " + getString(R.string.user_agent_custom));
 
-            case 125:
-                fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_twenty_five_percent);
-                selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_twenty_five_percent);
-                break;
-
-            case 150:
-                fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_fifty_percent);
-                selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_fifty_percent);
-                break;
-
-            case 175:
-                fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_seventy_five_percent);
-                selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_seventy_five_percent);
-                break;
-
-            case 200:
-                fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.two_hundred_percent);
-                selectedFontSizeMenuItem = menu.findItem(R.id.font_size_two_hundred_percent);
-                break;
-
-            default:
-                fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent);
-                selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent);
-                break;
+            // Select the Custom radio box.
+            menu.findItem(R.id.user_agent_custom).setChecked(true);
         }
 
-        // Set the font size title and select the current size menu item.
-        fontSizeMenuItem.setTitle(fontSizeTitle);
-        selectedFontSizeMenuItem.setChecked(true);
+        // Set the font size title.
+        fontSizeMenuItem.setTitle(getString(R.string.font_size) + " - " + fontSize + "%");
 
         // Run all the other default commands.
         super.onPrepareOptionsMenu(menu);
@@ -1370,6 +1408,46 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Consume the event.
                 return true;
 
+            case R.id.proxy_none:
+                // Update the proxy mode.
+                proxyMode = ProxyHelper.NONE;
+
+                // Apply the proxy mode.
+                applyProxy(true);
+
+                // Consume the event.
+                return true;
+
+            case R.id.proxy_tor:
+                // Update the proxy mode.
+                proxyMode = ProxyHelper.TOR;
+
+                // Apply the proxy mode.
+                applyProxy(true);
+
+                // Consume the event.
+                return true;
+
+            case R.id.proxy_i2p:
+                // Update the proxy mode.
+                proxyMode = ProxyHelper.I2P;
+
+                // Apply the proxy mode.
+                applyProxy(true);
+
+                // Consume the event.
+                return true;
+
+            case R.id.proxy_custom:
+                // Update the proxy mode.
+                proxyMode = ProxyHelper.CUSTOM;
+
+                // Apply the proxy mode.
+                applyProxy(true);
+
+                // Consume the event.
+                return true;
+
             case R.id.user_agent_privacy_browser:
                 // Update the user agent.
                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[0]);
@@ -1500,58 +1578,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Consume the event.
                 return true;
 
-            case R.id.font_size_twenty_five_percent:
-                // Set the font size.
-                currentWebView.getSettings().setTextZoom(25);
-
-                // Consume the event.
-                return true;
-
-            case R.id.font_size_fifty_percent:
-                // Set the font size.
-                currentWebView.getSettings().setTextZoom(50);
-
-                // Consume the event.
-                return true;
-
-            case R.id.font_size_seventy_five_percent:
-                // Set the font size.
-                currentWebView.getSettings().setTextZoom(75);
-
-                // Consume the event.
-                return true;
-
-            case R.id.font_size_one_hundred_percent:
-                // Set the font size.
-                currentWebView.getSettings().setTextZoom(100);
-
-                // Consume the event.
-                return true;
-
-            case R.id.font_size_one_hundred_twenty_five_percent:
-                // Set the font size.
-                currentWebView.getSettings().setTextZoom(125);
-
-                // Consume the event.
-                return true;
-
-            case R.id.font_size_one_hundred_fifty_percent:
-                // Set the font size.
-                currentWebView.getSettings().setTextZoom(150);
-
-                // Consume the event.
-                return true;
-
-            case R.id.font_size_one_hundred_seventy_five_percent:
-                // Set the font size.
-                currentWebView.getSettings().setTextZoom(175);
-
-                // Consume the event.
-                return true;
+            case R.id.font_size:
+                // Instantiate the font size dialog.
+                DialogFragment fontSizeDialogFragment = FontSizeDialog.displayDialog(currentWebView.getSettings().getTextZoom());
 
-            case R.id.font_size_two_hundred_percent:
-                // Set the font size.
-                currentWebView.getSettings().setTextZoom(200);
+                // Show the font size dialog.
+                fontSizeDialogFragment.show(getSupportFragmentManager(), getString(R.string.font_size));
 
                 // Consume the event.
                 return true;
@@ -1672,12 +1704,22 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Consume the event.
                 return true;
 
+            case R.id.save_as_archive:
+                // Instantiate the save webpage archive dialog.
+                DialogFragment saveWebpageArchiveDialogFragment = SaveWebpageDialog.saveWebpage(StoragePermissionDialog.SAVE_ARCHIVE);
+
+                // Show the save webpage archive dialog.
+                saveWebpageArchiveDialogFragment.show(getSupportFragmentManager(), getString(R.string.save_webpage));
+
+                // Consume the event.
+                return true;
+
             case R.id.save_as_image:
                 // Instantiate the save webpage image dialog.
-                DialogFragment saveWebpageImageDialogFragment = new SaveWebpageImageDialog();
+                DialogFragment saveWebpageImageDialogFragment = SaveWebpageDialog.saveWebpage(StoragePermissionDialog.SAVE_IMAGE);
 
                 // Show the save webpage image dialog.
-                saveWebpageImageDialogFragment.show(getSupportFragmentManager(), getString(R.string.save_as_image));
+                saveWebpageImageDialogFragment.show(getSupportFragmentManager(), getString(R.string.save_webpage));
 
                 // Consume the event.
                 return true;
@@ -1736,16 +1778,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Consume the event.
                 return true;
 
-            case R.id.proxy_through_orbot:
-                // Toggle the proxy through Orbot variable.
-                proxyThroughOrbot = !proxyThroughOrbot;
-
-                // Apply the proxy through Orbot settings.
-                applyProxyThroughOrbot(true);
-
-                // Consume the event.
-                return true;
-
             case R.id.refresh:
                 if (menuItem.getTitle().equals(getString(R.string.refresh))) {  // The refresh button was pushed.
                     // Reload the current WebView.
@@ -1791,23 +1823,20 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 break;
 
             case R.id.home:
-                // Select the homepage based on the proxy through Orbot status.
-                if (proxyThroughOrbot) {
-                    // Load the Tor homepage.
-                    loadUrl(sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value)));
-                } else {
-                    // Load the normal homepage.
-                    loadUrl(sharedPreferences.getString("homepage", getString(R.string.homepage_default_value)));
-                }
+                // Load the homepage.
+                loadUrl(currentWebView, sharedPreferences.getString("homepage", getString(R.string.homepage_default_value)));
                 break;
 
             case R.id.back:
                 if (currentWebView.canGoBack()) {
-                    // Reset the current domain name so that navigation works if third-party requests are blocked.
-                    currentWebView.resetCurrentDomainName();
+                    // Get the current web back forward list.
+                    WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList();
 
-                    // Set navigating history so that the domain settings are applied when the new URL is loaded.
-                    currentWebView.setNavigatingHistory(true);
+                    // Get the previous entry URL.
+                    String previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.getCurrentIndex() - 1).getUrl();
+
+                    // Apply the domain settings.
+                    applyDomainSettings(currentWebView, previousUrl, false, false);
 
                     // Load the previous website in the history.
                     currentWebView.goBack();
@@ -1816,11 +1845,14 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
             case R.id.forward:
                 if (currentWebView.canGoForward()) {
-                    // Reset the current domain name so that navigation works if third-party requests are blocked.
-                    currentWebView.resetCurrentDomainName();
+                    // Get the current web back forward list.
+                    WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList();
+
+                    // Get the next entry URL.
+                    String nextUrl = webBackForwardList.getItemAtIndex(webBackForwardList.getCurrentIndex() + 1).getUrl();
 
-                    // Set navigating history so that the domain settings are applied when the new URL is loaded.
-                    currentWebView.setNavigatingHistory(true);
+                    // Apply the domain settings.
+                    applyDomainSettings(currentWebView, nextUrl, false, false);
 
                     // Load the next website in the history.
                     currentWebView.goForward();
@@ -1835,6 +1867,14 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 urlHistoryDialogFragment.show(getSupportFragmentManager(), getString(R.string.history));
                 break;
 
+            case R.id.open:
+                // Instantiate the open file dialog.
+                DialogFragment openDialogFragment = new OpenDialog();
+
+                // Show the open file dialog.
+                openDialogFragment.show(getSupportFragmentManager(), getString(R.string.open));
+                break;
+
             case R.id.requests:
                 // Populate the resource requests.
                 RequestsActivity.resourceRequests = currentWebView.getResourceRequests();
@@ -1969,7 +2009,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     }
 
     @Override
-    public void onConfigurationChanged(Configuration newConfig) {
+    public void onConfigurationChanged(@NonNull Configuration newConfig) {
         // Run the default commands.
         super.onConfigurationChanged(newConfig);
 
@@ -2025,7 +2065,16 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
                 // Add an Open in New Tab entry.
                 menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
-                    // Load the link URL in a new tab.
+                    // Load the link URL in a new tab and move to it.
+                    addNewTab(linkUrl, true);
+
+                    // Consume the event.
+                    return true;
+                });
+
+                // Add an Open in Background entry.
+                menu.add(R.string.open_in_background).setOnMenuItemClickListener((MenuItem item) -> {
+                    // Load the link URL in a new tab but do not move to it.
                     addNewTab(linkUrl, false);
 
                     // Consume the event.
@@ -2082,7 +2131,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                                 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
                             } else {  // Show the permission request directly.
                                 // Request the permission.  The download dialog will be launched by `onRequestPermissionResult()`.
-                                ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
+                                ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_DOWNLOAD_FILE_REQUEST_CODE);
                             }
                         } else {  // The storage permission has already been granted.
                             // Get a handle for the download file alert dialog.
@@ -2153,7 +2202,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Add an Open in New Tab entry.
                 menu.add(R.string.open_image_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
                     // Load the image in a new tab.
-                    addNewTab(imageUrl, false);
+                    addNewTab(imageUrl, true);
 
                     // Consume the event.
                     return true;
@@ -2162,7 +2211,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Add a View Image entry.
                 menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
                     // Load the image in the current tab.
-                    loadUrl(imageUrl);
+                    loadUrl(currentWebView, imageUrl);
 
                     // Consume the event.
                     return true;
@@ -2188,7 +2237,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                                 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
                             } else {  // Show the permission request directly.
                                 // Request the permission.  The download dialog will be launched by `onRequestPermissionResult()`.
-                                ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
+                                ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_DOWNLOAD_IMAGE_REQUEST_CODE);
                             }
                         } else {  // The storage permission has already been granted.
                             // Get a handle for the download image alert dialog.
@@ -2259,7 +2308,16 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
                 // Add an Open in New Tab entry.
                 menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
-                    // Load the link URL in a new tab.
+                    // Load the link URL in a new tab and move to it.
+                    addNewTab(linkUrl, true);
+
+                    // Consume the event.
+                    return true;
+                });
+
+                // Add an Open in Background entry.
+                menu.add(R.string.open_in_background).setOnMenuItemClickListener((MenuItem item) -> {
+                    // Lod the link URL in a new tab but do not move to it.
                     addNewTab(linkUrl, false);
 
                     // Consume the event.
@@ -2268,8 +2326,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
                 // Add an Open Image in New Tab entry.
                 menu.add(R.string.open_image_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
-                    // Load the image in a new tab.
-                    addNewTab(imageUrl, false);
+                    // Load the image in a new tab and move to it.
+                    addNewTab(imageUrl, true);
 
                     // Consume the event.
                     return true;
@@ -2278,7 +2336,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Add a View Image entry.
                 menu.add(R.string.view_image).setOnMenuItemClickListener((MenuItem item) -> {
                    // View the image in the current tab.
-                   loadUrl(imageUrl);
+                   loadUrl(currentWebView, imageUrl);
 
                    // Consume the event.
                    return true;
@@ -2304,7 +2362,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                                 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
                             } else {  // Show the permission request directly.
                                 // Request the permission.  The download dialog will be launched by `onRequestPermissionResult()`.
-                                ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
+                                ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_DOWNLOAD_IMAGE_REQUEST_CODE);
                             }
                         } else {  // The storage permission has already been granted.
                             // Get a handle for the download image alert dialog.
@@ -2360,9 +2418,15 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Get a handle for the bookmarks list view.
         ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
 
+        // Get the dialog.
+        Dialog dialog = dialogFragment.getDialog();
+
+        // Remove the incorrect lint warning below that the dialog might be null.
+        assert dialog != null;
+
         // Get the views from the dialog fragment.
-        EditText createBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_name_edittext);
-        EditText createBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_url_edittext);
+        EditText createBookmarkNameEditText = dialog.findViewById(R.id.create_bookmark_name_edittext);
+        EditText createBookmarkUrlEditText = dialog.findViewById(R.id.create_bookmark_url_edittext);
 
         // Extract the strings from the edit texts.
         String bookmarkNameString = createBookmarkNameEditText.getText().toString();
@@ -2398,10 +2462,16 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Get a handle for the bookmarks list view.
         ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
 
+        // Get the dialog.
+        Dialog dialog = dialogFragment.getDialog();
+
+        // Remove the incorrect lint warning below that the dialog might be null.
+        assert dialog != null;
+
         // Get handles for the views in the dialog fragment.
-        EditText createFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.create_folder_name_edittext);
-        RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon_radiobutton);
-        ImageView folderIconImageView = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon);
+        EditText createFolderNameEditText = dialog.findViewById(R.id.create_folder_name_edittext);
+        RadioButton defaultFolderIconRadioButton = dialog.findViewById(R.id.create_folder_default_icon_radiobutton);
+        ImageView folderIconImageView = dialog.findViewById(R.id.create_folder_default_icon);
 
         // Get new folder name string.
         String folderNameString = createFolderNameEditText.getText().toString();
@@ -2454,10 +2524,16 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
     @Override
     public void onSaveBookmark(DialogFragment dialogFragment, int selectedBookmarkDatabaseId, Bitmap favoriteIconBitmap) {
-        // Get handles for the views from `dialogFragment`.
-        EditText editBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_name_edittext);
-        EditText editBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_url_edittext);
-        RadioButton currentBookmarkIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_current_icon_radiobutton);
+        // Get the dialog.
+        Dialog dialog = dialogFragment.getDialog();
+
+        // Remove the incorrect lint warning below that the dialog might be null.
+        assert dialog != null;
+
+        // Get handles for the views from the dialog.
+        EditText editBookmarkNameEditText = dialog.findViewById(R.id.edit_bookmark_name_edittext);
+        EditText editBookmarkUrlEditText = dialog.findViewById(R.id.edit_bookmark_url_edittext);
+        RadioButton currentBookmarkIconRadioButton = dialog.findViewById(R.id.edit_bookmark_current_icon_radiobutton);
 
         // Store the bookmark strings.
         String bookmarkNameString = editBookmarkNameEditText.getText().toString();
@@ -2489,11 +2565,17 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
     @Override
     public void onSaveBookmarkFolder(DialogFragment dialogFragment, int selectedFolderDatabaseId, Bitmap favoriteIconBitmap) {
+        // Get the dialog.
+        Dialog dialog = dialogFragment.getDialog();
+
+        // Remove the incorrect lint warning below that the dialog might be null.
+        assert dialog != null;
+
         // Get handles for the views from `dialogFragment`.
-        EditText editFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_folder_name_edittext);
-        RadioButton currentFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_current_icon_radiobutton);
-        RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_radiobutton);
-        ImageView defaultFolderIconImageView = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_imageview);
+        EditText editFolderNameEditText = dialog.findViewById(R.id.edit_folder_name_edittext);
+        RadioButton currentFolderIconRadioButton = dialog.findViewById(R.id.edit_folder_current_icon_radiobutton);
+        RadioButton defaultFolderIconRadioButton = dialog.findViewById(R.id.edit_folder_default_icon_radiobutton);
+        ImageView defaultFolderIconImageView = dialog.findViewById(R.id.edit_folder_default_icon_imageview);
 
         // Get the new folder name.
         String newFolderNameString = editFolderNameEditText.getText().toString();
@@ -2574,66 +2656,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         switch (downloadType) {
             case DownloadLocationPermissionDialog.DOWNLOAD_FILE:
                 // Request the WRITE_EXTERNAL_STORAGE permission with a file request code.
-                ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
+                ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_DOWNLOAD_FILE_REQUEST_CODE);
                 break;
 
             case DownloadLocationPermissionDialog.DOWNLOAD_IMAGE:
                 // Request the WRITE_EXTERNAL_STORAGE permission with an image request code.
-                ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
-                break;
-        }
-    }
-
-    @Override
-    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
-        // Get a handle for the fragment manager.
-        FragmentManager fragmentManager = getSupportFragmentManager();
-
-        switch (requestCode) {
-            case DOWNLOAD_FILE_REQUEST_CODE:
-                // Show the download file alert dialog.  When the dialog closes, the correct command will be used based on the permission status.
-                DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(downloadUrl, downloadContentDisposition, downloadContentLength);
-
-                // On API 23, displaying the fragment must be delayed or the app will crash.
-                if (Build.VERSION.SDK_INT == 23) {
-                    new Handler().postDelayed(() -> downloadFileDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
-                } else {
-                    downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
-                }
-
-                // Reset the download variables.
-                downloadUrl = "";
-                downloadContentDisposition = "";
-                downloadContentLength = 0;
-                break;
-
-            case DOWNLOAD_IMAGE_REQUEST_CODE:
-                // Show the download image alert dialog.  When the dialog closes, the correct command will be used based on the permission status.
-                DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(downloadImageUrl);
-
-                // On API 23, displaying the fragment must be delayed or the app will crash.
-                if (Build.VERSION.SDK_INT == 23) {
-                    new Handler().postDelayed(() -> downloadImageDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
-                } else {
-                    downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
-                }
-
-                // Reset the image URL variable.
-                downloadImageUrl = "";
-                break;
-
-            case SAVE_WEBPAGE_IMAGE_REQUEST_CODE:
-                // Check to see if the storage permission was granted.  If the dialog was canceled the grant result will be empty.
-                if ((grantResults.length > 0) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) {  // The storage permission was granted.
-                    // Save the webpage image.
-                    new SaveWebpageImage(this, currentWebView).execute(saveWebsiteImageFilePath);
-                } else {  // The storage permission was not granted.
-                    // Display an error snackbar.
-                    Snackbar.make(currentWebView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show();
-                }
-
-                // Reset the save website image file path.
-                saveWebsiteImageFilePath = "";
+                ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_DOWNLOAD_IMAGE_REQUEST_CODE);
                 break;
         }
     }
@@ -2661,8 +2689,14 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 downloadRequest.addRequestHeader("Cookie", cookies);
             }
 
+            // Get the dialog.
+            Dialog dialog = dialogFragment.getDialog();
+
+            // Remove the incorrect lint warning below that the dialog might be null.
+            assert dialog != null;
+
             // Get the file name from the dialog fragment.
-            EditText downloadImageNameEditText = dialogFragment.getDialog().findViewById(R.id.download_image_name);
+            EditText downloadImageNameEditText = dialog.findViewById(R.id.download_image_name);
             String imageName = downloadImageNameEditText.getText().toString();
 
             // Specify the download location.
@@ -2716,8 +2750,14 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 downloadRequest.addRequestHeader("Cookie", cookies);
             }
 
-            // Get the file name from the dialog fragment.
-            EditText downloadFileNameEditText = dialogFragment.getDialog().findViewById(R.id.download_file_name);
+            // Get the dialog.
+            Dialog dialog = dialogFragment.getDialog();
+
+            // Remove the incorrect lint warning below that the dialog might be null.
+            assert dialog != null;
+
+            // Get the file name from the dialog.
+            EditText downloadFileNameEditText = dialog.findViewById(R.id.download_file_name);
             String fileName = downloadFileNameEditText.getText().toString();
 
             // Specify the download location.
@@ -2748,7 +2788,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         }
     }
 
-    // Override `onBackPressed` to handle the navigation drawer and and the WebView.
+    // Override `onBackPressed` to handle the navigation drawer and and the WebViews.
     @Override
     public void onBackPressed() {
         // Get a handle for the drawer layout and the tab layout.
@@ -2769,12 +2809,86 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Load the new folder.
                 loadBookmarksFolder();
             }
+        } else if (displayingFullScreenVideo) {  // A full screen video is shown.
+            // Get a handle for the layouts.
+            FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
+            RelativeLayout mainContentRelativeLayout = findViewById(R.id.main_content_relativelayout);
+            FrameLayout fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout);
+
+            // Re-enable the screen timeout.
+            fullScreenVideoFrameLayout.setKeepScreenOn(false);
+
+            // Unset the full screen video flag.
+            displayingFullScreenVideo = false;
+
+            // Remove all the views from the full screen video frame layout.
+            fullScreenVideoFrameLayout.removeAllViews();
+
+            // Hide the full screen video frame layout.
+            fullScreenVideoFrameLayout.setVisibility(View.GONE);
+
+            // Enable the sliding drawers.
+            drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
+
+            // Show the main content relative layout.
+            mainContentRelativeLayout.setVisibility(View.VISIBLE);
+
+            // Apply the appropriate full screen mode flags.
+            if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) {  // Privacy Browser is currently in full screen browsing mode.
+                // Hide the app bar if specified.
+                if (hideAppBar) {
+                    // Get handles for the views.
+                    LinearLayout tabsLinearLayout = findViewById(R.id.tabs_linearlayout);
+                    ActionBar actionBar = getSupportActionBar();
+
+                    // Remove the incorrect lint warning below that the action bar might be null.
+                    assert actionBar != null;
+
+                    // Hide the tab linear layout.
+                    tabsLinearLayout.setVisibility(View.GONE);
+
+                    // Hide the action bar.
+                    actionBar.hide();
+                }
+
+                // Hide the banner ad in the free flavor.
+                if (BuildConfig.FLAVOR.contentEquals("free")) {
+                    AdHelper.hideAd(findViewById(R.id.adview));
+                }
+
+                // Remove the translucent status flag.  This is necessary so the root frame layout can fill the entire screen.
+                getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+
+                /* Hide the system bars.
+                 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
+                 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
+                 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
+                 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
+                 */
+                rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
+                        View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
+            } else {  // Switch to normal viewing mode.
+                // Remove the `SYSTEM_UI` flags from the root frame layout.
+                rootFrameLayout.setSystemUiVisibility(0);
+
+                // Add the translucent status flag.
+                getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+            }
+
+            // Reload the ad for the free flavor if not in full screen mode.
+            if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
+                // Reload the ad.
+                AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
+            }
         } else if (currentWebView.canGoBack()) {  // There is at least one item in the current WebView history.
-            // Reset the current domain name so that navigation works if third-party requests are blocked.
-            currentWebView.resetCurrentDomainName();
+            // Get the current web back forward list.
+            WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList();
 
-            // Set navigating history so that the domain settings are applied when the new URL is loaded.
-            currentWebView.setNavigatingHistory(true);
+            // Get the previous entry URL.
+            String previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.getCurrentIndex() - 1).getUrl();
+
+            // Apply the domain settings.
+            applyDomainSettings(currentWebView, previousUrl, false, false);
 
             // Go back.
             currentWebView.goBack();
@@ -2782,8 +2896,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             // Close the current tab.
             closeCurrentTab();
         } else {  // There isn't anything to do in Privacy Browser.
-            // Run the default commands.
-            super.onBackPressed();
+            // Close Privacy Browser.  `finishAndRemoveTask()` also removes Privacy Browser from the recent app list.
+            if (Build.VERSION.SDK_INT >= 21) {
+                finishAndRemoveTask();
+            } else {
+                finish();
+            }
 
             // Manually kill Privacy Browser.  Otherwise, it is glitchy when restarted.
             System.exit(0);
@@ -2792,39 +2910,86 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
     // Process the results of a file browse.
     @Override
-    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+    public void onActivityResult(int requestCode, int resultCode, Intent returnedIntent) {
+        // Run the default commands.
+        super.onActivityResult(requestCode, resultCode, returnedIntent);
+
         // Run the commands that correlate to the specified request code.
         switch (requestCode) {
-            case FILE_UPLOAD_REQUEST_CODE:
+            case BROWSE_FILE_UPLOAD_REQUEST_CODE:
                 // File uploads only work on API >= 21.
                 if (Build.VERSION.SDK_INT >= 21) {
                     // Pass the file to the WebView.
-                    fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, data));
+                    fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, returnedIntent));
                 }
                 break;
 
-            case BROWSE_SAVE_WEBPAGE_IMAGE_REQUEST_CODE:
+            case BROWSE_SAVE_WEBPAGE_REQUEST_CODE:
                 // Don't do anything if the user pressed back from the file picker.
                 if (resultCode == Activity.RESULT_OK) {
                     // Get a handle for the save dialog fragment.
-                    DialogFragment saveWebpageImageDialogFragment= (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.save_as_image));
+                    DialogFragment saveWebpageDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.save_webpage));
 
                     // Only update the file name if the dialog still exists.
-                    if (saveWebpageImageDialogFragment != null) {
-                        // Get a handle for the save webpage image dialog.
-                        Dialog saveWebpageImageDialog = saveWebpageImageDialogFragment.getDialog();
+                    if (saveWebpageDialogFragment != null) {
+                        // Get a handle for the save webpage dialog.
+                        Dialog saveWebpageDialog = saveWebpageDialogFragment.getDialog();
+
+                        // Remove the incorrect lint warning below that the dialog might be null.
+                        assert saveWebpageDialog != null;
 
                         // Get a handle for the file name edit text.
-                        EditText fileNameEditText = saveWebpageImageDialog.findViewById(R.id.file_name_edittext);
+                        EditText fileNameEditText = saveWebpageDialog.findViewById(R.id.file_name_edittext);
 
                         // Instantiate the file name helper.
                         FileNameHelper fileNameHelper = new FileNameHelper();
 
-                        // Convert the file name URI to a file name path.
-                        String fileNamePath = fileNameHelper.convertUriToFileNamePath(data.getData());
+                        // Get the file path if it isn't null.
+                        if (returnedIntent.getData() != null) {
+                            // Convert the file name URI to a file name path.
+                            String fileNamePath = fileNameHelper.convertUriToFileNamePath(returnedIntent.getData());
 
-                        // Set the file name path as the text of the file name edit text.
-                        fileNameEditText.setText(fileNamePath);
+                            // Set the file name path as the text of the file name edit text.
+                            fileNameEditText.setText(fileNamePath);
+
+                            // Move the cursor to the end of the file name edit text.
+                            fileNameEditText.setSelection(fileNamePath.length());
+                        }
+                    }
+                }
+                break;
+
+            case BROWSE_OPEN_REQUEST_CODE:
+                // Don't do anything if the user pressed back from the file picker.
+                if (resultCode == Activity.RESULT_OK) {
+                    // Get a handle for the open dialog fragment.
+                    DialogFragment openDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.open));
+
+                    // Only update the file name if the dialog still exists.
+                    if (openDialogFragment != null) {
+                        // Get a handle for the open dialog.
+                        Dialog openDialog = openDialogFragment.getDialog();
+
+                        // Remove the incorrect lint warning below that the dialog might be null.
+                        assert openDialog != null;
+
+                        // Get a handle for the file name edit text.
+                        EditText fileNameEditText = openDialog.findViewById(R.id.file_name_edittext);
+
+                        // Instantiate the file name helper.
+                        FileNameHelper fileNameHelper = new FileNameHelper();
+
+                        // Get the file path if it isn't null.
+                        if (returnedIntent.getData() != null) {
+                            // Convert the file name URI to a file name path.
+                            String fileNamePath = fileNameHelper.convertUriToFileNamePath(returnedIntent.getData());
+
+                            // Set the file name path as the text of the file name edit text.
+                            fileNameEditText.setText(fileNamePath);
+
+                            // Move the cursor to the end of the file name edit text.
+                            fileNameEditText.setSelection(fileNamePath.length());
+                        }
                     }
                 }
                 break;
@@ -2898,18 +3063,18 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         urlEditText.clearFocus();
 
         // Make it so.
-        loadUrl(url);
+        loadUrl(currentWebView, url);
     }
 
-    private void loadUrl(String url) {
+    private void loadUrl(NestedScrollWebView nestedScrollWebView, String url) {
         // Sanitize the URL.
         url = sanitizeUrl(url);
 
         // Apply the domain settings.
-        applyDomainSettings(currentWebView, url, true, false);
+        applyDomainSettings(nestedScrollWebView, url, true, false);
 
         // Load the URL.
-        currentWebView.loadUrl(url, customHeaders);
+        nestedScrollWebView.loadUrl(url, customHeaders);
     }
 
     public void findPreviousOnPage(View view) {
@@ -2953,19 +3118,108 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     }
 
     @Override
-    public void onSaveWebpageImage(DialogFragment dialogFragment) {
+    public void onApplyNewFontSize(DialogFragment dialogFragment) {
+        // Get the dialog.
+        Dialog dialog = dialogFragment.getDialog();
+
+        // Remove the incorrect lint warning below tha the dialog might be null.
+        assert dialog != null;
+
+        // Get a handle for the font size edit text.
+        EditText fontSizeEditText = dialog.findViewById(R.id.font_size_edittext);
+
+        // Initialize the new font size variable with the current font size.
+        int newFontSize = currentWebView.getSettings().getTextZoom();
+
+        // Get the font size from the edit text.
+        try {
+            newFontSize = Integer.valueOf(fontSizeEditText.getText().toString());
+        } catch (Exception exception) {
+            // If the edit text does not contain a valid font size do nothing.
+        }
+
+        // Apply the new font size.
+        currentWebView.getSettings().setTextZoom(newFontSize);
+    }
+
+    @Override
+    public void onOpen(DialogFragment dialogFragment) {
+        // Get the dialog.
+        Dialog dialog = dialogFragment.getDialog();
+
+        // Remove the incorrect lint warning below that the dialog might be null.
+        assert dialog != null;
+
+        // Get a handle for the file name edit text.
+        EditText fileNameEditText = dialog.findViewById(R.id.file_name_edittext);
+
+        // Get the file path string.
+        openFilePath = fileNameEditText.getText().toString();
+
+        // Check to see if the storage permission is needed.
+        if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {  // The storage permission has been granted.
+            // Open the file.
+            currentWebView.loadUrl("file://" + openFilePath);
+        } else {  // The storage permission has not been granted.
+            // Get the external private directory file.
+            File externalPrivateDirectoryFile = getExternalFilesDir(null);
+
+            // Remove the incorrect lint error below that the file might be null.
+            assert externalPrivateDirectoryFile != null;
+
+            // Get the external private directory string.
+            String externalPrivateDirectory = externalPrivateDirectoryFile.toString();
+
+            // Check to see if the file path is in the external private directory.
+            if (openFilePath.startsWith(externalPrivateDirectory)) {  // the file path is in the external private directory.
+                // Open the file.
+                currentWebView.loadUrl("file://" + openFilePath);
+            } else {  // The file path is in a public directory.
+                // Check if the user has previously denied the storage permission.
+                if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
+                    // Instantiate the storage permission alert dialog.
+                    DialogFragment storagePermissionDialogFragment = StoragePermissionDialog.displayDialog(StoragePermissionDialog.OPEN);
+
+                    // Show the storage permission alert dialog.  The permission will be requested the the dialog is closed.
+                    storagePermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.storage_permission));
+                } else {  // Show the permission request directly.
+                    // Request the write external storage permission.  The file will be opened when it finishes.
+                    ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_OPEN_REQUEST_CODE);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void onSaveWebpage(int saveType, DialogFragment dialogFragment) {
+        // Get the dialog.
+        Dialog dialog = dialogFragment.getDialog();
+
+        // Remove the incorrect lint warning below that the dialog might be null.
+        assert dialog != null;
+
         // Get a handle for the file name edit text.
-        EditText fileNameEditText = dialogFragment.getDialog().findViewById(R.id.file_name_edittext);
+        EditText fileNameEditText = dialog.findViewById(R.id.file_name_edittext);
 
         // Get the file path string.
-        saveWebsiteImageFilePath = fileNameEditText.getText().toString();
+        saveWebpageFilePath = fileNameEditText.getText().toString();
 
         // Check to see if the storage permission is needed.
         if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {  // The storage permission has been granted.
-            // Save the webpage image.
-            new SaveWebpageImage(this, currentWebView).execute(saveWebsiteImageFilePath);
+            //Save the webpage according to the save type.
+            switch (saveType) {
+                case StoragePermissionDialog.SAVE_ARCHIVE:
+                    // Save the webpage archive.
+                    currentWebView.saveWebArchive(saveWebpageFilePath);
+                    break;
+
+                case StoragePermissionDialog.SAVE_IMAGE:
+                    // Save the webpage image.
+                    new SaveWebpageImage(this, currentWebView).execute(saveWebpageFilePath);
+                    break;
+            }
         } else {  // The storage permission has not been granted.
-            // Get the external private directory `File`.
+            // Get the external private directory file.
             File externalPrivateDirectoryFile = getExternalFilesDir(null);
 
             // Remove the incorrect lint error below that the file might be null.
@@ -2975,29 +3229,144 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             String externalPrivateDirectory = externalPrivateDirectoryFile.toString();
 
             // Check to see if the file path is in the external private directory.
-            if (saveWebsiteImageFilePath.startsWith(externalPrivateDirectory)) {  // The file path is in the external private directory.
-                // Save the webpage image.
-                new SaveWebpageImage(this, currentWebView).execute(saveWebsiteImageFilePath);
+            if (saveWebpageFilePath.startsWith(externalPrivateDirectory)) {  // The file path is in the external private directory.
+                // Save the webpage according to the save type.
+                switch (saveType) {
+                    case StoragePermissionDialog.SAVE_ARCHIVE:
+                        // Save the webpage archive.
+                        currentWebView.saveWebArchive(saveWebpageFilePath);
+                        break;
+
+                    case StoragePermissionDialog.SAVE_IMAGE:
+                        // Save the webpage image.
+                        new SaveWebpageImage(this, currentWebView).execute(saveWebpageFilePath);
+                        break;
+                }
             } else {  // The file path is in a public directory.
                 // Check if the user has previously denied the storage permission.
                 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
                     // Instantiate the storage permission alert dialog.
-                    DialogFragment storagePermissionDialogFragment = new StoragePermissionDialog();
+                    DialogFragment storagePermissionDialogFragment = StoragePermissionDialog.displayDialog(saveType);
 
                     // Show the storage permission alert dialog.  The permission will be requested when the dialog is closed.
                     storagePermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.storage_permission));
                 } else {  // Show the permission request directly.
-                    // Request the write external storage permission.  The webpage image will be saved when it finishes.
-                    ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, SAVE_WEBPAGE_IMAGE_REQUEST_CODE);
+                    switch (saveType) {
+                        case StoragePermissionDialog.SAVE_ARCHIVE:
+                            // Request the write external storage permission.  The webpage archive will be saved when it finishes.
+                            ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_WEBPAGE_ARCHIVE_REQUEST_CODE);
+                            break;
+
+                        case StoragePermissionDialog.SAVE_IMAGE:
+                            // Request the write external storage permission.  The webpage image will be saved when it finishes.
+                            ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_WEBPAGE_IMAGE_REQUEST_CODE);
+                            break;
+                    }
                 }
             }
         }
     }
 
     @Override
-    public void onCloseStoragePermissionDialog() {
-        // Request the write external storage permission.  The webpage image will be saved when it finishes.
-        ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, SAVE_WEBPAGE_IMAGE_REQUEST_CODE);
+    public void onCloseStoragePermissionDialog(int requestType) {
+        switch (requestType) {
+            case StoragePermissionDialog.OPEN:
+                // Request the write external storage permission.  The file will be opened when it finishes.
+                ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_OPEN_REQUEST_CODE);
+                break;
+
+            case StoragePermissionDialog.SAVE_ARCHIVE:
+                // Request the write external storage permission.  The webpage archive will be saved when it finishes.
+                ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_WEBPAGE_ARCHIVE_REQUEST_CODE);
+                break;
+
+            case StoragePermissionDialog.SAVE_IMAGE:
+                // Request the write external storage permission.  The webpage image will be saved when it finishes.
+                ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_WEBPAGE_IMAGE_REQUEST_CODE);
+                break;
+        }
+    }
+
+    @Override
+    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+        // Get a handle for the fragment manager.
+        FragmentManager fragmentManager = getSupportFragmentManager();
+
+        switch (requestCode) {
+            case PERMISSION_DOWNLOAD_FILE_REQUEST_CODE:
+                // Show the download file alert dialog.  When the dialog closes, the correct command will be used based on the permission status.
+                DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(downloadUrl, downloadContentDisposition, downloadContentLength);
+
+                // On API 23, displaying the fragment must be delayed or the app will crash.
+                if (Build.VERSION.SDK_INT == 23) {
+                    new Handler().postDelayed(() -> downloadFileDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
+                } else {
+                    downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
+                }
+
+                // Reset the download variables.
+                downloadUrl = "";
+                downloadContentDisposition = "";
+                downloadContentLength = 0;
+                break;
+
+            case PERMISSION_DOWNLOAD_IMAGE_REQUEST_CODE:
+                // Show the download image alert dialog.  When the dialog closes, the correct command will be used based on the permission status.
+                DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(downloadImageUrl);
+
+                // On API 23, displaying the fragment must be delayed or the app will crash.
+                if (Build.VERSION.SDK_INT == 23) {
+                    new Handler().postDelayed(() -> downloadImageDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
+                } else {
+                    downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
+                }
+
+                // Reset the image URL variable.
+                downloadImageUrl = "";
+                break;
+
+            case PERMISSION_OPEN_REQUEST_CODE:
+                // Check to see if the storage permission was granted.  If the dialog was canceled the grant results will be empty.
+                if ((grantResults.length > 0) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) {  // The storage permission was granted.
+                    // Load the file.
+                    currentWebView.loadUrl("file://" + openFilePath);
+                } else {  // The storage permission was not granted.
+                    // Display an error snackbar.
+                    Snackbar.make(currentWebView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show();
+                }
+
+                // Reset the open file path.
+                openFilePath = "";
+                break;
+
+            case PERMISSION_SAVE_WEBPAGE_ARCHIVE_REQUEST_CODE:
+                // Check to see if the storage permission was granted.  If the dialog was canceled the grant results will be empty.
+                if ((grantResults.length > 0) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) {  // The storage permission was granted.
+                    // Save the webpage archive.
+                    currentWebView.saveWebArchive(saveWebpageFilePath);
+                } else {  // The storage permission was not granted.
+                    // Display an error snackbar.
+                    Snackbar.make(currentWebView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show();
+                }
+
+                // Reset the save webpage file path.
+                saveWebpageFilePath = "";
+                break;
+
+            case PERMISSION_SAVE_WEBPAGE_IMAGE_REQUEST_CODE:
+                // Check to see if the storage permission was granted.  If the dialog was canceled the grant results will be empty.
+                if ((grantResults.length > 0) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) {  // The storage permission was granted.
+                    // Save the webpage image.
+                    new SaveWebpageImage(this, currentWebView).execute(saveWebpageFilePath);
+                } else {  // The storage permission was not granted.
+                    // Display an error snackbar.
+                    Snackbar.make(currentWebView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show();
+                }
+
+                // Reset the save webpage file path.
+                saveWebpageFilePath = "";
+                break;
+        }
     }
 
     private void applyAppSettings() {
@@ -3015,11 +3384,21 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         sanitizeGoogleAnalytics = sharedPreferences.getBoolean("google_analytics", true);
         sanitizeFacebookClickIds = sharedPreferences.getBoolean("facebook_click_ids", true);
         sanitizeTwitterAmpRedirects = sharedPreferences.getBoolean("twitter_amp_redirects", true);
-        proxyThroughOrbot = sharedPreferences.getBoolean("proxy_through_orbot", false);
+        proxyMode = sharedPreferences.getString("proxy", getString(R.string.proxy_default_value));
         fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean("full_screen_browsing_mode", false);
         hideAppBar = sharedPreferences.getBoolean("hide_app_bar", true);
         scrollAppBar = sharedPreferences.getBoolean("scroll_app_bar", true);
 
+        // Get the search string.
+        String searchString = sharedPreferences.getString("search", getString(R.string.search_default_value));
+
+        // Set the search string.
+        if (searchString.equals("Custom URL")) {  // A custom search string is used.
+            searchURL = sharedPreferences.getString("search_custom_url", getString(R.string.search_custom_url_default_value));
+        } else {  // A custom search string is not used.
+            searchURL = searchString;
+        }
+
         // Get handles for the views that need to be modified.
         FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
         AppBarLayout appBarLayout = findViewById(R.id.appbar_layout);
@@ -3032,8 +3411,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Remove the incorrect lint warning below that the action bar might be null.
         assert actionBar != null;
 
-        // Apply the proxy through Orbot settings.
-        applyProxyThroughOrbot(false);
+        // Apply the proxy.
+        applyProxy(false);
 
         // Set Do Not Track status.
         if (doNotTrackEnabled) {
@@ -3199,61 +3578,60 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             }
         });
 
-        // Initialize the Orbot status and the waiting for Orbot trackers.
-        orbotStatus = "unknown";
-        waitingForOrbot = false;
-
-        // Create an Orbot status `BroadcastReceiver`.
+        // Create an Orbot status broadcast receiver.
         orbotStatusBroadcastReceiver = new BroadcastReceiver() {
             @Override
             public void onReceive(Context context, Intent intent) {
                 // Store the content of the status message in `orbotStatus`.
                 orbotStatus = intent.getStringExtra("org.torproject.android.intent.extra.STATUS");
 
-                // If Privacy Browser is waiting on Orbot, load the website now that Orbot is connected.
-                if (orbotStatus.equals("ON") && waitingForOrbot) {
-                    // Reset the waiting for Orbot status.
-                    waitingForOrbot = false;
+                // If Privacy Browser is waiting on the proxy, load the website now that Orbot is connected.
+                if ((orbotStatus != null) && orbotStatus.equals("ON") && waitingForProxy) {
+                    // Reset the waiting for proxy status.
+                    waitingForProxy = false;
 
-                    // Get the intent that started the app.
-                    Intent launchingIntent = getIntent();
+                    // Get a handle for the waiting for proxy dialog.
+                    DialogFragment waitingForProxyDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.waiting_for_proxy_dialog));
 
-                    // Get the information from the intent.
-                    String launchingIntentAction = launchingIntent.getAction();
-                    Uri launchingIntentUriData = launchingIntent.getData();
+                    // Dismiss the waiting for proxy dialog if it is displayed.
+                    if (waitingForProxyDialogFragment != null) {
+                        waitingForProxyDialogFragment.dismiss();
+                    }
 
-                    // If the intent action is a web search, perform the search.
-                    if ((launchingIntentAction != null) && launchingIntentAction.equals(Intent.ACTION_WEB_SEARCH)) {
-                        // Create an encoded URL string.
-                        String encodedUrlString;
+                    // Reload existing URLs and load any URLs that are waiting for the proxy.
+                    for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
+                        // Get the WebView tab fragment.
+                        WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
 
-                        // Sanitize the search input and convert it to a search.
-                        try {
-                            encodedUrlString = URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8");
-                        } catch (UnsupportedEncodingException exception) {
-                            encodedUrlString = "";
-                        }
+                        // Get the fragment view.
+                        View fragmentView = webViewTabFragment.getView();
 
-                        // Load the completed search URL.
-                        loadUrl(searchURL + encodedUrlString);
-                    } else if (launchingIntentUriData != null){  // Check to see if the intent contains a new URL.
-                        // Load the URL from the intent.
-                        loadUrl(launchingIntentUriData.toString());
-                    } else {  // The is no URL in the intent.
-                        // Select the homepage based on the proxy through Orbot status.
-                        if (proxyThroughOrbot) {
-                            // Load the Tor homepage.
-                            loadUrl(sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value)));
-                        } else {
-                            // Load the normal homepage.
-                            loadUrl(sharedPreferences.getString("homepage", getString(R.string.homepage_default_value)));
+                        // Only process the WebViews if they exist.
+                        if (fragmentView != null) {
+                            // Get the nested scroll WebView from the tab fragment.
+                            NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
+
+                            // Get the waiting for proxy URL string.
+                            String waitingForProxyUrlString = nestedScrollWebView.getWaitingForProxyUrlString();
+
+                            // Load the pending URL if it exists.
+                            if (!waitingForProxyUrlString.isEmpty()) {  // A URL is waiting to be loaded.
+                                // Load the URL.
+                                loadUrl(nestedScrollWebView, waitingForProxyUrlString);
+
+                                // Reset the waiting for proxy URL string.
+                                nestedScrollWebView.resetWaitingForProxyUrlString();
+                            } else {  // No URL is waiting to be loaded.
+                                // Reload the existing URL.
+                                nestedScrollWebView.reload();
+                            }
                         }
                     }
                 }
             }
         };
 
-        // Register `orbotStatusBroadcastReceiver` on `this` context.
+        // Register the Orbot status broadcast receiver on `this` context.
         this.registerReceiver(orbotStatusBroadcastReceiver, new IntentFilter("org.torproject.android.intent.action.STATUS"));
 
         // Get handles for views that need to be modified.
@@ -3271,12 +3649,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Listen for touches on the navigation menu.
         navigationView.setNavigationItemSelectedListener(this);
 
-        // Get handles for the navigation menu and the back and forward menu items.  The menu is zero-based.
+        // Get handles for the navigation menu and the back and forward menu items.  The menu is based.
         Menu navigationMenu = navigationView.getMenu();
         MenuItem navigationBackMenuItem = navigationMenu.getItem(2);
         MenuItem navigationForwardMenuItem = navigationMenu.getItem(3);
         MenuItem navigationHistoryMenuItem = navigationMenu.getItem(4);
-        MenuItem navigationRequestsMenuItem = navigationMenu.getItem(5);
+        MenuItem navigationRequestsMenuItem = navigationMenu.getItem(6);
 
         // Update the web view pager every time a tab is modified.
         webViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@@ -3468,10 +3846,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
         bookmarksListView.setOnItemClickListener((parent, view, position, id) -> {
             // Convert the id from long to int to match the format of the bookmarks database.
-            int databaseID = (int) id;
+            int databaseId = (int) id;
+
+            // Get the bookmark cursor for this ID.
+            Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseId);
 
-            // Get the bookmark cursor for this ID and move it to the first row.
-            Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseID);
+            // Move the bookmark cursor to the first row.
             bookmarkCursor.moveToFirst();
 
             // Act upon the bookmark according to the type.
@@ -3483,7 +3863,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 loadBookmarksFolder();
             } else {  // The selected bookmark is not a folder.
                 // Load the bookmark URL.
-                loadUrl(bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_URL)));
+                loadUrl(currentWebView, bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_URL)));
 
                 // Close the bookmarks drawer.
                 drawerLayout.closeDrawer(GravityCompat.END);
@@ -3504,13 +3884,20 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Save the current folder name, which is used in `onSaveEditBookmarkFolder()`.
                 oldFolderNameString = bookmarksCursor.getString(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
 
-                // Show the edit bookmark folder `AlertDialog` and name the instance `@string/edit_folder`.
+                // Instantiate the edit folder bookmark dialog.
                 DialogFragment editBookmarkFolderDialog = EditBookmarkFolderDialog.folderDatabaseId(databaseId, currentWebView.getFavoriteOrDefaultIcon());
+
+                // Show the edit folder bookmark dialog.
                 editBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.edit_folder));
             } else {
-                // Show the edit bookmark `AlertDialog` and name the instance `@string/edit_bookmark`.
-                DialogFragment editBookmarkDialog = EditBookmarkDialog.bookmarkDatabaseId(databaseId, currentWebView.getFavoriteOrDefaultIcon());
-                editBookmarkDialog.show(getSupportFragmentManager(), getString(R.string.edit_bookmark));
+                // Get the bookmark cursor for this ID.
+                Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseId);
+
+                // Move the bookmark cursor to the first row.
+                bookmarkCursor.moveToFirst();
+
+                // Load the bookmark in a new tab but do not switch to the tab or close the drawer.
+                addNewTab(bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_URL)), false);
             }
 
             // Consume the event.
@@ -3529,7 +3916,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity);
         drawerHeaderPaddingBottom = (int) (8 * screenDensity);
 
-        // The drawer listener is used to update the navigation menu.`
+        // The drawer listener is used to update the navigation menu.
         drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() {
             @Override
             public void onDrawerSlide(@NonNull View drawerView, float slideOffset) {
@@ -3594,6 +3981,30 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         bareWebView.destroy();
     }
 
+    @Override
+    public void navigateHistory(String url, int steps) {
+        // Apply the domain settings.
+        applyDomainSettings(currentWebView, url, false, false);
+
+        // Load the history entry.
+        currentWebView.goBackOrForward(steps);
+    }
+
+    @Override
+    public void pinnedErrorGoBack() {
+        // Get the current web back forward list.
+        WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList();
+
+        // Get the previous entry URL.
+        String previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.getCurrentIndex() - 1).getUrl();
+
+        // Apply the domain settings.
+        applyDomainSettings(currentWebView, previousUrl, false, false);
+
+        // Go back.
+        currentWebView.goBack();
+    }
+
     // `reloadWebsite` is used if returning from the Domains activity.  Otherwise JavaScript might not function correctly if it is newly enabled.
     @SuppressLint("SetJavaScriptEnabled")
     private boolean applyDomainSettings(NestedScrollWebView nestedScrollWebView, String url, boolean resetTab, boolean reloadWebsite) {
@@ -3851,10 +4262,16 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 }
 
                 // Apply the font size.
-                if (fontSize == 0) {  // Apply the default font size.
-                    nestedScrollWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
-                } else {  // Apply the specified font size.
-                    nestedScrollWebView.getSettings().setTextZoom(fontSize);
+                try {  // Try the specified font size to see if it is valid.
+                    if (fontSize == 0) {  // Apply the default font size.
+                            // Try to set the font size from the value in the app settings.
+                            nestedScrollWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
+                    } else {  // Apply the font size from domain settings.
+                        nestedScrollWebView.getSettings().setTextZoom(fontSize);
+                    }
+                } catch (Exception exception) {  // The specified font size is invalid
+                    // Set the font size to be 100%
+                    nestedScrollWebView.getSettings().setTextZoom(100);
                 }
 
                 // Set the user agent.
@@ -3990,9 +4407,17 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     nestedScrollWebView.getSettings().setJavaScriptEnabled(defaultJavaScriptEnabled);
                 }
 
-                // Apply the default settings.
+                // Apply the default first-party cookie setting.
                 cookieManager.setAcceptCookie(nestedScrollWebView.getAcceptFirstPartyCookies());
-                nestedScrollWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
+
+                // Apply the default font size setting.
+                try {
+                    // Try to set the font size from the value in the app settings.
+                    nestedScrollWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
+                } catch (Exception exception) {
+                    // If the app settings value is invalid, set the font size to 100%.
+                    nestedScrollWebView.getSettings().setTextZoom(100);
+                }
 
                 // Apply the form data setting if the API < 26.
                 if (Build.VERSION.SDK_INT < 26) {
@@ -4064,92 +4489,129 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         return !nestedScrollWebView.getSettings().getUserAgentString().equals(initialUserAgent);
     }
 
-    private void applyProxyThroughOrbot(boolean reloadWebsite) {
+    private void applyProxy(boolean reloadWebViews) {
         // Get a handle for the shared preferences.
         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
 
-        // Get the search and theme preferences.
-        String torSearchString = sharedPreferences.getString("tor_search", getString(R.string.tor_search_default_value));
-        String torSearchCustomUrlString = sharedPreferences.getString("tor_search_custom_url", getString(R.string.tor_search_custom_url_default_value));
-        String searchString = sharedPreferences.getString("search", getString(R.string.search_default_value));
-        String searchCustomUrlString = sharedPreferences.getString("search_custom_url", getString(R.string.search_custom_url_default_value));
+        // Get the theme preferences.
         boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
 
         // Get a handle for the app bar layout.
         AppBarLayout appBarLayout = findViewById(R.id.appbar_layout);
 
-        // Set the homepage, search, and proxy options.
-        if (proxyThroughOrbot) {  // Set the Tor options.
-            // Set the search URL.
-            if (torSearchString.equals("Custom URL")) {  // Get the custom URL string.
-                searchURL = torSearchCustomUrlString;
-            } else {  // Use the string from the pre-built list.
-                searchURL = torSearchString;
-            }
+        // Set the proxy according to the mode.  `this` refers to the current activity where an alert dialog might be displayed.
+        ProxyHelper.setProxy(getApplicationContext(), appBarLayout, proxyMode);
 
-            // Set the proxy.  `this` refers to the current activity where an `AlertDialog` might be displayed.
-            OrbotProxyHelper.setProxy(getApplicationContext(), this, "localhost", "8118");
+        // Reset the waiting for proxy tracker.
+        waitingForProxy = false;
 
-            // Set the app bar background to indicate proxying through Orbot is enabled.
-            if (darkTheme) {
-                appBarLayout.setBackgroundResource(R.color.dark_blue_30);
-            } else {
-                appBarLayout.setBackgroundResource(R.color.blue_50);
-            }
+        // Update the user interface and reload the WebViews if requested.
+        switch (proxyMode) {
+            case ProxyHelper.NONE:
+                // Set the default app bar layout background.
+                if (darkTheme) {
+                    appBarLayout.setBackgroundResource(R.color.gray_900);
+                } else {
+                    appBarLayout.setBackgroundResource(R.color.gray_100);
+                }
+                break;
 
-            // Check to see if Orbot is ready.
-            if (!orbotStatus.equals("ON")) {  // Orbot is not ready.
-                // Set `waitingForOrbot`.
-                waitingForOrbot = true;
+            case ProxyHelper.TOR:
+                // Set the app bar background to indicate proxying through Orbot is enabled.
+                if (darkTheme) {
+                    appBarLayout.setBackgroundResource(R.color.dark_blue_30);
+                } else {
+                    appBarLayout.setBackgroundResource(R.color.blue_50);
+                }
 
-                // Disable the wide view port so that the waiting for Orbot text is displayed correctly.
-                currentWebView.getSettings().setUseWideViewPort(false);
+                // Check to see if Orbot is installed.
+                try {
+                    // Get the package manager.
+                    PackageManager packageManager = getPackageManager();
 
-                // Load a waiting page.  `null` specifies no encoding, which defaults to ASCII.
-                currentWebView.loadData("<html><body><br/><center><h1>" + getString(R.string.waiting_for_orbot) + "</h1></center></body></html>", "text/html", null);
-            } else if (reloadWebsite) {  // Orbot is ready and the website should be reloaded.
-                // Reload the website.
-                currentWebView.reload();
-            }
-        } else {  // Set the non-Tor options.
-            // Set the search URL.
-            if (searchString.equals("Custom URL")) {  // Get the custom URL string.
-                searchURL = searchCustomUrlString;
-            } else {  // Use the string from the pre-built list.
-                searchURL = searchString;
-            }
+                    // Check to see if Orbot is in the list.  This will throw an error and drop to the catch section if it isn't installed.
+                    packageManager.getPackageInfo("org.torproject.android", 0);
 
-            // Reset the proxy to default.  The host is `""` and the port is `"0"`.
-            OrbotProxyHelper.setProxy(getApplicationContext(), this, "", "0");
+                    // Check to see if the proxy is ready.
+                    if (!orbotStatus.equals("ON")) {  // Orbot is not ready.
+                        // Set the waiting for proxy status.
+                        waitingForProxy = true;
 
-            // Set the default app bar layout background.
-            if (darkTheme) {
-                appBarLayout.setBackgroundResource(R.color.gray_900);
-            } else {
-                appBarLayout.setBackgroundResource(R.color.gray_100);
-            }
+                        // Show the waiting for proxy dialog if it isn't already displayed.
+                        if (getSupportFragmentManager().findFragmentByTag(getString(R.string.waiting_for_proxy_dialog)) == null) {
+                            // Get a handle for the waiting for proxy alert dialog.
+                            DialogFragment waitingForProxyDialogFragment = new WaitingForProxyDialog();
 
-            // Reset `waitingForOrbot.
-            waitingForOrbot = false;
+                            // Display the waiting for proxy alert dialog.
+                            waitingForProxyDialogFragment.show(getSupportFragmentManager(), getString(R.string.waiting_for_proxy_dialog));
+                        }
+                    }
+                } catch (PackageManager.NameNotFoundException exception) {  // Orbot is not installed.
+                    // Show the Orbot not installed dialog if it is not already displayed.
+                    if (getSupportFragmentManager().findFragmentByTag(getString(R.string.proxy_not_installed_dialog)) == null) {
+                        // Get a handle for the Orbot not installed alert dialog.
+                        DialogFragment orbotNotInstalledDialogFragment = ProxyNotInstalledDialog.displayDialog(proxyMode);
+
+                        // Display the Orbot not installed alert dialog.
+                        orbotNotInstalledDialogFragment.show(getSupportFragmentManager(), getString(R.string.proxy_not_installed_dialog));
+                    }
+                }
+                break;
 
-            // Reload the WebViews if requested.
-            if (reloadWebsite) {
-                // Reload the WebViews.
-                for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
-                    // Get the WebView tab fragment.
-                    WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
+            case ProxyHelper.I2P:
+                // Set the app bar background to indicate proxying through Orbot is enabled.
+                if (darkTheme) {
+                    appBarLayout.setBackgroundResource(R.color.dark_blue_30);
+                } else {
+                    appBarLayout.setBackgroundResource(R.color.blue_50);
+                }
 
-                    // Get the fragment view.
-                    View fragmentView = webViewTabFragment.getView();
+                // Check to see if I2P is installed.
+                try {
+                    // Get the package manager.
+                    PackageManager packageManager = getPackageManager();
+
+                    // Check to see if I2P is in the list.  This will throw an error and drop to the catch section if it isn't installed.
+                    packageManager.getPackageInfo("org.torproject.android", 0);
+                } catch (PackageManager.NameNotFoundException exception) {  // I2P is not installed.
+                    // Sow the I2P not installed dialog if it is not already displayed.
+                    if (getSupportFragmentManager().findFragmentByTag(getString(R.string.proxy_not_installed_dialog)) == null) {
+                        // Get a handle for the waiting for proxy alert dialog.
+                        DialogFragment i2pNotInstalledDialogFragment = ProxyNotInstalledDialog.displayDialog(proxyMode);
+
+                        // Display the I2P not installed alert dialog.
+                        i2pNotInstalledDialogFragment.show(getSupportFragmentManager(), getString(R.string.proxy_not_installed_dialog));
+                    }
+                }
+                break;
 
-                    // Only reload the WebViews if they exist.
-                    if (fragmentView != null) {
-                        // Get the nested scroll WebView from the tab fragment.
-                        NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
+            case ProxyHelper.CUSTOM:
+                // Set the app bar background to indicate proxying through Orbot is enabled.
+                if (darkTheme) {
+                    appBarLayout.setBackgroundResource(R.color.dark_blue_30);
+                } else {
+                    appBarLayout.setBackgroundResource(R.color.blue_50);
+                }
+                break;
+        }
 
-                        // Reload the WebView.
-                        nestedScrollWebView.reload();
-                    }
+        // Reload the WebViews if requested and not waiting for the proxy.
+        if (reloadWebViews && !waitingForProxy) {
+            // Reload the WebViews.
+            for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
+                // Get the WebView tab fragment.
+                WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
+
+                // Get the fragment view.
+                View fragmentView = webViewTabFragment.getView();
+
+                // Only reload the WebViews if they exist.
+                if (fragmentView != null) {
+                    // Get the nested scroll WebView from the tab fragment.
+                    NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
+
+                    // Reload the WebView.
+                    nestedScrollWebView.reload();
                 }
             }
         }
@@ -4245,12 +4707,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             String urlString = urlEditText.getText().toString();
 
             // Highlight the URL according to the protocol.
-            if (urlString.startsWith("file://")) {  // This is a file URL.
-                // De-emphasize only the protocol.
-                urlEditText.getText().setSpan(initialGrayColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-            } else if (urlString.startsWith("content://")) {
-                // De-emphasize only the protocol.
-                urlEditText.getText().setSpan(initialGrayColorSpan, 0, 10, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+            if (urlString.startsWith("file://") || urlString.startsWith("content://")) {  // This is a file or content URL.
+                // De-emphasize everything before the file name.
+                urlEditText.getText().setSpan(initialGrayColorSpan, 0, urlString.lastIndexOf("/") + 1,Spanned.SPAN_INCLUSIVE_INCLUSIVE);
             } else {  // This is a web URL.
                 // Get the index of the `/` immediately after the domain name.
                 int endOfDomainName = urlString.indexOf("/", (urlString.indexOf("//") + 2));
@@ -4988,7 +5447,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                         downloadLocationPermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.download_location));
                     } else {  // Show the permission request directly.
                         // Request the permission.  The download dialog will be launched by `onRequestPermissionResult()`.
-                        ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
+                        ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_DOWNLOAD_FILE_REQUEST_CODE);
                     }
                 } else {  // The storage permission has already been granted.
                     // Get a handle for the download file alert dialog.
@@ -5202,6 +5661,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
                 // Show the full screen video frame layout.
                 fullScreenVideoFrameLayout.setVisibility(View.VISIBLE);
+
+                // Disable the screen timeout while the video is playing.  YouTube does this automatically, but not all other videos do.
+                fullScreenVideoFrameLayout.setKeepScreenOn(true);
             }
 
             // Exit full screen video.
@@ -5210,6 +5672,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Get a handle for the full screen video frame layout.
                 FrameLayout fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout);
 
+                // Re-enable the screen timeout.
+                fullScreenVideoFrameLayout.setKeepScreenOn(false);
+
                 // Unset the full screen video flag.
                 displayingFullScreenVideo = false;
 
@@ -5279,7 +5744,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     Intent fileChooserIntent = fileChooserParams.createIntent();
 
                     // Open the file chooser.
-                    startActivityForResult(fileChooserIntent, FILE_UPLOAD_REQUEST_CODE);
+                    startActivityForResult(fileChooserIntent, BROWSE_FILE_UPLOAD_REQUEST_CODE);
                 }
                 return true;
             }
@@ -5395,7 +5860,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 Menu navigationMenu = navigationView.getMenu();
 
                 // Get a handle for the navigation requests menu item.  The menu is 0 based.
-                MenuItem navigationRequestsMenuItem = navigationMenu.getItem(5);
+                MenuItem navigationRequestsMenuItem = navigationMenu.getItem(6);
 
                 // Create an empty web resource response to be used if the resource request is blocked.
                 WebResourceResponse emptyWebResourceResponse = new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes()));
@@ -5775,64 +6240,47 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Hide the keyboard.
                 inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0);
 
-                // Check to see if Privacy Browser is waiting on Orbot.
-                if (!waitingForOrbot) {  // Process the URL.
-                    // Get the current page position.
-                    int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
-
-                    // Update the URL text bar if the page is currently selected.
-                    if (tabLayout.getSelectedTabPosition() == currentPagePosition) {
-                        // Clear the focus from the URL edit text.
-                        urlEditText.clearFocus();
-
-                        // Display the formatted URL text.
-                        urlEditText.setText(url);
-
-                        // Apply text highlighting to `urlTextBox`.
-                        highlightUrlText();
-                    }
+                // Get the current page position.
+                int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
 
-                    // Reset the list of host IP addresses.
-                    nestedScrollWebView.clearCurrentIpAddresses();
+                // Update the URL text bar if the page is currently selected.
+                if (tabLayout.getSelectedTabPosition() == currentPagePosition) {
+                    // Clear the focus from the URL edit text.
+                    urlEditText.clearFocus();
 
-                    // Get a URI for the current URL.
-                    Uri currentUri = Uri.parse(url);
+                    // Display the formatted URL text.
+                    urlEditText.setText(url);
 
-                    // Get the IP addresses for the host.
-                    new GetHostIpAddresses(activity, getSupportFragmentManager(), nestedScrollWebView).execute(currentUri.getHost());
+                    // Apply text highlighting to `urlTextBox`.
+                    highlightUrlText();
+                }
 
-                    // Apply any custom domain settings if the URL was loaded by navigating history.
-                    if (nestedScrollWebView.getNavigatingHistory()) {
-                        // Reset navigating history.
-                        nestedScrollWebView.setNavigatingHistory(false);
+                // Reset the list of host IP addresses.
+                nestedScrollWebView.clearCurrentIpAddresses();
 
-                        // Apply the domain settings.
-                        boolean userAgentChanged = applyDomainSettings(nestedScrollWebView, url, true, false);
+                // Get a URI for the current URL.
+                Uri currentUri = Uri.parse(url);
 
-                        // Manually load the URL if the user agent has changed, which will have caused the previous URL to be reloaded.
-                        if (userAgentChanged) {
-                            loadUrl(url);
-                        }
-                    }
+                // Get the IP addresses for the host.
+                new GetHostIpAddresses(activity, getSupportFragmentManager(), nestedScrollWebView).execute(currentUri.getHost());
 
-                    // Replace Refresh with Stop if the options menu has been created.  (The first WebView typically begins loading before the menu items are instantiated.)
-                    if (optionsMenu != null) {
-                        // Get a handle for the refresh menu item.
-                        MenuItem refreshMenuItem = optionsMenu.findItem(R.id.refresh);
+                // Replace Refresh with Stop if the options menu has been created.  (The first WebView typically begins loading before the menu items are instantiated.)
+                if (optionsMenu != null) {
+                    // Get a handle for the refresh menu item.
+                    MenuItem refreshMenuItem = optionsMenu.findItem(R.id.refresh);
 
-                        // Set the title.
-                        refreshMenuItem.setTitle(R.string.stop);
+                    // Set the title.
+                    refreshMenuItem.setTitle(R.string.stop);
 
-                        // Get the app bar and theme preferences.
-                        boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false);
+                    // Get the app bar and theme preferences.
+                    boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false);
 
-                        // If the icon is displayed in the AppBar, set it according to the theme.
-                        if (displayAdditionalAppBarIcons) {
-                            if (darkTheme) {
-                                refreshMenuItem.setIcon(R.drawable.close_dark);
-                            } else {
-                                refreshMenuItem.setIcon(R.drawable.close_light);
-                            }
+                    // If the icon is displayed in the AppBar, set it according to the theme.
+                    if (displayAdditionalAppBarIcons) {
+                        if (darkTheme) {
+                            refreshMenuItem.setIcon(R.drawable.close_dark);
+                        } else {
+                            refreshMenuItem.setIcon(R.drawable.close_light);
                         }
                     }
                 }
@@ -5892,82 +6340,79 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     }
                 }
 
-                // Update the URL text box and apply domain settings if not waiting on Orbot.
-                if (!waitingForOrbot) {
-                    // Get the current page position.
-                    int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
+                // Get the current page position.
+                int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
 
-                    // Check the current website information against any pinned domain information if the current IP addresses have been loaded.
-                    if ((nestedScrollWebView.hasPinnedSslCertificate() || nestedScrollWebView.hasPinnedIpAddresses()) && nestedScrollWebView.hasCurrentIpAddresses() &&
-                            !nestedScrollWebView.ignorePinnedDomainInformation()) {
-                        CheckPinnedMismatchHelper.checkPinnedMismatch(getSupportFragmentManager(), nestedScrollWebView);
-                    }
+                // Check the current website information against any pinned domain information if the current IP addresses have been loaded.
+                if ((nestedScrollWebView.hasPinnedSslCertificate() || nestedScrollWebView.hasPinnedIpAddresses()) && nestedScrollWebView.hasCurrentIpAddresses() &&
+                        !nestedScrollWebView.ignorePinnedDomainInformation()) {
+                    CheckPinnedMismatchHelper.checkPinnedMismatch(getSupportFragmentManager(), nestedScrollWebView);
+                }
 
-                    // Get the current URL from the nested scroll WebView.  This is more accurate than using the URL passed into the method, which is sometimes not the final one.
-                    String currentUrl = nestedScrollWebView.getUrl();
+                // Get the current URL from the nested scroll WebView.  This is more accurate than using the URL passed into the method, which is sometimes not the final one.
+                String currentUrl = nestedScrollWebView.getUrl();
 
-                    // Get the current tab.
-                    TabLayout.Tab tab = tabLayout.getTabAt(currentPagePosition);
+                // Get the current tab.
+                TabLayout.Tab tab = tabLayout.getTabAt(currentPagePosition);
 
-                    // Update the URL text bar if the page is currently selected and the user is not currently typing in the URL edit text.
-                    // Crash records show that, in some crazy way, it is possible for the current URL to be blank at this point.
-                    // Probably some sort of race condition when Privacy Browser is being resumed.
-                    if ((tabLayout.getSelectedTabPosition() == currentPagePosition) && !urlEditText.hasFocus() && (currentUrl != null)) {
-                        // Check to see if the URL is `about:blank`.
-                        if (currentUrl.equals("about:blank")) {  // The WebView is blank.
-                            // Display the hint in the URL edit text.
-                            urlEditText.setText("");
+                // Update the URL text bar if the page is currently selected and the user is not currently typing in the URL edit text.
+                // Crash records show that, in some crazy way, it is possible for the current URL to be blank at this point.
+                // Probably some sort of race condition when Privacy Browser is being resumed.
+                if ((tabLayout.getSelectedTabPosition() == currentPagePosition) && !urlEditText.hasFocus() && (currentUrl != null)) {
+                    // Check to see if the URL is `about:blank`.
+                    if (currentUrl.equals("about:blank")) {  // The WebView is blank.
+                        // Display the hint in the URL edit text.
+                        urlEditText.setText("");
 
-                            // Request focus for the URL text box.
-                            urlEditText.requestFocus();
+                        // Request focus for the URL text box.
+                        urlEditText.requestFocus();
 
-                            // Display the keyboard.
-                            inputMethodManager.showSoftInput(urlEditText, 0);
+                        // Display the keyboard.
+                        inputMethodManager.showSoftInput(urlEditText, 0);
 
-                            // Apply the domain settings.  This clears any settings from the previous domain.
-                            applyDomainSettings(nestedScrollWebView, "", true, false);
+                        // Apply the domain settings.  This clears any settings from the previous domain.
+                        applyDomainSettings(nestedScrollWebView, "", true, false);
 
-                            // Only populate the title text view if the tab has been fully created.
-                            if (tab != null) {
-                                // Get the custom view from the tab.
-                                View tabView = tab.getCustomView();
+                        // Only populate the title text view if the tab has been fully created.
+                        if (tab != null) {
+                            // Get the custom view from the tab.
+                            View tabView = tab.getCustomView();
 
-                                // Remove the incorrect warning below that the current tab view might be null.
-                                assert tabView != null;
+                            // Remove the incorrect warning below that the current tab view might be null.
+                            assert tabView != null;
 
-                                // Get the title text view from the tab.
-                                TextView tabTitleTextView = tabView.findViewById(R.id.title_textview);
+                            // Get the title text view from the tab.
+                            TextView tabTitleTextView = tabView.findViewById(R.id.title_textview);
 
-                                // Set the title as the tab text.
-                                tabTitleTextView.setText(R.string.new_tab);
-                            }
-                        } else {  // The WebView has loaded a webpage.
-                            // Update the URL edit text if it is not currently being edited.
-                            if (!urlEditText.hasFocus()) {
-                                // Sanitize the current URL.  This removes unwanted URL elements that were added by redirects, so that they won't be included if the URL is shared.
-                                String sanitizedUrl = sanitizeUrl(currentUrl);
+                            // Set the title as the tab text.
+                            tabTitleTextView.setText(R.string.new_tab);
+                        }
+                    } else {  // The WebView has loaded a webpage.
+                        // Update the URL edit text if it is not currently being edited.
+                        if (!urlEditText.hasFocus()) {
+                            // Sanitize the current URL.  This removes unwanted URL elements that were added by redirects, so that they won't be included if the URL is shared.
+                            String sanitizedUrl = sanitizeUrl(currentUrl);
 
-                                // Display the final URL.  Getting the URL from the WebView instead of using the one provided by `onPageFinished()` makes websites like YouTube function correctly.
-                                urlEditText.setText(sanitizedUrl);
+                            // Display the final URL.  Getting the URL from the WebView instead of using the one provided by `onPageFinished()` makes websites like YouTube function correctly.
+                            urlEditText.setText(sanitizedUrl);
 
-                                // Apply text highlighting to the URL.
-                                highlightUrlText();
-                            }
+                            // Apply text highlighting to the URL.
+                            highlightUrlText();
+                        }
 
-                            // Only populate the title text view if the tab has been fully created.
-                            if (tab != null) {
-                                // Get the custom view from the tab.
-                                View tabView = tab.getCustomView();
+                        // Only populate the title text view if the tab has been fully created.
+                        if (tab != null) {
+                            // Get the custom view from the tab.
+                            View tabView = tab.getCustomView();
 
-                                // Remove the incorrect warning below that the current tab view might be null.
-                                assert tabView != null;
+                            // Remove the incorrect warning below that the current tab view might be null.
+                            assert tabView != null;
 
-                                // Get the title text view from the tab.
-                                TextView tabTitleTextView = tabView.findViewById(R.id.title_textview);
+                            // Get the title text view from the tab.
+                            TextView tabTitleTextView = tabView.findViewById(R.id.title_textview);
 
-                                // Set the title as the tab text.  Sometimes `onReceivedTitle()` is not called, especially when navigating history.
-                                tabTitleTextView.setText(nestedScrollWebView.getTitle());
-                            }
+                            // Set the title as the tab text.  Sometimes `onReceivedTitle()` is not called, especially when navigating history.
+                            tabTitleTextView.setText(nestedScrollWebView.getTitle());
                         }
                     }
                 }
@@ -6028,42 +6473,43 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             // Apply the app settings from the shared preferences.
             applyAppSettings();
 
-            // Load the website if not waiting for Orbot to connect.
-            if (!waitingForOrbot) {
-                // Get the intent that started the app.
-                Intent launchingIntent = getIntent();
+            // Initialize the URL to load string.
+            String urlToLoadString;
 
-                // Get the information from the intent.
-                String launchingIntentAction = launchingIntent.getAction();
-                Uri launchingIntentUriData = launchingIntent.getData();
+            // Get the intent that started the app.
+            Intent launchingIntent = getIntent();
 
-                // If the intent action is a web search, perform the search.
-                if ((launchingIntentAction != null) && launchingIntentAction.equals(Intent.ACTION_WEB_SEARCH)) {
-                    // Create an encoded URL string.
-                    String encodedUrlString;
+            // Get the information from the intent.
+            String launchingIntentAction = launchingIntent.getAction();
+            Uri launchingIntentUriData = launchingIntent.getData();
+
+            // Parse the launching intent URL.
+            if ((launchingIntentAction != null) && launchingIntentAction.equals(Intent.ACTION_WEB_SEARCH)) {  // The intent contains a search string.
+                // Create an encoded URL string.
+                String encodedUrlString;
+
+                // Sanitize the search input and convert it to a search.
+                try {
+                    encodedUrlString = URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8");
+                } catch (UnsupportedEncodingException exception) {
+                    encodedUrlString = "";
+                }
 
-                    // Sanitize the search input and convert it to a search.
-                    try {
-                        encodedUrlString = URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8");
-                    } catch (UnsupportedEncodingException exception) {
-                        encodedUrlString = "";
-                    }
+                // Store the web search as the URL to load.
+                urlToLoadString = searchURL + encodedUrlString;
+            } else if (launchingIntentUriData != null){  // The intent contains a URL.
+                // Store the URL.
+                urlToLoadString = launchingIntentUriData.toString();
+            } else {  // The is no URL in the intent.
+                // Store the homepage to be loaded.
+                urlToLoadString = sharedPreferences.getString("homepage", getString(R.string.homepage_default_value));
+            }
 
-                    // Load the completed search URL.
-                    loadUrl(searchURL + encodedUrlString);
-                } else if (launchingIntentUriData != null){  // Check to see if the intent contains a new URL.
-                    // Load the URL from the intent.
-                    loadUrl(launchingIntentUriData.toString());
-                } else {  // The is no URL in the intent.
-                    // Select the homepage based on the proxy through Orbot status.
-                    if (proxyThroughOrbot) {
-                        // Load the Tor homepage.
-                        loadUrl(sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value)));
-                    } else {
-                        // Load the normal homepage.
-                        loadUrl(sharedPreferences.getString("homepage", getString(R.string.homepage_default_value)));
-                    }
-                }
+            // Load the website if not waiting for the proxy.
+            if (waitingForProxy) {  // Store the URL to be loaded in the Nested Scroll WebView.
+                nestedScrollWebView.setWaitingForProxyUrlString(urlToLoadString);
+            } else {  // Load the URL.
+                loadUrl(nestedScrollWebView, urlToLoadString);
             }
         } else {  // This is not the first tab.
             // Apply the domain settings.