X-Git-Url: https://gitweb.stoutner.com/?a=blobdiff_plain;f=app%2Fsrc%2Fmain%2Fjava%2Fcom%2Fstoutner%2Fprivacybrowser%2Factivities%2FMainWebViewActivity.kt;h=042af73dce4f9258440b6da318dcdbfaac7ba25d;hb=refs%2Fheads%2Fmaster;hp=204023e332e89df7fc8e47780877d43ebf22903f;hpb=e065315a36c804626a7dba38d3edad05e9fdb473;p=PrivacyBrowserAndroid.git diff --git a/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.kt b/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.kt index 204023e3..dc479345 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.kt +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.kt @@ -1,22 +1,22 @@ -/* - * Copyright 2015-2024 Soren Stoutner . +/* SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2015-2025 Soren Stoutner * * Download cookie code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner . * - * This file is part of Privacy Browser Android . + * This file is part of 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 . + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . */ 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 @@ -250,15 +255,19 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook // Declare the public static variables. lateinit var appBarLayout: AppBarLayout + lateinit var defaultFavoriteIconBitmap : Bitmap } // Declare the class variables. 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 @@ -277,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 @@ -325,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 @@ -336,6 +346,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook private lateinit var webViewDefaultUserAgent: String private lateinit var webViewThemeEntryValuesStringArray: Array private lateinit var webViewViewPager2: ViewPager2 + private lateinit var windowInsetsController: WindowInsetsController private lateinit var ultraList: ArrayList>> private lateinit var urlEditText: EditText private lateinit var urlRelativeLayout: RelativeLayout @@ -350,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 @@ -367,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 @@ -388,17 +402,18 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook private var savedStateArrayList: ArrayList? = null private var savedTabPosition = 0 private var scrollAppBar = false + private var sortBookmarksAlphabetically = false private var ultraPrivacy: ArrayList>>? = 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(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. @@ -413,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(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. @@ -486,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(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. @@ -512,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) @@ -563,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. @@ -580,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) @@ -593,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 @@ -627,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) @@ -656,6 +704,15 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook // Update the bookmarks drawer pinned image view. updateBookmarksDrawerPinnedImageView() + // Get the default favorite icon drawable. + val favoriteIconDrawable = AppCompatResources.getDrawable(this, R.drawable.world) + + // Cast the favorite icon drawable to a bitmap drawable. + val favoriteIconBitmapDrawable = (favoriteIconDrawable as BitmapDrawable?)!! + + // Store the default favorite icon bitmap. + defaultFavoriteIconBitmap = favoriteIconBitmapDrawable.bitmap + // Initialize the app. initializeApp() @@ -677,34 +734,8 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook exitFullScreenVideo() // It shouldn't be possible for the currentWebView to be null, but crash logs indicate it sometimes happens. } else if (currentWebView != null && currentWebView!!.canGoBack()) { // There is at least one item in the current WebView history. - // Get the current web back forward list. - val webBackForwardList = currentWebView!!.copyBackForwardList() - - // Get the previous entry data. - val previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.currentIndex - 1).url - val previousFavoriteIcon = webBackForwardList.getItemAtIndex(webBackForwardList.currentIndex - 1).favicon - - // Apply the domain settings. - applyDomainSettings(currentWebView!!, previousUrl, resetTab = false, reloadWebsite = false, loadUrl = false) - - // Get the current tab. - val tab = tabLayout.getTabAt(tabLayout.selectedTabPosition)!! - - // Get the custom view from the tab. - val tabView = tab.customView!! - - // Get the favorite icon image view from the tab. - val tabFavoriteIconImageView = tabView.findViewById(R.id.favorite_icon_imageview) - - // Set the previous favorite icon if it isn't null. - if (previousFavoriteIcon != null) - tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(previousFavoriteIcon, 64, 64, true)) - - // Go back. - currentWebView!!.goBack() - - // Update the URL edit text after a delay. - updateUrlEditTextAfterDelay() + // Navigate back one page. + navigateHistory(-1) } else { // Close the current tab. // A view is required because the method is also called by an XML `onClick`. closeTab(null) @@ -882,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. @@ -891,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. @@ -909,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() - savedNestedScrollWebViewStateArrayList = ArrayList() + savedStateArrayList = ArrayList() + savedNestedScrollWebViewStateArrayList = ArrayList() // Get the URLs from each tab. for (i in 0 until webViewStateAdapter!!.itemCount) { @@ -949,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) } } @@ -1178,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) @@ -1466,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()) @@ -1486,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. } @@ -1869,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) @@ -2067,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?: "" @@ -2237,66 +2273,16 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook R.id.back -> { // Back. // Check if the WebView can go back. if (currentWebView!!.canGoBack()) { - // Get the current web back forward list. - val webBackForwardList = currentWebView!!.copyBackForwardList() - - // Get the previous entry data. - val previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.currentIndex - 1).url - val previousFavoriteIcon = webBackForwardList.getItemAtIndex(webBackForwardList.currentIndex - 1).favicon!! - - // Apply the domain settings. - applyDomainSettings(currentWebView!!, previousUrl, resetTab = false, reloadWebsite = false, loadUrl = false) - - // Get the current tab. - val tab = tabLayout.getTabAt(tabLayout.selectedTabPosition)!! - - // Get the custom view from the tab. - val tabView = tab.customView!! - - // Get the favorite icon image view from the tab. - val tabFavoriteIconImageView = tabView.findViewById(R.id.favorite_icon_imageview) - - // Set the previous favorite icon. - tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(previousFavoriteIcon, 64, 64, true)) - - // Load the previous website in the history. - currentWebView!!.goBack() - - // Update the URL edit text after a delay. - updateUrlEditTextAfterDelay() + // Navigate back one page. + navigateHistory(-1) } } R.id.forward -> { // Forward. // Check if the WebView can go forward. if (currentWebView!!.canGoForward()) { - // Get the current web back forward list. - val webBackForwardList = currentWebView!!.copyBackForwardList() - - // Get the next entry data. - val nextUrl = webBackForwardList.getItemAtIndex(webBackForwardList.currentIndex + 1).url - val nextFavoriteIcon = webBackForwardList.getItemAtIndex(webBackForwardList.currentIndex + 1).favicon!! - - // Apply the domain settings. - applyDomainSettings(currentWebView!!, nextUrl, resetTab = false, reloadWebsite = false, loadUrl = false) - - // Get the current tab. - val tab = tabLayout.getTabAt(tabLayout.selectedTabPosition)!! - - // Get the custom view from the tab. - val tabView = tab.customView!! - - // Get the favorite icon image view from the tab. - val tabFavoriteIconImageView = tabView.findViewById(R.id.favorite_icon_imageview) - - // Set the next favorite icon. - tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(nextFavoriteIcon, 64, 64, true)) - - // Load the next website in the history. - currentWebView!!.goForward() - - // Update the URL edit text after a delay. - updateUrlEditTextAfterDelay() + // Navigate forward one page. + navigateHistory(+1) } } @@ -2357,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 @@ -2371,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 @@ -2516,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. @@ -2597,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) } @@ -2861,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 @@ -2896,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) } @@ -3010,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]) @@ -3100,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. @@ -3117,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. @@ -3150,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 @@ -3171,10 +3220,10 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook nestedScrollWebView.clearPinnedSslCertificate() nestedScrollWebView.pinnedIpAddresses = "" - // Reset the favorite icon if specified. + // Reset the tab if specified. if (resetTab) { // Initialize the favorite icon. - nestedScrollWebView.initializeFavoriteIcon() + nestedScrollWebView.resetFavoriteIcon() // Get the current page position. val currentPagePosition = webViewStateAdapter!!.getPositionForId(nestedScrollWebView.webViewFragmentId) @@ -3196,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) @@ -3609,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. @@ -3664,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. @@ -3694,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. @@ -3792,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. } @@ -3925,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) } @@ -3951,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(R.id.create_bookmark_name_edittext) - val createBookmarkUrlEditText = dialog.findViewById(R.id.create_bookmark_url_edittext) + val webpageFavoriteIconRadioButton = dialog.findViewById(R.id.webpage_favorite_icon_radiobutton) + val webpageFavoriteIconImageView = dialog.findViewById(R.id.webpage_favorite_icon_imageview) + val customIconImageView = dialog.findViewById(R.id.custom_icon_imageview) + val bookmarkNameEditText = dialog.findViewById(R.id.bookmark_name_edittext) + val bookmarkUrlEditText = dialog.findViewById(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() @@ -3979,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) @@ -3988,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(R.id.default_folder_icon_radiobutton) + val defaultFolderIconImageView = dialog.findViewById(R.id.default_folder_icon_imageview) + val webpageFavoriteIconRadioButton = dialog.findViewById(R.id.webpage_favorite_icon_radiobutton) + val webpageFavoriteIconImageView = dialog.findViewById(R.id.webpage_favorite_icon_imageview) + val customIconImageView = dialog.findViewById(R.id.custom_icon_imageview) val folderNameEditText = dialog.findViewById(R.id.folder_name_edittext) - val defaultIconRadioButton = dialog.findViewById(R.id.default_icon_radiobutton) - val defaultIconImageView = dialog.findViewById(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() @@ -4037,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) @@ -4047,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. + 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 + // 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. @@ -4076,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) + } } } @@ -4204,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)) @@ -4393,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()) } }) @@ -4446,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. @@ -4533,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. + 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. @@ -4553,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. @@ -4575,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) - } } } }) @@ -4614,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)) @@ -4652,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 @@ -4731,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) { @@ -4778,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. @@ -4907,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. @@ -4952,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) @@ -4973,7 +5192,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook val tabFavoriteIconImageView = tabView.findViewById(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)) } } } @@ -5017,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 - // 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. + 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 - // 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. @@ -5055,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() + // Create a file chooser intent. + val fileChooserIntent = Intent(Intent.ACTION_GET_CONTENT) - // 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) + // Request an openable file. + fileChooserIntent.addCategory(Intent.CATEGORY_OPENABLE) - // Request an openable file. - genericFileChooserIntent.addCategory(Intent.CATEGORY_OPENABLE) + // 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 . + fileChooserIntent.type = "*/*" - // Set the file type to everything. - genericFileChooserIntent.type = "*/*" - - // Launch the generic file chooser intent. - browseFileUploadActivityResultLauncher.launch(genericFileChooserIntent) - } + // Launch the file chooser intent. + browseFileUploadActivityResultLauncher.launch(fileChooserIntent) // Handle the event. return true @@ -5102,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 @@ -5122,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 @@ -5142,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) @@ -5504,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?) { @@ -5566,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 @@ -5601,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() @@ -5654,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() @@ -5743,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)) @@ -5824,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) { @@ -5878,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 = "" @@ -5942,15 +6200,56 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook loadUrl(currentWebView!!, urlString) } - override fun navigateHistory(url: String, steps: Int) { + override fun navigateHistory(steps: Int) { + // Get the current web back forward list. + val webBackForwardList = currentWebView!!.copyBackForwardList() + + // Calculate the target index. + val targetIndex = webBackForwardList.currentIndex + steps + + // Get the previous entry data. + val previousUrl = webBackForwardList.getItemAtIndex(targetIndex).url + val previousFavoriteIcon = webBackForwardList.getItemAtIndex(targetIndex).favicon + // Apply the domain settings. - applyDomainSettings(currentWebView!!, url, resetTab = false, reloadWebsite = false, loadUrl = false) + applyDomainSettings(currentWebView!!, previousUrl, resetTab = false, reloadWebsite = false, loadUrl = false) + + // Get the current tab. + val tab = tabLayout.getTabAt(tabLayout.selectedTabPosition)!! + + // Get the custom view from the tab. + val tabView = tab.customView!! + + // Get the favorite icon image view from the tab. + val tabFavoriteIconImageView = tabView.findViewById(R.id.favorite_icon_imageview) + + // Store the previous favorite icon. + if (previousFavoriteIcon == null) + currentWebView!!.setFavoriteIcon(defaultFavoriteIconBitmap) + else + 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) - // Update the URL edit text after a delay. - updateUrlEditTextAfterDelay() + // Create a handler to update the URL edit box. + val urlEditTextUpdateHandler = Handler(Looper.getMainLooper()) + + // Create a runnable to update the URL edit box. + val urlEditTextUpdateRunnable = Runnable { + // Update the URL edit text. + urlEditText.setText(currentWebView!!.url) + + // Disable the wide viewport if the source is being viewed. + if (currentWebView!!.url!!.startsWith("view-source:")) + currentWebView!!.settings.useWideViewPort = false + } + + // Update the URL edit text after 50 milliseconds, so that the WebView has enough time to navigate to the new URL. + urlEditTextUpdateHandler.postDelayed(urlEditTextUpdateRunnable, 50) } override fun openFile(dialogFragment: DialogFragment) { @@ -5971,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) @@ -6007,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. @@ -6018,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 @@ -6039,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 @@ -6055,20 +6355,8 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook } override fun pinnedErrorGoBack() { - // Get the current web back forward list. - val webBackForwardList = currentWebView!!.copyBackForwardList() - - // Get the previous entry URL. - val previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.currentIndex - 1).url - - // Apply the domain settings. - applyDomainSettings(currentWebView!!, previousUrl, resetTab = false, reloadWebsite = false, loadUrl = false) - - // Go back. - currentWebView!!.goBack() - - // Update the URL edit text after a delay. - updateUrlEditTextAfterDelay() + // Navigate back one page. + navigateHistory(-1) } private fun sanitizeUrl(urlString: String): String { @@ -6104,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 . @@ -6147,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 @@ -6180,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. + 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. @@ -6206,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() @@ -6356,22 +6653,4 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook invalidateOptionsMenu() } } - - fun updateUrlEditTextAfterDelay() { - // Create a handler to update the URL edit box. - val urlEditTextUpdateHandler = Handler(Looper.getMainLooper()) - - // Create a runnable to update the URL edit box. - val urlEditTextUpdateRunnable = Runnable { - // Update the URL edit text. - urlEditText.setText(currentWebView!!.url) - - // Disable the wide viewport if the source is being viewed. - if (currentWebView!!.url!!.startsWith("view-source:")) - currentWebView!!.settings.useWideViewPort = false - } - - // Update the URL edit text after 50 milliseconds, so that the WebView has enough time to navigate to the new URL. - urlEditTextUpdateHandler.postDelayed(urlEditTextUpdateRunnable, 50) - } }