+ // Save a copy of the title when it changes.
+ @Override
+ public void onReceivedTitle(WebView view, String title) {
+ // Get the current page position.
+ int currentPosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
+
+ // Get the current tab.
+ TabLayout.Tab 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.
+ View tabView = tab.getCustomView();
+
+ // 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.
+ TextView tabTitleTextView = tabView.findViewById(R.id.title_textview);
+
+ // Set the title according to the URL.
+ if (title.equals("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.setText(title);
+ }
+ }
+ }
+ }
+
+ // Enter full screen video.
+ @Override
+ public void onShowCustomView(View video, CustomViewCallback callback) {
+ // Set the full screen video flag.
+ displayingFullScreenVideo = true;
+
+ // Hide the keyboard.
+ inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0);
+
+ // Hide the coordinator layout.
+ coordinatorLayout.setVisibility(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.
+ */
+ rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
+ 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.setVisibility(View.VISIBLE);
+
+ // Disable the screen timeout while the video is playing. YouTube does this automatically, but not all other videos do.
+ fullScreenVideoFrameLayout.setKeepScreenOn(true);
+ }
+
+ // Exit full screen video.
+ @Override
+ public void onHideCustomView() {
+ // Exit the full screen video.
+ exitFullScreenVideo();
+ }
+
+ // Upload files.
+ @Override
+ public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
+ // Store the file path callback.
+ fileChooserCallback = filePathCallback;
+
+ // Create an intent to open a chooser based on the file chooser parameters.
+ Intent fileChooserIntent = fileChooserParams.createIntent();
+
+ // Get a handle for the package manager.
+ PackageManager packageManager = getPackageManager();
+
+ // 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.
+ Intent genericFileChooserIntent = new Intent(Intent.ACTION_GET_CONTENT);
+
+ // Request an openable file.
+ genericFileChooserIntent.addCategory(Intent.CATEGORY_OPENABLE);
+
+ // Set the file type to everything.
+ genericFileChooserIntent.setType("*/*");
+
+ // Launch the generic file chooser intent.
+ browseFileUploadActivityResultLauncher.launch(genericFileChooserIntent);
+ }
+ return true;
+ }
+ });
+
+ nestedScrollWebView.setWebViewClient(new WebViewClient() {
+ // `shouldOverrideUrlLoading` makes this WebView the default handler for URLs inside the app, so that links are not kicked out to other apps.
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest webResourceRequest) {
+ // Get the URL from the web resource request.
+ String url = webResourceRequest.getUrl().toString();
+
+ // Sanitize the url.
+ url = sanitizeUrl(url);
+
+ // Handle the URL according to the type.
+ if (url.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, url);
+
+ // 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.
+ return true;
+ } else if (url.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.
+ Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
+
+ // Parse the url and set it as the data for the intent.
+ emailIntent.setData(Uri.parse(url));
+
+ // Open the email program in a new task instead of as part of Privacy Browser.
+ emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ try {
+ // Make it so.
+ startActivity(emailIntent);
+ } catch (ActivityNotFoundException exception) {
+ // 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.
+ return true;
+ } else if (url.startsWith("tel:")) { // Load the phone number in the dialer.
+ // Open the dialer and load the phone number, but wait for the user to place the call.
+ Intent dialIntent = new Intent(Intent.ACTION_DIAL);
+
+ // Add the phone number to the intent.
+ dialIntent.setData(Uri.parse(url));
+
+ // Open the dialer in a new task instead of as part of Privacy Browser.
+ dialIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ try {
+ // Make it so.
+ startActivity(dialIntent);
+ } catch (ActivityNotFoundException exception) {
+ // 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.
+ return true;
+ } else { // Load a system chooser to select an app that can handle the URL.
+ // Open an app that can handle the URL.
+ Intent genericIntent = new Intent(Intent.ACTION_VIEW);
+
+ // Add the URL to the intent.
+ genericIntent.setData(Uri.parse(url));
+
+ // 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.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ // Start the app or display a snackbar if no app is available to handle the URL.
+ try {
+ startActivity(genericIntent);
+ } catch (ActivityNotFoundException exception) {
+ Snackbar.make(nestedScrollWebView, getString(R.string.unrecognized_url) + " " + url, Snackbar.LENGTH_SHORT).show();
+ }
+
+ // Returning true indicates Privacy Browser is handling the URL by creating an intent.
+ return true;
+ }
+ }
+
+ // Check requests against the block lists. The deprecated `shouldInterceptRequest()` must be used until minimum API >= 21.
+ @Override
+ public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest webResourceRequest) {
+ // Get the URL.
+ String url = webResourceRequest.getUrl().toString();
+
+ // Check to see if the resource request is for the main URL.
+ if (url.equals(nestedScrollWebView.getCurrentUrl())) {
+ // `return null` loads the resource request, which should never be blocked if it is the main URL.
+ return null;
+ }
+
+ // Wait until the blocklists 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) {
+ // The wait must be synchronized, which only lets one thread run on it at a time, or `java.lang.IllegalMonitorStateException` is thrown.
+ synchronized (this) {
+ try {
+ // Check to see if the blocklists have been populated after 100 ms.
+ wait(100);
+ } catch (InterruptedException exception) {
+ // Do nothing.
+ }
+ }
+ }
+
+ // Create an empty web resource response to be used if the resource request is blocked.
+ WebResourceResponse emptyWebResourceResponse = new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes()));
+
+ // Reset the whitelist results tracker.
+ String[] whitelistResultStringArray = null;
+
+ // Initialize the third party request tracker.
+ boolean isThirdPartyRequest = false;
+
+ // Get the current URL. `.getUrl()` throws an error because operations on the WebView cannot be made from this thread.
+ String currentBaseDomain = nestedScrollWebView.getCurrentDomainName();
+
+ // Store a copy of the current domain for use in later requests.
+ String currentDomain = currentBaseDomain;
+
+ // Get the request host name.
+ String requestBaseDomain = webResourceRequest.getUrl().getHost();
+
+ // Only check for third-party requests if the current base domain is not empty and the request domain is not null.
+ if (!currentBaseDomain.isEmpty() && (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.equals(requestBaseDomain);
+ }
+
+ // Get the current WebView page position.
+ int webViewPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
+
+ // Determine if the WebView is currently displayed.
+ boolean webViewDisplayed = (webViewPagePosition == tabLayout.getSelectedTabPosition());
+
+ // Block third-party requests if enabled.
+ if (isThirdPartyRequest && nestedScrollWebView.getBlockAllThirdPartyRequests()) {
+ // Add the result to the resource requests.
+ nestedScrollWebView.addResourceRequest(new String[]{BlocklistHelper.REQUEST_THIRD_PARTY, url});
+
+ // Increment the blocked requests counters.
+ nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
+ nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS);
+
+ // Update the titles of the blocklist menu items if the WebView is currently displayed.
+ if (webViewDisplayed) {
+ // Updating the UI must be run from the UI thread.
+ activity.runOnUiThread(() -> {
+ // Update the menu item titles.
+ navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
+
+ // Update the options menu if it has been populated.
+ if (optionsMenu != null) {
+ optionsBlocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
+ optionsBlockAllThirdPartyRequestsMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " +
+ getString(R.string.block_all_third_party_requests));
+ }
+ });
+ }