+ // Add the off-screen scrolling layout.
+ swipeRefreshLayoutParams.behavior = AppBarLayout.ScrollingViewBehavior()
+ } else { // The app bar is not scrolled when it is displayed.
+ // The swipe refresh layout must be manually moved below the app bar layout.
+ swipeRefreshLayout.setPadding(0, appBarHeight, 0, 0)
+
+ // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
+ swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10 + appBarHeight, defaultProgressViewEndOffset + appBarHeight)
+ }
+ }
+
+ // 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
+ }
+
+ // Consume the double-tap.
+ true
+ } else { // Do not consume the double-tap because full screen browsing mode is disabled.
+ // Return false.
+ false
+ }
+ }
+
+ override fun onFling(motionEvent1: MotionEvent?, motionEvent2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
+ // Scroll the bottom app bar if enabled.
+ if (bottomAppBar && scrollAppBar && !objectAnimator.isRunning && (motionEvent1 != null)) {
+ // Calculate the Y change.
+ val motionY = motionEvent2.y - motionEvent1.y
+
+ // Scroll the app bar if the change is greater than 50 pixels.
+ if (motionY > 50) {
+ // Animate the bottom app bar onto the screen.
+ objectAnimator = ObjectAnimator.ofFloat(appBarLayout, "translationY", 0f)
+ } else if (motionY < -50) {
+ // Animate the bottom app bar off the screen.
+ objectAnimator = ObjectAnimator.ofFloat(appBarLayout, "translationY", appBarLayout.height.toFloat())
+ }
+
+ // Make it so.
+ objectAnimator.start()
+ }
+
+ // Do not consume the event.
+ return false
+ }
+ })
+
+ // Pass all touch events on the WebView through the double-tap gesture detector.
+ nestedScrollWebView.setOnTouchListener { view: View, motionEvent: MotionEvent? ->
+ // Call `performClick()` on the view, which is required for accessibility.
+ view.performClick()
+
+ // Check for double-taps.
+ doubleTapGestureDetector.onTouchEvent(motionEvent!!)
+ }
+
+ // Register the WebView for a context menu. This is used to see link targets and download images.
+ registerForContextMenu(nestedScrollWebView)
+
+ // Allow the downloading of files.
+ nestedScrollWebView.setDownloadListener { downloadUrlString: String?, userAgent: String?, contentDisposition: String?, mimetype: String?, contentLength: Long ->
+ // Check the download preference.
+ if (downloadWithExternalApp) { // Download with an external app.
+ downloadUrlWithExternalApp(downloadUrlString!!)
+ } else { // Handle the download inside of Privacy Browser.
+ // Define a formatted file size string.
+
+ // Process the content length if it contains data.
+ val formattedFileSizeString = if (contentLength > 0) { // The content length is greater than 0.
+ // Format the content length as a string.
+ NumberFormat.getInstance().format(contentLength) + " " + getString(R.string.bytes)
+ } else { // The content length is not greater than 0.
+ // Set the formatted file size string to be `unknown size`.
+ getString(R.string.unknown_size)
+ }
+
+ // Get the file name from the content disposition.
+ val fileNameString = UrlHelper.getFileName(this, contentDisposition, mimetype, downloadUrlString!!)
+
+ // Instantiate the save dialog.
+ val saveDialogFragment = SaveDialog.saveUrl(downloadUrlString, fileNameString, formattedFileSizeString, userAgent!!, nestedScrollWebView.acceptCookies)
+
+ // Try to show the dialog. The download listener continues to function even when the WebView is paused. Attempting to display a dialog in that state leads to a crash.
+ try {
+ // Show the save dialog.
+ saveDialogFragment.show(supportFragmentManager, getString(R.string.save_dialog))
+ } catch (exception: Exception) { // The dialog could not be shown.
+ // Add the dialog to the pending dialog array list. It will be displayed in `onStart()`.
+ pendingDialogsArrayList.add(PendingDialogDataClass(saveDialogFragment, getString(R.string.save_dialog)))
+ }
+ }
+ }
+
+ // Update the find on page count.
+ nestedScrollWebView.setFindListener { activeMatchOrdinal, numberOfMatches, isDoneCounting ->
+ if (isDoneCounting && (numberOfMatches == 0)) { // There are no matches.
+ // Set the find on page count text view to be `0/0`.
+ findOnPageCountTextView.setText(R.string.zero_of_zero)
+ } else if (isDoneCounting) { // There are matches.
+ // The active match ordinal is zero-based.
+ val activeMatch = activeMatchOrdinal + 1
+
+ // Build the match string.
+ val matchString = "$activeMatch/$numberOfMatches"
+
+ // Update the find on page count text view.
+ findOnPageCountTextView.text = matchString
+ }
+ }
+
+ // Process scroll changes.
+ nestedScrollWebView.setOnScrollChangeListener { _: View?, _: Int, _: Int, _: Int, _: Int ->
+ // Set the swipe to refresh status.
+ if (nestedScrollWebView.swipeToRefresh) // Only enable swipe to refresh if the WebView is scrolled to the top.
+ 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.
+ nestedScrollWebView.webChromeClient = object : WebChromeClient() {
+ // Update the progress bar when a page is loading.
+ override fun onProgressChanged(view: WebView, progress: Int) {
+ // Update the progress bar.
+ progressBar.progress = progress
+
+ // Set the visibility of the progress bar.
+ if (progress < 100) {
+ // Show the progress bar.
+ progressBar.visibility = View.VISIBLE
+ } else {
+ // Hide the progress bar.
+ progressBar.visibility = View.GONE
+
+ //Stop the swipe to refresh indicator if it is running
+ swipeRefreshLayout.isRefreshing = false
+
+ // Make the current WebView visible. If this is a new tab, the current WebView would have been created invisible in `webview_framelayout` to prevent a white background splash in night mode.
+ nestedScrollWebView.visibility = View.VISIBLE
+ }
+ }
+
+ // Set the favorite icon when it changes.
+ override fun onReceivedIcon(view: WebView, icon: Bitmap) {
+ // 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())) {
+ // Store the new favorite icon.
+ nestedScrollWebView.setFavoriteIcon(icon)
+
+ // Get the current page position.
+ val currentPosition = webViewStateAdapter!!.getPositionForId(nestedScrollWebView.webViewFragmentId)
+
+ // Get the current tab.
+ val tab = tabLayout.getTabAt(currentPosition)
+
+ // Check to see if the tab has been populated.
+ if (tab != null) {
+ // Get the custom view from the tab.
+ val tabView = tab.customView
+
+ // Check to see if the custom tab view has been populated.
+ if (tabView != null) {
+ // Get the favorite icon image view from the tab.
+ val tabFavoriteIconImageView = tabView.findViewById<ImageView>(R.id.favorite_icon_imageview)
+
+ // Display the favorite icon in the tab.
+ tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(icon, 64, 64, true))
+ }
+ }
+ }
+ }
+
+ // Save a copy of the title when it changes.
+ override fun onReceivedTitle(view: WebView, title: String) {
+ // Get the current page position.
+ val currentPosition = webViewStateAdapter!!.getPositionForId(nestedScrollWebView.webViewFragmentId)
+
+ // Get the current tab.
+ val tab = tabLayout.getTabAt(currentPosition)
+
+ // Only populate the title text view if the tab has been fully created.
+ if (tab != null) {
+ // Get the custom view from the tab.
+ val tabView = tab.customView
+
+ // Only populate the title text view if the tab view has been fully populated.
+ if (tabView != null) {
+ // Get the title text view from the tab.
+ val tabTitleTextView = tabView.findViewById<TextView>(R.id.title_textview)
+
+ // Set the title according to the URL.
+ if (title == "about:blank") {
+ // Set the title to indicate a new tab.
+ tabTitleTextView.setText(R.string.new_tab)
+ } else {
+ // Set the title as the tab text.
+ tabTitleTextView.text = title
+ }
+ }
+ }
+ }
+
+ // Enter full screen video.
+ override fun onShowCustomView(video: View, callback: CustomViewCallback) {
+ // Set the full screen video flag.
+ displayingFullScreenVideo = true
+
+ // Hide the keyboard.
+ inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.windowToken, 0)
+
+ // Hide the coordinator layout.
+ coordinatorLayout.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.
+ */
+
+ // 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
+
+ // 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)
+
+ // 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
+ }
+
+ // Exit full screen video.
+ override fun onHideCustomView() {
+ // Exit the full screen video.
+ exitFullScreenVideo()
+ }
+
+ // Upload files.
+ override fun onShowFileChooser(webView: WebView, filePathCallback: ValueCallback<Array<Uri>>, fileChooserParams: FileChooserParams): Boolean {
+ // Store the file path callback.
+ fileChooserCallback = filePathCallback
+
+ // Create an intent to open a chooser based on the file chooser parameters.
+ val fileChooserIntent = fileChooserParams.createIntent()
+
+ // Check to see if the file chooser intent resolves to an installed package.
+ if (fileChooserIntent.resolveActivity(packageManager) != null) { // The file chooser intent is fine.
+ // Launch the file chooser intent.
+ browseFileUploadActivityResultLauncher.launch(fileChooserIntent)
+ } else { // The file chooser intent will cause a crash.
+ // Create a generic intent to open a chooser.
+ val genericFileChooserIntent = Intent(Intent.ACTION_GET_CONTENT)
+
+ // Request an openable file.
+ genericFileChooserIntent.addCategory(Intent.CATEGORY_OPENABLE)
+
+ // Set the file type to everything.
+ genericFileChooserIntent.type = "*/*"
+
+ // Launch the generic file chooser intent.
+ browseFileUploadActivityResultLauncher.launch(genericFileChooserIntent)
+ }
+
+ // Handle the event.
+ return true
+ }
+ }
+ nestedScrollWebView.webViewClient = object : WebViewClient() {
+ // `shouldOverrideUrlLoading` makes this WebView the default handler for URLs inside the app, so that links are not kicked out to other apps.
+ override fun shouldOverrideUrlLoading(view: WebView, webResourceRequest: WebResourceRequest): Boolean {
+ // Get the URL from the web resource request.
+ var requestUrlString = webResourceRequest.url.toString()
+
+ // Sanitize the url.
+ requestUrlString = sanitizeUrl(requestUrlString)
+
+ // Handle the URL according to the type.
+ return if (requestUrlString.startsWith("http")) { // Load the URL in Privacy Browser.
+ // Load the URL. By using `loadUrl()`, instead of `loadUrlFromBase()`, the Referer header will never be sent.
+ loadUrl(nestedScrollWebView, requestUrlString)
+
+ // Returning true indicates that Privacy Browser is manually handling the loading of the URL.
+ // Custom headers cannot be added if false is returned and the WebView handles the loading of the URL.
+ true
+ } else if (requestUrlString.startsWith("mailto:")) { // Load the email address in an external email program.
+ // 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(requestUrlString)
+
+ // Open the email program in a new task instead of as part of Privacy Browser.
+ emailIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+
+ try {
+ // Make it so.
+ startActivity(emailIntent)
+ } catch (exception: ActivityNotFoundException) {
+ // Display a snackbar.
+ Snackbar.make(currentWebView!!, getString(R.string.error, exception), Snackbar.LENGTH_INDEFINITE).show()
+ }
+
+ // Returning true indicates Privacy Browser is handling the URL by creating an intent.
+ true
+ } else if (requestUrlString.startsWith("tel:")) { // Load the phone number in the dialer.
+ // Create a dial intent.
+ val dialIntent = Intent(Intent.ACTION_DIAL)
+
+ // Add the phone number to the intent.
+ dialIntent.data = Uri.parse(requestUrlString)
+
+ // Open the dialer in a new task instead of as part of Privacy Browser.
+ dialIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+
+ try {
+ // Make it so.
+ startActivity(dialIntent)
+ } catch (exception: ActivityNotFoundException) {
+ // Display a snackbar.
+ Snackbar.make(currentWebView!!, getString(R.string.error, exception), Snackbar.LENGTH_INDEFINITE).show()
+ }
+
+ // Returning true indicates Privacy Browser is handling the URL by creating an intent.
+ true
+ } else { // Load a system chooser to select an app that can handle the URL.
+ // Create a generic intent to open an app.
+ val genericIntent = Intent(Intent.ACTION_VIEW)
+
+ // Add the URL to the intent.
+ genericIntent.data = Uri.parse(requestUrlString)
+
+ // List all apps that can handle the URL instead of just opening the first one.
+ genericIntent.addCategory(Intent.CATEGORY_BROWSABLE)
+
+ // Open the app in a new task instead of as part of Privacy Browser.
+ genericIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+
+ try {
+ // Make it so.
+ startActivity(genericIntent)
+ } catch (exception: ActivityNotFoundException) {
+ // Display a snackbar.
+ Snackbar.make(nestedScrollWebView, getString(R.string.unrecognized_url, requestUrlString), Snackbar.LENGTH_SHORT).show()
+ }
+
+ // Returning true indicates Privacy Browser is handling the URL by creating an intent.
+ true
+ }
+ }
+
+ // Check requests against the block lists.
+ override fun shouldInterceptRequest(view: WebView, webResourceRequest: WebResourceRequest): WebResourceResponse? {
+ // Get the URL.
+ val requestUrlString = webResourceRequest.url.toString()
+
+ // Check to see if the resource request is for the main URL.
+ if (requestUrlString == nestedScrollWebView.currentUrl) {
+ // `return null` loads the resource request, which should never be blocked if it is the main URL.
+ return null
+ }
+
+ // Wait until the filter lists have been populated. When Privacy Browser is being resumed after having the process killed in the background it will try to load the URLs immediately.
+ while (ultraPrivacy == null) {
+ try {
+ // Check to see if the filter lists have been populated after 100 ms.
+ Thread.sleep(100)
+ } catch (exception: InterruptedException) {
+ // Do nothing.
+ }
+ }
+
+ // Create an empty web resource response to be used if the resource request is blocked.
+ val emptyWebResourceResponse = WebResourceResponse("text/plain", "utf8", ByteArrayInputStream("".toByteArray()))
+
+ // Initialize the variables.
+ var allowListResultStringArray: Array<String>? = null
+ var isThirdPartyRequest = false
+
+ // Get the current URL. `.getUrl()` throws an error because operations on the WebView cannot be made from this thread.
+ var currentBaseDomain = nestedScrollWebView.currentDomainName
+
+ // Store a copy of the current domain for use in later requests.
+ val currentDomain = currentBaseDomain
+
+ // Get the request host name.
+ var requestBaseDomain = webResourceRequest.url.host
+
+ // Only check for third-party requests if the current base domain is not empty and the request domain is not null.
+ if (currentBaseDomain.isNotEmpty() && (requestBaseDomain != null)) {
+ // Determine the current base domain.
+ while (currentBaseDomain.indexOf(".", currentBaseDomain.indexOf(".") + 1) > 0) { // There is at least one subdomain.
+ // Remove the first subdomain.
+ currentBaseDomain = currentBaseDomain.substring(currentBaseDomain.indexOf(".") + 1)
+ }
+
+ // Determine the request base domain.
+ while (requestBaseDomain!!.indexOf(".", requestBaseDomain.indexOf(".") + 1) > 0) { // There is at least one subdomain.
+ // Remove the first subdomain.
+ requestBaseDomain = requestBaseDomain.substring(requestBaseDomain.indexOf(".") + 1)
+ }
+
+ // Update the third party request tracker.
+ isThirdPartyRequest = currentBaseDomain != requestBaseDomain
+ }
+
+ // Get the current WebView page position.
+ val webViewPagePosition = webViewStateAdapter!!.getPositionForId(nestedScrollWebView.webViewFragmentId)
+
+ // Determine if the WebView is currently displayed.
+ val webViewDisplayed = (webViewPagePosition == tabLayout.selectedTabPosition)
+
+ // Block third-party requests if enabled.
+ if (isThirdPartyRequest && nestedScrollWebView.blockAllThirdPartyRequests) {
+ // Add the result to the resource requests.
+ nestedScrollWebView.addResourceRequest(arrayOf(REQUEST_THIRD_PARTY, requestUrlString))
+
+ // Increment the blocked requests counters.
+ nestedScrollWebView.incrementRequestsCount(BLOCKED_REQUESTS)
+ nestedScrollWebView.incrementRequestsCount(THIRD_PARTY_REQUESTS)