]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blobdiff - app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.kt
Bump the target API to 36. https://redmine.stoutner.com/issues/1283
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / activities / MainWebViewActivity.kt
index b6086b4631889ec806ad3d3e596319f642591827..dc479345a235ae6031bf073a70e87e186193a7ff 100644 (file)
@@ -1,22 +1,22 @@
-/*
- * Copyright 2015-2024 Soren Stoutner <soren@stoutner.com>.
+/* SPDX-License-Identifier: GPL-3.0-or-later
+ * SPDX-FileCopyrightText: 2015-2025 Soren Stoutner <soren@stoutner.com>
  *
  * Download cookie code contributed 2017 Hendrik Knackstedt.  Copyright assigned to Soren Stoutner <soren@stoutner.com>.
  *
- * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
+ * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android/>.
  *
- * Privacy Browser Android is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
+ * This program is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later
+ * version.
  *
- * Privacy Browser Android is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+ * details.
  *
- * You should have received a copy of the GNU General Public License
- * along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>.
+ * You should have received a copy of the GNU General Public License along with
+ * this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
 package com.stoutner.privacybrowser.activities
@@ -63,12 +63,14 @@ import android.view.MenuItem
 import android.view.MotionEvent
 import android.view.View
 import android.view.ViewGroup
+import android.view.WindowInsets
+import android.view.WindowInsetsController
 import android.view.WindowManager
 import android.view.inputmethod.InputMethodManager
 import android.webkit.CookieManager
 import android.webkit.HttpAuthHandler
-import android.webkit.ValueCallback
 import android.webkit.SslErrorHandler
+import android.webkit.ValueCallback
 import android.webkit.WebChromeClient
 import android.webkit.WebResourceRequest
 import android.webkit.WebResourceResponse
@@ -99,6 +101,9 @@ import androidx.appcompat.content.res.AppCompatResources
 import androidx.appcompat.widget.Toolbar
 import androidx.coordinatorlayout.widget.CoordinatorLayout
 import androidx.core.content.ContextCompat
+import androidx.core.graphics.scale
+import androidx.core.net.toUri
+import androidx.core.view.isGone
 import androidx.core.view.GravityCompat
 import androidx.cursoradapter.widget.CursorAdapter
 import androidx.drawerlayout.widget.DrawerLayout
@@ -257,9 +262,12 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
     private lateinit var appBar: ActionBar
     private lateinit var checkFilterListHelper: CheckFilterListHelper
     private lateinit var bookmarksCursorAdapter: CursorAdapter
-    private lateinit var bookmarksListView: ListView
     private lateinit var bookmarksDrawerPinnedImageView: ImageView
+    private lateinit var bookmarksFrameLayout: FrameLayout
+    private lateinit var bookmarksHeaderLinearLayout: LinearLayout
+    private lateinit var bookmarksListView: ListView
     private lateinit var bookmarksTitleTextView: TextView
+    private lateinit var browserFrameLayout: FrameLayout
     private lateinit var coordinatorLayout: CoordinatorLayout
     private lateinit var cookieManager: CookieManager
     private lateinit var defaultFontSizeString: String
@@ -278,12 +286,14 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
     private lateinit var findOnPageLinearLayout: LinearLayout
     private lateinit var fullScreenVideoFrameLayout: FrameLayout
     private lateinit var initialGrayColorSpan: ForegroundColorSpan
+    private lateinit var inputMethodManager: InputMethodManager
     private lateinit var navigationBackMenuItem: MenuItem
     private lateinit var navigationForwardMenuItem: MenuItem
     private lateinit var navigationHistoryMenuItem: MenuItem
     private lateinit var navigationRequestsMenuItem: MenuItem
     private lateinit var navigationScrollToBottomMenuItem: MenuItem
     private lateinit var navigationView: NavigationView
+    private lateinit var oldFullScreenVideoFrameLayout: FrameLayout
     private lateinit var optionsAddOrEditDomainMenuItem: MenuItem
     private lateinit var optionsBlockAllThirdPartyRequestsMenuItem: MenuItem
     private lateinit var optionsClearCookiesMenuItem: MenuItem
@@ -326,7 +336,6 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
     private lateinit var optionsWideViewportMenuItem: MenuItem
     private lateinit var proxyHelper: ProxyHelper
     private lateinit var redColorSpan: ForegroundColorSpan
-    private lateinit var rootFrameLayout: FrameLayout
     private lateinit var saveUrlString: String
     private lateinit var searchURL: String
     private lateinit var sharedPreferences: SharedPreferences
@@ -337,6 +346,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
     private lateinit var webViewDefaultUserAgent: String
     private lateinit var webViewThemeEntryValuesStringArray: Array<String>
     private lateinit var webViewViewPager2: ViewPager2
+    private lateinit var windowInsetsController: WindowInsetsController
     private lateinit var ultraList: ArrayList<List<Array<String>>>
     private lateinit var urlEditText: EditText
     private lateinit var urlRelativeLayout: RelativeLayout
@@ -351,6 +361,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
     private var bookmarksDatabaseHelper: BookmarksDatabaseHelper? = null
     private var bookmarksDrawerPinned = false
     private var bottomAppBar = false
+    private var closeNavigationDrawer = false
     private var currentWebView: NestedScrollWebView? = null
     private var defaultBlockAllThirdPartyRequests = false
     private var defaultCookies = false
@@ -368,7 +379,9 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
     private var defaultUltraPrivacy = true
     private var defaultWideViewport = true
     private var displayAdditionalAppBarIcons = false
+    private var displayUnderCutouts = false
     private var displayingFullScreenVideo = false
+    private var displayingInitialTab = true
     private var domainsDatabaseHelper: DomainsDatabaseHelper? = null
     private var downloadWithExternalApp = false
     private var fullScreenBrowsingModeEnabled = false
@@ -389,17 +402,18 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
     private var savedStateArrayList: ArrayList<Bundle>? = null
     private var savedTabPosition = 0
     private var scrollAppBar = false
+    private var sortBookmarksAlphabetically = false
     private var ultraPrivacy: ArrayList<List<Array<String>>>? = null
     private var waitingForProxy = false
 
-    // Define the save webpage image activity result launcher.  It must be defined before `onCreate()` is run or the app will crash.
+    // Define the browse file upload activity result launcher.  It must be defined before `onCreate()` is run or the app will crash.
     private val browseFileUploadActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult ->
         // Pass the file to the WebView.
         fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(activityResult.resultCode, activityResult.data))
     }
 
     // Define the save URL activity result launcher.  It must be defined before `onCreate()` is run or the app will crash.
-    private val saveUrlActivityResultLauncher = registerForActivityResult<String, Uri>(ActivityResultContracts.CreateDocument("*/*")) { fileUri ->
+    private val saveUrlActivityResultLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("*/*")) { fileUri ->
         // Only save the URL if the file URI is not null, which happens if the user exited the file picker by pressing back.
         if (fileUri != null) {
             // Instantiate the save URL coroutine.
@@ -414,7 +428,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
     }
 
     // Define the save webpage archive activity result launcher.  It must be defined before `onCreate()` is run or the app will crash.
-    private val saveWebpageArchiveActivityResultLauncher = registerForActivityResult<String, Uri>(ActivityResultContracts.CreateDocument("multipart/related")) { fileUri ->
+    private val saveWebpageArchiveActivityResultLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("multipart/related")) { 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) {
             // Get a cursor from the content resolver.
@@ -487,7 +501,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
     }
 
     // Define the save webpage image activity result launcher.  It must be defined before `onCreate()` is run or the app will crash.
-    private val saveWebpageImageActivityResultLauncher = registerForActivityResult<String, Uri>(ActivityResultContracts.CreateDocument("image/png")) { fileUri ->
+    private val saveWebpageImageActivityResultLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("image/png")) { fileUri ->
         // Only save the webpage image if the file URI is not null, which happens if the user exited the file picker by pressing back.
         if (fileUri != null) {
             // Instantiate the save webpage image coroutine.
@@ -513,10 +527,10 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
         val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
         bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false)
         displayAdditionalAppBarIcons = sharedPreferences.getBoolean(getString(R.string.display_additional_app_bar_icons_key), false)
-        val displayUnderCutouts = sharedPreferences.getBoolean(getString(R.string.display_under_cutouts_key), false)
+        displayUnderCutouts = sharedPreferences.getBoolean(getString(R.string.display_under_cutouts_key), false)
 
-        // Display under cutouts if specified.  This must be done here as it doesn't appear to work correctly if handled after the app is fully initialized.
-        if (displayUnderCutouts) {
+        // Set the display under cutouts mode.  This must be done here as it doesn't appear to work correctly if handled after the app is fully initialized.
+        if ((Build.VERSION.SDK_INT < 35) && displayUnderCutouts) {
             if (Build.VERSION.SDK_INT >= 30)
                 window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
             else if (Build.VERSION.SDK_INT >= 28)
@@ -564,11 +578,11 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                 // Store the saved instance state variables.  The deprecated `getParcelableArrayList` can be upgraded once the minimum API >= 33.
                 bookmarksDrawerPinned = savedInstanceState.getBoolean(BOOKMARKS_DRAWER_PINNED)
                 @Suppress("DEPRECATION")
-                savedStateArrayList = savedInstanceState.getParcelableArrayList(SAVED_STATE_ARRAY_LIST)
-                @Suppress("DEPRECATION")
                 savedNestedScrollWebViewStateArrayList = savedInstanceState.getParcelableArrayList(SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST)
-                savedTabPosition = savedInstanceState.getInt(SAVED_TAB_POSITION)
                 savedProxyMode = savedInstanceState.getString(PROXY_MODE)
+                @Suppress("DEPRECATION")
+                savedStateArrayList = savedInstanceState.getParcelableArrayList(SAVED_STATE_ARRAY_LIST)
+                savedTabPosition = savedInstanceState.getInt(SAVED_TAB_POSITION)
             }
 
             // 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.
@@ -581,7 +595,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                 setContentView(R.layout.main_framelayout_top_appbar)
 
             // Get handles for the views.
-            rootFrameLayout = findViewById(R.id.root_framelayout)
+            browserFrameLayout = findViewById(R.id.browser_framelayout)
             drawerLayout = findViewById(R.id.drawerlayout)
             coordinatorLayout = findViewById(R.id.coordinatorlayout)
             appBarLayout = findViewById(R.id.appbar_layout)
@@ -594,11 +608,28 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
             swipeRefreshLayout = findViewById(R.id.swiperefreshlayout)
             webViewViewPager2 = findViewById(R.id.webview_viewpager2)
             navigationView = findViewById(R.id.navigationview)
+            bookmarksFrameLayout = findViewById(R.id.bookmarks_framelayout)
+            bookmarksHeaderLinearLayout = findViewById(R.id.bookmarks_header_linearlayout)
             bookmarksListView = findViewById(R.id.bookmarks_drawer_listview)
             bookmarksTitleTextView = findViewById(R.id.bookmarks_title_textview)
             bookmarksDrawerPinnedImageView = findViewById(R.id.bookmarks_drawer_pinned_imageview)
+            oldFullScreenVideoFrameLayout = findViewById(R.id.old_full_screen_video_framelayout)
             fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout)
 
+            // Get a handle for the window inset controller.
+            if (Build.VERSION.SDK_INT >= 30)
+                windowInsetsController = browserFrameLayout.windowInsetsController!!
+
+            // Set the layout to fit the system windows according to the API.
+            if (Build.VERSION.SDK_INT >= 35) {
+                // Set the browser frame layout to fit system windows.
+                browserFrameLayout.fitsSystemWindows = true
+            } else {
+                // Set the layouts to fit system windows.
+                coordinatorLayout.fitsSystemWindows = true
+                bookmarksFrameLayout.fitsSystemWindows = true
+            }
+
             // Get a handle for the navigation menu.
             val navigationMenu = navigationView.menu
 
@@ -628,6 +659,22 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
             urlRelativeLayout = findViewById(R.id.url_relativelayout)
             urlEditText = findViewById(R.id.url_edittext)
 
+            // Store the URL when it is changed.  This enables the restoring of partially-typed URLs when tabs change.
+            urlEditText.addTextChangedListener(object : TextWatcher {
+                override fun beforeTextChanged(charSequence: CharSequence, start: Int, count: Int, after: Int) {
+                    // Do nothing.
+                }
+
+                override fun onTextChanged(charSequence: CharSequence, start: Int, before: Int, count: Int) {
+                    // Do nothing.
+                }
+
+                override fun afterTextChanged(editable: Editable) {
+                    // Store the URL
+                    currentWebView?.currentUrl = editable.toString()
+                }
+            })
+
             // Initially disable the sliding drawers.  They will be enabled once the filter lists are loaded.
             drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
 
@@ -866,8 +913,8 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
         if (proxyMode != ProxyHelper.NONE)
             applyProxy(false)
 
-        // Reapply any system UI flags.
-        if (displayingFullScreenVideo || inFullScreenBrowsingMode) {  // The system is displaying a website or a video in full screen mode.
+        // Reapply any system UI flags on older APIs.
+        if ((Build.VERSION.SDK_INT < 30) && (displayingFullScreenVideo || inFullScreenBrowsingMode)) {  // The system is displaying a website or a video in full screen mode.
             /* Hide the system bars.
              * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
              * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
@@ -875,9 +922,8 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
              * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
              */
 
-            // The deprecated command can be switched to `WindowInsetsController` once the minimum API >= 30.
             @Suppress("DEPRECATION")
-            rootFrameLayout.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
+            browserFrameLayout.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
         }
 
         // Show any pending dialogs.
@@ -893,15 +939,15 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
         pendingDialogsArrayList.clear()
     }
 
-    public override fun onSaveInstanceState(savedInstanceState: Bundle) {
+    public override fun onSaveInstanceState(outState: Bundle) {
         // Run the default commands.
-        super.onSaveInstanceState(savedInstanceState)
+        super.onSaveInstanceState(outState)
 
         // Only save the instance state if the WebView state adapter is not null, which will be the case if the app is restarting to change the initial app theme.
         if (webViewStateAdapter != null) {
             // Initialize the saved state array lists.
-            savedStateArrayList = ArrayList<Bundle>()
-            savedNestedScrollWebViewStateArrayList = ArrayList<Bundle>()
+            savedStateArrayList = ArrayList()
+            savedNestedScrollWebViewStateArrayList = ArrayList()
 
             // Get the URLs from each tab.
             for (i in 0 until webViewStateAdapter!!.itemCount) {
@@ -933,11 +979,11 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
             val currentTabPosition = tabLayout.selectedTabPosition
 
             // Store the saved states in the bundle.
-            savedInstanceState.putBoolean(BOOKMARKS_DRAWER_PINNED, bookmarksDrawerPinned)
-            savedInstanceState.putString(PROXY_MODE, proxyMode)
-            savedInstanceState.putParcelableArrayList(SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST, savedNestedScrollWebViewStateArrayList)
-            savedInstanceState.putParcelableArrayList(SAVED_STATE_ARRAY_LIST, savedStateArrayList)
-            savedInstanceState.putInt(SAVED_TAB_POSITION, currentTabPosition)
+            outState.putBoolean(BOOKMARKS_DRAWER_PINNED, bookmarksDrawerPinned)
+            outState.putString(PROXY_MODE, proxyMode)
+            outState.putParcelableArrayList(SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST, savedNestedScrollWebViewStateArrayList)
+            outState.putParcelableArrayList(SAVED_STATE_ARRAY_LIST, savedStateArrayList)
+            outState.putInt(SAVED_TAB_POSITION, currentTabPosition)
         }
     }
 
@@ -1162,23 +1208,29 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
         val privateDataDirectoryString = applicationInfo.dataDir
 
         // Get the storage directories.
-        val localStorageDirectory = File("$privateDataDirectoryString/app_webview/Local Storage/")
-        val indexedDBDirectory = File("$privateDataDirectoryString/app_webview/IndexedDB")
+        val localStorageDirectory = File("$privateDataDirectoryString/app_webview/Default/Local Storage/")
+        val sessionStorageDirectory = File("$privateDataDirectoryString/app_webview/Default/Session Storage/")
+        val indexedDBDirectory = File("$privateDataDirectoryString/app_webview/Default/IndexedDB")
 
         // Initialize the number of files counters.
         var localStorageDirectoryNumberOfFiles = 0
+        var sessionStorageDirectoryNumberOfFiles = 0
         var indexedDBDirectoryNumberOfFiles = 0
 
         // Get a count of the number of files in the Local Storage directory.  The list can be null, in which case a `0` is returned.
         if (localStorageDirectory.exists())
             localStorageDirectoryNumberOfFiles = (localStorageDirectory.list())?.size ?: 0
 
+        // Get a count of the number of files in the Local Storage directory.  The list can be null, in which case a `0` is returned.
+        if (sessionStorageDirectory.exists())
+            sessionStorageDirectoryNumberOfFiles = (sessionStorageDirectory.list())?.size ?: 0
+
         // Get a count of the number of files in the IndexedDB directory.  The list can be null, in which case a `0` is returned.
         if (indexedDBDirectory.exists())
             indexedDBDirectoryNumberOfFiles = (indexedDBDirectory.list())?.size ?: 0
 
         // Enable Clear DOM Storage if there is any.
-        optionsClearDomStorageMenuItem.isEnabled = localStorageDirectoryNumberOfFiles > 0 || indexedDBDirectoryNumberOfFiles > 0
+        optionsClearDomStorageMenuItem.isEnabled = localStorageDirectoryNumberOfFiles > 0 || sessionStorageDirectoryNumberOfFiles > 0 || indexedDBDirectoryNumberOfFiles > 0
 
         // Enable Clear Data if any of the submenu items are enabled.
         optionsClearDataMenuItem.isEnabled = (optionsClearCookiesMenuItem.isEnabled || optionsClearDomStorageMenuItem.isEnabled)
@@ -1450,11 +1502,8 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                     .addCallback(object : Snackbar.Callback() {
                         override fun onDismissed(snackbar: Snackbar, event: Int) {
                             if (event != DISMISS_EVENT_ACTION) {  // The snackbar was dismissed without the undo button being pushed.
-                                // Get a handle for the web storage.
-                                val webStorage = WebStorage.getInstance()
-
-                                // Delete the DOM Storage.
-                                webStorage.deleteAllData()
+                                // Ask web storage to clear the DOM storage.
+                                WebStorage.getInstance().deleteAllData()
 
                                 // Initialize a handler to manually delete the DOM storage files and directories.
                                 val deleteDomStorageHandler = Handler(Looper.getMainLooper())
@@ -1470,20 +1519,26 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                                         val privateDataDirectoryString = applicationInfo.dataDir
 
                                         // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
-                                        val deleteLocalStorageProcess = runtime.exec(arrayOf("rm", "-rf", "$privateDataDirectoryString/app_webview/Local Storage/"))
+                                        val deleteLocalStorageProcess = runtime.exec(arrayOf("rm", "-rf", "$privateDataDirectoryString/app_webview/Default/Local Storage/"))
+                                        val deleteSessionStorageProcess = runtime.exec(arrayOf("rm", "-rf", "$privateDataDirectoryString/app_webview/Default/Session Storage/"))
+                                        val deleteWebDataProcess = runtime.exec(arrayOf("rm", "-rf", "$privateDataDirectoryString/app_webview/Default/Web Data"))
+                                        val deleteWebDataJournalProcess = runtime.exec(arrayOf("rm", "-rf", "$privateDataDirectoryString/app_webview/Default/Web Data-journal"))
 
                                         // Multiple commands must be used because `Runtime.exec()` does not like `*`.
-                                        val deleteIndexProcess = runtime.exec("rm -rf $privateDataDirectoryString/app_webview/IndexedDB")
-                                        val deleteQuotaManagerProcess = runtime.exec("rm -f $privateDataDirectoryString/app_webview/QuotaManager")
-                                        val deleteQuotaManagerJournalProcess = runtime.exec("rm -f $privateDataDirectoryString/app_webview/QuotaManager-journal")
-                                        val deleteDatabasesProcess = runtime.exec("rm -rf $privateDataDirectoryString/app_webview/databases")
+                                        val deleteIndexProcess = runtime.exec("rm -rf $privateDataDirectoryString/app_webview/Default/IndexedDB")
+                                        val deleteWebStorageProcess = runtime.exec("rm -rf $privateDataDirectoryString/app_webview/Default/WebStorage")
+                                        val deleteDatabasesProcess = runtime.exec("rm -rf $privateDataDirectoryString/app_webview/Default/databases")
+                                        val deleteBlobStorageProcess = runtime.exec("rm -rf $privateDataDirectoryString/app_webview/Default/blob_storage")
 
                                         // Wait for the processes to finish.
                                         deleteLocalStorageProcess.waitFor()
+                                        deleteSessionStorageProcess.waitFor()
+                                        deleteWebDataProcess.waitFor()
+                                        deleteWebDataJournalProcess.waitFor()
                                         deleteIndexProcess.waitFor()
-                                        deleteQuotaManagerProcess.waitFor()
-                                        deleteQuotaManagerJournalProcess.waitFor()
+                                        deleteWebStorageProcess.waitFor()
                                         deleteDatabasesProcess.waitFor()
+                                        deleteBlobStorageProcess.waitFor()
                                     } catch (exception: Exception) {
                                         // Do nothing if an error is thrown.
                                     }
@@ -1853,9 +1908,6 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                     // Set the focus on the find on page edit text.
                     findOnPageEditText.requestFocus()
 
-                    // Get a handle for the input method manager.
-                    val inputMethodManager = (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
-
                     // Display the keyboard.  `0` sets no input flags.
                     inputMethodManager.showSoftInput(findOnPageEditText, 0)
                 }, 200)
@@ -2051,7 +2103,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                     startActivity(domainsIntent)
                 } else {  // Add a new domain.
                     // Get the current URI.
-                    val currentUri = Uri.parse(currentWebView!!.url)
+                    val currentUri = currentWebView!!.url!!.toUri()
 
                     // Get the current domain from the URI.  Use an empty string if it is null.
                     val currentDomain = currentUri.host?: ""
@@ -2291,7 +2343,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                         val genericFileManagerIntent = Intent(Intent.ACTION_VIEW)
 
                         // Open the download directory.
-                        genericFileManagerIntent.setDataAndType(Uri.parse(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString()), DocumentsContract.Document.MIME_TYPE_DIR)
+                        genericFileManagerIntent.setDataAndType(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString().toUri(), DocumentsContract.Document.MIME_TYPE_DIR)
 
                         // Launch as a new task so that the file manager and Privacy Browser show as separate windows in the recent tasks list.
                         genericFileManagerIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
@@ -2305,7 +2357,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                             val alternateFileManagerIntent = Intent(Intent.ACTION_VIEW)
 
                             // Open the download directory.
-                            alternateFileManagerIntent.setDataAndType(Uri.parse(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString()), "resource/folder")
+                            alternateFileManagerIntent.setDataAndType(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString().toUri(), "resource/folder")
 
                             // Launch as a new task so that the file manager and Privacy Browser show as separate windows in the recent tasks list.
                             alternateFileManagerIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
@@ -2450,6 +2502,15 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                 // Set the target URL as the context menu title.
                 contextMenu.setHeaderTitle(linkUrl)
 
+                // Get a new message from the WebView handler.
+                val hrefMessage = currentWebView!!.handler.obtainMessage()
+
+                // Request the focus node href.
+                currentWebView!!.requestFocusNodeHref(hrefMessage)
+
+                // Get the link text from the href message.
+                val linkText = hrefMessage.data.getString("title")
+
                 // Add an open in new tab entry.
                 contextMenu.add(R.string.open_in_new_tab).setOnMenuItemClickListener {
                     // Load the link URL in a new tab and move to it.
@@ -2531,6 +2592,20 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                     true
                 }
 
+                // Add a Copy Text entry if the link text is not null.
+                if (linkText != null) {
+                    contextMenu.add(R.string.copy_text).setOnMenuItemClickListener {
+                        // Save the link URL in a clip data.
+                        val srcAnchorTypeTextClipData = ClipData.newPlainText(getString(R.string.copy_text), linkText)
+
+                        // Set the clip data as the clipboard's primary clip.
+                        clipboardManager.setPrimaryClip(srcAnchorTypeTextClipData)
+
+                        // Consume the event.
+                        true
+                    }
+                }
+
                 // Add an empty cancel entry, which by default closes the context menu.
                 contextMenu.add(R.string.cancel)
             }
@@ -2795,13 +2870,22 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                 // Set the target URL as the title of the context menu.
                 contextMenu.setHeaderTitle(linkUrl)
 
+                // Get a new message from the WebView handler.
+                val hrefMessage = currentWebView!!.handler.obtainMessage()
+
+                // Request the focus node href.
+                currentWebView!!.requestFocusNodeHref(hrefMessage)
+
+                // Get the link text from the href message.
+                val linkText = hrefMessage.data.getString("title")
+
                 // Add a write email entry.
                 contextMenu.add(R.string.write_email).setOnMenuItemClickListener {
                     // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
                     val emailIntent = Intent(Intent.ACTION_SENDTO)
 
                     // Parse the url and set it as the data for the intent.
-                    emailIntent.data = Uri.parse("mailto:$linkUrl")
+                    emailIntent.data = "mailto:$linkUrl".toUri()
 
                     // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser.
                     emailIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
@@ -2830,6 +2914,20 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                     true
                 }
 
+                // Add a Copy Text entry if the link text is not null.
+                if (linkText != null) {
+                    contextMenu.add(R.string.copy_text).setOnMenuItemClickListener {
+                        // Save the link URL in a clip data.
+                        val srcEmailTypeTextClipData = ClipData.newPlainText(getString(R.string.copy_text), linkText)
+
+                        // Set the clip data as the clipboard's primary clip.
+                        clipboardManager.setPrimaryClip(srcEmailTypeTextClipData)
+
+                        // Consume the event.
+                        true
+                    }
+                }
+
                 // Add an empty cancel entry, which by default closes the context menu.
                 contextMenu.add(R.string.cancel)
             }
@@ -2944,8 +3042,10 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
         proxyMode = sharedPreferences.getString(getString(R.string.proxy_key), getString(R.string.proxy_default_value))!!
         fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean(getString(R.string.full_screen_browsing_mode_key), false)
         hideAppBar = sharedPreferences.getBoolean(getString(R.string.hide_app_bar_key), true)
+        displayUnderCutouts = sharedPreferences.getBoolean(getString(R.string.display_under_cutouts_key), false)
         val downloadProvider = sharedPreferences.getString(getString(R.string.download_provider_key), getString(R.string.download_provider_default_value))!!
         scrollAppBar = sharedPreferences.getBoolean(getString(R.string.scroll_app_bar_key), false)
+        sortBookmarksAlphabetically = sharedPreferences.getBoolean(getString(R.string.sort_bookmarks_alphabetically_key), false)
 
         // Determine if downloading should be handled by an external app.
         downloadWithExternalApp = (downloadProvider == downloadProviderEntryValuesStringArray[2])
@@ -3034,8 +3134,12 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
             }
         }
 
+        // Force an exit of full screen browsing mode if it is now disabled in settings.
+        if (!fullScreenBrowsingModeEnabled)
+            inFullScreenBrowsingMode = false
+
         // Update the full screen browsing mode settings.
-        if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) {  // Privacy Browser is currently in full screen browsing mode.
+        if (inFullScreenBrowsingMode) {  // Privacy Browser is currently in full screen browsing mode.
             // Update the visibility of the app bar, which might have changed in the settings.
             if (hideAppBar) {
                 // Hide the tab linear layout.
@@ -3051,30 +3155,41 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                 appBar.show()
             }
 
-            /* Hide the system bars.
-             * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
-             * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
-             * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
-             * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
-             */
+            // Re-enforce the fullscreen flags if the API < 30.
+            if (Build.VERSION.SDK_INT < 30) {
+                /* Hide the system bars.
+                 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
+                 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
+                 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
+                 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
+                 */
 
-            // The deprecated command can be switched to `WindowInsetsController` once the minimum API >= 30.
-            @Suppress("DEPRECATION")
-            rootFrameLayout.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
+                @Suppress("DEPRECATION")
+                window.addFlags(View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
+            }
         } else {  // Privacy Browser is not in full screen browsing mode.
-            // Reset the full screen tracker, which could be true if Privacy Browser was in full screen mode before entering settings and full screen browsing was disabled.
-            inFullScreenBrowsingMode = false
-
             // Show the tab linear layout.
             tabsLinearLayout.visibility = View.VISIBLE
 
             // Show the app bar.
             appBar.show()
 
-            // Remove the `SYSTEM_UI` flags from the root frame layout.  The deprecated command can be switched to `WindowInsetsController` once the minimum API >= 30.
-            @Suppress("DEPRECATION")
-            rootFrameLayout.systemUiVisibility = 0
+            // Remove the fullscreen flags if the API < 30.
+            if (Build.VERSION.SDK_INT < 30) {
+                /* Show the system bars.
+                 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
+                 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
+                 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
+                 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
+                 */
+
+                @Suppress("DEPRECATION")
+                window.clearFlags(View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
+            }
         }
+
+        // Load the bookmarks folder.
+        loadBookmarksFolder()
     }
 
     // `reloadWebsite` is used if returning from the Domains activity.  Otherwise JavaScript might not function correctly if it is newly enabled.
@@ -3084,7 +3199,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
         nestedScrollWebView.currentUrl = url!!
 
         // Parse the URL into a URI.
-        val uri = Uri.parse(url)
+        val uri = url.toUri()
 
         // Extract the domain from the URI.
         var newHostName = uri.host
@@ -3130,7 +3245,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                     nestedScrollWebView.previousWebpageTitle = tabTitleTextView.text.toString()
 
                     // Set the default favorite icon as the favorite icon for this tab.
-                    tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(nestedScrollWebView.getFavoriteIcon(), 64, 64, true))
+                    tabFavoriteIconImageView.setImageBitmap(nestedScrollWebView.getFavoriteIcon().scale(128, 128))
 
                     // Set the loading title text.
                     tabTitleTextView.setText(R.string.loading)
@@ -3543,14 +3658,24 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                 // Get the color background int from the typed value.
                 val colorBackgroundInt = colorBackgroundTypedValue.data
 
-                // Set the default app bar layout background.
+                // Set the default app bar and status bar backgrounds.
                 appBarLayout.setBackgroundColor(colorBackgroundInt)
+
+                // Set the background color if the API < 35 (Android 15).  https://redmine.stoutner.com/issues/1169
+                @Suppress("DEPRECATION")
+                if (Build.VERSION.SDK_INT < 35)
+                    window.statusBarColor = colorBackgroundInt
             }
 
             ProxyHelper.TOR -> {
-                // Set the app bar background to indicate proxying is enabled.
+                // Set the app bar and status bar backgrounds to indicate proxying is enabled.
                 appBarLayout.setBackgroundResource(R.color.blue_background)
 
+                // Set the background color if the API < 35 (Android 15).  https://redmine.stoutner.com/issues/1169
+                @Suppress("DEPRECATION")
+                if (Build.VERSION.SDK_INT < 35)
+                    window.statusBarColor = getColor(R.color.blue_background)
+
                 // Check to see if Orbot is installed.
                 try {
                     // Get the package manager.
@@ -3598,9 +3723,14 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
             }
 
             ProxyHelper.I2P -> {
-                // Set the app bar background to indicate proxying is enabled.
+                // Set the app bar and status bar backgrounds to indicate proxying is enabled.
                 appBarLayout.setBackgroundResource(R.color.blue_background)
 
+                // Set the background color if the API < 35 (Android 15).  https://redmine.stoutner.com/issues/1169
+                @Suppress("DEPRECATION")
+                if (Build.VERSION.SDK_INT < 35)
+                    window.statusBarColor = getColor(R.color.blue_background)
+
                 // Check to see if I2P is installed.
                 try {
                     // Check to see if the F-Droid flavor is installed.  This will throw an error and drop to the catch section if it isn't installed.
@@ -3628,9 +3758,15 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                 }
             }
 
-            ProxyHelper.CUSTOM ->
-                // Set the app bar background to indicate proxying is enabled.
+            ProxyHelper.CUSTOM -> {
+                // Set the app bar and status bar backgrounds to indicate proxying is enabled.
                 appBarLayout.setBackgroundResource(R.color.blue_background)
+
+                // Set the background color if the API < 35 (Android 15).  https://redmine.stoutner.com/issues/1169
+                @Suppress("DEPRECATION")
+                if (Build.VERSION.SDK_INT < 35)
+                    window.statusBarColor = getColor(R.color.blue_background)
+            }
         }
 
         // Reload the WebViews if requested and not waiting for the proxy.
@@ -3726,20 +3862,26 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
             // Manually delete the DOM storage files and directories, as web storage sometimes will not flush its changes to disk before system exit is run.
             try {
                 // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
-                val deleteLocalStorageProcess = runtime.exec(arrayOf("rm", "-rf", "$privateDataDirectoryString/app_webview/Local Storage/"))
+                val deleteLocalStorageProcess = runtime.exec(arrayOf("rm", "-rf", "$privateDataDirectoryString/app_webview/Default/Local Storage/"))
+                val deleteSessionStorageProcess = runtime.exec(arrayOf("rm", "-rf", "$privateDataDirectoryString/app_webview/Default/Session Storage/"))
+                val deleteWebDataProcess = runtime.exec(arrayOf("rm", "-rf", "$privateDataDirectoryString/app_webview/Default/Web Data"))
+                val deleteWebDataJournalProcess = runtime.exec(arrayOf("rm", "-rf", "$privateDataDirectoryString/app_webview/Default/Web Data-journal"))
 
                 // Multiple commands must be used because `Runtime.exec()` does not like `*`.
-                val deleteIndexProcess = runtime.exec("rm -rf $privateDataDirectoryString/app_webview/IndexedDB")
-                val deleteQuotaManagerProcess = runtime.exec("rm -f $privateDataDirectoryString/app_webview/QuotaManager")
-                val deleteQuotaManagerJournalProcess = runtime.exec("rm -f $privateDataDirectoryString/app_webview/QuotaManager-journal")
-                val deleteDatabaseProcess = runtime.exec("rm -rf $privateDataDirectoryString/app_webview/databases")
+                val deleteIndexProcess = runtime.exec("rm -rf $privateDataDirectoryString/app_webview/Default/IndexedDB")
+                val deleteWebStorageProcess = runtime.exec("rm -rf $privateDataDirectoryString/app_webview/Default/WebStorage")
+                val deleteDatabasesProcess = runtime.exec("rm -rf $privateDataDirectoryString/app_webview/Default/databases")
+                val deleteBlobStorageProcess = runtime.exec("rm -rf $privateDataDirectoryString/app_webview/Default/blob_storage")
 
                 // Wait until the processes have finished.
                 deleteLocalStorageProcess.waitFor()
+                deleteSessionStorageProcess.waitFor()
+                deleteWebDataProcess.waitFor()
+                deleteWebDataJournalProcess.waitFor()
                 deleteIndexProcess.waitFor()
-                deleteQuotaManagerProcess.waitFor()
-                deleteQuotaManagerJournalProcess.waitFor()
-                deleteDatabaseProcess.waitFor()
+                deleteWebStorageProcess.waitFor()
+                deleteDatabasesProcess.waitFor()
+                deleteBlobStorageProcess.waitFor()
             } catch (exception: Exception) {
                 // Do nothing if an error is thrown.
             }
@@ -3859,9 +4001,6 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
         // Show the toolbar.
         toolbar.visibility = View.VISIBLE
 
-        // Get a handle for the input method manager.
-        val inputMethodManager = (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
-
         // Hide the keyboard.
         inputMethodManager.hideSoftInputFromWindow(toolbar.windowToken, 0)
     }
@@ -3885,17 +4024,32 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
         }
     }
 
-    override fun createBookmark(dialogFragment: DialogFragment, favoriteIconBitmap: Bitmap) {
+    override fun createBookmark(dialogFragment: DialogFragment) {
         // Get the dialog.
         val dialog = dialogFragment.dialog!!
 
         // Get the views from the dialog fragment.
-        val createBookmarkNameEditText = dialog.findViewById<EditText>(R.id.create_bookmark_name_edittext)
-        val createBookmarkUrlEditText = dialog.findViewById<EditText>(R.id.create_bookmark_url_edittext)
+        val webpageFavoriteIconRadioButton = dialog.findViewById<RadioButton>(R.id.webpage_favorite_icon_radiobutton)
+        val webpageFavoriteIconImageView = dialog.findViewById<ImageView>(R.id.webpage_favorite_icon_imageview)
+        val customIconImageView = dialog.findViewById<ImageView>(R.id.custom_icon_imageview)
+        val bookmarkNameEditText = dialog.findViewById<EditText>(R.id.bookmark_name_edittext)
+        val bookmarkUrlEditText = dialog.findViewById<EditText>(R.id.bookmark_url_edittext)
 
         // Extract the strings from the edit texts.
-        val bookmarkNameString = createBookmarkNameEditText.text.toString()
-        val bookmarkUrlString = createBookmarkUrlEditText.text.toString()
+        val bookmarkNameString = bookmarkNameEditText.text.toString()
+        val bookmarkUrlString = bookmarkUrlEditText.text.toString()
+
+        // Get the selected favorite icon drawable.
+        val favoriteIconDrawable = if (webpageFavoriteIconRadioButton.isChecked)  // Use the webpage favorite icon.
+            webpageFavoriteIconImageView.drawable
+        else  // Use the custom icon.
+            customIconImageView.drawable
+
+        // Cast the favorite icon bitmap to a bitmap drawable
+        val favoriteIconBitmapDrawable = favoriteIconDrawable as BitmapDrawable
+
+        // Convert the favorite icon bitmap drawable to a bitmap.
+        val favoriteIconBitmap = favoriteIconBitmapDrawable.bitmap
 
         // Create a favorite icon byte array output stream.
         val favoriteIconByteArrayOutputStream = ByteArrayOutputStream()
@@ -3913,7 +4067,10 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
         bookmarksDatabaseHelper!!.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolderId, newBookmarkDisplayOrder, favoriteIconByteArray)
 
         // Update the bookmarks cursor with the current contents of this folder.
-        bookmarksCursor = bookmarksDatabaseHelper!!.getBookmarksByDisplayOrder(currentBookmarksFolderId)
+        bookmarksCursor = if (sortBookmarksAlphabetically)
+            bookmarksDatabaseHelper!!.getBookmarksSortedAlphabetically(currentBookmarksFolderId)
+        else
+            bookmarksDatabaseHelper!!.getBookmarksByDisplayOrder(currentBookmarksFolderId)
 
         // Update the list view.
         bookmarksCursorAdapter.changeCursor(bookmarksCursor)
@@ -3922,32 +4079,34 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
         bookmarksListView.setSelection(newBookmarkDisplayOrder)
     }
 
-    override fun createBookmarkFolder(dialogFragment: DialogFragment, favoriteIconBitmap: Bitmap) {
+    override fun createBookmarkFolder(dialogFragment: DialogFragment) {
         // Get the dialog.
         val dialog = dialogFragment.dialog!!
 
         // Get handles for the views in the dialog fragment.
+        val defaultFolderIconRadioButton = dialog.findViewById<RadioButton>(R.id.default_folder_icon_radiobutton)
+        val defaultFolderIconImageView = dialog.findViewById<ImageView>(R.id.default_folder_icon_imageview)
+        val webpageFavoriteIconRadioButton = dialog.findViewById<RadioButton>(R.id.webpage_favorite_icon_radiobutton)
+        val webpageFavoriteIconImageView = dialog.findViewById<ImageView>(R.id.webpage_favorite_icon_imageview)
+        val customIconImageView = dialog.findViewById<ImageView>(R.id.custom_icon_imageview)
         val folderNameEditText = dialog.findViewById<EditText>(R.id.folder_name_edittext)
-        val defaultIconRadioButton = dialog.findViewById<RadioButton>(R.id.default_icon_radiobutton)
-        val defaultIconImageView = dialog.findViewById<ImageView>(R.id.default_icon_imageview)
 
         // Get new folder name string.
         val folderNameString = folderNameEditText.text.toString()
 
         // Set the folder icon bitmap according to the dialog.
-        val folderIconBitmap: Bitmap = if (defaultIconRadioButton.isChecked) {  // Use the default folder icon.
-            // Get the default folder icon drawable.
-            val folderIconDrawable = defaultIconImageView.drawable
-
-            // Convert the folder icon drawable to a bitmap drawable.
-            val folderIconBitmapDrawable = folderIconDrawable as BitmapDrawable
-
-            // Convert the folder icon bitmap drawable to a bitmap.
-            folderIconBitmapDrawable.bitmap
-        } else {  // Use the WebView favorite icon.
-            // Copy the favorite icon bitmap to the folder icon bitmap.
-            favoriteIconBitmap
-        }
+        val folderIconDrawable = if (defaultFolderIconRadioButton.isChecked)  // Use the default folder icon.
+            defaultFolderIconImageView.drawable
+        else if (webpageFavoriteIconRadioButton.isChecked)  // Use the webpage favorite icon.
+            webpageFavoriteIconImageView.drawable
+        else  // Use the custom icon.
+            customIconImageView.drawable
+
+        // Cast the folder icon bitmap to a bitmap drawable.
+        val folderIconBitmapDrawable = folderIconDrawable as BitmapDrawable
+
+        // Convert the folder icon bitmap drawable to a bitmap.
+        val folderIconBitmap = folderIconBitmapDrawable.bitmap
 
         // Create a folder icon byte array output stream.
         val folderIconByteArrayOutputStream = ByteArrayOutputStream()
@@ -3971,7 +4130,10 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
         bookmarksDatabaseHelper!!.createFolder(folderNameString, currentBookmarksFolderId, displayOrder = 0, folderIconByteArray)
 
         // Update the bookmarks cursor with the current contents of this folder.
-        bookmarksCursor = bookmarksDatabaseHelper!!.getBookmarksByDisplayOrder(currentBookmarksFolderId)
+        bookmarksCursor = if (sortBookmarksAlphabetically)
+            bookmarksDatabaseHelper!!.getBookmarksSortedAlphabetically(currentBookmarksFolderId)
+        else
+            bookmarksDatabaseHelper!!.getBookmarksByDisplayOrder(currentBookmarksFolderId)
 
         // Update the list view.
         bookmarksCursorAdapter.changeCursor(bookmarksCursor)
@@ -3981,26 +4143,58 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
     }
 
     private fun exitFullScreenVideo() {
-        // Re-enable the screen timeout.
-        fullScreenVideoFrameLayout.keepScreenOn = false
-
         // Unset the full screen video flag.
         displayingFullScreenVideo = false
 
-        // Remove all the views from the full screen video frame layout.
-        fullScreenVideoFrameLayout.removeAllViews()
+        // Hide the full screen video according to the API and display under cutouts status.
+        if ((Build.VERSION.SDK_INT < 35) && displayUnderCutouts) {  // The device is running API < 35 and display under cutouts is enabled.
+            // Re-enable the screen timeout.
+            oldFullScreenVideoFrameLayout.keepScreenOn = false
+
+            // Remove all the views from the full screen video frame layout.
+            oldFullScreenVideoFrameLayout.removeAllViews()
 
-        // Hide the full screen video frame layout.
-        fullScreenVideoFrameLayout.visibility = View.GONE
+            // Hide the full screen video frame layout.
+            oldFullScreenVideoFrameLayout.visibility = View.GONE
+        } else {  // The device is running API >= 35 or display under cutouts is disabled.
+            // Re-enable the screen timeout.
+            fullScreenVideoFrameLayout.keepScreenOn = false
+
+            // Remove all the views from the full screen video frame layout.
+            fullScreenVideoFrameLayout.removeAllViews()
+
+            // Hide the full screen video frame layout.
+            fullScreenVideoFrameLayout.visibility = View.GONE
+        }
 
         // Enable the sliding drawers.
         drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
 
-        // Show the coordinator layout.
-        coordinatorLayout.visibility = View.VISIBLE
+        // Show the browser view according to the API and display under cutouts status.
+        if ((Build.VERSION.SDK_INT < 35) && displayUnderCutouts) {  // The device is running API < 35 and display under cutouts is enabled.
+            // Display the app bar if it is not supposed to be hidden, the `||` ensures that `!hideAppBar` is only evaluated in `inFullScreenBrowsingMode == true`.
+            if (!inFullScreenBrowsingMode || !hideAppBar) {
+                // Show the tab linear layout.
+                tabsLinearLayout.visibility = View.VISIBLE
+
+                // Show the app bar.
+                appBar.show()
+            }
+
+            // Display the swipe refresh layout (which includes the WebView).
+            swipeRefreshLayout.visibility = View.VISIBLE
+
+            // Enable fits system windows if not in full screen browsing mode.
+            if (!inFullScreenBrowsingMode) {
+                coordinatorLayout.fitsSystemWindows = true
+                bookmarksFrameLayout.fitsSystemWindows = true
+            }
+        } else {  // The device is running API >= 35 or display under cutouts is disabled.
+            browserFrameLayout.visibility = View.VISIBLE
+        }
 
         // Apply the appropriate full screen mode flags.
-        if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) {  // Privacy Browser is currently in full screen browsing mode.
+        if (inFullScreenBrowsingMode) {  // Privacy Browser is currently in full screen browsing mode.
             // Hide the app bar if specified.
             if (hideAppBar) {
                 // Hide the tab linear layout.
@@ -4010,20 +4204,33 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                 appBar.hide()
             }
 
-            /* Hide the system bars.
-             * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
-             * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
-             * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
-             * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
-             */
+            if (Build.VERSION.SDK_INT < 30) {
+                /* Hide the system bars.
+                 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
+                 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
+                 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
+                 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
+                 */
 
-            // The deprecated command can be switched to `WindowInsetsController` once the minimum API >= 30.
-            @Suppress("DEPRECATION")
-            rootFrameLayout.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
+                @Suppress("DEPRECATION")
+                window.addFlags(View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
+            }
         } else {  // Switch to normal viewing mode.
-            // Remove the `SYSTEM_UI` flags from the root frame layout.  The deprecated command can be switched to `WindowInsetsController` once the minimum API >= 30.
-            @Suppress("DEPRECATION")
-            rootFrameLayout.systemUiVisibility = 0
+            // Show the system bars according to the API.
+            if (Build.VERSION.SDK_INT >= 30) {
+                // Show the system bars.
+                windowInsetsController.show(WindowInsets.Type.systemBars())
+            } else {
+                /* Show the system bars.
+                 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
+                 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
+                 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
+                 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
+                 */
+
+                @Suppress("DEPRECATION")
+                window.clearFlags(View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
+            }
         }
     }
 
@@ -4138,7 +4345,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
     @SuppressLint("ClickableViewAccessibility")
     private fun initializeApp() {
         // Get a handle for the input method.
-        val inputMethodManager = (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
+        inputMethodManager = (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
 
         // Initialize the color spans for highlighting the URLs.
         initialGrayColorSpan = ForegroundColorSpan(getColor(R.color.gray_500))
@@ -4327,13 +4534,17 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
 
         // Search for the string on the page whenever a character changes in the find on page edit text.
         findOnPageEditText.addTextChangedListener(object : TextWatcher {
-            override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
+            override fun beforeTextChanged(charSequence: CharSequence, start: Int, count: Int, after: Int) {
+                // Do nothing.
+            }
 
-            override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
+            override fun onTextChanged(charSequence: CharSequence, start: Int, before: Int, count: Int) {
+                // Do nothing.
+            }
 
-            override fun afterTextChanged(s: Editable) {
+            override fun afterTextChanged(editable: Editable) {
                 // Search for the text in the WebView if it is not null.  Sometimes on resume after a period of non-use the WebView will be null.
-                currentWebView?.findAllAsync(findOnPageEditText.text.toString())
+                currentWebView?.findAllAsync(editable.toString())
             }
         })
 
@@ -4380,9 +4591,6 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
         drawerLayout.setDrawerTitle(GravityCompat.START, getString(R.string.navigation_drawer))
         drawerLayout.setDrawerTitle(GravityCompat.END, getString(R.string.bookmarks))
 
-        // Load the bookmarks folder.
-        loadBookmarksFolder()
-
         // Handle clicks on bookmarks.
         bookmarksListView.onItemClickListener = AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, _: Int, id: Long ->
             // Convert the id from long to int to match the format of the bookmarks database.
@@ -4467,12 +4675,48 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
         drawerLayout.addDrawerListener(object : DrawerLayout.DrawerListener {
             override fun onDrawerSlide(drawerView: View, slideOffset: Float) {}
 
-            override fun onDrawerOpened(drawerView: View) {}
+            override fun onDrawerOpened(drawerView: View) {
+                // Close the navigation drawer if requested.  <https://redmine.stoutner.com/issues/1267>
+                if (closeNavigationDrawer) {
+                    // Reset the close navigation drawer flag.
+                    closeNavigationDrawer = false
+
+                    // Close the navigation drawer.
+                    drawerLayout.closeDrawer(GravityCompat.START)
+                }
+            }
 
             override fun onDrawerClosed(drawerView: View) {}
 
             override fun onDrawerStateChanged(newState: Int) {
                 if (newState == DrawerLayout.STATE_SETTLING || newState == DrawerLayout.STATE_DRAGGING) {  // A drawer is opening or closing.
+                    // Adjust the scroll position of the navigation drawer.
+                    if (bottomAppBar && navigationDrawerFirstView) {  // The bottom app bar is in use.
+                        // Reset the navigation drawer first view flag.
+                        navigationDrawerFirstView = false
+
+                        // Get a handle for the navigation recycler view.
+                        val navigationRecyclerView = navigationView.getChildAt(0) as RecyclerView
+
+                        // Get the navigation linear layout manager.
+                        val navigationLinearLayoutManager = navigationRecyclerView.layoutManager as LinearLayoutManager
+
+                        // Scroll the navigation drawer to the bottom.
+                        navigationLinearLayoutManager.scrollToPositionWithOffset(14, 0)
+                    } else if (Build.VERSION.SDK_INT < 35 && navigationDrawerFirstView) {  // The top app bar is in use and the API < 35 (which causes the drawer to scroll down for some reason).
+                        // Reset the navigation drawer first view flag.
+                        navigationDrawerFirstView = false
+
+                        // Get a handle for the navigation recycler view.
+                        val navigationRecyclerView = navigationView.getChildAt(0) as RecyclerView
+
+                        // Get the navigation linear layout manager.
+                        val navigationLinearLayoutManager = navigationRecyclerView.layoutManager as LinearLayoutManager
+
+                        // Scroll the navigation drawer to the top.  <
+                        navigationLinearLayoutManager.scrollToPositionWithOffset(0, 0)
+                    }
+
                     // Update the navigation menu items if the WebView is not null.
                     if (currentWebView != null) {
                         // Set the enabled status of the menu items.
@@ -4487,13 +4731,13 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                             navigationScrollToBottomMenuItem.title = getString(R.string.scroll_to_bottom)
 
                             // Set the icon.
-                            navigationScrollToBottomMenuItem.icon = AppCompatResources.getDrawable(applicationContext, R.drawable.move_down_enabled)
+                            navigationScrollToBottomMenuItem.icon = AppCompatResources.getDrawable(applicationContext, R.drawable.move_to_bottom_enabled)
                         } else {  // The WebView is not scrolled to the top.
                             // Set the title.
                             navigationScrollToBottomMenuItem.title = getString(R.string.scroll_to_top)
 
                             // Set the icon.
-                            navigationScrollToBottomMenuItem.icon = AppCompatResources.getDrawable(applicationContext, R.drawable.move_up_enabled)
+                            navigationScrollToBottomMenuItem.icon = AppCompatResources.getDrawable(applicationContext, R.drawable.move_to_top_enabled)
                         }
 
                         // Display the number of blocked requests.
@@ -4509,20 +4753,6 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                     // Clear the focus from from the WebView if it is not null, which can happen if a user opens a drawer while the browser is being resumed.
                     // Clearing the focus from the WebView removes any text selection markers and context menus, which otherwise draw above the open drawers.
                     currentWebView?.clearFocus()
-
-                    if (bottomAppBar && navigationDrawerFirstView) {
-                        // Reset the navigation drawer first view flag.
-                        navigationDrawerFirstView = false
-
-                        // Get a handle for the navigation recycler view.
-                        val navigationRecyclerView = navigationView.getChildAt(0) as RecyclerView
-
-                        // Get the navigation linear layout manager.
-                        val navigationLinearLayoutManager = navigationRecyclerView.layoutManager as LinearLayoutManager
-
-                        // Scroll the navigation drawer to the bottom.
-                        navigationLinearLayoutManager.scrollToPositionWithOffset(13, 0)
-                    }
                 }
             }
         })
@@ -4548,6 +4778,15 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
 
     @SuppressLint("ClickableViewAccessibility")
     override fun initializeWebView(nestedScrollWebView: NestedScrollWebView, pagePosition: Int, progressBar: ProgressBar, urlString: String, restoringState: Boolean) {
+        // Fix the bookmarks drawer top padding on API <= 29.
+        if (Build.VERSION.SDK_INT <= 29) {
+            // Set the top padding according to the app bar location.
+            if (bottomAppBar)
+                bookmarksListView.setPadding(bookmarksListView.paddingLeft, swipeRefreshLayout.top, bookmarksListView.paddingRight, bookmarksListView.paddingBottom)
+            else
+                bookmarksHeaderLinearLayout.setPadding(bookmarksHeaderLinearLayout.paddingLeft, appBarLayout.top, bookmarksHeaderLinearLayout.paddingRight, bookmarksHeaderLinearLayout.paddingBottom)
+        }
+
         // Get the WebView theme.
         val webViewTheme = sharedPreferences.getString(getString(R.string.webview_theme_key), getString(R.string.webview_theme_default_value))
 
@@ -4586,8 +4825,8 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
             }
         }
 
-        // Get a handle for the input method manager.
-        val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
+        // Disable using the web cache.
+        nestedScrollWebView.settings.cacheMode = WebSettings.LOAD_NO_CACHE
 
         // Set the app bar scrolling.
         nestedScrollWebView.isNestedScrollingEnabled = scrollAppBar
@@ -4665,16 +4904,50 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                             }
                         }
 
-                        /* Hide the system bars.
-                         * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
-                         * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
-                         * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
-                         * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
-                         */
+                        // Hide the system bars.
+                        if (Build.VERSION.SDK_INT >= 30) {
+                            // Set the system bars to display transiently when swiped.
+                            windowInsetsController.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+
+                            // Hide the system bars.
+                            windowInsetsController.hide(WindowInsets.Type.systemBars())
+                        } else {
+                            /* Hide the system bars.
+                             * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
+                             * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
+                             * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
+                             * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
+                             */
+
+                            @Suppress("DEPRECATION")
+                            window.addFlags(View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
+                        }
+
+                        // Disable fits system windows according to the configuration.
+                        if (displayUnderCutouts) {  // Display under cutouts.
+                            // Disable fits system windows according to the API.
+                            if (Build.VERSION.SDK_INT >= 35) {  // The device is running API >= 35.
+                                // Disable fits system windows.
+                                browserFrameLayout.fitsSystemWindows = false
+
+                                // Manually update the padding, which isn't done on API >= 35
+                                browserFrameLayout.setPadding(0, 0, 0, 0)
+                            } else {  // The device is running API < 35.
+                                // Disable fits system windows.
+                                coordinatorLayout.fitsSystemWindows = false
+                                bookmarksFrameLayout.fitsSystemWindows = false
+
+                                // Remove any padding on the bookmarks frame layout.
+                                bookmarksFrameLayout.setPadding(0, 0, 0, 0)
+                            }
+                        } else if (Build.VERSION.SDK_INT < 35) {  // The device is running API < 35 and display under cutouts is disabled.
+                            // Disable fits system windows to display under the navigation bar.
+                            coordinatorLayout.fitsSystemWindows = false
+                            bookmarksFrameLayout.fitsSystemWindows = false
 
-                        // The deprecated command can be switched to `WindowInsetsController` once the minimum API >= 30.
-                        @Suppress("DEPRECATION")
-                        rootFrameLayout.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
+                            // Remove any padding on the bookmarks frame layout.
+                            bookmarksFrameLayout.setPadding(0, 0, 0, 0)
+                        }
                     } else {  // Switch to normal viewing mode.
                         // Show the app bar if it was hidden.
                         if (hideAppBar) {
@@ -4712,9 +4985,36 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                             }
                         }
 
-                        // Remove the `SYSTEM_UI` flags from the root frame layout.  The deprecated command can be switched to `WindowInsetsController` once the minimum API >= 30.
-                        @Suppress("DEPRECATION")
-                        rootFrameLayout.systemUiVisibility = 0
+                        // Enable fits system windows if display under cutouts is enabled.
+                        if (displayUnderCutouts) {
+                            // Enable fits system windows according to the API.
+                            if (Build.VERSION.SDK_INT >= 35) {
+                                browserFrameLayout.fitsSystemWindows = true
+                            } else {
+                                coordinatorLayout.fitsSystemWindows = true
+                                bookmarksFrameLayout.fitsSystemWindows = true
+                            }
+                        } else if (Build.VERSION.SDK_INT < 35) {  // The device is running API < 35 and display under cutouts is disabled.
+                            // Enable fits system windows.
+                            coordinatorLayout.fitsSystemWindows = true
+                            bookmarksFrameLayout.fitsSystemWindows = true
+                        }
+
+                        // Show the system bars according to the API.
+                        if (Build.VERSION.SDK_INT >= 30) {
+                            // Show the system bars.
+                            windowInsetsController.show(WindowInsets.Type.systemBars())
+                        } else {
+                            /* Show the system bars.
+                             * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
+                             * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
+                             * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
+                             * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
+                             */
+
+                            @Suppress("DEPRECATION")
+                            window.clearFlags(View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
+                        }
                     }
 
                     // Consume the double-tap.
@@ -4841,21 +5141,6 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                 swipeRefreshLayout.isEnabled = nestedScrollWebView.scrollY == 0
             else  // Disable swipe to refresh.
                 swipeRefreshLayout.isEnabled = false
-
-            // Reinforce the system UI visibility flags if in full screen browsing mode.
-            // This hides the status and navigation bars, which are displayed if other elements are shown, like dialog boxes, the options menu, or the keyboard.
-            if (inFullScreenBrowsingMode) {
-                /* Hide the system bars.
-                 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
-                 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
-                 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
-                 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
-                 */
-
-                // The deprecated command can be switched to `WindowInsetsController` once the minimum API >= 30.
-                @Suppress("DEPRECATION")
-                rootFrameLayout.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
-            }
         }
 
         // Set the web chrome client.
@@ -4886,7 +5171,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                 // 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.visibility == View.GONE) && (icon.height > nestedScrollWebView.getFavoriteIconHeight())) {
+                if ((progressBar.isGone) && (icon.height > nestedScrollWebView.getFavoriteIconHeight())) {
                     // Store the new favorite icon.
                     nestedScrollWebView.setFavoriteIcon(icon)
 
@@ -4907,7 +5192,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                             val tabFavoriteIconImageView = tabView.findViewById<ImageView>(R.id.favorite_icon_imageview)
 
                             // Display the favorite icon in the tab.
-                            tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(icon, 64, 64, true))
+                            tabFavoriteIconImageView.setImageBitmap(icon.scale(128, 128))
                         }
                     }
                 }
@@ -4951,31 +5236,69 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                 // Hide the keyboard.
                 inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.windowToken, 0)
 
-                // Hide the coordinator layout.
-                coordinatorLayout.visibility = View.GONE
+                // Hide the browser view according to the API.
+                if ((Build.VERSION.SDK_INT < 35) && displayUnderCutouts) {  // The device is running API < 35 and display under cutouts is enabled.
+                    // Hide the tab linear layout.
+                    tabsLinearLayout.visibility = View.GONE
 
-                /* Hide the system bars.
-                 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
-                 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
-                 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
-                 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
-                 */
+                    // Hide the app bar.
+                    appBar.hide()
 
-                // The deprecated command can be switched to `WindowInsetsController` once the minimum API >= 30.
-                @Suppress("DEPRECATION")
-                rootFrameLayout.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
+                    // Hide the swipe refresh layout (which includes the WebView).
+                    swipeRefreshLayout.visibility = View.GONE
+
+                    // Disable fits system windows.
+                    coordinatorLayout.fitsSystemWindows = false
+                    bookmarksFrameLayout.fitsSystemWindows = false
+
+                    // Remove any padding from the bookmark frame layout
+                    bookmarksFrameLayout.setPadding(0, 0, 0, 0)
+                } else {  // The device is running API >= 35 or display under cutouts is disabled.
+                    // Hide the browser view.
+                    browserFrameLayout.visibility = View.GONE
+                }
+
+                // Hide the system bars according to the API.
+                if (Build.VERSION.SDK_INT >= 30) {
+                    // Set the system bars to show transiently when swiped.
+                    windowInsetsController.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+
+                    // Hide the system bars.
+                    windowInsetsController.hide(WindowInsets.Type.systemBars())
+                } else {
+                    /* Hide the system bars.
+                     * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
+                     * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
+                     * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
+                     * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
+                     */
+
+                    @Suppress("DEPRECATION")
+                    window.addFlags(View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
+                }
 
                 // Disable the sliding drawers.
                 drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
 
-                // Add the video view to the full screen video frame layout.
-                fullScreenVideoFrameLayout.addView(video)
+                if ((Build.VERSION.SDK_INT < 35) && displayUnderCutouts) {
+                    // Add the video view to the full screen video frame layout.
+                    oldFullScreenVideoFrameLayout.addView(video)
+
+                    // Show the full screen video frame layout.
+                    oldFullScreenVideoFrameLayout.visibility = View.VISIBLE
+
+                    // Disable the screen timeout while the video is playing.  YouTube does this automatically, but not all other videos do.
+                    oldFullScreenVideoFrameLayout.keepScreenOn = true
+                } else {
+                    // Add the video view to the full screen video frame layout.
+                    fullScreenVideoFrameLayout.addView(video)
 
-                // Show the full screen video frame layout.
-                fullScreenVideoFrameLayout.visibility = View.VISIBLE
+                    // Show the full screen video frame layout.
+                    fullScreenVideoFrameLayout.visibility = View.VISIBLE
 
-                // Disable the screen timeout while the video is playing.  YouTube does this automatically, but not all other videos do.
-                fullScreenVideoFrameLayout.keepScreenOn = true
+                    // Disable the screen timeout while the video is playing.  YouTube does this automatically, but not all other videos do.
+                    fullScreenVideoFrameLayout.keepScreenOn = true
+                }
             }
 
             // Exit full screen video.
@@ -4989,26 +5312,18 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                 // Store the file path callback.
                 fileChooserCallback = filePathCallback
 
-                // Create an intent to open a chooser based on the file chooser parameters.
-                val fileChooserIntent = fileChooserParams.createIntent()
-
-                // Check to see if the file chooser intent resolves to an installed package.
-                if (fileChooserIntent.resolveActivity(packageManager) != null) {  // The file chooser intent is fine.
-                    // 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.
-                    val genericFileChooserIntent = Intent(Intent.ACTION_GET_CONTENT)
+                // Create a file chooser intent.
+                val fileChooserIntent = Intent(Intent.ACTION_GET_CONTENT)
 
-                    // Request an openable file.
-                    genericFileChooserIntent.addCategory(Intent.CATEGORY_OPENABLE)
+                // Request an openable file.
+                fileChooserIntent.addCategory(Intent.CATEGORY_OPENABLE)
 
-                    // Set the file type to everything.
-                    genericFileChooserIntent.type = "*/*"
+                // Set the file type to everything.  The file chooser params cannot be used to create the intent because it only selects the first specified file type.
+                // See <https://redmine.stoutner.com/issues/1197>.
+                fileChooserIntent.type = "*/*"
 
-                    // Launch the generic file chooser intent.
-                    browseFileUploadActivityResultLauncher.launch(genericFileChooserIntent)
-                }
+                // Launch the file chooser intent.
+                browseFileUploadActivityResultLauncher.launch(fileChooserIntent)
 
                 // Handle the event.
                 return true
@@ -5036,7 +5351,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                     val emailIntent = Intent(Intent.ACTION_SENDTO)
 
                     // Parse the url and set it as the data for the intent.
-                    emailIntent.data = Uri.parse(requestUrlString)
+                    emailIntent.data = requestUrlString.toUri()
 
                     // Open the email program in a new task instead of as part of Privacy Browser.
                     emailIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
@@ -5056,7 +5371,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                     val dialIntent = Intent(Intent.ACTION_DIAL)
 
                     // Add the phone number to the intent.
-                    dialIntent.data = Uri.parse(requestUrlString)
+                    dialIntent.data = requestUrlString.toUri()
 
                     // Open the dialer in a new task instead of as part of Privacy Browser.
                     dialIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
@@ -5076,7 +5391,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                     val genericIntent = Intent(Intent.ACTION_VIEW)
 
                     // Add the URL to the intent.
-                    genericIntent.data = Uri.parse(requestUrlString)
+                    genericIntent.data = requestUrlString.toUri()
 
                     // List all apps that can handle the URL instead of just opening the first one.
                     genericIntent.addCategory(Intent.CATEGORY_BROWSABLE)
@@ -5438,8 +5753,14 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                 // Instantiate an HTTP authentication dialog.
                 val httpAuthenticationDialogFragment = HttpAuthenticationDialog.displayDialog(host, realm, nestedScrollWebView.webViewFragmentId)
 
-                // Show the HTTP authentication dialog.
-                httpAuthenticationDialogFragment.show(supportFragmentManager, getString(R.string.http_authentication))
+                // Try to show the dialog.  WebView can receive an HTTP authentication request even after the app has been paused.  Attempting to display a dialog in that state leads to a crash.
+                try {
+                    // Show the HTTP authentication dialog.
+                    httpAuthenticationDialogFragment.show(supportFragmentManager, getString(R.string.http_authentication))
+                } 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(PendingDialogDataClass(httpAuthenticationDialogFragment, getString(R.string.http_authentication)))
+                }
             }
 
             override fun onPageStarted(webView: WebView, url: String, favicon: Bitmap?) {
@@ -5500,7 +5821,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                 nestedScrollWebView.currentIpAddresses = ""
 
                 // Get a URI for the current URL.
-                val currentUri = Uri.parse(url)
+                val currentUri = url.toUri()
 
                 // Get the current domain name.
                 val currentDomainName = currentUri.host
@@ -5535,15 +5856,15 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                         optionsRefreshMenuItem.setIcon(R.drawable.refresh_enabled)
                 }
 
+                // Clear the cache.  `true` includes disk files.
+                nestedScrollWebView.clearCache(true)
+                
                 // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
                 // which links to `/data/data/com.stoutner.privacybrowser.standard`.
                 val privateDataDirectoryString = applicationInfo.dataDir
 
-                // Clear the cache, history, and logcat if Incognito Mode is enabled.
+                // Clear the history, and logcat if Incognito Mode is enabled.
                 if (incognitoModeEnabled) {
-                    // Clear the cache.  `true` includes disk files.
-                    nestedScrollWebView.clearCache(true)
-
                     // Clear the back/forward history.
                     nestedScrollWebView.clearHistory()
 
@@ -5588,7 +5909,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                     // Check to see if the URL is `about:blank`.
                     if (currentUrl == "about:blank") {  // The WebView is blank.
                         // Display the hint in the URL edit text.
-                        urlEditText.setText("")
+                        urlEditText.text = null
 
                         // Request focus for the URL text box.
                         urlEditText.requestFocus()
@@ -5677,7 +5998,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                     // Instantiate an SSL certificate error alert dialog.
                     val sslCertificateErrorDialogFragment = SslCertificateErrorDialog.displayDialog(error, nestedScrollWebView.webViewFragmentId)
 
-                    // Try to show the dialog.  The SSL error handler continues to function even when the WebView is paused.  Attempting to display a dialog in that state leads to a crash.
+                    // Try to show the dialog.  The SSL error handler continues to function even when the app has been stopped.  Attempting to display a dialog in that state leads to a crash.
                     try {
                         // Show the SSL certificate error dialog.
                         sslCertificateErrorDialogFragment.show(supportFragmentManager, getString(R.string.ssl_certificate_error))
@@ -5758,7 +6079,10 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
 
     private fun loadBookmarksFolder() {
         // Update the bookmarks cursor with the contents of the bookmarks database for the current folder.
-        bookmarksCursor = bookmarksDatabaseHelper!!.getBookmarksByDisplayOrder(currentBookmarksFolderId)
+        bookmarksCursor = if (sortBookmarksAlphabetically)
+            bookmarksDatabaseHelper!!.getBookmarksSortedAlphabetically(currentBookmarksFolderId)
+        else
+            bookmarksDatabaseHelper!!.getBookmarksByDisplayOrder(currentBookmarksFolderId)
 
         // Populate the bookmarks cursor adapter.
         bookmarksCursorAdapter = object : CursorAdapter(this, bookmarksCursor, false) {
@@ -5812,7 +6136,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
 
     private fun loadUrlFromTextBox() {
         // Get the text from URL text box and convert it to a string.  trim() removes white spaces from the beginning and end of the string.
-        var unformattedUrlString = urlEditText.text.toString().trim { it <= ' ' }
+        var unformattedUrlString = urlEditText.text.toString().trim()
 
         // Create the formatted URL string.
         var urlString = ""
@@ -5899,11 +6223,14 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
         // Get the favorite icon image view from the tab.
         val tabFavoriteIconImageView = tabView.findViewById<ImageView>(R.id.favorite_icon_imageview)
 
-        // Set the previous favorite icon.
+        // Store the previous favorite icon.
         if (previousFavoriteIcon == null)
-            tabFavoriteIconImageView.setImageBitmap(defaultFavoriteIconBitmap)
+            currentWebView!!.setFavoriteIcon(defaultFavoriteIconBitmap)
         else
-            tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(previousFavoriteIcon, 64, 64, true))
+            currentWebView!!.setFavoriteIcon(previousFavoriteIcon)
+
+        // Display the previous favorite icon in the tab.
+        tabFavoriteIconImageView.setImageBitmap(currentWebView!!.getFavoriteIcon().scale(128, 128))
 
         // Load the history entry.
         currentWebView!!.goBackOrForward(steps)
@@ -5943,7 +6270,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
         if (mhtCheckBox.isChecked) {  // Force opening of an MHT file.
             try {
                 // Get the MHT file input stream.
-                val mhtFileInputStream = contentResolver.openInputStream(Uri.parse(openFilePath))
+                val mhtFileInputStream = contentResolver.openInputStream(openFilePath.toUri())
 
                 // Create a temporary MHT file.
                 val temporaryMhtFile = File.createTempFile(TEMPORARY_MHT_FILE, ".mht", cacheDir)
@@ -5979,6 +6306,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
             currentWebView!!.loadUrl(openFilePath)
         }
     }
+
     // The view parameter cannot be removed because it is called from the layout onClick.
     fun openNavigationDrawer(@Suppress("UNUSED_PARAMETER")view: View) {
         // Open the navigation drawer.
@@ -5990,7 +6318,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
         val openWithAppIntent = Intent(Intent.ACTION_VIEW)
 
         // Set the URI but not the MIME type.  This should open all available apps.
-        openWithAppIntent.data = Uri.parse(url)
+        openWithAppIntent.data = url.toUri()
 
         // Flag the intent to open in a new task.
         openWithAppIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
@@ -6011,7 +6339,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
         val openWithBrowserIntent = Intent(Intent.ACTION_VIEW)
 
         // Set the URI and the MIME type.  `"text/html"` should load browser options.
-        openWithBrowserIntent.setDataAndType(Uri.parse(url), "text/html")
+        openWithBrowserIntent.setDataAndType(url.toUri(), "text/html")
 
         // Flag the intent to open in a new task.
         openWithBrowserIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
@@ -6064,7 +6392,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
         val downloadManager = getSystemService(DOWNLOAD_SERVICE) as DownloadManager
 
         // Parse the URL.
-        val downloadRequest = DownloadManager.Request(Uri.parse(saveUrlString))
+        val downloadRequest = DownloadManager.Request(saveUrlString.toUri())
 
         // Pass cookies to download manager if cookies are enabled.  This is required to download files from websites that require a login.
         // Code contributed 2017 Hendrik Knackstedt.  Copyright assigned to Soren Stoutner <soren@stoutner.com>.
@@ -6107,7 +6435,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
         val downloadIntent = Intent()
 
         // Set the URI and the mime type.
-        downloadIntent.setDataAndType(Uri.parse(url), "text/html")
+        downloadIntent.setDataAndType(url.toUri(), "text/html")
 
         // Flag the intent to open in a new task.
         downloadIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
@@ -6140,6 +6468,18 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
         // Stop the swipe to refresh indicator if it is running
         swipeRefreshLayout.isRefreshing = false
 
+        // Open the navigation drawer if the bottom app bar is enabled and this is the first tab.  <https://redmine.stoutner.com/issues/1267>
+        if (displayingInitialTab && bottomAppBar) {
+            // Open the navigation drawer.
+            drawerLayout.openDrawer(GravityCompat.START)
+
+            // Set the close navigation drawer flag.
+            closeNavigationDrawer = true
+        }
+
+        // Set the displaying initial tab flag to be false.
+        displayingInitialTab = false
+
         // Try to set the current WebView.  This will fail if the WebView has not yet been populated.
         try {
             // Get the WebView tab fragment.
@@ -6166,17 +6506,14 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
             // Update the privacy icons.  `true` redraws the icons in the app bar.
             updatePrivacyIcons(true)
 
-            // Get a handle for the input method manager.
-            val inputMethodManager = (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
-
             // Get the current URL.
-            val urlString = currentWebView!!.url
+            val urlString = currentWebView!!.currentUrl
 
             // Update the URL edit text if not loading a new intent.  Otherwise, this will be handled by `onPageStarted()` (if called) and `onPageFinished()`.
             if (!loadingNewIntent) {  // A new intent is not being loaded.
-                if ((urlString == null) || (urlString == "about:blank")) {  // The WebView is blank.
+                if (urlString.isBlank()) {  // The WebView is blank.
                     // Display the hint in the URL edit text.
-                    urlEditText.setText("")
+                    urlEditText.text = null
 
                     // Request focus for the URL text box.
                     urlEditText.requestFocus()