+ //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)
+
+ // Update the titles of the filter lists menu items if the WebView is currently displayed.
+ if (webViewDisplayed) {
+ // Updating the UI must be run from the UI thread.
+ runOnUiThread {
+ // Update the menu item titles.
+ navigationRequestsMenuItem.title = getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
+
+ // Update the options menu if it has been populated.
+ if (optionsMenu != null) {
+ optionsFilterListsMenuItem.title = getString(R.string.filterlists) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
+ optionsBlockAllThirdPartyRequestsMenuItem.title =
+ nestedScrollWebView.getRequestsCount(THIRD_PARTY_REQUESTS).toString() + " - " + getString(R.string.block_all_third_party_requests)
+ }
+ }
+ }
+
+ // The resource request was blocked. Return an empty web resource response.
+ return emptyWebResourceResponse
+ }
+
+ // Check UltraList if it is enabled.
+ if (nestedScrollWebView.ultraListEnabled) {
+ // Check the URL against UltraList.
+ val ultraListResults = checkFilterListHelper.checkFilterList(currentDomain, requestUrlString, isThirdPartyRequest, ultraList)
+
+ // Process the UltraList results.
+ if (ultraListResults[0] == REQUEST_BLOCKED) { // The resource request matched UltraList's block list.
+ // Add the result to the resource requests.
+ nestedScrollWebView.addResourceRequest(arrayOf(ultraListResults[0], ultraListResults[1], ultraListResults[2], ultraListResults[3], ultraListResults[4], ultraListResults[5]))
+
+ // Increment the blocked requests counters.
+ nestedScrollWebView.incrementRequestsCount(BLOCKED_REQUESTS)
+ nestedScrollWebView.incrementRequestsCount(com.stoutner.privacybrowser.views.ULTRALIST)
+
+ // Update the titles of the filter lists menu items if the WebView is currently displayed.
+ if (webViewDisplayed) {
+ // Updating the UI must be run from the UI thread.
+ runOnUiThread {
+ // Update the menu item titles.
+ navigationRequestsMenuItem.title = getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
+
+ // Update the options menu if it has been populated.
+ if (optionsMenu != null) {
+ optionsFilterListsMenuItem.title = getString(R.string.filterlists) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
+ optionsUltraListMenuItem.title = nestedScrollWebView.getRequestsCount(com.stoutner.privacybrowser.views.ULTRALIST).toString() + " - " + getString(R.string.ultralist)
+ }
+ }
+ }
+
+ // The resource request was blocked. Return an empty web resource response.
+ return emptyWebResourceResponse
+ } else if (ultraListResults[0] == REQUEST_ALLOWED) { // The resource request matched UltraList's allow list.
+ // Add an allow list entry to the resource requests array.
+ nestedScrollWebView.addResourceRequest(arrayOf(ultraListResults[0], ultraListResults[1], ultraListResults[2], ultraListResults[3], ultraListResults[4], ultraListResults[5]))
+
+ // The resource request has been allowed by UltraList. `return null` loads the requested resource.
+ return null
+ }
+ }
+
+ // Check UltraPrivacy if it is enabled.
+ if (nestedScrollWebView.ultraPrivacyEnabled) {
+ // Check the URL against UltraPrivacy.
+ val ultraPrivacyResults = checkFilterListHelper.checkFilterList(currentDomain, requestUrlString, isThirdPartyRequest, ultraPrivacy!!)
+
+ // Process the UltraPrivacy results.
+ if (ultraPrivacyResults[0] == REQUEST_BLOCKED) { // The resource request matched UltraPrivacy's block list.
+ // Add the result to the resource requests.
+ nestedScrollWebView.addResourceRequest(arrayOf(ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4],
+ ultraPrivacyResults[5]))
+
+ // Increment the blocked requests counters.
+ nestedScrollWebView.incrementRequestsCount(BLOCKED_REQUESTS)
+ nestedScrollWebView.incrementRequestsCount(ULTRAPRIVACY)
+
+ // Update the titles of the filter lists menu items if the WebView is currently displayed.
+ if (webViewDisplayed) {
+ // Updating the UI must be run from the UI thread.
+ runOnUiThread {
+ // Update the menu item titles.
+ navigationRequestsMenuItem.title = getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
+
+ // Update the options menu if it has been populated.
+ if (optionsMenu != null) {
+ optionsFilterListsMenuItem.title = getString(R.string.filterlists) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
+ optionsUltraPrivacyMenuItem.title = nestedScrollWebView.getRequestsCount(ULTRAPRIVACY).toString() + " - " + getString(R.string.ultraprivacy)
+ }
+ }
+ }
+
+ // The resource request was blocked. Return an empty web resource response.
+ return emptyWebResourceResponse
+ } else if (ultraPrivacyResults[0] == REQUEST_ALLOWED) { // The resource request matched UltraPrivacy's allow list.
+ // Add an allow list entry to the resource requests array.
+ nestedScrollWebView.addResourceRequest(arrayOf(ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4],
+ ultraPrivacyResults[5]))
+
+ // The resource request has been allowed by UltraPrivacy. `return null` loads the requested resource.
+ return null
+ }
+ }
+
+ // Check EasyList if it is enabled.
+ if (nestedScrollWebView.easyListEnabled) {
+ // Check the URL against EasyList.
+ val easyListResults = checkFilterListHelper.checkFilterList(currentDomain, requestUrlString, isThirdPartyRequest, easyList)
+
+ // Process the EasyList results.
+ if (easyListResults[0] == REQUEST_BLOCKED) { // The resource request matched EasyList's block list.
+ // Add the result to the resource requests.
+ nestedScrollWebView.addResourceRequest(arrayOf(easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5]))
+
+ // Increment the blocked requests counters.
+ nestedScrollWebView.incrementRequestsCount(BLOCKED_REQUESTS)
+ nestedScrollWebView.incrementRequestsCount(EASYLIST)
+
+ // Update the titles of the filter lists menu items if the WebView is currently displayed.
+ if (webViewDisplayed) {
+ // Updating the UI must be run from the UI thread.
+ runOnUiThread {
+ // Update the menu item titles.
+ navigationRequestsMenuItem.title = getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
+
+ // Update the options menu if it has been populated.
+ if (optionsMenu != null) {
+ optionsFilterListsMenuItem.title = getString(R.string.filterlists) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
+ optionsEasyListMenuItem.title = nestedScrollWebView.getRequestsCount(EASYLIST).toString() + " - " + getString(R.string.easylist)
+ }
+ }
+ }
+
+ // The resource request was blocked. Return an empty web resource response.
+ return emptyWebResourceResponse
+ } else if (easyListResults[0] == REQUEST_ALLOWED) { // The resource request matched EasyList's allow list.
+ // Update the allow list result string array tracker.
+ allowListResultStringArray = arrayOf(easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5])
+ }
+ }
+
+ // Check EasyPrivacy if it is enabled.
+ if (nestedScrollWebView.easyPrivacyEnabled) {
+ // Check the URL against EasyPrivacy.
+ val easyPrivacyResults = checkFilterListHelper.checkFilterList(currentDomain, requestUrlString, isThirdPartyRequest, easyPrivacy)
+
+ // Process the EasyPrivacy results.
+ if (easyPrivacyResults[0] == REQUEST_BLOCKED) { // The resource request matched EasyPrivacy's block list.
+ // Add the result to the resource requests.
+ nestedScrollWebView.addResourceRequest(arrayOf(easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[4], easyPrivacyResults[5]))
+
+ // Increment the blocked requests counters.
+ nestedScrollWebView.incrementRequestsCount(BLOCKED_REQUESTS)
+ nestedScrollWebView.incrementRequestsCount(EASYPRIVACY)
+
+ // Update the titles of the filter lists menu items if the WebView is currently displayed.
+ if (webViewDisplayed) {
+ // Updating the UI must be run from the UI thread.
+ runOnUiThread {
+ // Update the menu item titles.
+ navigationRequestsMenuItem.title = getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
+
+ // Update the options menu if it has been populated.
+ if (optionsMenu != null) {
+ optionsFilterListsMenuItem.title = getString(R.string.filterlists) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
+ optionsEasyPrivacyMenuItem.title = nestedScrollWebView.getRequestsCount(EASYPRIVACY).toString() + " - " + getString(R.string.easyprivacy)
+ }
+ }
+ }
+
+ // The resource request was blocked. Return an empty web resource response.
+ return emptyWebResourceResponse
+ } else if (easyPrivacyResults[0] == REQUEST_ALLOWED) { // The resource request matched EasyPrivacy's allow list.
+ // Update the allow list result string array tracker.
+ allowListResultStringArray = arrayOf(easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[4], easyPrivacyResults[5])
+ }
+ }
+
+ // Check Fanboy’s Annoyance List if it is enabled.
+ if (nestedScrollWebView.fanboysAnnoyanceListEnabled) {
+ // Check the URL against Fanboy's Annoyance List.
+ val fanboysAnnoyanceListResults = checkFilterListHelper.checkFilterList(currentDomain, requestUrlString, isThirdPartyRequest, fanboysAnnoyanceList)