]> 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 0f412f01d888cd3a638369907e07f1a1c57d21ac..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>.
  *
@@ -56,7 +56,6 @@ 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;
@@ -153,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;
@@ -240,11 +240,6 @@ 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;
 
@@ -263,7 +258,6 @@ 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;
@@ -271,14 +265,17 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     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;
@@ -485,16 +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.
-            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);
-        }
-
         // 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);
 
@@ -510,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.
@@ -527,143 +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);
-        bookmarksDrawerPinnedImageView = findViewById(R.id.bookmarks_drawer_pinned_imageview);
-        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);
 
-        // Update the bookmarks drawer pinned image view.
-        updateBookmarksDrawerPinnedImageView();
+            // Instantiate the helpers.
+            bookmarksDatabaseHelper = new BookmarksDatabaseHelper(this);
+            domainsDatabaseHelper = new DomainsDatabaseHelper(this);
+            proxyHelper = new ProxyHelper();
 
-        // Initialize the app.
-        initializeApp();
+            // Update the bookmarks drawer pinned image view.
+            updateBookmarksDrawerPinnedImageView();
 
-        // Apply the app settings from the shared preferences.
-        applyAppSettings();
+            // Initialize the app.
+            initializeApp();
 
-        // 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();
+            // 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);
+            // Instantiate the populate blocklists coroutine.
+            PopulateBlocklistsCoroutine populateBlocklistsCoroutine = new PopulateBlocklistsCoroutine(this);
 
-        // Populate the blocklists.
-        populateBlocklistsCoroutine.populateBlocklists(this);
+            // Populate the blocklists.
+            populateBlocklistsCoroutine.populateBlocklists(this);
+        }
     }
 
     @Override
@@ -743,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);
         }
@@ -813,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();
+                }
             }
         }
 
@@ -872,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();
+                }
             }
         }
 
@@ -900,44 +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.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);
+            // 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
@@ -2265,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
@@ -2972,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);
@@ -2980,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);
             }
         });
 
@@ -4213,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);
@@ -4371,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;
@@ -4395,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());
@@ -4421,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);
             }
@@ -4864,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.
@@ -4880,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();
 
@@ -5140,10 +5104,10 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 }
 
                 // Get the file name from the content disposition.
-                String fileNameString = PrepareSaveDialogCoroutine.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.
@@ -5232,7 +5196,9 @@ 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 and the new favorite icon height is greater than the current favorite icon height.  This prevents low resolution icons from replacing high resolution one.
+                // 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.setFavoriteIcon(icon);
@@ -5367,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);
 
@@ -5848,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);
@@ -5984,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.