]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blobdiff - app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java
Bump the minimum API to 24 (Android 7).
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / activities / MainWebViewActivity.java
index e1a8ca78f0b25e39d28b5348d394f30463a6e988..39b95dca65d30d4ab5f35a3b31b1d47fd6969873 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015-2022 Soren Stoutner <soren@stoutner.com>.
+ * Copyright 2015-2023 Soren Stoutner <soren@stoutner.com>.
  *
  * Download cookie code contributed 2017 Hendrik Knackstedt.  Copyright assigned to Soren Stoutner <soren@stoutner.com>.
  *
@@ -46,19 +46,16 @@ import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.net.http.SslCertificate;
 import android.net.http.SslError;
-import android.os.AsyncTask;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Environment;
 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.provider.OpenableColumns;
 import android.text.Editable;
-import android.text.Spanned;
 import android.text.TextWatcher;
 import android.text.style.ForegroundColorSpan;
 import android.util.Patterns;
@@ -100,6 +97,7 @@ import android.widget.RelativeLayout;
 import android.widget.TextView;
 
 import androidx.activity.OnBackPressedCallback;
+import androidx.activity.result.ActivityResult;
 import androidx.activity.result.ActivityResultCallback;
 import androidx.activity.result.ActivityResultLauncher;
 import androidx.activity.result.contract.ActivityResultContracts;
@@ -115,6 +113,7 @@ import androidx.core.view.GravityCompat;
 import androidx.drawerlayout.widget.DrawerLayout;
 import androidx.fragment.app.DialogFragment;
 import androidx.fragment.app.Fragment;
+import androidx.preference.PreferenceManager;
 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
 import androidx.viewpager.widget.ViewPager;
 import androidx.webkit.WebSettingsCompat;
@@ -128,16 +127,15 @@ import com.google.android.material.tabs.TabLayout;
 
 import com.stoutner.privacybrowser.R;
 import com.stoutner.privacybrowser.adapters.WebViewPagerAdapter;
-import com.stoutner.privacybrowser.asynctasks.GetHostIpAddresses;
-import com.stoutner.privacybrowser.asynctasks.PopulateBlocklists;
-import com.stoutner.privacybrowser.asynctasks.PrepareSaveDialog;
 import com.stoutner.privacybrowser.asynctasks.SaveUrl;
 import com.stoutner.privacybrowser.asynctasks.SaveWebpageImage;
-import com.stoutner.privacybrowser.dataclasses.PendingDialog;
+import com.stoutner.privacybrowser.coroutines.GetHostIpAddressesCoroutine;
+import com.stoutner.privacybrowser.coroutines.PopulateBlocklistsCoroutine;
+import com.stoutner.privacybrowser.coroutines.PrepareSaveDialogCoroutine;
+import com.stoutner.privacybrowser.dataclasses.PendingDialogDataClass;
 import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog;
 import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog;
 import com.stoutner.privacybrowser.dialogs.CreateHomeScreenShortcutDialog;
-import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDialog;
 import com.stoutner.privacybrowser.dialogs.FontSizeDialog;
 import com.stoutner.privacybrowser.dialogs.HttpAuthenticationDialog;
 import com.stoutner.privacybrowser.dialogs.OpenDialog;
@@ -154,6 +152,7 @@ import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper;
 import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper;
 import com.stoutner.privacybrowser.helpers.ProxyHelper;
 import com.stoutner.privacybrowser.helpers.SanitizeUrlHelper;
+import com.stoutner.privacybrowser.helpers.UrlHelper;
 import com.stoutner.privacybrowser.views.NestedScrollWebView;
 
 import java.io.ByteArrayInputStream;
@@ -185,14 +184,13 @@ import java.util.concurrent.Executors;
 import kotlin.Pair;
 
 public class MainWebViewActivity extends AppCompatActivity implements CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener,
-        EditBookmarkFolderDialog.EditBookmarkFolderListener, FontSizeDialog.UpdateFontSizeListener, NavigationView.OnNavigationItemSelectedListener, OpenDialog.OpenListener,
-        PinnedMismatchDialog.PinnedMismatchListener, PopulateBlocklists.PopulateBlocklistsListener, SaveDialog.SaveListener, UrlHistoryDialog.NavigateHistoryListener,
-        WebViewTabFragment.NewTabListener {
+        FontSizeDialog.UpdateFontSizeListener, NavigationView.OnNavigationItemSelectedListener, OpenDialog.OpenListener, PinnedMismatchDialog.PinnedMismatchListener,
+        PopulateBlocklistsCoroutine.PopulateBlocklistsListener, SaveDialog.SaveListener, UrlHistoryDialog.NavigateHistoryListener, WebViewTabFragment.NewTabListener {
 
     // Define the public static variables.
     public static final ExecutorService executorService = Executors.newFixedThreadPool(4);
     public static String orbotStatus = "unknown";
-    public static final ArrayList<PendingDialog> pendingDialogsArrayList =  new ArrayList<>();
+    public static final ArrayList<PendingDialogDataClass> pendingDialogsArrayList =  new ArrayList<>();
     public static String proxyMode = ProxyHelper.NONE;
 
     // Declare the public static variables.
@@ -211,15 +209,12 @@ 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 = 12;
 
-    // Define the start activity for result request codes.  The public static entry is accessed from `OpenDialog()`.
-    private final int BROWSE_FILE_UPLOAD_REQUEST_CODE = 0;
-    public final static int BROWSE_OPEN_REQUEST_CODE = 1;
-
     // Define the saved instance state constants.
+    private final String BOOKMARKS_DRAWER_PINNED = "bookmarks_drawer_pinned";
+    private final String PROXY_MODE = "proxy_mode";
     private final String SAVED_STATE_ARRAY_LIST = "saved_state_array_list";
     private final String SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST = "saved_nested_scroll_webview_state_array_list";
     private final String SAVED_TAB_POSITION = "saved_tab_position";
-    private final String PROXY_MODE = "proxy_mode";
 
     // Define the saved instance state variables.
     private ArrayList<Bundle> savedStateArrayList;
@@ -227,10 +222,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     private int savedTabPosition;
     private String savedProxyMode;
 
-    // Define the class variables.
-    @SuppressWarnings("rawtypes")
-    AsyncTask populateBlocklists;
-
     // The current WebView is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`,
     // `findNextOnPage()`, `closeFindOnPage()`, `loadUrlFromTextBox()`, `onSslMismatchBack()`, `applyProxy()`, and `applyDomainSettings()`.
     private NestedScrollWebView currentWebView;
@@ -249,20 +240,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     // The action bar drawer toggle is initialized in `onCreate()` and used in `onResume()`.
     private ActionBarDrawerToggle actionBarDrawerToggle;
 
-    // The color spans are used in `onCreate()` and `highlightUrlText()`.
-    private ForegroundColorSpan redColorSpan;
-    private ForegroundColorSpan initialGrayColorSpan;
-    private ForegroundColorSpan finalGrayColorSpan;
-
     // `bookmarksCursor` is used in `onDestroy()`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
     private Cursor bookmarksCursor;
 
     // `bookmarksCursorAdapter` is used in `onCreateBookmark()`, `onCreateBookmarkFolder()` `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
     private CursorAdapter bookmarksCursorAdapter;
 
-    // `oldFolderNameString` is used in `onCreate()` and `onSaveEditBookmarkFolder()`.
-    private String oldFolderNameString;
-
     // `fileChooserCallback` is used in `onCreate()` and `onActivityResult()`.
     private ValueCallback<Uri[]> fileChooserCallback;
 
@@ -275,21 +258,24 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     private BookmarksDatabaseHelper bookmarksDatabaseHelper;
     private DomainsDatabaseHelper domainsDatabaseHelper;
     private ProxyHelper proxyHelper;
-    private SanitizeUrlHelper sanitizeUrlHelper;
 
     // Declare the class variables
+    private boolean bookmarksDrawerPinned;
     private boolean bottomAppBar;
     private boolean displayAdditionalAppBarIcons;
     private boolean displayingFullScreenVideo;
     private boolean downloadWithExternalApp;
+    private ForegroundColorSpan finalGrayColorSpan;
     private boolean fullScreenBrowsingModeEnabled;
     private boolean hideAppBar;
-    private boolean incognitoModeEnabled;
     private boolean inFullScreenBrowsingMode;
+    private boolean incognitoModeEnabled;
+    private ForegroundColorSpan initialGrayColorSpan;
     private boolean loadingNewIntent;
     private BroadcastReceiver orbotStatusBroadcastReceiver;
     private boolean reapplyAppSettingsOnRestart;
     private boolean reapplyDomainSettingsOnRestart;
+    private ForegroundColorSpan redColorSpan;
     private boolean sanitizeAmpRedirects;
     private boolean sanitizeTrackingQueries;
     private boolean scrollAppBar;
@@ -301,19 +287,20 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     private String saveUrlString = "";
 
     // Declare the class views.
-    private FrameLayout rootFrameLayout;
-    private DrawerLayout drawerLayout;
-    private CoordinatorLayout coordinatorLayout;
-    private Toolbar toolbar;
-    private RelativeLayout urlRelativeLayout;
-    private EditText urlEditText;
     private ActionBar actionBar;
+    private CoordinatorLayout coordinatorLayout;
+    private ImageView bookmarksDrawerPinnedImageView;
+    private DrawerLayout drawerLayout;
     private LinearLayout findOnPageLinearLayout;
+    private FrameLayout fullScreenVideoFrameLayout;
+    private FrameLayout rootFrameLayout;
+    private SwipeRefreshLayout swipeRefreshLayout;
     private LinearLayout tabsLinearLayout;
     private TabLayout tabLayout;
-    private SwipeRefreshLayout swipeRefreshLayout;
+    private Toolbar toolbar;
+    private EditText urlEditText;
+    private RelativeLayout urlRelativeLayout;
     private ViewPager webViewPager;
-    private FrameLayout fullScreenVideoFrameLayout;
 
     // Declare the class menus.
     private Menu optionsMenu;
@@ -391,11 +378,30 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 public void onActivityResult(Uri fileUri) {
                     // Only save the webpage archive if the file URI is not null, which happens if the user exited the file picker by pressing back.
                     if (fileUri != null) {
+                        // Initialize the file name string from the file URI last path segment.
+                        String temporaryFileNameString = fileUri.getLastPathSegment();
+
+                        // Query the exact file name if the API >= 26.
+                        if (Build.VERSION.SDK_INT >= 26) {
+                            // Get a cursor from the content resolver.
+                            Cursor contentResolverCursor = resultLauncherActivityHandle.getContentResolver().query(fileUri, null, null, null);
+
+                            // Move to the fist row.
+                            contentResolverCursor.moveToFirst();
+
+                            // Get the file name from the cursor.
+                            temporaryFileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
+
+                            // Close the cursor.
+                            contentResolverCursor.close();
+                        }
+
+                        // Save the final file name string so it can be used inside the lambdas.  This will no longer be needed once this activity has transitioned to Kotlin.
+                        String finalFileNameString = temporaryFileNameString;
+
                         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 {
@@ -420,29 +426,11 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                                         mhtOutputStream.close();
                                         temporaryMhtFileInputStream.close();
 
-                                        // Initialize the file name string from the file URI last path segment.
-                                        String fileNameString = fileUri.getLastPathSegment();
-
-                                        // Query the exact file name if the API >= 26.
-                                        if (Build.VERSION.SDK_INT >= 26) {
-                                            // Get a cursor from the content resolver.
-                                            Cursor contentResolverCursor = resultLauncherActivityHandle.getContentResolver().query(fileUri, null, null, null);
-
-                                            // Move to the fist row.
-                                            contentResolverCursor.moveToFirst();
-
-                                            // Get the file name from the cursor.
-                                            fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
-
-                                            // Close the cursor.
-                                            contentResolverCursor.close();
-                                        }
-
                                         // Display a snackbar.
-                                        Snackbar.make(currentWebView, getString(R.string.file_saved) + "  " + fileNameString, Snackbar.LENGTH_SHORT).show();
+                                        Snackbar.make(currentWebView, getString(R.string.saved, finalFileNameString), Snackbar.LENGTH_SHORT).show();
                                     } catch (Exception exception) {
                                         // Display a snackbar with the exception.
-                                        Snackbar.make(currentWebView, getString(R.string.error_saving_file) + "  " + exception, Snackbar.LENGTH_INDEFINITE).show();
+                                        Snackbar.make(currentWebView, getString(R.string.error_saving_file, finalFileNameString, exception), Snackbar.LENGTH_INDEFINITE).show();
                                     } finally {
                                         // Delete the temporary MHT file.
                                         //noinspection ResultOfMethodCallIgnored
@@ -450,12 +438,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                                     }
                                 } 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();
+                                    Snackbar.make(currentWebView, getString(R.string.error_saving_file, finalFileNameString, getString(R.string.unknown_error)), Snackbar.LENGTH_INDEFINITE).show();
                                 }
                             });
                         } catch (IOException ioException) {
                             // Display a snackbar with the IO exception.
-                            Snackbar.make(currentWebView, getString(R.string.error_saving_file) + "  " + ioException, Snackbar.LENGTH_INDEFINITE).show();
+                            Snackbar.make(currentWebView, getString(R.string.error_saving_file, finalFileNameString, ioException), Snackbar.LENGTH_INDEFINITE).show();
                         }
                     }
                 }
@@ -474,6 +462,16 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 }
             });
 
+    // Define the save webpage image activity result launcher.  It must be defined before `onCreate()` is run or the app will crash.
+    private final ActivityResultLauncher<Intent> browseFileUploadActivityResultLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
+            new ActivityResultCallback<ActivityResult>() {
+                @Override
+                public void onActivityResult(ActivityResult activityResult) {
+                    // Pass the file to the WebView.
+                    fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(activityResult.getResultCode(), activityResult.getData()));
+                }
+            });
+
     // Remove the warning about needing to override `performClick()` when using an `OnTouchListener` with WebView.
     @SuppressLint("ClickableViewAccessibility")
     @Override
@@ -484,15 +482,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Populate the result launcher activity.  This will no longer be needed once the activity has transitioned to Kotlin.
         resultLauncherActivityHandle = this;
 
-        // Check to see if the activity has been restarted.
-        if (savedInstanceState != null) {
-            // Store the saved instance state variables.
-            savedStateArrayList = savedInstanceState.getParcelableArrayList(SAVED_STATE_ARRAY_LIST);
-            savedNestedScrollWebViewStateArrayList = savedInstanceState.getParcelableArrayList(SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST);
-            savedTabPosition = savedInstanceState.getInt(SAVED_TAB_POSITION);
-            savedProxyMode = savedInstanceState.getString(PROXY_MODE);
-        }
-
         // Initialize the default preference values the first time the program is run.  `false` keeps this command from resetting any current preferences back to default.
         PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
 
@@ -508,6 +497,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Get the theme entry values string array.
         String[] appThemeEntryValuesStringArray = getResources().getStringArray(R.array.app_theme_entry_values);
 
+        // Get the current theme status.
+        int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
+
         // Set the app theme according to the preference.  A switch statement cannot be used because the theme entry values string array is not a compile time constant.
         if (appTheme.equals(appThemeEntryValuesStringArray[1])) {  // The light theme is selected.
             // Apply the light theme.
@@ -525,138 +517,161 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             }
         }
 
-        // Disable screenshots if not allowed.
-        if (!allowScreenshots) {
-            getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
-        }
+        // Do not continue if the app theme is different than the OS theme.  The app always initially starts in the OS theme.
+        // If the user has specified the opposite theme should be used, the app will restart in that mode after the above `setDefaultNightMode()` code processes.  However, the restart is delayed.
+        // If the blacklist coroutine starts below it will continue to run during the restart, which leads to indeterminate behavior, with the system often not knowing how many tabs exist.
+        // See https://redmine.stoutner.com/issues/952.
+        if (appTheme.equals(appThemeEntryValuesStringArray[0]) ||  // The system default theme is used.
+                (appTheme.equals(appThemeEntryValuesStringArray[1]) && currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) ||  // The app is running in day theme as desired.
+                (appTheme.equals(appThemeEntryValuesStringArray[2]) && currentThemeStatus == Configuration.UI_MODE_NIGHT_YES)) {  // The app is running in night theme as desired.
+
+            // Disable screenshots if not allowed.
+            if (!allowScreenshots) {
+                getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
+            }
 
-        // Enable the drawing of the entire webpage.  This makes it possible to save a website image.  This must be done before anything else happens with the WebView.
-        WebView.enableSlowWholeDocumentDraw();
+            // Check to see if the activity has been restarted.
+            if (savedInstanceState != null) {
+                // Store the saved instance state variables.
+                bookmarksDrawerPinned = savedInstanceState.getBoolean(BOOKMARKS_DRAWER_PINNED);
+                savedStateArrayList = savedInstanceState.getParcelableArrayList(SAVED_STATE_ARRAY_LIST);
+                savedNestedScrollWebViewStateArrayList = savedInstanceState.getParcelableArrayList(SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST);
+                savedTabPosition = savedInstanceState.getInt(SAVED_TAB_POSITION);
+                savedProxyMode = savedInstanceState.getString(PROXY_MODE);
+            }
 
-        // Set the content view according to the position of the app bar.
-        if (bottomAppBar) setContentView(R.layout.main_framelayout_bottom_appbar);
-        else setContentView(R.layout.main_framelayout_top_appbar);
+            // Enable the drawing of the entire webpage.  This makes it possible to save a website image.  This must be done before anything else happens with the WebView.
+            WebView.enableSlowWholeDocumentDraw();
 
-        // Get handles for the views.
-        rootFrameLayout = findViewById(R.id.root_framelayout);
-        drawerLayout = findViewById(R.id.drawerlayout);
-        coordinatorLayout = findViewById(R.id.coordinatorlayout);
-        appBarLayout = findViewById(R.id.appbar_layout);
-        toolbar = findViewById(R.id.toolbar);
-        findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
-        tabsLinearLayout = findViewById(R.id.tabs_linearlayout);
-        tabLayout = findViewById(R.id.tablayout);
-        swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
-        webViewPager = findViewById(R.id.webviewpager);
-        NavigationView navigationView = findViewById(R.id.navigationview);
-        fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout);
+            // Set the content view according to the position of the app bar.
+            if (bottomAppBar) setContentView(R.layout.main_framelayout_bottom_appbar);
+            else setContentView(R.layout.main_framelayout_top_appbar);
 
-        // Get a handle for the navigation menu.
-        Menu navigationMenu = navigationView.getMenu();
+            // Get handles for the views.
+            rootFrameLayout = findViewById(R.id.root_framelayout);
+            drawerLayout = findViewById(R.id.drawerlayout);
+            coordinatorLayout = findViewById(R.id.coordinatorlayout);
+            appBarLayout = findViewById(R.id.appbar_layout);
+            toolbar = findViewById(R.id.toolbar);
+            findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
+            tabsLinearLayout = findViewById(R.id.tabs_linearlayout);
+            tabLayout = findViewById(R.id.tablayout);
+            swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
+            webViewPager = findViewById(R.id.webviewpager);
+            NavigationView navigationView = findViewById(R.id.navigationview);
+            bookmarksDrawerPinnedImageView = findViewById(R.id.bookmarks_drawer_pinned_imageview);
+            fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout);
 
-        // Get handles for the navigation menu items.
-        navigationBackMenuItem = navigationMenu.findItem(R.id.back);
-        navigationForwardMenuItem = navigationMenu.findItem(R.id.forward);
-        navigationHistoryMenuItem = navigationMenu.findItem(R.id.history);
-        navigationRequestsMenuItem = navigationMenu.findItem(R.id.requests);
+            // Get a handle for the navigation menu.
+            Menu navigationMenu = navigationView.getMenu();
 
-        // Listen for touches on the navigation menu.
-        navigationView.setNavigationItemSelectedListener(this);
+            // Get handles for the navigation menu items.
+            navigationBackMenuItem = navigationMenu.findItem(R.id.back);
+            navigationForwardMenuItem = navigationMenu.findItem(R.id.forward);
+            navigationHistoryMenuItem = navigationMenu.findItem(R.id.history);
+            navigationRequestsMenuItem = navigationMenu.findItem(R.id.requests);
 
-        // Get a handle for the app compat delegate.
-        AppCompatDelegate appCompatDelegate = getDelegate();
+            // Listen for touches on the navigation menu.
+            navigationView.setNavigationItemSelectedListener(this);
 
-        // Set the support action bar.
-        appCompatDelegate.setSupportActionBar(toolbar);
+            // Get a handle for the app compat delegate.
+            AppCompatDelegate appCompatDelegate = getDelegate();
 
-        // Get a handle for the action bar.
-        actionBar = appCompatDelegate.getSupportActionBar();
+            // Set the support action bar.
+            appCompatDelegate.setSupportActionBar(toolbar);
 
-        // Remove the incorrect lint warning below that the action bar might be null.
-        assert actionBar != null;
+            // Get a handle for the action bar.
+            actionBar = appCompatDelegate.getSupportActionBar();
 
-        // Add the custom layout, which shows the URL text bar.
-        actionBar.setCustomView(R.layout.url_app_bar);
-        actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
+            // Remove the incorrect lint warning below that the action bar might be null.
+            assert actionBar != null;
 
-        // Get handles for the views in the URL app bar.
-        urlRelativeLayout = findViewById(R.id.url_relativelayout);
-        urlEditText = findViewById(R.id.url_edittext);
+            // Add the custom layout, which shows the URL text bar.
+            actionBar.setCustomView(R.layout.url_app_bar);
+            actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
 
-        // Create the hamburger icon at the start of the AppBar.
-        actionBarDrawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.open_navigation_drawer, R.string.close_navigation_drawer);
+            // Get handles for the views in the URL app bar.
+            urlRelativeLayout = findViewById(R.id.url_relativelayout);
+            urlEditText = findViewById(R.id.url_edittext);
 
-        // Initially disable the sliding drawers.  They will be enabled once the blocklists are loaded.
-        drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
+            // Create the hamburger icon at the start of the AppBar.
+            actionBarDrawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.open_navigation_drawer, R.string.close_navigation_drawer);
 
-        // Initially hide the user interface so that only the blocklist loading screen is shown (if reloading).
-        drawerLayout.setVisibility(View.GONE);
+            // Initially disable the sliding drawers.  They will be enabled once the blocklists are loaded.
+            drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
 
-        // Initialize the web view pager adapter.
-        webViewPagerAdapter = new WebViewPagerAdapter(getSupportFragmentManager());
+            // Initially hide the user interface so that only the blocklist loading screen is shown (if reloading).
+            drawerLayout.setVisibility(View.GONE);
 
-        // Set the pager adapter on the web view pager.
-        webViewPager.setAdapter(webViewPagerAdapter);
+            // Initialize the web view pager adapter.
+            webViewPagerAdapter = new WebViewPagerAdapter(getSupportFragmentManager());
 
-        // Store up to 100 tabs in memory.
-        webViewPager.setOffscreenPageLimit(100);
+            // Set the pager adapter on the web view pager.
+            webViewPager.setAdapter(webViewPagerAdapter);
 
-        // Instantiate the helpers.
-        bookmarksDatabaseHelper = new BookmarksDatabaseHelper(this);
-        domainsDatabaseHelper = new DomainsDatabaseHelper(this);
-        proxyHelper = new ProxyHelper();
-        sanitizeUrlHelper = new SanitizeUrlHelper();
+            // Store up to 100 tabs in memory.
+            webViewPager.setOffscreenPageLimit(100);
 
-        // Initialize the app.
-        initializeApp();
+            // Instantiate the helpers.
+            bookmarksDatabaseHelper = new BookmarksDatabaseHelper(this);
+            domainsDatabaseHelper = new DomainsDatabaseHelper(this);
+            proxyHelper = new ProxyHelper();
 
-        // Apply the app settings from the shared preferences.
-        applyAppSettings();
+            // Update the bookmarks drawer pinned image view.
+            updateBookmarksDrawerPinnedImageView();
 
-        // Control what the system back command does.
-        OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(true) {
-            @Override
-            public void handleOnBackPressed() {
-                // Process the different back options.
-                if (drawerLayout.isDrawerVisible(GravityCompat.START)) {  // The navigation drawer is open.
-                    // Close the navigation drawer.
-                    drawerLayout.closeDrawer(GravityCompat.START);
-                } else if (drawerLayout.isDrawerVisible(GravityCompat.END)){  // The bookmarks drawer is open.
-                    // close the bookmarks drawer.
-                    drawerLayout.closeDrawer(GravityCompat.END);
-                } else if (displayingFullScreenVideo) {  // A full screen video is shown.
-                    // Exit the full screen video.
-                    exitFullScreenVideo();
-                } else if (currentWebView.canGoBack()) {  // There is at least one item in the current WebView history.
-                    // 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, false);
-
-                    // Go back.
-                    currentWebView.goBack();
-                } else if (tabLayout.getTabCount() > 1) {  // There are at least two tabs.
-                    // Close the current tab.
-                    closeCurrentTab();
-                } else {  // There isn't anything to do in Privacy Browser.
-                    // Close Privacy Browser.  `finishAndRemoveTask()` also removes Privacy Browser from the recent app list.
-                    finishAndRemoveTask();
-
-                    // Manually kill Privacy Browser.  Otherwise, it is glitchy when restarted.
-                    System.exit(0);
+            // Initialize the app.
+            initializeApp();
+
+            // Apply the app settings from the shared preferences.
+            applyAppSettings();
+
+            // Control what the system back command does.
+            OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(true) {
+                @Override
+                public void handleOnBackPressed() {
+                    // Process the different back options.
+                    if (drawerLayout.isDrawerVisible(GravityCompat.START)) {  // The navigation drawer is open.
+                        // Close the navigation drawer.
+                        drawerLayout.closeDrawer(GravityCompat.START);
+                    } else if (drawerLayout.isDrawerVisible(GravityCompat.END)) {  // The bookmarks drawer is open.
+                        // close the bookmarks drawer.
+                        drawerLayout.closeDrawer(GravityCompat.END);
+                    } else if (displayingFullScreenVideo) {  // A full screen video is shown.
+                        // Exit the full screen video.
+                        exitFullScreenVideo();
+                        // It shouldn't be possible for the currentWebView to be null, but crash logs indicate it sometimes happens.
+                    } else if ((currentWebView != null) && (currentWebView.canGoBack())) {  // There is at least one item in the current WebView history.
+                        // 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, false);
+
+                        // Go back.
+                        currentWebView.goBack();
+                    } else if (tabLayout.getTabCount() > 1) {  // There are at least two tabs.
+                        // Close the current tab.
+                        closeCurrentTab();
+                    } else {  // There isn't anything to do in Privacy Browser.
+                        // Run clear and exit.
+                        clearAndExit();
+                    }
                 }
-            }
-        };
+            };
+
+            // Register the on back pressed callback.
+            getOnBackPressedDispatcher().addCallback(this, onBackPressedCallback);
 
-        // Register the on back pressed callback.
-        getOnBackPressedDispatcher().addCallback(this, onBackPressedCallback);
+            // Instantiate the populate blocklists coroutine.
+            PopulateBlocklistsCoroutine populateBlocklistsCoroutine = new PopulateBlocklistsCoroutine(this);
 
-        // Populate the blocklists.
-        populateBlocklists = new PopulateBlocklists(this, this).execute();
+            // Populate the blocklists.
+            populateBlocklistsCoroutine.populateBlocklists(this);
+        }
     }
 
     @Override
@@ -736,6 +751,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 }
             }
         } else {  // The app has been restarted.
+            // Set the saved tab position to be the size of the saved state array list.  The tab position is 0 based, meaning the at the new tab will be the tab position that is restored.
+            savedTabPosition = savedStateArrayList.size();
+
             // Replace the intent that started the app with this one.  This will load the tab after the others have been restored.
             setIntent(intent);
         }
@@ -806,21 +824,23 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Run the default commands.
         super.onStart();
 
-        // Resume any WebViews.
-        for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
-            // Get the WebView tab fragment.
-            WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
+        // Resume any WebViews if the pager adapter exists.  If the app is restarting to change the initial app theme it won't have been populated yet.
+        if (webViewPagerAdapter != null) {
+            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();
+                // Get the fragment view.
+                View fragmentView = webViewTabFragment.getView();
 
-            // Only resume the WebViews if they exist (they won't when the app is first created).
-            if (fragmentView != null) {
-                // Get the nested scroll WebView from the tab fragment.
-                NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
+                // Only resume the WebViews if they exist (they won't when the app is first created).
+                if (fragmentView != null) {
+                    // Get the nested scroll WebView from the tab fragment.
+                    NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
 
-                // Resume the nested scroll WebView.
-                nestedScrollWebView.onResume();
+                    // Resume the nested scroll WebView.
+                    nestedScrollWebView.onResume();
+                }
             }
         }
 
@@ -849,10 +869,10 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Show any pending dialogs.
         for (int i = 0; i < pendingDialogsArrayList.size(); i++) {
             // Get the pending dialog from the array list.
-            PendingDialog pendingDialog = pendingDialogsArrayList.get(i);
+            PendingDialogDataClass pendingDialogDataClass = pendingDialogsArrayList.get(i);
 
             // Show the pending dialog.
-            pendingDialog.dialogFragment.show(getSupportFragmentManager(), pendingDialog.tag);
+            pendingDialogDataClass.dialogFragment.show(getSupportFragmentManager(), pendingDialogDataClass.tag);
         }
 
         // Clear the pending dialogs array list.
@@ -865,20 +885,24 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Run the default commands.
         super.onStop();
 
-        for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
-            // Get the WebView tab fragment.
-            WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
+        // Only pause the WebViews if the pager adapter is not null, which is the case if the app is restarting to change the initial app theme.
+        if (webViewPagerAdapter != null) {
+            // Pause each web view.
+            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();
+                // Get the fragment view.
+                View fragmentView = webViewTabFragment.getView();
 
-            // Only pause the WebViews if they exist (they won't when the app is first created).
-            if (fragmentView != null) {
-                // Get the nested scroll WebView from the tab fragment.
-                NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
+                // Only pause the WebViews if they exist (they won't when the app is first created).
+                if (fragmentView != null) {
+                    // Get the nested scroll WebView from the tab fragment.
+                    NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
 
-                // Pause the nested scroll WebView.
-                nestedScrollWebView.onPause();
+                    // Pause the nested scroll WebView.
+                    nestedScrollWebView.onPause();
+                }
             }
         }
 
@@ -893,43 +917,47 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Run the default commands.
         super.onSaveInstanceState(savedInstanceState);
 
-        // Create the saved state array lists.
-        ArrayList<Bundle> savedStateArrayList = new ArrayList<>();
-        ArrayList<Bundle> savedNestedScrollWebViewStateArrayList = new ArrayList<>();
+        // Only save the instance state if the WebView pager adapter is not null, which will be the case if the app is restarting to change the initial app theme.
+        if (webViewPagerAdapter != null) {
+            // Create the saved state array lists.
+            ArrayList<Bundle> savedStateArrayList = new ArrayList<>();
+            ArrayList<Bundle> savedNestedScrollWebViewStateArrayList = new ArrayList<>();
 
-        // Get the URLs from each tab.
-        for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
-            // Get the WebView tab fragment.
-            WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
+            // Get the URLs from each tab.
+            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();
+                // Get the fragment view.
+                View fragmentView = webViewTabFragment.getView();
 
-            if (fragmentView != null) {
-                // Get the nested scroll WebView from the tab fragment.
-                NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
+                if (fragmentView != null) {
+                    // Get the nested scroll WebView from the tab fragment.
+                    NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
 
-                // Create saved state bundle.
-                Bundle savedStateBundle = new Bundle();
+                    // Create saved state bundle.
+                    Bundle savedStateBundle = new Bundle();
 
-                // Get the current states.
-                nestedScrollWebView.saveState(savedStateBundle);
-                Bundle savedNestedScrollWebViewStateBundle = nestedScrollWebView.saveNestedScrollWebViewState();
+                    // Get the current states.
+                    nestedScrollWebView.saveState(savedStateBundle);
+                    Bundle savedNestedScrollWebViewStateBundle = nestedScrollWebView.saveNestedScrollWebViewState();
 
-                // Store the saved states in the array lists.
-                savedStateArrayList.add(savedStateBundle);
-                savedNestedScrollWebViewStateArrayList.add(savedNestedScrollWebViewStateBundle);
+                    // Store the saved states in the array lists.
+                    savedStateArrayList.add(savedStateBundle);
+                    savedNestedScrollWebViewStateArrayList.add(savedNestedScrollWebViewStateBundle);
+                }
             }
-        }
 
-        // Get the current tab position.
-        int currentTabPosition = tabLayout.getSelectedTabPosition();
+            // Get the current tab position.
+            int currentTabPosition = tabLayout.getSelectedTabPosition();
 
-        // Store the saved states in the bundle.
-        savedInstanceState.putParcelableArrayList(SAVED_STATE_ARRAY_LIST, savedStateArrayList);
-        savedInstanceState.putParcelableArrayList(SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST, savedNestedScrollWebViewStateArrayList);
-        savedInstanceState.putInt(SAVED_TAB_POSITION, currentTabPosition);
-        savedInstanceState.putString(PROXY_MODE, proxyMode);
+            // Store the saved states in the bundle.
+            savedInstanceState.putBoolean(BOOKMARKS_DRAWER_PINNED, bookmarksDrawerPinned);
+            savedInstanceState.putString(PROXY_MODE, proxyMode);
+            savedInstanceState.putParcelableArrayList(SAVED_STATE_ARRAY_LIST, savedStateArrayList);
+            savedInstanceState.putParcelableArrayList(SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST, savedNestedScrollWebViewStateArrayList);
+            savedInstanceState.putInt(SAVED_TAB_POSITION, currentTabPosition);
+        }
     }
 
     @Override
@@ -949,11 +977,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             bookmarksDatabaseHelper.close();
         }
 
-        // Stop populating the blocklists if the AsyncTask is running in the background.
-        if (populateBlocklists != null) {
-            populateBlocklists.cancel(true);
-        }
-
         // Run the default commands.
         super.onDestroy();
     }
@@ -1021,6 +1044,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Disable the clear form data menu item if the API >= 26 so that the status of the main Clear Data is calculated correctly.
         optionsClearFormDataMenuItem.setEnabled(Build.VERSION.SDK_INT < 26);
 
+        // Only display the dark WebView menu item if the API >= 29.
+        optionsDarkWebViewMenuItem.setVisible(Build.VERSION.SDK_INT >= 29);
+
         // Set the status of the additional app bar icons.  Setting the refresh menu item to `SHOW_AS_ACTION_ALWAYS` makes it appear even on small devices like phones.
         if (displayAdditionalAppBarIcons) {
             optionsRefreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
@@ -1101,16 +1127,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             // Get the current theme status.
             int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
 
-            // Enable dark WebView if the API is < 33 or if night mode is enabled.
-            optionsDarkWebViewMenuItem.setEnabled((Build.VERSION.SDK_INT < 33) || (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES));
+            // Enable dark WebView if night mode is enabled.
+            optionsDarkWebViewMenuItem.setEnabled(currentThemeStatus == Configuration.UI_MODE_NIGHT_YES);
 
-            // Set the checkbox status for dark WebView if the WebView supports it.
-            if ((Build.VERSION.SDK_INT >= 33) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {  // The device is running API >= 33 and algorithmic darkening is supported.
+            // Set the checkbox status for dark WebView if the device is running API >= 29 and algorithmic darkening is supported.
+            if ((Build.VERSION.SDK_INT >= 29) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING))
                 optionsDarkWebViewMenuItem.setChecked(WebSettingsCompat.isAlgorithmicDarkeningAllowed(currentWebView.getSettings()));
-            } else if ((Build.VERSION.SDK_INT < 33) && WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {  // The device is running API < 33 and the WebView supports force dark.
-                //noinspection deprecation
-                optionsDarkWebViewMenuItem.setChecked(WebSettingsCompat.getForceDark(currentWebView.getSettings()) == WebSettingsCompat.FORCE_DARK_ON);
-            }
         }
 
         // Set the cookies menu item checked status.
@@ -1787,23 +1809,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             // Consume the event.
             return true;
         } else if (menuItemId == R.id.dark_webview) {  // Dark WebView.
-            // Check to see if dark WebView is supported by this WebView.
-            if ((Build.VERSION.SDK_INT >= 33) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {  // The device is running API >= 33 and algorithmic darkening is supported.
-                // Toggle algorithmic darkening.
+            // Toggle dark WebView if supported.
+            if ((Build.VERSION.SDK_INT >= 29) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING))
                 WebSettingsCompat.setAlgorithmicDarkeningAllowed(currentWebView.getSettings(), !WebSettingsCompat.isAlgorithmicDarkeningAllowed(currentWebView.getSettings()));
-            } else if ((Build.VERSION.SDK_INT < 33) && WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {  // The device is running API < 33 and the WebView supports force dark.
-                // Toggle the dark WebView setting.
-                //noinspection deprecation
-                if (WebSettingsCompat.getForceDark(currentWebView.getSettings()) == WebSettingsCompat.FORCE_DARK_ON) {  // Dark WebView is currently enabled.
-                    // Turn off dark WebView.
-                    //noinspection deprecation
-                    WebSettingsCompat.setForceDark(currentWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
-                } else {  // Dark WebView is currently disabled.
-                    // Turn on dark WebView.
-                    //noinspection deprecation
-                    WebSettingsCompat.setForceDark(currentWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
-                }
-            }
 
             // Consume the event.
             return true;
@@ -1861,8 +1869,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 downloadUrlWithExternalApp(currentWebView.getCurrentUrl());
             } else {  // Handle the download inside of Privacy Browser.
                 // 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(), currentWebView.getSettings().getUserAgentString(),
-                        currentWebView.getAcceptCookies()).execute(currentWebView.getCurrentUrl());
+                PrepareSaveDialogCoroutine.prepareSaveDialog(this, getSupportFragmentManager(), currentWebView.getCurrentUrl(), currentWebView.getSettings().getUserAgentString(),
+                        currentWebView.getAcceptCookies());
             }
 
             // Consume the event.
@@ -1882,7 +1890,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         } else if (menuItemId == R.id.add_to_homescreen) {  // Add to homescreen.
             // Instantiate the create home screen shortcut dialog.
             DialogFragment createHomeScreenShortcutDialogFragment = CreateHomeScreenShortcutDialog.createDialog(currentWebView.getTitle(), currentWebView.getUrl(),
-                    currentWebView.getFavoriteOrDefaultIcon());
+                    currentWebView.getFavoriteIcon());
 
             // Show the create home screen shortcut dialog.
             createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getString(R.string.create_shortcut));
@@ -2277,7 +2285,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         super.onPostCreate(savedInstanceState);
 
         // Sync the state of the DrawerToggle after the default `onRestoreInstanceState()` has finished.  This creates the navigation drawer icon.
-        actionBarDrawerToggle.syncState();
+        // If the app is restarting to change the app theme the action bar drawer toggle will not yet be populated.
+        if (actionBarDrawerToggle != null)
+            actionBarDrawerToggle.syncState();
     }
 
     @Override
@@ -2358,8 +2368,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                         downloadUrlWithExternalApp(linkUrl);
                     } else {  // Handle the download inside of Privacy Browser.
                         // 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(), currentWebView.getSettings().getUserAgentString(),
-                                currentWebView.getAcceptCookies()).execute(linkUrl);
+                        PrepareSaveDialogCoroutine.prepareSaveDialog(this, getSupportFragmentManager(), linkUrl, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptCookies());
                     }
 
                     // Consume the event.
@@ -2430,8 +2439,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                         downloadUrlWithExternalApp(imageUrl);
                     } else {  // Handle the download inside of Privacy Browser.
                         // 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(), currentWebView.getSettings().getUserAgentString(),
-                                currentWebView.getAcceptCookies()).execute(imageUrl);
+                        PrepareSaveDialogCoroutine.prepareSaveDialog(this, getSupportFragmentManager(), imageUrl, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptCookies());
                     }
 
                     // Consume the event.
@@ -2535,8 +2543,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                         downloadUrlWithExternalApp(imageUrl);
                     } else {  // Handle the download inside of Privacy Browser.
                         // 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(), currentWebView.getSettings().getUserAgentString(),
-                                currentWebView.getAcceptCookies()).execute(imageUrl);
+                        PrepareSaveDialogCoroutine.prepareSaveDialog(this, getSupportFragmentManager(), imageUrl, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptCookies());
                     }
 
                     // Consume the event.
@@ -2562,8 +2569,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                         downloadUrlWithExternalApp(linkUrl);
                     } else {  // Handle the download inside of Privacy Browser.
                         // 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(), currentWebView.getSettings().getUserAgentString(),
-                                currentWebView.getAcceptCookies()).execute(linkUrl);
+                        PrepareSaveDialogCoroutine.prepareSaveDialog(this, getSupportFragmentManager(), linkUrl, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptCookies());
                     }
 
                     // Consume the event.
@@ -2731,144 +2737,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         bookmarksListView.setSelection(0);
     }
 
-    @Override
-    public void onSaveBookmarkFolder(DialogFragment dialogFragment, int selectedFolderDatabaseId, @NonNull Bitmap favoriteIconBitmap) {
-        // Remove the incorrect lint warning below that the dialog fragment might be null.
-        assert dialogFragment != null;
-
-        // 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.
-        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();
-
-        // Check if the favorite icon has changed.
-        if (currentFolderIconRadioButton.isChecked()) {  // Only the name has changed.
-            // Update the name in the database.
-            bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString);
-        } else if (!currentFolderIconRadioButton.isChecked() && newFolderNameString.equals(oldFolderNameString)) {  // Only the icon has changed.
-            // Create the new folder icon Bitmap.
-            Bitmap folderIconBitmap;
-
-            // Populate the new folder icon bitmap.
-            if (defaultFolderIconRadioButton.isChecked()) {
-                // Get the default folder icon drawable.
-                Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
-
-                // Convert the folder icon drawable to a bitmap drawable.
-                BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
-
-                // Convert the folder icon bitmap drawable to a bitmap.
-                folderIconBitmap = folderIconBitmapDrawable.getBitmap();
-            } else {  // Use the `WebView` favorite icon.
-                // Copy the favorite icon bitmap to the folder icon bitmap.
-                folderIconBitmap = favoriteIconBitmap;
-            }
-
-            // Create a folder icon byte array output stream.
-            ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
-
-            // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
-            folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
-
-            // Convert the folder icon byte array stream to a byte array.
-            byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
-
-            // 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.
-            Bitmap folderIconBitmap;
-            if (defaultFolderIconRadioButton.isChecked()) {
-                // Get the default folder icon drawable.
-                Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
-
-                // Convert the folder icon drawable to a bitmap drawable.
-                BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
-
-                // Convert the folder icon bitmap drawable to a bitmap.
-                folderIconBitmap = folderIconBitmapDrawable.getBitmap();
-            } else {  // Use the `WebView` favorite icon.
-                // Copy the favorite icon bitmap to the folder icon bitmap.
-                folderIconBitmap = favoriteIconBitmap;
-            }
-
-            // Create a folder icon byte array output stream.
-            ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
-
-            // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
-            folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
-
-            // Convert the folder icon byte array stream to a byte array.
-            byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
-
-            // Update the folder name and icon in the database.
-            bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, newFolderIconByteArray);
-        }
-
-        // Update the bookmarks cursor with the current contents of this folder.
-        bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
-
-        // Update the list view.
-        bookmarksCursorAdapter.changeCursor(bookmarksCursor);
-    }
-
-    // Process the results of a file browse.
-    @Override
-    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 BROWSE_FILE_UPLOAD_REQUEST_CODE:
-                // Pass the file to the WebView.
-                fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, returnedIntent));
-                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);
-
-                        // Get the file name URI from the intent.
-                        Uri fileNameUri = returnedIntent.getData();
-
-                        // Get the file name string from the URI.
-                        String fileNameString = fileNameUri.toString();
-
-                        // Set the file name text.
-                        fileNameEditText.setText(fileNameString);
-
-                        // Move the cursor to the end of the file name edit text.
-                        fileNameEditText.setSelection(fileNameString.length());
-                    }
-                }
-                break;
-        }
-    }
-
     private void loadUrlFromTextBox() {
         // Get the text from urlTextBox and convert it to a string.  trim() removes white spaces from the beginning and end of the string.
         String unformattedUrlString = urlEditText.getText().toString().trim();
@@ -3126,7 +2994,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Remove the formatting from the URL edit text when the user is editing the text.
         urlEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
             if (hasFocus) {  // The user is editing the URL text box.
-                // Remove the highlighting.
+                // Remove the syntax highlighting.
                 urlEditText.getText().removeSpan(redColorSpan);
                 urlEditText.getText().removeSpan(initialGrayColorSpan);
                 urlEditText.getText().removeSpan(finalGrayColorSpan);
@@ -3134,8 +3002,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Move to the beginning of the string.
                 urlEditText.setSelection(0);
 
-                // Reapply the highlighting.
-                highlightUrlText();
+                // Reapply the syntax highlighting.
+                UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan);
             }
         });
 
@@ -3278,7 +3146,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             @Override
             public void onTabReselected(TabLayout.Tab tab) {
                 // Instantiate the View SSL Certificate dialog.
-                DialogFragment viewSslCertificateDialogFragment = ViewSslCertificateDialog.displayDialog(currentWebView.getWebViewFragmentId(), currentWebView.getFavoriteOrDefaultIcon());
+                DialogFragment viewSslCertificateDialogFragment = ViewSslCertificateDialog.displayDialog(currentWebView.getWebViewFragmentId(), currentWebView.getFavoriteIcon());
 
                 // Display the View SSL Certificate dialog.
                 viewSslCertificateDialogFragment.show(getSupportFragmentManager(), getString(R.string.view_ssl_certificate));
@@ -3294,7 +3162,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Set the launch bookmarks activity FAB to launch the bookmarks activity.
         launchBookmarksActivityFab.setOnClickListener(v -> {
             // Get a copy of the favorite icon bitmap.
-            Bitmap favoriteIconBitmap = currentWebView.getFavoriteOrDefaultIcon();
+            Bitmap favoriteIconBitmap = currentWebView.getFavoriteIcon();
 
             // Create a favorite icon byte array output stream.
             ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
@@ -3321,7 +3189,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Set the create new bookmark folder FAB to display an alert dialog.
         createBookmarkFolderFab.setOnClickListener(v -> {
             // Create a create bookmark folder dialog.
-            DialogFragment createBookmarkFolderDialog = CreateBookmarkFolderDialog.createBookmarkFolder(currentWebView.getFavoriteOrDefaultIcon());
+            DialogFragment createBookmarkFolderDialog = CreateBookmarkFolderDialog.createBookmarkFolder(currentWebView.getFavoriteIcon());
 
             // Show the create bookmark folder dialog.
             createBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.create_folder));
@@ -3330,7 +3198,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Set the create new bookmark FAB to display an alert dialog.
         createBookmarkFab.setOnClickListener(view -> {
             // Instantiate the create bookmark dialog.
-            DialogFragment createBookmarkDialog = CreateBookmarkDialog.createBookmark(currentWebView.getUrl(), currentWebView.getTitle(), currentWebView.getFavoriteOrDefaultIcon());
+            DialogFragment createBookmarkDialog = CreateBookmarkDialog.createBookmark(currentWebView.getUrl(), currentWebView.getTitle(), currentWebView.getFavoriteIcon());
 
             // Display the create bookmark dialog.
             createBookmarkDialog.show(getSupportFragmentManager(), getString(R.string.create_bookmark));
@@ -3425,14 +3293,16 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Load the bookmark URL.
                 loadUrl(currentWebView, bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_URL)));
 
-                // Close the bookmarks drawer.
-                drawerLayout.closeDrawer(GravityCompat.END);
+                // Close the bookmarks drawer if it is not pinned.
+                if (!bookmarksDrawerPinned)
+                    drawerLayout.closeDrawer(GravityCompat.END);
             }
 
-            // Close the `Cursor`.
+            // Close the cursor.
             bookmarkCursor.close();
         });
 
+        // Handle long-presses on bookmarks.
         bookmarksListView.setOnItemLongClickListener((parent, view, position, id) -> {
             // Convert the database ID from `long` to `int`.
             int databaseId = (int) id;
@@ -3442,14 +3312,23 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
             // Check to see if the bookmark is a folder.
             if (isFolder) {  // The bookmark is a folder.
-                // Save the current folder name, which is used in `onSaveEditBookmarkFolder()`.
-                oldFolderNameString = bookmarksCursor.getString(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME));
+                // Get a cursor of all the bookmarks in the folder.
+                Cursor bookmarksCursor = bookmarksDatabaseHelper.getFolderBookmarks(databaseId);
+
+                // Move to the first entry in the cursor.
+                bookmarksCursor.moveToFirst();
 
-                // Instantiate the edit folder bookmark dialog.
-                DialogFragment editBookmarkFolderDialog = EditBookmarkFolderDialog.folderDatabaseId(databaseId, currentWebView.getFavoriteOrDefaultIcon());
+                // Open each bookmark
+                for (int i = 0; i < bookmarksCursor.getCount(); i++) {
+                    // Load the bookmark in a new tab, moving to the tab for the first bookmark if the drawer is not pinned.
+                    addNewTab(bookmarksCursor.getString(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_URL)), (!bookmarksDrawerPinned && (i == 0)));
+
+                    // Move to the next bookmark.
+                    bookmarksCursor.moveToNext();
+                }
 
-                // Show the edit folder bookmark dialog.
-                editBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.edit_folder));
+                // Close the cursor.
+                bookmarksCursor.close();
             } else {  // The bookmark is not a folder.
                 // Get the bookmark cursor for this ID.
                 Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseId);
@@ -3457,13 +3336,17 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Move the bookmark cursor to the first row.
                 bookmarkCursor.moveToFirst();
 
-                // Load the bookmark in a new tab.
-                addNewTab(bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_URL)), true);
+                // Load the bookmark in a new tab and move to the tab if the drawer is not pinned.
+                addNewTab(bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_URL)), !bookmarksDrawerPinned);
 
-                // Close the bookmarks drawer.
-                drawerLayout.closeDrawer(GravityCompat.END);
+                // Close the cursor.
+                bookmarkCursor.close();
             }
 
+            // Close the bookmarks drawer if it is not pinned.
+            if (!bookmarksDrawerPinned)
+                drawerLayout.closeDrawer(GravityCompat.END);
+
             // Consume the event.
             return true;
         });
@@ -3738,7 +3621,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     TextView tabTitleTextView = tabCustomView.findViewById(R.id.title_textview);
 
                     // Set the default favorite icon as the favorite icon for this tab.
-                    tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(nestedScrollWebView.getFavoriteOrDefaultIcon(), 64, 64, true));
+                    tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(nestedScrollWebView.getFavoriteIcon(), 64, 64, true));
 
                     // Set the loading title text.
                     tabTitleTextView.setText(R.string.loading);
@@ -3951,8 +3834,11 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
                         // Update the swipe refresh layout.
                         if (defaultSwipeToRefresh) {  // Swipe to refresh is enabled.
-                            // Only enable the swipe refresh layout if the WebView is scrolled to the top.  It is updated every time the scroll changes.
-                            swipeRefreshLayout.setEnabled(currentWebView.getScrollY() == 0);
+                            // Update the status of the swipe refresh layout if the current WebView is not null (crash reports indicate that in some unexpected way it sometimes is null).
+                            if (currentWebView != null) {
+                                // Only enable the swipe refresh layout if the WebView is scrolled to the top.  It is updated every time the scroll changes.
+                                swipeRefreshLayout.setEnabled(currentWebView.getScrollY() == 0);
+                            }
                         } else {  // Swipe to refresh is disabled.
                             // Disable the swipe refresh layout.
                             swipeRefreshLayout.setEnabled(false);
@@ -3963,8 +3849,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                         // Store the swipe to refresh status in the nested scroll WebView.
                         nestedScrollWebView.setSwipeToRefresh(true);
 
-                        // Only enable the swipe refresh layout if the WebView is scrolled to the top.  It is updated every time the scroll changes.
-                        swipeRefreshLayout.setEnabled(currentWebView.getScrollY() == 0);
+
+                        // Update the status of the swipe refresh layout if the current WebView is not null (crash reports indicate that in some unexpected way it sometimes is null).
+                        if (currentWebView != null) {
+                            // Only enable the swipe refresh layout if the WebView is scrolled to the top.  It is updated every time the scroll changes.
+                            swipeRefreshLayout.setEnabled(currentWebView.getScrollY() == 0);
+                        }
                         break;
 
                     case DomainsDatabaseHelper.DISABLED:
@@ -3976,8 +3866,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                         break;
                 }
 
-                // Check to see if WebView themes are supported.
-                if ((Build.VERSION.SDK_INT >= 33) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {  // The device is running API >= 33 and algorithmic darkening is supported.
+                // Set the WebView theme if device is running API >= 29 and algorithmic darkening is supported.
+                if ((Build.VERSION.SDK_INT >= 29) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {
                     // Set the WebView theme.
                     switch (webViewThemeInt) {
                         case DomainsDatabaseHelper.SYSTEM_DEFAULT:
@@ -4007,48 +3897,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                             WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.getSettings(), true);
                             break;
                     }
-                } else if ((Build.VERSION.SDK_INT < 33) && WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {  // The device is running API < 33 and the WebView supports force dark.
-                    // Set the WebView theme.
-                    switch (webViewThemeInt) {
-                        case DomainsDatabaseHelper.SYSTEM_DEFAULT:
-                            // Set the WebView theme.  A switch statement cannot be used because the WebView theme entry values string array is not a compile time constant.
-                            if (webViewTheme.equals(webViewThemeEntryValuesStringArray[1])) {  // The light theme is selected.
-                                // Turn off the WebView dark mode.
-                                //noinspection deprecation
-                                WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
-                            } else if (webViewTheme.equals(webViewThemeEntryValuesStringArray[2])) {  // The dark theme is selected.
-                                // Turn on the WebView dark mode.
-                                //noinspection deprecation
-                                WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
-                            } else {  // The system default theme is selected.
-                                // Get the current system theme status.
-                                int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
-
-                                // Set the WebView theme according to the current system theme status.
-                                if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {  // The system is in day mode.
-                                    // Turn off the WebView dark mode.
-                                    //noinspection deprecation
-                                    WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
-                                } else {  // The system is in night mode.
-                                    // Turn on the WebView dark mode.
-                                    //noinspection deprecation
-                                    WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
-                                }
-                            }
-                            break;
-
-                        case DomainsDatabaseHelper.LIGHT_THEME:
-                            // Turn off the WebView dark mode.
-                            //noinspection deprecation
-                            WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
-                            break;
-
-                        case DomainsDatabaseHelper.DARK_THEME:
-                            // Turn on the WebView dark mode.
-                            //noinspection deprecation
-                            WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
-                            break;
-                    }
                 }
 
                 // Set the viewport.
@@ -4119,8 +3967,11 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
                 // Update the swipe refresh layout.
                 if (defaultSwipeToRefresh) {  // Swipe to refresh is enabled.
-                    // Only enable the swipe refresh layout if the WebView is scrolled to the top.  It is updated every time the scroll changes.
-                    swipeRefreshLayout.setEnabled(currentWebView.getScrollY() == 0);
+                    // Update the status of the swipe refresh layout if the current WebView is not null (crash reports indicate that in some unexpected way it sometimes is null).
+                    if (currentWebView != null) {
+                        // Only enable the swipe refresh layout if the WebView is scrolled to the top.  It is updated every time the scroll changes.
+                        swipeRefreshLayout.setEnabled(currentWebView.getScrollY() == 0);
+                    }
                 } else {  // Swipe to refresh is disabled.
                     // Disable the swipe refresh layout.
                     swipeRefreshLayout.setEnabled(false);
@@ -4154,8 +4005,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                         nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
                 }
 
-                // Apply the WebView theme if supported by the installed WebView.
-                if ((Build.VERSION.SDK_INT >= 33) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {  // The device is running API >= 33 and algorithmic darkening is supported.
+                // Set the WebView theme if device is running API >= 29 and algorithmic darkening is supported.
+                if ((Build.VERSION.SDK_INT >= 29) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {
                     // Set the WebView theme.  A switch statement cannot be used because the WebView theme entry values string array is not a compile time constant.
                     if (webViewTheme.equals(webViewThemeEntryValuesStringArray[1])) {  // the light theme is selected.
                         // Turn off algorithmic darkening.
@@ -4170,31 +4021,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                         // Set the algorithmic darkening according to the current system theme status.
                         WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.getSettings(), currentThemeStatus == Configuration.UI_MODE_NIGHT_YES);
                     }
-                } else if ((Build.VERSION.SDK_INT < 33) && WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {  // The device is running API < 33 and the WebView supports force dark.
-                    // Set the WebView theme.  A switch statement cannot be used because the WebView theme entry values string array is not a compile time constant.
-                    if (webViewTheme.equals(webViewThemeEntryValuesStringArray[1])) {  // The light theme is selected.
-                        // Turn off the WebView dark mode.
-                        //noinspection deprecation
-                        WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
-                    } else if (webViewTheme.equals(webViewThemeEntryValuesStringArray[2])) {  // The dark theme is selected.
-                        // Turn on the WebView dark mode.
-                        //noinspection deprecation
-                        WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
-                    } else {  // The system default theme is selected.
-                        // Get the current system theme status.
-                        int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
-
-                        // Set the WebView theme according to the current system theme status.
-                        if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {  // The system is in day mode.
-                            // Turn off the WebView dark mode.
-                            //noinspection deprecation
-                            WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
-                        } else {  // The system is in night mode.
-                            // Turn on the WebView dark mode.
-                            //noinspection deprecation
-                            WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
-                        }
-                    }
                 }
 
                 // Set the viewport.
@@ -4283,7 +4109,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                                 waitingForProxyDialogFragment.show(getSupportFragmentManager(), getString(R.string.waiting_for_proxy_dialog));
                             } catch (Exception waitingForTorException) {
                                 // Add the dialog to the pending dialog array list.  It will be displayed in `onStart()`.
-                                pendingDialogsArrayList.add(new PendingDialog(waitingForProxyDialogFragment, getString(R.string.waiting_for_proxy_dialog)));
+                                pendingDialogsArrayList.add(new PendingDialogDataClass(waitingForProxyDialogFragment, getString(R.string.waiting_for_proxy_dialog)));
                             }
                         }
                     }
@@ -4299,7 +4125,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                             orbotNotInstalledDialogFragment.show(getSupportFragmentManager(), getString(R.string.proxy_not_installed_dialog));
                         } catch (Exception orbotNotInstalledException) {
                             // Add the dialog to the pending dialog array list.  It will be displayed in `onStart()`.
-                            pendingDialogsArrayList.add(new PendingDialog(orbotNotInstalledDialogFragment, getString(R.string.proxy_not_installed_dialog)));
+                            pendingDialogsArrayList.add(new PendingDialogDataClass(orbotNotInstalledDialogFragment, getString(R.string.proxy_not_installed_dialog)));
                         }
                     }
                 }
@@ -4335,7 +4161,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                                 i2pNotInstalledDialogFragment.show(getSupportFragmentManager(), getString(R.string.proxy_not_installed_dialog));
                             } catch (Exception i2pNotInstalledException) {
                                 // Add the dialog to the pending dialog array list.  It will be displayed in `onStart()`.
-                                pendingDialogsArrayList.add(new PendingDialog(i2pNotInstalledDialogFragment, getString(R.string.proxy_not_installed_dialog)));
+                                pendingDialogsArrayList.add(new PendingDialogDataClass(i2pNotInstalledDialogFragment, getString(R.string.proxy_not_installed_dialog)));
                             }
                         }
                     }
@@ -4409,64 +4235,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         }
     }
 
-    private void highlightUrlText() {
-        // Only highlight the URL text if the box is not currently selected.
-        if (!urlEditText.hasFocus()) {
-            // Get the URL string.
-            String urlString = urlEditText.getText().toString();
-
-            // Highlight the URL according to the protocol.
-            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));
-
-                // Create a base URL string.
-                String baseUrl;
-
-                // Get the base URL.
-                if (endOfDomainName > 0) {  // There is at least one character after the base URL.
-                    // Get the base URL.
-                    baseUrl = urlString.substring(0, endOfDomainName);
-                } else {  // There are no characters after the base URL.
-                    // Set the base URL to be the entire URL string.
-                    baseUrl = urlString;
-                }
-
-                // Get the index of the last `.` in the domain.
-                int lastDotIndex = baseUrl.lastIndexOf(".");
-
-                // Get the index of the penultimate `.` in the domain.
-                int penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1);
-
-                // Markup the beginning of the URL.
-                if (urlString.startsWith("http://")) {  // Highlight the protocol of connections that are not encrypted.
-                    urlEditText.getText().setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-
-                    // De-emphasize subdomains.
-                    if (penultimateDotIndex > 0) {  // There is more than one subdomain in the domain name.
-                        urlEditText.getText().setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-                    }
-                } else if (urlString.startsWith("https://")) {  // De-emphasize the protocol of connections that are encrypted.
-                    if (penultimateDotIndex > 0) {  // There is more than one subdomain in the domain name.
-                        // De-emphasize the protocol and the additional subdomains.
-                        urlEditText.getText().setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-                    } else {  // There is only one subdomain in the domain name.
-                        // De-emphasize only the protocol.
-                        urlEditText.getText().setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-                    }
-                }
-
-                // De-emphasize the text after the domain name.
-                if (endOfDomainName > 0) {
-                    urlEditText.getText().setSpan(finalGrayColorSpan, endOfDomainName, urlString.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-                }
-            }
-        }
-    }
-
     private void loadBookmarksFolder() {
         // Update the bookmarks cursor with the contents of the bookmarks database for the current folder.
         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
@@ -4567,11 +4335,11 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     private String sanitizeUrl(String url) {
         // Sanitize tracking queries.
         if (sanitizeTrackingQueries)
-            url = sanitizeUrlHelper.sanitizeTrackingQueries(url);
+            url = SanitizeUrlHelper.sanitizeTrackingQueries(url);
 
         // Sanitize AMP redirects.
         if (sanitizeAmpRedirects)
-            url = sanitizeUrlHelper.sanitizeAmpRedirects(url);
+            url = SanitizeUrlHelper.sanitizeAmpRedirects(url);
 
         // Return the sanitized URL.
         return url;
@@ -4591,7 +4359,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             // Add the first tab.
             addNewTab("", true);
         } else {  // The activity has been restarted.
-            // Restore each tab.  Once the minimum API >= 24, a `forEach()` command can be used.
+            // Restore each tab.
             for (int i = 0; i < savedStateArrayList.size(); i++) {
                 // Add a new tab.
                 tabLayout.addTab(tabLayout.newTab());
@@ -4617,7 +4385,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             if (savedTabPosition == 0) {  // The first tab is selected.
                 // Set the first page as the current WebView.
                 setCurrentWebView(0);
-            } else {  // the first tab is not selected.
+            } else {  // The first tab is not selected.
                 // Move to the selected tab.
                 webViewPager.setCurrentItem(savedTabPosition);
             }
@@ -4981,6 +4749,22 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         }
     }
 
+    public void toggleBookmarksDrawerPinned(View view) {
+        // Toggle the bookmarks drawer pinned tracker.
+        bookmarksDrawerPinned = !bookmarksDrawerPinned;
+
+        // Update the bookmarks drawer pinned image view.
+        updateBookmarksDrawerPinnedImageView();
+    }
+
+    private void updateBookmarksDrawerPinnedImageView() {
+        // Set the current icon.
+        if (bookmarksDrawerPinned)
+            bookmarksDrawerPinnedImageView.setImageResource(R.drawable.pin_selected);
+        else
+            bookmarksDrawerPinnedImageView.setImageResource(R.drawable.pin);
+    }
+
     private void setCurrentWebView(int pageNumber) {
         // Stop the swipe to refresh indicator if it is running
         swipeRefreshLayout.setRefreshing(false);
@@ -5044,8 +4828,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     // Display the current URL in the URL text box.
                     urlEditText.setText(url);
 
-                    // Highlight the URL text.
-                    highlightUrlText();
+                    // Highlight the URL syntax.
+                    UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan);
                 }
             } else {  // A new intent is being loaded.
                 // Reset the loading new intent tracker.
@@ -5060,7 +4844,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Remove any background on the URL relative layout.
                 urlRelativeLayout.setBackground(ResourcesCompat.getDrawable(getResources(), R.color.transparent, null));
             }
-        } else {  // The fragment has not been populated.  Try again in 100 milliseconds.
+        } else if (pageNumber == savedTabPosition){  // The app is being restored but the saved tab position fragment has not been populated yet.  Try again in 100 milliseconds.
             // Create a handler to set the current WebView.
             Handler setCurrentWebViewHandler = new Handler();
 
@@ -5087,8 +4871,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Get the WebView theme entry values string array.
         String[] webViewThemeEntryValuesStringArray = getResources().getStringArray(R.array.webview_theme_entry_values);
 
-        // Apply the WebView theme if supported by the installed WebView.
-        if ((Build.VERSION.SDK_INT >= 33) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {  // The device is running API >= 33 and algorithmic darkening is supported.
+        // Set the WebView theme if device is running API >= 29 and algorithmic darkening is supported.
+        if ((Build.VERSION.SDK_INT >= 29) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {
             // Set the WebView them.  A switch statement cannot be used because the WebView theme entry values string array is not a compile time constant.
             if (webViewTheme.equals(webViewThemeEntryValuesStringArray[1])) {  // The light theme is selected.
                 // Turn off algorithmic darkening.
@@ -5117,39 +4901,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.getSettings(), true);
                 }
             }
-        } else if ((Build.VERSION.SDK_INT < 33) && WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {  // The device is running API < 33 and the WebView supports force dark.
-            // Set the WebView theme.  A switch statement cannot be used because the WebView theme entry values string array is not a compile time constant.
-            if (webViewTheme.equals(webViewThemeEntryValuesStringArray[1])) {  // The light theme is selected.
-                // Turn off the WebView dark mode.
-                //noinspection deprecation
-                WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
-
-                // Make the WebView visible. The WebView was created invisible in `webview_framelayout` to prevent a white background splash in night mode.
-                // If the system is currently in night mode, showing the WebView will be handled in `onProgressChanged()`.
-                nestedScrollWebView.setVisibility(View.VISIBLE);
-            } else if (webViewTheme.equals(webViewThemeEntryValuesStringArray[2])) {  // The dark theme is selected.
-                // Turn on the WebView dark mode.
-                //noinspection deprecation
-                WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
-            } else {  // The system default theme is selected.
-                // Get the current system theme status.
-                int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
-
-                // Set the WebView theme according to the current system theme status.
-                if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {  // The system is in day mode.
-                    // Turn off the WebView dark mode.
-                    //noinspection deprecation
-                    WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
-
-                    // Make the WebView visible. The WebView was created invisible in `webview_framelayout` to prevent a white background splash in night mode.
-                    // If the system is currently in night mode, showing the WebView will be handled in `onProgressChanged()`.
-                    nestedScrollWebView.setVisibility(View.VISIBLE);
-                } else {  // The system is in night mode.
-                    // Turn on the WebView dark mode.
-                    //noinspection deprecation
-                    WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
-                }
-            }
         }
 
         // Get a handle for the activity
@@ -5353,10 +5104,10 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 }
 
                 // Get the file name from the content disposition.
-                String fileNameString = PrepareSaveDialog.getFileNameFromHeaders(this, contentDisposition, mimetype, downloadUrl);
+                String fileNameString = UrlHelper.getFileName(this, contentDisposition, mimetype, downloadUrl);
 
                 // Instantiate the save dialog.
-                DialogFragment saveDialogFragment = SaveDialog.saveUrl(downloadUrl, formattedFileSizeString, fileNameString, userAgent,
+                DialogFragment saveDialogFragment = SaveDialog.saveUrl(downloadUrl, fileNameString, formattedFileSizeString, userAgent,
                         nestedScrollWebView.getAcceptCookies());
 
                 // Try to show the dialog.  The download listener continues to function even when the WebView is paused.  Attempting to display a dialog in that state leads to a crash.
@@ -5365,7 +5116,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     saveDialogFragment.show(getSupportFragmentManager(), getString(R.string.save_dialog));
                 } catch (Exception exception) {  // The dialog could not be shown.
                     // Add the dialog to the pending dialog array list.  It will be displayed in `onStart()`.
-                    pendingDialogsArrayList.add(new PendingDialog(saveDialogFragment, getString(R.string.save_dialog)));
+                    pendingDialogsArrayList.add(new PendingDialogDataClass(saveDialogFragment, getString(R.string.save_dialog)));
                 }
             }
         });
@@ -5445,10 +5196,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             // Set the favorite icon when it changes.
             @Override
             public void onReceivedIcon(WebView view, Bitmap icon) {
-                // Only update the favorite icon if the website has finished loading.
-                if (progressBar.getVisibility() == View.GONE) {
+                // Only update the favorite icon if the website has finished loading and the new favorite icon height is greater than the current favorite icon height.
+                // This prevents low resolution icons from replacing high resolution one.
+                // The check for the visibility of the progress bar can possibly be removed once https://redmine.stoutner.com/issues/747 is fixed.
+                if ((progressBar.getVisibility() == View.GONE) && (icon.getHeight() > nestedScrollWebView.getFavoriteIconHeight())) {
                     // Store the new favorite icon.
-                    nestedScrollWebView.setFavoriteOrDefaultIcon(icon);
+                    nestedScrollWebView.setFavoriteIcon(icon);
 
                     // Get the current page position.
                     int currentPosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
@@ -5559,8 +5312,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
                 // Check to see if the file chooser intent resolves to an installed package.
                 if (fileChooserIntent.resolveActivity(packageManager) != null) {  // The file chooser intent is fine.
-                    // Start the file chooser intent.
-                    startActivityForResult(fileChooserIntent, BROWSE_FILE_UPLOAD_REQUEST_CODE);
+                    // Launch the file chooser intent.
+                    browseFileUploadActivityResultLauncher.launch(fileChooserIntent);
                 } else {  // The file chooser intent will cause a crash.
                     // Create a generic intent to open a chooser.
                     Intent genericFileChooserIntent = new Intent(Intent.ACTION_GET_CONTENT);
@@ -5571,8 +5324,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     // Set the file type to everything.
                     genericFileChooserIntent.setType("*/*");
 
-                    // Start the generic file chooser intent.
-                    startActivityForResult(genericFileChooserIntent, BROWSE_FILE_UPLOAD_REQUEST_CODE);
+                    // Launch the generic file chooser intent.
+                    browseFileUploadActivityResultLauncher.launch(genericFileChooserIntent);
                 }
                 return true;
             }
@@ -5580,9 +5333,11 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
         nestedScrollWebView.setWebViewClient(new WebViewClient() {
             // `shouldOverrideUrlLoading` makes this WebView the default handler for URLs inside the app, so that links are not kicked out to other apps.
-            // The deprecated `shouldOverrideUrlLoading` must be used until API >= 24.
             @Override
-            public boolean shouldOverrideUrlLoading(WebView view, String url) {
+            public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest webResourceRequest) {
+                // Get the URL from the web resource request.
+                String url = webResourceRequest.getUrl().toString();
+
                 // Sanitize the url.
                 url = sanitizeUrl(url);
 
@@ -6061,8 +5816,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     // Display the formatted URL text.
                     urlEditText.setText(url);
 
-                    // Apply text highlighting to the URL text box.
-                    highlightUrlText();
+                    // Highlight the URL syntax.
+                    UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan);
 
                     // Hide the keyboard.
                     inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0);
@@ -6074,8 +5829,13 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Get a URI for the current URL.
                 Uri currentUri = Uri.parse(url);
 
-                // Get the IP addresses for the host.
-                new GetHostIpAddresses(activity, getSupportFragmentManager(), nestedScrollWebView).execute(currentUri.getHost());
+                // Get the current domain name.
+                String currentDomainName = currentUri.getHost();
+
+                if ((currentDomainName != null) && !currentDomainName.isEmpty()) {
+                    // Get the IP addresses for the current URI.
+                    GetHostIpAddressesCoroutine.getAddresses(currentDomainName, nestedScrollWebView, getSupportFragmentManager(), getString(R.string.pinned_mismatch));
+                }
 
                 // 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) {
@@ -6192,8 +5952,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                             // 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();
+                            // Highlight the URL syntax.
+                            UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan);
                         }
 
                         // Only populate the title text view if the tab has been fully created.
@@ -6262,7 +6022,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                         sslCertificateErrorDialogFragment.show(getSupportFragmentManager(), getString(R.string.ssl_certificate_error));
                     } catch (Exception exception) {
                         // Add the dialog to the pending dialog array list.  It will be displayed in `onStart()`.
-                        pendingDialogsArrayList.add(new PendingDialog(sslCertificateErrorDialogFragment, getString(R.string.ssl_certificate_error)));
+                        pendingDialogsArrayList.add(new PendingDialogDataClass(sslCertificateErrorDialogFragment, getString(R.string.ssl_certificate_error)));
                     }
                 }
             }