]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blobdiff - app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java
Remove third-party cookies. https://redmine.stoutner.com/issues/601
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / activities / MainWebViewActivity.java
index 6eeeb253b25a1492d0b689133b7220167886bd1a..b00e19a7e05c24d5c3e7bf0341c2a2cfdf6d19b7 100644 (file)
@@ -21,7 +21,6 @@
 
 package com.stoutner.privacybrowser.activities;
 
-import android.Manifest;
 import android.annotation.SuppressLint;
 import android.app.Activity;
 import android.app.Dialog;
@@ -31,7 +30,6 @@ import android.content.ActivityNotFoundException;
 import android.content.BroadcastReceiver;
 import android.content.ClipData;
 import android.content.ClipboardManager;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
@@ -50,11 +48,13 @@ import android.net.http.SslError;
 import android.os.AsyncTask;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.Environment;
 import android.os.Handler;
 import android.os.Message;
 import android.preference.PreferenceManager;
 import android.print.PrintDocumentAdapter;
 import android.print.PrintManager;
+import android.provider.DocumentsContract;
 import android.text.Editable;
 import android.text.Spanned;
 import android.text.TextWatcher;
@@ -84,6 +84,7 @@ import android.webkit.WebView;
 import android.webkit.WebViewClient;
 import android.webkit.WebViewDatabase;
 import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
 import android.widget.CursorAdapter;
 import android.widget.EditText;
 import android.widget.FrameLayout;
@@ -102,9 +103,6 @@ import androidx.appcompat.app.AppCompatActivity;
 import androidx.appcompat.app.AppCompatDelegate;
 import androidx.appcompat.widget.Toolbar;
 import androidx.coordinatorlayout.widget.CoordinatorLayout;
-import androidx.core.app.ActivityCompat;
-import androidx.core.content.ContextCompat;
-import androidx.core.content.FileProvider;
 import androidx.core.content.res.ResourcesCompat;
 import androidx.core.view.GravityCompat;
 import androidx.drawerlayout.widget.DrawerLayout;
@@ -140,7 +138,6 @@ 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;
@@ -149,14 +146,17 @@ import com.stoutner.privacybrowser.helpers.AdHelper;
 import com.stoutner.privacybrowser.helpers.BlocklistHelper;
 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper;
 import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper;
-import com.stoutner.privacybrowser.helpers.FileNameHelper;
 import com.stoutner.privacybrowser.helpers.ProxyHelper;
 import com.stoutner.privacybrowser.views.NestedScrollWebView;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
 import java.io.UnsupportedEncodingException;
 import java.net.MalformedURLException;
 import java.net.URL;
@@ -176,8 +176,8 @@ import java.util.concurrent.Executors;
 
 public class MainWebViewActivity extends AppCompatActivity implements CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener,
         EditBookmarkFolderDialog.EditBookmarkFolderListener, FontSizeDialog.UpdateFontSizeListener, NavigationView.OnNavigationItemSelectedListener, OpenDialog.OpenListener,
-        PinnedMismatchDialog.PinnedMismatchListener, PopulateBlocklists.PopulateBlocklistsListener, SaveWebpageDialog.SaveWebpageListener, StoragePermissionDialog.StoragePermissionDialogListener,
-        UrlHistoryDialog.NavigateHistoryListener, WebViewTabFragment.NewTabListener {
+        PinnedMismatchDialog.PinnedMismatchListener, PopulateBlocklists.PopulateBlocklistsListener, SaveWebpageDialog.SaveWebpageListener, UrlHistoryDialog.NavigateHistoryListener,
+        WebViewTabFragment.NewTabListener {
 
     // The executor service handles background tasks.  It is accessed from `ViewSourceActivity`.
     public static ExecutorService executorService = Executors.newFixedThreadPool(4);
@@ -203,10 +203,10 @@ 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.  The public static entries are accessed from `OpenDialog()` and `SaveWebpageDialog()`.
-    public final static int BROWSE_OPEN_REQUEST_CODE = 0;
-    public final static int BROWSE_SAVE_WEBPAGE_REQUEST_CODE = 1;
-    private final int BROWSE_FILE_UPLOAD_REQUEST_CODE = 2;
+    // Define the start activity for result request codes.  The public static entries are accessed from `OpenDialog()` and `SaveWebpageDialog()`.
+    private final int BROWSE_FILE_UPLOAD_REQUEST_CODE = 0;
+    public final static int BROWSE_OPEN_REQUEST_CODE = 1;
+    public final static int BROWSE_SAVE_WEBPAGE_REQUEST_CODE = 2;
 
     // The proxy mode is public static so it can be accessed from `ProxyHelper()`.
     // It is also used in `onRestart()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `applyAppSettings()`, and `applyProxy()`.
@@ -315,11 +315,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     private boolean sanitizeFacebookClickIds;
     private boolean sanitizeTwitterAmpRedirects;
 
-    // The file path strings are used in `onSaveWebpage()` and `onRequestPermissionResult()`
-    private String openFilePath;
-    private String saveWebpageUrl;
-    private String saveWebpageFilePath;
-
     // Declare the class views.
     private FrameLayout rootFrameLayout;
     private DrawerLayout drawerLayout;
@@ -346,8 +341,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     private MenuItem navigationRequestsMenuItem;
     private MenuItem optionsPrivacyMenuItem;
     private MenuItem optionsRefreshMenuItem;
-    private MenuItem optionsFirstPartyCookiesMenuItem;
-    private MenuItem optionsThirdPartyCookiesMenuItem;
+    private MenuItem optionsCookiesMenuItem;
     private MenuItem optionsDomStorageMenuItem;
     private MenuItem optionsSaveFormDataMenuItem;
     private MenuItem optionsClearDataMenuItem;
@@ -827,8 +821,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Get handles for the class menu items.
         optionsPrivacyMenuItem = menu.findItem(R.id.javascript);
         optionsRefreshMenuItem = menu.findItem(R.id.refresh);
-        optionsFirstPartyCookiesMenuItem = menu.findItem(R.id.first_party_cookies);
-        optionsThirdPartyCookiesMenuItem = menu.findItem(R.id.third_party_cookies);
+        optionsCookiesMenuItem = menu.findItem(R.id.cookies);
         optionsDomStorageMenuItem = menu.findItem(R.id.dom_storage);
         optionsSaveFormDataMenuItem = menu.findItem(R.id.save_form_data);  // Form data can be removed once the minimum API >= 26.
         optionsClearDataMenuItem = menu.findItem(R.id.clear_data);
@@ -876,9 +869,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Set the initial status of the privacy icons.  `false` does not call `invalidateOptionsMenu` as the last step.
         updatePrivacyIcons(false);
 
-        // Only display third-party cookies if API >= 21
-        optionsThirdPartyCookiesMenuItem.setVisible(Build.VERSION.SDK_INT >= 21);
-
         // Only display the form data menu items if the API < 26.
         optionsSaveFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
         optionsClearFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
@@ -902,11 +892,11 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         if (displayAdditionalAppBarIcons) {
             optionsRefreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
             bookmarksMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
-            optionsFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+            optionsCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
         } else { //Do not display the additional icons.
             optionsRefreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
             bookmarksMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
-            optionsFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
+            optionsCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
         }
 
         // Replace Refresh with Stop if a URL is already loading.
@@ -980,15 +970,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             optionsUltraPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.ULTRAPRIVACY) + " - " + getString(R.string.ultraprivacy));
             optionsBlockAllThirdPartyRequestsMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " + getString(R.string.block_all_third_party_requests));
 
-            // Only modify third-party cookies if the API >= 21.
-            if (Build.VERSION.SDK_INT >= 21) {
-                // Set the status of the third-party cookies checkbox.
-                optionsThirdPartyCookiesMenuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView));
-
-                // Enable third-party cookies if first-party cookies are enabled.
-                optionsThirdPartyCookiesMenuItem.setEnabled(cookieManager.acceptCookie());
-            }
-
             // Enable DOM Storage if JavaScript is enabled.
             optionsDomStorageMenuItem.setEnabled(currentWebView.getSettings().getJavaScriptEnabled());
 
@@ -998,8 +979,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             }
         }
 
-        // Set the checked status of the first party cookies menu item.
-        optionsFirstPartyCookiesMenuItem.setChecked(cookieManager.acceptCookie());
+        // Set the cookies menu item checked status.
+        optionsCookiesMenuItem.setChecked(cookieManager.acceptCookie());
 
         // Enable Clear Cookies if there are any.
         optionsClearCookiesMenuItem.setEnabled(cookieManager.hasCookies());
@@ -1218,12 +1199,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
             // Consume the event.
             return true;
-        } else if (menuItemId == R.id.first_party_cookies) {  // First-party cookies.
+        } else if (menuItemId == R.id.cookies) {  // Cookies.
             // Switch the first-party cookie status.
             cookieManager.setAcceptCookie(!cookieManager.acceptCookie());
 
-            // Store the first-party cookie status.
-            currentWebView.setAcceptFirstPartyCookies(cookieManager.acceptCookie());
+            // Store the cookie status.
+            currentWebView.setAcceptCookies(cookieManager.acceptCookie());
 
             // Update the menu checkbox.
             menuItem.setChecked(cookieManager.acceptCookie());
@@ -1232,10 +1213,10 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             updatePrivacyIcons(true);
 
             // Display a snackbar.
-            if (cookieManager.acceptCookie()) {  // First-party cookies are enabled.
-                Snackbar.make(webViewPager, R.string.first_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
+            if (cookieManager.acceptCookie()) {  // Cookies are enabled.
+                Snackbar.make(webViewPager, R.string.cookies_enabled, Snackbar.LENGTH_SHORT).show();
             } else if (currentWebView.getSettings().getJavaScriptEnabled()) {  // JavaScript is still enabled.
-                Snackbar.make(webViewPager, R.string.first_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
+                Snackbar.make(webViewPager, R.string.cookies_disabled, Snackbar.LENGTH_SHORT).show();
             } else {  // Privacy mode.
                 Snackbar.make(webViewPager, R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
             }
@@ -1243,28 +1224,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             // Reload the current WebView.
             currentWebView.reload();
 
-            // Consume the event.
-            return true;
-        } else if (menuItemId == R.id.third_party_cookies) {  // Third-party cookies.
-            // Only act if the API >= 21.  Otherwise, there are no third-party cookie controls.
-            if (Build.VERSION.SDK_INT >= 21) {
-                // Toggle the status of thirdPartyCookiesEnabled.
-                cookieManager.setAcceptThirdPartyCookies(currentWebView, !cookieManager.acceptThirdPartyCookies(currentWebView));
-
-                // Update the menu checkbox.
-                menuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView));
-
-                // Display a snackbar.
-                if (cookieManager.acceptThirdPartyCookies(currentWebView)) {
-                    Snackbar.make(webViewPager, R.string.third_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
-                } else {
-                    Snackbar.make(webViewPager, R.string.third_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
-                }
-
-                // Reload the current WebView.
-                currentWebView.reload();
-            }
-
             // Consume the event.
             return true;
         } else if (menuItemId == R.id.dom_storage) {  // DOM storage.
@@ -1756,30 +1715,29 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             PrintDocumentAdapter printDocumentAdapter = currentWebView.createPrintDocumentAdapter();
 
             // Print the document.
-            printManager.print(getString(R.string.privacy_browser_web_page), printDocumentAdapter, null);
+            printManager.print(getString(R.string.privacy_browser_webpage), printDocumentAdapter, null);
 
             // Consume the event.
             return true;
         } else if (menuItemId == R.id.save_url) {  // Save URL.
             // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
-            new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
-                    currentWebView.getAcceptFirstPartyCookies()).execute(currentWebView.getCurrentUrl());
+            new PrepareSaveDialog(this, this, getSupportFragmentManager(), SaveWebpageDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
+                    currentWebView.getAcceptCookies()).execute(currentWebView.getCurrentUrl());
 
             // Consume the event.
             return true;
-        } else if (menuItemId == R.id.save_archive) {  // Save archive.
+        } else if (menuItemId == R.id.save_archive) {
             // Instantiate the save dialog.
-            DialogFragment saveArchiveFragment = SaveWebpageDialog.saveWebpage(StoragePermissionDialog.SAVE_ARCHIVE, null, null, getString(R.string.webpage_mht), null,
+            DialogFragment saveArchiveFragment = SaveWebpageDialog.saveWebpage(SaveWebpageDialog.SAVE_ARCHIVE, currentWebView.getCurrentUrl(), null, null, null,
                     false);
 
             // Show the save dialog.  It must be named `save_dialog` so that the file picker can update the file name.
             saveArchiveFragment.show(getSupportFragmentManager(), getString(R.string.save_dialog));
 
-            // Consume the event.
             return true;
         } else if (menuItemId == R.id.save_image) {  // Save image.
             // Instantiate the save dialog.
-            DialogFragment saveImageFragment = SaveWebpageDialog.saveWebpage(StoragePermissionDialog.SAVE_IMAGE, null, null, getString(R.string.webpage_png), null,
+            DialogFragment saveImageFragment = SaveWebpageDialog.saveWebpage(SaveWebpageDialog.SAVE_IMAGE, currentWebView.getCurrentUrl(), null, null, null,
                     false);
 
             // Show the save dialog.  It must be named `save_dialog` so that the file picker can update the file name.
@@ -2037,14 +1995,50 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             // Make it so.
             startActivity(requestsIntent);
         } else if (menuItemId == R.id.downloads) {  // Downloads.
-            // Launch the system Download Manager.
-            Intent downloadManagerIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
+            // Try the default system download manager.
+            try {
+                // Launch the default system Download Manager.
+                Intent defaultDownloadManagerIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
 
-            // Launch as a new task so that Download Manager and Privacy Browser show as separate windows in the recent tasks list.
-            downloadManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+                // Launch as a new task so that the download manager and Privacy Browser show as separate windows in the recent tasks list.
+                defaultDownloadManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
 
-            // Make it so.
-            startActivity(downloadManagerIntent);
+                // Make it so.
+                startActivity(defaultDownloadManagerIntent);
+            } catch (Exception defaultDownloadManagerException) {
+                // Try a generic file manager.
+                try {
+                    // Create a generic file manager intent.
+                    Intent genericFileManagerIntent = new Intent(Intent.ACTION_VIEW);
+
+                    // Open the download directory.
+                    genericFileManagerIntent.setDataAndType(Uri.parse(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString()), DocumentsContract.Document.MIME_TYPE_DIR);
+
+                    // Launch as a new task so that the file manager and Privacy Browser show as separate windows in the recent tasks list.
+                    genericFileManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+                    // Make it so.
+                    startActivity(genericFileManagerIntent);
+                } catch (Exception genericFileManagerException) {
+                    // Try an alternate file manager.
+                    try {
+                        // Create an alternate file manager intent.
+                        Intent alternateFileManagerIntent = new Intent(Intent.ACTION_VIEW);
+
+                        // Open the download directory.
+                        alternateFileManagerIntent.setDataAndType(Uri.parse(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString()), "resource/folder");
+
+                        // Launch as a new task so that the file manager and Privacy Browser show as separate windows in the recent tasks list.
+                        alternateFileManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+                        // Open the alternate file manager.
+                        startActivity(alternateFileManagerIntent);
+                    } catch (Exception alternateFileManagerException) {
+                        // Display a snackbar.
+                        Snackbar.make(currentWebView, R.string.no_file_manager_detected, Snackbar.LENGTH_INDEFINITE).show();
+                    }
+                }
+            }
         } else if (menuItemId == R.id.domains) {  // Domains.
             // Set the flag to reapply the domain settings on restart when returning from Domain Settings.
             reapplyDomainSettingsOnRestart = true;
@@ -2240,8 +2234,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Add a Save URL entry.
                 menu.add(R.string.save_url).setOnMenuItemClickListener((MenuItem item) -> {
                     // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
-                    new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
-                            currentWebView.getAcceptFirstPartyCookies()).execute(linkUrl);
+                    new PrepareSaveDialog(this, this, getSupportFragmentManager(), SaveWebpageDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
+                            currentWebView.getAcceptCookies()).execute(linkUrl);
 
                     // Consume the event.
                     return true;
@@ -2307,8 +2301,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Add a Save Image entry.
                 menu.add(R.string.save_image).setOnMenuItemClickListener((MenuItem item) -> {
                    // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
-                    new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
-                            currentWebView.getAcceptFirstPartyCookies()).execute(imageUrl);
+                    new PrepareSaveDialog(this, this, getSupportFragmentManager(), SaveWebpageDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
+                            currentWebView.getAcceptCookies()).execute(imageUrl);
 
                     // Consume the event.
                     return true;
@@ -2407,8 +2401,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Add a Save Image entry.
                 menu.add(R.string.save_image).setOnMenuItemClickListener((MenuItem item) -> {
                     // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
-                    new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
-                            currentWebView.getAcceptFirstPartyCookies()).execute(imageUrl);
+                    new PrepareSaveDialog(this, this, getSupportFragmentManager(), SaveWebpageDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
+                            currentWebView.getAcceptCookies()).execute(imageUrl);
 
                     // Consume the event.
                     return true;
@@ -2429,8 +2423,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Add a Save URL entry.
                 menu.add(R.string.save_url).setOnMenuItemClickListener((MenuItem item) -> {
                     // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
-                    new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
-                            currentWebView.getAcceptFirstPartyCookies()).execute(linkUrl);
+                    new PrepareSaveDialog(this, this, getSupportFragmentManager(), SaveWebpageDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
+                            currentWebView.getAcceptCookies()).execute(linkUrl);
 
                     // Consume the event.
                     return true;
@@ -2544,20 +2538,20 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         assert dialog != null;
 
         // Get handles for the views in the dialog fragment.
-        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);
+        EditText folderNameEditText = dialog.findViewById(R.id.folder_name_edittext);
+        RadioButton defaultIconRadioButton = dialog.findViewById(R.id.default_icon_radiobutton);
+        ImageView defaultIconImageView = dialog.findViewById(R.id.default_icon_imageview);
 
         // Get new folder name string.
-        String folderNameString = createFolderNameEditText.getText().toString();
+        String folderNameString = folderNameEditText.getText().toString();
 
         // Create a folder icon bitmap.
         Bitmap folderIconBitmap;
 
         // Set the folder icon bitmap according to the dialog.
-        if (defaultFolderIconRadioButton.isChecked()) {  // Use the default folder icon.
+        if (defaultIconRadioButton.isChecked()) {  // Use the default folder icon.
             // Get the default folder icon drawable.
-            Drawable folderIconDrawable = folderIconImageView.getDrawable();
+            Drawable folderIconDrawable = defaultIconImageView.getDrawable();
 
             // Convert the folder icon drawable to a bitmap drawable.
             BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
@@ -2609,10 +2603,10 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         assert dialog != null;
 
         // Get handles for the views from the dialog.
-        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);
+        RadioButton currentFolderIconRadioButton = dialog.findViewById(R.id.current_icon_radiobutton);
+        RadioButton defaultFolderIconRadioButton = dialog.findViewById(R.id.default_icon_radiobutton);
+        ImageView defaultFolderIconImageView = dialog.findViewById(R.id.default_icon_imageview);
+        EditText editFolderNameEditText = dialog.findViewById(R.id.folder_name_edittext);
 
         // Get the new folder name.
         String newFolderNameString = editFolderNameEditText.getText().toString();
@@ -2652,7 +2646,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             // Update the folder icon in the database.
             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderIconByteArray);
         } else {  // The folder icon and the name have changed.
-            // Get the new folder icon `Bitmap`.
+            // Get the new folder icon bitmap.
             Bitmap folderIconBitmap;
             if (defaultFolderIconRadioButton.isChecked()) {
                 // Get the default folder icon drawable.
@@ -2799,76 +2793,66 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 }
                 break;
 
-            case BROWSE_SAVE_WEBPAGE_REQUEST_CODE:
+            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 save dialog fragment.
-                    DialogFragment saveWebpageDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.save_dialog));
+                    // 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 (saveWebpageDialogFragment != null) {
-                        // Get a handle for the save webpage dialog.
-                        Dialog saveWebpageDialog = saveWebpageDialogFragment.getDialog();
+                    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 saveWebpageDialog != null;
+                        assert openDialog != null;
 
                         // Get a handle for the file name edit text.
-                        EditText fileNameEditText = saveWebpageDialog.findViewById(R.id.file_name_edittext);
-                        TextView fileExistsWarningTextView = saveWebpageDialog.findViewById(R.id.file_exists_warning_textview);
-
-                        // Instantiate the file name helper.
-                        FileNameHelper fileNameHelper = new FileNameHelper();
+                        EditText fileNameEditText = openDialog.findViewById(R.id.file_name_edittext);
 
-                        // 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());
+                        // Get the file name URI from the intent.
+                        Uri fileNameUri = returnedIntent.getData();
 
-                            // Set the file name path as the text of the file name edit text.
-                            fileNameEditText.setText(fileNamePath);
+                        // Get the file name string from the URI.
+                        String fileNameString = fileNameUri.toString();
 
-                            // Move the cursor to the end of the file name edit text.
-                            fileNameEditText.setSelection(fileNamePath.length());
+                        // Set the file name text.
+                        fileNameEditText.setText(fileNameString);
 
-                            // Hide the file exists warning.
-                            fileExistsWarningTextView.setVisibility(View.GONE);
-                        }
+                        // Move the cursor to the end of the file name edit text.
+                        fileNameEditText.setSelection(fileNameString.length());
                     }
                 }
                 break;
 
-            case BROWSE_OPEN_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 open dialog fragment.
-                    DialogFragment openDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.open));
+                    // Get a handle for the save dialog fragment.
+                    DialogFragment saveWebpageDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.save_dialog));
 
                     // Only update the file name if the dialog still exists.
-                    if (openDialogFragment != null) {
-                        // Get a handle for the open dialog.
-                        Dialog openDialog = openDialogFragment.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 openDialog != null;
+                        assert saveWebpageDialog != null;
 
                         // Get a handle for the file name edit text.
-                        EditText fileNameEditText = openDialog.findViewById(R.id.file_name_edittext);
+                        EditText fileNameEditText = saveWebpageDialog.findViewById(R.id.file_name_edittext);
 
-                        // Instantiate the file name helper.
-                        FileNameHelper fileNameHelper = new FileNameHelper();
+                        // Get the file name URI from the intent.
+                        Uri fileNameUri = returnedIntent.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());
+                        // Get the file name string from the URI.
+                        String fileNameString = fileNameUri.toString();
 
-                            // Set the file name path as the text of the file name edit text.
-                            fileNameEditText.setText(fileNamePath);
+                        // Set the file name text.
+                        fileNameEditText.setText(fileNameString);
 
-                            // Move the cursor to the end of the file name edit text.
-                            fileNameEditText.setSelection(fileNamePath.length());
-                        }
+                        // Move the cursor to the end of the file name edit text.
+                        fileNameEditText.setSelection(fileNameString.length());
                     }
                 }
                 break;
@@ -3026,206 +3010,149 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Remove the incorrect lint warning below that the dialog might be null.
         assert dialog != null;
 
-        // Get a handle for the file name edit text.
+        // Get handles for the views.
         EditText fileNameEditText = dialog.findViewById(R.id.file_name_edittext);
+        CheckBox mhtCheckBox = dialog.findViewById(R.id.mht_checkbox);
 
         // Get the file path string.
-        openFilePath = fileNameEditText.getText().toString();
+        String openFilePath = fileNameEditText.getText().toString();
 
         // Apply the domain settings.  This resets the favorite icon and removes any domain settings.
-        applyDomainSettings(currentWebView, "file://" + openFilePath, true, false, false);
+        applyDomainSettings(currentWebView, openFilePath, true, false, false);
 
-        // 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}, StoragePermissionDialog.OPEN);
+        // Open the file according to the type.
+        if (mhtCheckBox.isChecked()) {  // Force opening of an MHT file.
+            try {
+                // Get the MHT file input stream.
+                InputStream mhtFileInputStream = getContentResolver().openInputStream(Uri.parse(openFilePath));
+
+                // Create a temporary MHT file.
+                File temporaryMhtFile = File.createTempFile("temporary_mht_file", ".mht", getCacheDir());
+
+                // Get a file output stream for the temporary MHT file.
+                FileOutputStream temporaryMhtFileOutputStream = new FileOutputStream(temporaryMhtFile);
+
+                // Create a transfer byte array.
+                byte[] transferByteArray = new byte[1024];
+
+                // Create an integer to track the number of bytes read.
+                int bytesRead;
+
+                // Copy the temporary MHT file input stream to the MHT output stream.
+                while ((bytesRead = mhtFileInputStream.read(transferByteArray)) > 0) {
+                    temporaryMhtFileOutputStream.write(transferByteArray, 0, bytesRead);
                 }
+
+                // Flush the temporary MHT file output stream.
+                temporaryMhtFileOutputStream.flush();
+
+                // Close the streams.
+                temporaryMhtFileOutputStream.close();
+                mhtFileInputStream.close();
+
+                // Load the temporary MHT file.
+                currentWebView.loadUrl(temporaryMhtFile.toString());
+            } catch (Exception exception) {
+                // Display a snackbar.
+                Snackbar.make(currentWebView, getString(R.string.error) + "  " + exception.toString(), Snackbar.LENGTH_INDEFINITE).show();
             }
+        } else {  // Let the WebView handle opening of the file.
+            // Open the file.
+            currentWebView.loadUrl(openFilePath);
         }
     }
 
     @Override
-    public void onSaveWebpage(int saveType, String originalUrlString, DialogFragment dialogFragment) {
+    public void onSaveWebpage(int saveType, @NonNull String originalUrlString, 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 edit texts.
-        EditText dialogUrlEditText = dialog.findViewById(R.id.url_edittext);
+        // Get a handle for the file name edit text.
         EditText fileNameEditText = dialog.findViewById(R.id.file_name_edittext);
 
-        // Store the URL.
-        if ((originalUrlString != null) && originalUrlString.startsWith("data:")) {
-            // Save the original URL.
-            saveWebpageUrl = originalUrlString;
-        } else {
-            // Get the URL from the edit text, which may have been modified.
-            saveWebpageUrl = dialogUrlEditText.getText().toString();
-        }
-
         // Get the file path from the edit text.
-        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 according to the save type.
-            switch (saveType) {
-                case StoragePermissionDialog.SAVE_URL:
-                    // Save the URL.
-                    new SaveUrl(this, this, saveWebpageFilePath, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptFirstPartyCookies()).execute(saveWebpageUrl);
-                    break;
-
-                case StoragePermissionDialog.SAVE_ARCHIVE:
-                    // Save the webpage archive.
-                    saveWebpageArchive(saveWebpageFilePath);
-                    break;
-
-                case StoragePermissionDialog.SAVE_IMAGE:
-                    // Save the webpage image.
-                    new SaveWebpageImage(this, this, saveWebpageFilePath, currentWebView).execute();
-                    break;
-            }
+        String saveWebpageFilePath = fileNameEditText.getText().toString();
 
-            // Reset the strings.
-            saveWebpageUrl = "";
-            saveWebpageFilePath = "";
-        } 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 (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_URL:
-                        // Save the URL.
-                        new SaveUrl(this, this, saveWebpageFilePath, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptFirstPartyCookies()).execute(saveWebpageUrl);
-                        break;
+        //Save the webpage according to the save type.
+        switch (saveType) {
+            case SaveWebpageDialog.SAVE_URL:
+                // Get a handle for the dialog URL edit text.
+                EditText dialogUrlEditText = dialog.findViewById(R.id.url_edittext);
 
-                    case StoragePermissionDialog.SAVE_ARCHIVE:
-                        // Save the webpage archive.
-                        saveWebpageArchive(saveWebpageFilePath);
-                        break;
+                // Define the save webpage URL.
+                String saveWebpageUrl;
 
-                    case StoragePermissionDialog.SAVE_IMAGE:
-                        // Save the webpage image.
-                        new SaveWebpageImage(this, this, saveWebpageFilePath, currentWebView).execute();
-                        break;
+                // Store the URL.
+                if (originalUrlString.startsWith("data:")) {
+                    // Save the original URL.
+                    saveWebpageUrl = originalUrlString;
+                } else {
+                    // Get the URL from the edit text, which may have been modified.
+                    saveWebpageUrl = dialogUrlEditText.getText().toString();
                 }
 
-                // Reset the strings.
-                saveWebpageUrl = "";
-                saveWebpageFilePath = "";
-            } 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(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 according to the save type.  The URL will be saved when it finishes.
-                    ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, saveType);
-                }
-            }
-        }
-    }
+                // Save the URL.
+                new SaveUrl(this, this, saveWebpageFilePath, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptCookies()).execute(saveWebpageUrl);
+                break;
 
-    @Override
-    public void onCloseStoragePermissionDialog(int requestType) {
-        // Request the write external storage permission according to the request type.  The file will be opened when it finishes.
-        ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestType);
+            case SaveWebpageDialog.SAVE_ARCHIVE:
+                try {
+                    // Create a temporary MHT file.
+                    File temporaryMhtFile = File.createTempFile("temporary_mht_file", ".mht", getCacheDir());
 
-    }
+                    // Save the temporary MHT file.
+                    currentWebView.saveWebArchive(temporaryMhtFile.toString(), false, callbackValue -> {
+                        if (callbackValue != null) {  // The temporary MHT file was saved successfully.
+                            try {
+                                // Create a temporary MHT file input stream.
+                                FileInputStream temporaryMhtFileInputStream = new FileInputStream(temporaryMhtFile);
 
-    @Override
-    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
-        //Only process the results if they exist (this method is triggered when a dialog is presented the first time for an app, but no grant results are included).
-        if (grantResults.length > 0) {
-            switch (requestCode) {
-                case StoragePermissionDialog.OPEN:
-                    // Check to see if the storage permission was granted.  If the dialog was canceled the grant results will be empty.
-                    if (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();
-                    }
-                    break;
-
-                case StoragePermissionDialog.SAVE_URL:
-                    // Check to see if the storage permission was granted.  If the dialog was canceled the grant results will be empty.
-                    if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {  // The storage permission was granted.
-                        // Save the raw URL.
-                        new SaveUrl(this, this, saveWebpageFilePath, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptFirstPartyCookies()).execute(saveWebpageUrl);
-                    } else {  // The storage permission was not granted.
-                        // Display an error snackbar.
-                        Snackbar.make(currentWebView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show();
-                    }
-                    break;
-
-                case StoragePermissionDialog.SAVE_ARCHIVE:
-                    // Check to see if the storage permission was granted.  If the dialog was canceled the grant results will be empty.
-                    if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {  // The storage permission was granted.
-                        // Save the webpage archive.
-                        saveWebpageArchive(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();
-                    }
-                    break;
-
-                case StoragePermissionDialog.SAVE_IMAGE:
-                    // Check to see if the storage permission was granted.  If the dialog was canceled the grant results will be empty.
-                    if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {  // The storage permission was granted.
-                        // Save the webpage image.
-                        new SaveWebpageImage(this, this, saveWebpageFilePath, currentWebView).execute();
-                    } else {  // The storage permission was not granted.
-                        // Display an error snackbar.
-                        Snackbar.make(currentWebView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show();
-                    }
-                    break;
-            }
+                                // Get an output stream for the save webpage file path.
+                                OutputStream mhtOutputStream = getContentResolver().openOutputStream(Uri.parse(saveWebpageFilePath));
+
+                                // Create a transfer byte array.
+                                byte[] transferByteArray = new byte[1024];
+
+                                // Create an integer to track the number of bytes read.
+                                int bytesRead;
+
+                                // Copy the temporary MHT file input stream to the MHT output stream.
+                                while ((bytesRead = temporaryMhtFileInputStream.read(transferByteArray)) > 0) {
+                                    mhtOutputStream.write(transferByteArray, 0, bytesRead);
+                                }
+
+                                // Close the streams.
+                                mhtOutputStream.close();
+                                temporaryMhtFileInputStream.close();
+
+                                // Display a snackbar.
+                                Snackbar.make(currentWebView, getString(R.string.file_saved) + "  " + currentWebView.getCurrentUrl(), Snackbar.LENGTH_SHORT).show();
+                            } catch (Exception exception) {
+                                // Display a snackbar with the exception.
+                                Snackbar.make(currentWebView, getString(R.string.error_saving_file) + "  " + exception.toString(), Snackbar.LENGTH_INDEFINITE).show();
+                            } finally {
+                                // Delete the temporary MHT file.
+                                //noinspection ResultOfMethodCallIgnored
+                                temporaryMhtFile.delete();
+                            }
+                        } else {  // There was an unspecified error while saving the temporary MHT file.
+                            // Display an error snackbar.
+                            Snackbar.make(currentWebView, getString(R.string.error_saving_file), Snackbar.LENGTH_INDEFINITE).show();
+                        }
+                    });
+                } catch (IOException ioException) {
+                    // Display a snackbar with the IO exception.
+                    Snackbar.make(currentWebView, getString(R.string.error_saving_file) + "  " + ioException.toString(), Snackbar.LENGTH_INDEFINITE).show();
+                }
+                break;
 
-            // Reset the strings.
-            openFilePath = "";
-            saveWebpageUrl = "";
-            saveWebpageFilePath = "";
+            case SaveWebpageDialog.SAVE_IMAGE:
+                // Save the webpage image.
+                new SaveWebpageImage(this, saveWebpageFilePath, currentWebView).execute();
+                break;
         }
     }
 
@@ -3642,7 +3569,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
         // Store the values from the shared preferences in variables.
         incognitoModeEnabled = sharedPreferences.getBoolean("incognito_mode", false);
-        boolean doNotTrackEnabled = sharedPreferences.getBoolean("do_not_track", false);
         sanitizeGoogleAnalytics = sharedPreferences.getBoolean("google_analytics", true);
         sanitizeFacebookClickIds = sharedPreferences.getBoolean("facebook_click_ids", true);
         sanitizeTwitterAmpRedirects = sharedPreferences.getBoolean("twitter_amp_redirects", true);
@@ -3673,13 +3599,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Apply the proxy.
         applyProxy(false);
 
-        // Set Do Not Track status.
-        if (doNotTrackEnabled) {
-            customHeaders.put("DNT", "1");
-        } else {
-            customHeaders.remove("DNT");
-        }
-
         // Get the current layout parameters.  Using coordinator layout parameters allows the `setBehavior()` command and using app bar layout parameters allows the `setScrollFlags()` command.
         CoordinatorLayout.LayoutParams swipeRefreshLayoutParams = (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams();
         AppBarLayout.LayoutParams toolbarLayoutParams = (AppBarLayout.LayoutParams) toolbar.getLayoutParams();
@@ -3788,7 +3707,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     }
 
     @Override
-    public void navigateHistory(String url, int steps) {
+    public void navigateHistory(@NonNull String url, int steps) {
         // Apply the domain settings.
         applyDomainSettings(currentWebView, url, false, false, false);
 
@@ -3954,8 +3873,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Get the settings from the cursor.
                 nestedScrollWebView.setDomainSettingsDatabaseId(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper._ID)));
                 nestedScrollWebView.getSettings().setJavaScriptEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)) == 1);
-                nestedScrollWebView.setAcceptFirstPartyCookies(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FIRST_PARTY_COOKIES)) == 1);
-                boolean domainThirdPartyCookiesEnabled = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_THIRD_PARTY_COOKIES)) == 1);
+                nestedScrollWebView.setAcceptCookies(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.COOKIES)) == 1);
                 nestedScrollWebView.getSettings().setDomStorageEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_DOM_STORAGE)) == 1);
                 // Form data can be removed once the minimum API >= 26.
                 boolean saveFormData = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FORM_DATA)) == 1);
@@ -4025,12 +3943,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 }
 
                 // Apply the cookie domain settings.
-                cookieManager.setAcceptCookie(nestedScrollWebView.getAcceptFirstPartyCookies());
-
-                // Set third-party cookies status if API >= 21.
-                if (Build.VERSION.SDK_INT >= 21) {
-                    cookieManager.setAcceptThirdPartyCookies(nestedScrollWebView, domainThirdPartyCookiesEnabled);
-                }
+                cookieManager.setAcceptCookie(nestedScrollWebView.getAcceptCookies());
 
                 // Apply the form data setting if the API < 26.
                 if (Build.VERSION.SDK_INT < 26) {
@@ -4209,8 +4122,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             } else {  // The new URL does not have custom domain settings.  Load the defaults.
                 // Store the values from the shared preferences.
                 nestedScrollWebView.getSettings().setJavaScriptEnabled(sharedPreferences.getBoolean("javascript", false));
-                nestedScrollWebView.setAcceptFirstPartyCookies(sharedPreferences.getBoolean("first_party_cookies", false));
-                boolean defaultThirdPartyCookiesEnabled = sharedPreferences.getBoolean("third_party_cookies", false);
+                nestedScrollWebView.setAcceptCookies(sharedPreferences.getBoolean(getString(R.string.cookies_key), false));
                 nestedScrollWebView.getSettings().setDomStorageEnabled(sharedPreferences.getBoolean("dom_storage", false));
                 boolean saveFormData = sharedPreferences.getBoolean("save_form_data", false);  // Form data can be removed once the minimum API >= 26.
                 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASYLIST, sharedPreferences.getBoolean("easylist", true));
@@ -4221,8 +4133,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRAPRIVACY, sharedPreferences.getBoolean("ultraprivacy", true));
                 nestedScrollWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS, sharedPreferences.getBoolean("block_all_third_party_requests", false));
 
-                // Apply the default first-party cookie setting.
-                cookieManager.setAcceptCookie(nestedScrollWebView.getAcceptFirstPartyCookies());
+                // Apply the default cookie setting.
+                cookieManager.setAcceptCookie(nestedScrollWebView.getAcceptCookies());
 
                 // Apply the default font size setting.
                 try {
@@ -4253,11 +4165,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Reset the pinned variables.
                 nestedScrollWebView.setDomainSettingsDatabaseId(-1);
 
-                // Set third-party cookies status if API >= 21.
-                if (Build.VERSION.SDK_INT >= 21) {
-                    cookieManager.setAcceptThirdPartyCookies(nestedScrollWebView, defaultThirdPartyCookiesEnabled);
-                }
-
                 // Get the array position of the user agent name.
                 int userAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
 
@@ -4417,7 +4324,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     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);
+                    packageManager.getPackageInfo("net.i2p.android.router", 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) {
@@ -4468,7 +4375,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             // Update the privacy icon.
             if (currentWebView.getSettings().getJavaScriptEnabled()) {  // JavaScript is enabled.
                 optionsPrivacyMenuItem.setIcon(R.drawable.javascript_enabled);
-            } else if (currentWebView.getAcceptFirstPartyCookies()) {  // JavaScript is disabled but cookies are enabled.
+            } else if (currentWebView.getAcceptCookies()) {  // JavaScript is disabled but cookies are enabled.
                 optionsPrivacyMenuItem.setIcon(R.drawable.warning);
             } else {  // All the dangerous features are disabled.
                 optionsPrivacyMenuItem.setIcon(R.drawable.privacy_mode);
@@ -4477,14 +4384,14 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             // Get the current theme status.
             int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
 
-            // Update the first-party cookies icon.
-            if (currentWebView.getAcceptFirstPartyCookies()) {  // First-party cookies are enabled.
-                optionsFirstPartyCookiesMenuItem.setIcon(R.drawable.cookies_enabled);
-            } else {  // First-party cookies are disabled.
+            // Update the cookies icon.
+            if (currentWebView.getAcceptCookies()) {  // Cookies are enabled.
+                optionsCookiesMenuItem.setIcon(R.drawable.cookies_enabled);
+            } else {  // Cookies are disabled.
                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
-                    optionsFirstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_day);
+                    optionsCookiesMenuItem.setIcon(R.drawable.cookies_disabled_day);
                 } else {
-                    optionsFirstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_night);
+                    optionsCookiesMenuItem.setIcon(R.drawable.cookies_disabled_night);
                 }
             }
 
@@ -4872,48 +4779,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         }
     }
 
-    private void saveWebpageArchive(String filePath) {
-        // Save the webpage archive.
-        currentWebView.saveWebArchive(filePath);
-
-        // Display a snackbar.
-        Snackbar saveWebpageArchiveSnackbar = Snackbar.make(currentWebView, getString(R.string.file_saved) + "  " + filePath, Snackbar.LENGTH_SHORT);
-
-        // Add an open option to the snackbar.
-        saveWebpageArchiveSnackbar.setAction(R.string.open, (View view) -> {
-            // Get a file for the file name string.
-            File file = new File(filePath);
-
-            // Declare a file URI variable.
-            Uri fileUri;
-
-            // Get the URI for the file according to the Android version.
-            if (Build.VERSION.SDK_INT >= 24) {  // Use a file provider.
-                fileUri = FileProvider.getUriForFile(this, getString(R.string.file_provider), file);
-            } else {  // Get the raw file path URI.
-                fileUri = Uri.fromFile(file);
-            }
-
-            // Get a handle for the content resolver.
-            ContentResolver contentResolver = getContentResolver();
-
-            // Create an open intent with `ACTION_VIEW`.
-            Intent openIntent = new Intent(Intent.ACTION_VIEW);
-
-            // Set the URI and the MIME type.
-            openIntent.setDataAndType(fileUri, contentResolver.getType(fileUri));
-
-            // Allow the app to read the file URI.
-            openIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
-
-            // Show the chooser.
-            startActivity(Intent.createChooser(openIntent, getString(R.string.open)));
-        });
-
-        // Show the snackbar.
-        saveWebpageArchiveSnackbar.show();
-    }
-
     private void clearAndExit() {
         // Get a handle for the shared preferences.
         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
@@ -5147,8 +5012,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             // Get a handle for the cookie manager.
             CookieManager cookieManager = CookieManager.getInstance();
 
-            // Set the first-party cookie status.
-            cookieManager.setAcceptCookie(currentWebView.getAcceptFirstPartyCookies());
+            // Set the cookie status.
+            cookieManager.setAcceptCookie(currentWebView.getAcceptCookies());
 
             // Update the privacy icons.  `true` redraws the icons in the app bar.
             updatePrivacyIcons(true);
@@ -5298,6 +5163,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Explicitly disable geolocation.
         nestedScrollWebView.getSettings().setGeolocationEnabled(false);
 
+        // Allow loading of file:// URLs.  This is necessary for opening MHT web archives, which are copies into a temporary cache location.
+        nestedScrollWebView.getSettings().setAllowFileAccess(true);
+
         // Create a double-tap gesture detector to toggle full-screen mode.
         GestureDetector doubleTapGestureDetector = new GestureDetector(getApplicationContext(), new GestureDetector.SimpleOnGestureListener() {
             // Override `onDoubleTap()`.  All other events are handled using the default settings.
@@ -5428,9 +5296,20 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             // Get the file name from the content disposition.
             String fileNameString = PrepareSaveDialog.getFileNameFromHeaders(this, contentDisposition, mimetype, downloadUrl);
 
+            // Prevent the dialog from displaying if the app window is not visible.
+            // The download listener continues to function even when the WebView is paused.  Attempting to display a dialog in that state leads to a crash.
+            while (!activity.getWindow().isActive()) {
+                try {
+                    // The window is not active.  Wait 1 second.
+                    wait(1000);
+                } catch (InterruptedException e) {
+                    // Do nothing.
+                }
+            }
+
             // Instantiate the save dialog.
-            DialogFragment saveDialogFragment = SaveWebpageDialog.saveWebpage(StoragePermissionDialog.SAVE_URL, downloadUrl, formattedFileSizeString, fileNameString, userAgent,
-                    nestedScrollWebView.getAcceptFirstPartyCookies());
+            DialogFragment saveDialogFragment = SaveWebpageDialog.saveWebpage(SaveWebpageDialog.SAVE_URL, downloadUrl, formattedFileSizeString, fileNameString, userAgent,
+                    nestedScrollWebView.getAcceptCookies());
 
             // Show the save dialog.  It must be named `save_dialog` so that the file picker can update the file name.
             saveDialogFragment.show(getSupportFragmentManager(), getString(R.string.save_dialog));
@@ -6260,7 +6139,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             @Override
             public void onPageFinished(WebView view, String url) {
                 // Flush any cookies to persistent storage.  The cookie manager has become very lazy about flushing cookies in recent versions.
-                if (nestedScrollWebView.getAcceptFirstPartyCookies() && Build.VERSION.SDK_INT >= 21) {
+                if (nestedScrollWebView.getAcceptCookies() && Build.VERSION.SDK_INT >= 21) {
                     CookieManager.getInstance().flush();
                 }
 
@@ -6429,6 +6308,17 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     // Store the SSL error handler.
                     nestedScrollWebView.setSslErrorHandler(handler);
 
+                    // Prevent the dialog from displaying if the app window is not visible.
+                    // The SSL error handler continues to function even when the WebView is paused.  Attempting to display a dialog in that state leads to a crash.
+                    while (!activity.getWindow().isActive()) {
+                        try {
+                            // The window is not active.  Wait 1 second.
+                            wait(1000);
+                        } catch (InterruptedException e) {
+                            // Do nothing.
+                        }
+                    }
+
                     // Instantiate an SSL certificate error alert dialog.
                     DialogFragment sslCertificateErrorDialogFragment = SslCertificateErrorDialog.displayDialog(error, nestedScrollWebView.getWebViewFragmentId());