+ private class WebViewPagerAdapter extends FragmentPagerAdapter {
+ // The WebView fragments list contains all the WebViews.
+ private LinkedList<WebViewTabFragment> webViewFragmentsList = new LinkedList<>();
+
+ // Define the constructor.
+ private WebViewPagerAdapter(FragmentManager fragmentManager){
+ // Run the default commands.
+ super(fragmentManager);
+ }
+
+ @Override
+ public int getCount() {
+ // Return the number of pages.
+ return webViewFragmentsList.size();
+ }
+
+ @Override
+ public int getItemPosition(@NonNull Object object) {
+ //noinspection SuspiciousMethodCalls
+ if (webViewFragmentsList.contains(object)) {
+ // The tab has not been deleted.
+ return POSITION_UNCHANGED;
+ } else {
+ // The tab has been deleted.
+ return POSITION_NONE;
+ }
+ }
+
+ @Override
+ public Fragment getItem(int pageNumber) {
+ // Get a WebView for a particular page. Page numbers are 0 indexed.
+ return webViewFragmentsList.get(pageNumber);
+ }
+
+ private void addPage() {
+ // Add a new page. The pages and tabs are 0 indexed, so the size of the current list equals the number of the next page.
+ webViewFragmentsList.add(WebViewTabFragment.createTab(webViewFragmentsList.size()));
+
+ // Update the view pager.
+ notifyDataSetChanged();
+ }
+
+ private void deletePage(int pageNumber) {
+ // Get a handle for the tab layout.
+ TabLayout tabLayout = findViewById(R.id.tablayout);
+
+ // TODO always move to the next tab if possible.
+ // Select a tab that is not being deleted.
+ if (pageNumber == 0) { // The first tab is being deleted.
+ // Get a handle for the second tab. The tabs are 0 indexed.
+ TabLayout.Tab secondTab = tabLayout.getTabAt(1);
+
+ // Remove the incorrect lint warning below that the second tab might be null.
+ assert secondTab != null;
+
+ // Select the second tab.
+ secondTab.select();
+ } else { // The first tab is not being deleted.
+ // Get a handle for the previous tab.
+ TabLayout.Tab previousTab = tabLayout.getTabAt(pageNumber - 1);
+
+ // Remove the incorrect lint warning below tha the previous tab might be null.
+ assert previousTab != null;
+
+ // Select the previous tab.
+ previousTab.select();
+ }
+
+ // Delete the page.
+ webViewFragmentsList.remove(pageNumber);
+
+ // Delete the tab.
+ tabLayout.removeTabAt(pageNumber);
+
+ // Update the view pager.
+ notifyDataSetChanged();
+ }
+ }
+
+ public void addTab(View view) {
+ // Add the new WebView page.
+ webViewPagerAdapter.addPage();
+
+ // Get a handle for the tab layout.
+ TabLayout tabLayout = findViewById(R.id.tablayout);
+
+ // Get a handle for the new tab. The tabs are 0 indexed.
+ TabLayout.Tab newTab = tabLayout.getTabAt(tabLayout.getTabCount() - 1);
+
+ // Remove the incorrect warning below that the new tab might be null.
+ assert newTab != null;
+
+ // Move the tab layout to the new tab.
+ newTab.select();
+ }
+
+ @Override
+ public void initializeWebView(int tabNumber, ProgressBar progressBar, NestedScrollWebView nestedScrollWebView) {
+ // Get handles for the activity views.
+ final FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
+ final DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
+ final RelativeLayout mainContentRelativeLayout = findViewById(R.id.main_content_relativelayout);
+ final ActionBar actionBar = getSupportActionBar();
+ final TabLayout tabLayout = findViewById(R.id.tablayout);
+ final SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
+
+ // Remove the incorrect lint warnings below that the some of the views might be null.
+ assert actionBar != null;
+
+ // TODO. Still doesn't work right.
+ // Create the tab if it doesn't already exist.
+ try {
+ TabLayout.Tab tab = tabLayout.getTabAt(tabNumber);
+
+ assert tab != null;
+
+ tab.getCustomView();
+ } catch (Exception exception) {
+ tabLayout.addTab(tabLayout.newTab());
+ }
+
+ // Get the current tab.
+ TabLayout.Tab currentTab = tabLayout.getTabAt(tabNumber);
+
+ // Remove the lint warning below that the current tab might be null.
+ assert currentTab != null;
+
+ // Set a custom view on the current tab.
+ currentTab.setCustomView(R.layout.custom_tab_view);
+
+ // Get the custom view from the tab.
+ View currentTabView = currentTab.getCustomView();
+
+ // Remove the incorrect warning below that the current tab view might be null.
+ assert currentTabView != null;
+
+ // Get the current views from the tab.
+ ImageView tabFavoriteIconImageView = currentTabView.findViewById(R.id.favorite_icon_imageview);
+ TextView tabTitleTextView = currentTabView.findViewById(R.id.title_textview);
+
+ // Get a handle for the shared preferences.
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+
+ // Get the relevant preferences.
+ boolean downloadWithExternalApp = sharedPreferences.getBoolean("download_with_external_app", false);
+
+ // Allow pinch to zoom.
+ nestedScrollWebView.getSettings().setBuiltInZoomControls(true);
+
+ // Hide zoom controls.
+ nestedScrollWebView.getSettings().setDisplayZoomControls(false);
+
+ // Don't allow mixed content (HTTP and HTTPS) on the same website.
+ if (Build.VERSION.SDK_INT >= 21) {
+ nestedScrollWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW);
+ }
+
+ // Set the WebView to use a wide viewport. Otherwise, some web pages will be scrunched and some content will render outside the screen.
+ nestedScrollWebView.getSettings().setUseWideViewPort(true);
+
+ // Set the WebView to load in overview mode (zoomed out to the maximum width).
+ nestedScrollWebView.getSettings().setLoadWithOverviewMode(true);
+
+ // Explicitly disable geolocation.
+ nestedScrollWebView.getSettings().setGeolocationEnabled(false);
+
+ // Create a double-tap gesture detector to toggle full-screen mode.
+ GestureDetector doubleTapGestureDetector = new GestureDetector(getApplicationContext(), new GestureDetector.SimpleOnGestureListener() {
+ // Override `onDoubleTap()`. All other events are handled using the default settings.
+ @Override
+ public boolean onDoubleTap(MotionEvent event) {
+ if (fullScreenBrowsingModeEnabled) { // Only process the double-tap if full screen browsing mode is enabled.
+ // Toggle the full screen browsing mode tracker.
+ inFullScreenBrowsingMode = !inFullScreenBrowsingMode;
+
+ // Toggle the full screen browsing mode.
+ if (inFullScreenBrowsingMode) { // Switch to full screen mode.
+ // Hide the app bar if specified.
+ if (hideAppBar) {
+ actionBar.hide();
+ }
+
+ // Hide the banner ad in the free flavor.
+ if (BuildConfig.FLAVOR.contentEquals("free")) {
+ AdHelper.hideAd(findViewById(R.id.adview));
+ }
+
+ // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+
+ /* 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);
+ } else { // Switch to normal viewing mode.
+ // Show the app bar.
+ actionBar.show();
+
+ // Show the banner ad in the free flavor.
+ if (BuildConfig.FLAVOR.contentEquals("free")) {
+ // Reload the ad.
+ AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
+ }
+
+ // Remove the `SYSTEM_UI` flags from the root frame layout.
+ rootFrameLayout.setSystemUiVisibility(0);
+
+ // Add the translucent status flag.
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+ }
+
+ // Consume the double-tap.
+ return true;
+ } else { // Do not consume the double-tap because full screen browsing mode is disabled.
+ return false;
+ }
+ }
+ });
+
+ // Pass all touch events on the WebView through the double-tap gesture detector.
+ nestedScrollWebView.setOnTouchListener((View view, MotionEvent event) -> {
+ // Call `performClick()` on the view, which is required for accessibility.
+ view.performClick();
+
+ // Send the event to the gesture detector.
+ return doubleTapGestureDetector.onTouchEvent(event);
+ });
+
+ // 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((String url, String userAgent, String contentDisposition, String mimetype, long contentLength) -> {
+ // Check if the download should be processed by an external app.
+ if (downloadWithExternalApp) { // Download with an external app.
+ // Create a download intent. Not specifying the action type will display the maximum number of options.
+ Intent downloadIntent = new Intent();
+
+ // Set the URI and the MIME type. Specifying `text/html` displays a good number of options.
+ downloadIntent.setDataAndType(Uri.parse(url), "text/html");
+
+ // Flag the intent to open in a new task.
+ downloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ // Show the chooser.
+ startActivity(Intent.createChooser(downloadIntent, getString(R.string.open_with)));
+ } else { // Download with Android's download manager.
+ // Check to see if the WRITE_EXTERNAL_STORAGE permission has already been granted.
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission has not been granted.
+ // The WRITE_EXTERNAL_STORAGE permission needs to be requested.
+
+ // Store the variables for future use by `onRequestPermissionsResult()`.
+ downloadUrl = url;
+ downloadContentDisposition = contentDisposition;
+ downloadContentLength = contentLength;
+
+ // Show a dialog if the user has previously denied the permission.
+ if (ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
+ // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
+ DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE);
+
+ // Show the download location permission alert dialog. The permission will be requested when the the dialog is closed.
+ downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
+ } else { // Show the permission request directly.
+ // Request the permission. The download dialog will be launched by `onRequestPermissionResult()`.
+ ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
+ }
+ } else { // The storage permission has already been granted.
+ // Get a handle for the download file alert dialog.
+ DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(url, contentDisposition, contentLength);
+
+ // Show the download file alert dialog.
+ downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
+ }
+ }
+ });
+
+ // Update the find on page count.
+ nestedScrollWebView.setFindListener(new WebView.FindListener() {
+ // Get a handle for `findOnPageCountTextView`.
+ final TextView findOnPageCountTextView = findViewById(R.id.find_on_page_count_textview);
+
+ @Override
+ public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) {
+ if ((isDoneCounting) && (numberOfMatches == 0)) { // There are no matches.
+ // Set `findOnPageCountTextView` to `0/0`.
+ findOnPageCountTextView.setText(R.string.zero_of_zero);
+ } else if (isDoneCounting) { // There are matches.
+ // `activeMatchOrdinal` is zero-based.
+ int activeMatch = activeMatchOrdinal + 1;
+
+ // Build the match string.
+ String matchString = activeMatch + "/" + numberOfMatches;
+
+ // Set `findOnPageCountTextView`.
+ findOnPageCountTextView.setText(matchString);
+ }
+ }
+ });
+
+ // Set the web chrome client.
+ nestedScrollWebView.setWebChromeClient(new WebChromeClient() {
+ // Update the progress bar when a page is loading.
+ @Override
+ public void onProgressChanged(WebView view, int progress) {
+ // Inject the night mode CSS if night mode is enabled.
+ if (nightMode) {
+ // `background-color: #212121` sets the background to be dark gray. `color: #BDBDBD` sets the text color to be light gray. `box-shadow: none` removes a lower underline on links
+ // used by WordPress. `text-decoration: none` removes all text underlines. `text-shadow: none` removes text shadows, which usually have a hard coded color.
+ // `border: none` removes all borders, which can also be used to underline text. `a {color: #1565C0}` sets links to be a dark blue.
+ // `::selection {background: #0D47A1}' sets the text selection highlight color to be a dark blue. `!important` takes precedent over any existing sub-settings.
+ nestedScrollWebView.evaluateJavascript("(function() {var parent = document.getElementsByTagName('head').item(0); var style = document.createElement('style'); style.type = 'text/css'; " +
+ "style.innerHTML = '* {background-color: #212121 !important; color: #BDBDBD !important; box-shadow: none !important; text-decoration: none !important;" +
+ "text-shadow: none !important; border: none !important;} a {color: #1565C0 !important;} ::selection {background: #0D47A1 !important;}'; parent.appendChild(style)})()", value -> {
+ // Initialize a handler to display `mainWebView`.
+ Handler displayWebViewHandler = new Handler();
+
+ // Setup a runnable to display `mainWebView` after a delay to allow the CSS to be applied.
+ Runnable displayWebViewRunnable = () -> {
+ // Only display `mainWebView` if the progress bar is gone. This prevents the display of the `WebView` while it is still loading.
+ if (progressBar.getVisibility() == View.GONE) {
+ nestedScrollWebView.setVisibility(View.VISIBLE);
+ }
+ };
+
+ // Displaying of `mainWebView` after 500 milliseconds.
+ displayWebViewHandler.postDelayed(displayWebViewRunnable, 500);
+ });
+ }
+
+ // Update the progress bar.
+ progressBar.setProgress(progress);
+
+ // Set the visibility of the progress bar.
+ if (progress < 100) {
+ // Show the progress bar.
+ progressBar.setVisibility(View.VISIBLE);
+ } else {
+ // Hide the progress bar.
+ progressBar.setVisibility(View.GONE);
+
+ // Display `mainWebView` if night mode is disabled.
+ // Because of a race condition between `applyDomainSettings` and `onPageStarted`, when night mode is set by domain settings the `WebView` may be hidden even if night mode is not
+ // currently enabled.
+ if (!nightMode) {
+ nestedScrollWebView.setVisibility(View.VISIBLE);
+ }
+
+ //Stop the swipe to refresh indicator if it is running
+ swipeRefreshLayout.setRefreshing(false);
+ }
+ }
+
+ // Set the favorite icon when it changes.
+ @Override
+ public void onReceivedIcon(WebView view, Bitmap icon) {
+ // Only update the favorite icon if the website has finished loading.
+ if (progressBar.getVisibility() == View.GONE) {
+ // Save a copy of the favorite icon.
+ // TODO. We need to save and access the icons for each tab.
+ favoriteIconBitmap = icon;
+
+ tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(icon, 64, 64, true));
+ }
+ }
+
+ // Save a copy of the title when it changes.
+ @Override
+ public void onReceivedTitle(WebView view, String title) {
+ // Save a copy of the title.
+ // TODO. Replace `webViewTitle` with `currentWebView.getTitle()`.
+ webViewTitle = title;
+
+ // Set the title as the tab text.
+ tabTitleTextView.setText(webViewTitle);
+ }
+
+ // Enter full screen video.
+ @Override
+ public void onShowCustomView(View video, CustomViewCallback callback) {
+ // Set the full screen video flag.
+ displayingFullScreenVideo = true;
+
+ // Pause the ad if this is the free flavor.
+ if (BuildConfig.FLAVOR.contentEquals("free")) {
+ // The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
+ AdHelper.pauseAd(findViewById(R.id.adview));
+ }
+
+ // Hide the keyboard.
+ inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0);
+
+ // Hide the main content relative layout.
+ mainContentRelativeLayout.setVisibility(View.GONE);
+
+ // Remove the translucent status bar overlay on the `Drawer Layout`, which is special and needs its own command.
+ drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
+
+ // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+
+ /* 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);
+ }
+
+ // Exit full screen video.
+ @Override
+ public void onHideCustomView() {
+ // 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 frame layout.
+ fullScreenVideoFrameLayout.setVisibility(View.GONE);
+
+ // Enable the sliding drawers.
+ drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
+
+ // Show the main content relative layout.
+ mainContentRelativeLayout.setVisibility(View.VISIBLE);
+
+ // Apply the appropriate full screen mode the `SYSTEM_UI` flags.
+ if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode.
+ // Hide the app bar if specified.
+ if (hideAppBar) {
+ actionBar.hide();
+ }
+
+ // Hide the banner ad in the free flavor.
+ if (BuildConfig.FLAVOR.contentEquals("free")) {
+ AdHelper.hideAd(findViewById(R.id.adview));
+ }
+
+ // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+
+ /* 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);
+ } else { // Switch to normal viewing mode.
+ // Remove the `SYSTEM_UI` flags from the root frame layout.
+ rootFrameLayout.setSystemUiVisibility(0);
+
+ // Add the translucent status flag.
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+ }
+
+ // Reload the ad for the free flavor if not in full screen mode.
+ if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
+ // Reload the ad.
+ AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
+ }
+ }
+
+ // Upload files.
+ @Override
+ public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
+ // Show the file chooser if the device is running API >= 21.
+ if (Build.VERSION.SDK_INT >= 21) {
+ // Store the file path callback.
+ fileChooserCallback = filePathCallback;
+
+ // Create an intent to open a chooser based ont the file chooser parameters.
+ Intent fileChooserIntent = fileChooserParams.createIntent();
+
+ // Open the file chooser. Currently only one `startActivityForResult` exists in this activity, so the request code, used to differentiate them, is simply `0`.
+ startActivityForResult(fileChooserIntent, 0);
+ }
+ 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.
+ // The deprecated `shouldOverrideUrlLoading` must be used until API >= 24.
+ @SuppressWarnings("deprecation")
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView view, String url) {
+ if (url.startsWith("http")) { // Load the URL in Privacy Browser.
+ // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
+ formattedUrlString = "";
+
+ // Apply the domain settings for the new URL. `applyDomainSettings` doesn't do anything if the domain has not changed.
+ boolean userAgentChanged = applyDomainSettings(url, true, false);
+
+ // Check if the user agent has changed.
+ if (userAgentChanged) {
+ // Manually load the URL. The changing of the user agent will cause WebView to reload the previous URL.
+ nestedScrollWebView.loadUrl(url, customHeaders);
+
+ // Returning true indicates that Privacy Browser is manually handling the loading of the URL.
+ return true;
+ } else {
+ // Returning false causes the current WebView to handle the URL and prevents it from adding redirects to the history list.
+ return false;
+ }
+ } 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);
+
+ // Make it so.
+ startActivity(emailIntent);
+
+ // 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);
+
+ // Make it so.
+ startActivity(dialIntent);
+
+ // 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.
+ @SuppressWarnings("deprecation")
+ @Override
+ public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
+ // 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.
+ whiteListResultStringArray = null;
+
+ // Initialize the third party request tracker.
+ boolean isThirdPartyRequest = false;
+
+ // Initialize the current domain string.
+ String currentDomain = "";
+
+ // Nobody is happy when comparing null strings.
+ if (!(formattedUrlString == null) && !(url == null)) {
+ // Get the domain strings to URIs.
+ Uri currentDomainUri = Uri.parse(formattedUrlString);
+ Uri requestDomainUri = Uri.parse(url);
+
+ // Get the domain host names.
+ String currentBaseDomain = currentDomainUri.getHost();
+ String requestBaseDomain = requestDomainUri.getHost();
+
+ // Update the current domain variable.
+ currentDomain = currentBaseDomain;
+
+ // Only compare the current base domain and the request base domain if neither is null.
+ if (!(currentBaseDomain == null) && !(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);
+ }
+ }
+
+ // Block third-party requests if enabled.
+ if (isThirdPartyRequest && blockAllThirdPartyRequests) {
+ // Increment the blocked requests counters.
+ blockedRequests++;
+ thirdPartyBlockedRequests++;
+
+ // Update the titles of the blocklist menu items. This must be run from the UI thread.
+ activity.runOnUiThread(() -> {
+ navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
+ blocklistsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
+ blockAllThirdPartyRequestsMenuItem.setTitle(thirdPartyBlockedRequests + " - " + getString(R.string.block_all_third_party_requests));
+ });
+
+ // Add the request to the log.
+ resourceRequests.add(new String[]{String.valueOf(REQUEST_THIRD_PARTY), url});
+
+ // Return an empty web resource response.
+ return emptyWebResourceResponse;
+ }
+
+ // Check UltraPrivacy if it is enabled.
+ if (ultraPrivacyEnabled) {
+ if (blockListHelper.isBlocked(currentDomain, url, isThirdPartyRequest, ultraPrivacy)) {
+ // Increment the blocked requests counters.
+ blockedRequests++;
+ ultraPrivacyBlockedRequests++;
+
+ // Update the titles of the blocklist menu items. This must be run from the UI thread.
+ activity.runOnUiThread(() -> {
+ navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
+ blocklistsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
+ ultraPrivacyMenuItem.setTitle(ultraPrivacyBlockedRequests + " - " + getString(R.string.ultraprivacy));
+ });
+
+ // The resource request was blocked. Return an empty web resource response.
+ return emptyWebResourceResponse;
+ }
+
+ // If the whitelist result is not null, the request has been allowed by UltraPrivacy.
+ if (whiteListResultStringArray != null) {
+ // Add a whitelist entry to the resource requests array.
+ resourceRequests.add(whiteListResultStringArray);
+
+ // The resource request has been allowed by UltraPrivacy. `return null` loads the requested resource.
+ return null;
+ }
+ }
+
+ // Check EasyList if it is enabled.
+ if (easyListEnabled) {
+ if (blockListHelper.isBlocked(currentDomain, url, isThirdPartyRequest, easyList)) {
+ // Increment the blocked requests counters.
+ blockedRequests++;
+ easyListBlockedRequests++;
+
+ // Update the titles of the blocklist menu items. This must be run from the UI thread.
+ activity.runOnUiThread(() -> {
+ navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
+ blocklistsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
+ easyListMenuItem.setTitle(easyListBlockedRequests + " - " + getString(R.string.easylist));
+ });
+
+ // Reset the whitelist results tracker (because otherwise it will sometimes add results to the list due to a race condition).
+ whiteListResultStringArray = null;
+
+ // The resource request was blocked. Return an empty web resource response.
+ return emptyWebResourceResponse;
+ }
+ }
+
+ // Check EasyPrivacy if it is enabled.
+ if (easyPrivacyEnabled) {
+ if (blockListHelper.isBlocked(currentDomain, url, isThirdPartyRequest, easyPrivacy)) {
+ // Increment the blocked requests counters.
+ blockedRequests++;
+ easyPrivacyBlockedRequests++;
+
+ // Update the titles of the blocklist menu items. This must be run from the UI thread.
+ activity.runOnUiThread(() -> {
+ navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
+ blocklistsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
+ easyPrivacyMenuItem.setTitle(easyPrivacyBlockedRequests + " - " + getString(R.string.easyprivacy));
+ });
+
+ // Reset the whitelist results tracker (because otherwise it will sometimes add results to the list due to a race condition).
+ whiteListResultStringArray = null;
+
+ // The resource request was blocked. Return an empty web resource response.
+ return emptyWebResourceResponse;
+ }
+ }
+
+ // Check Fanboy’s Annoyance List if it is enabled.
+ if (fanboysAnnoyanceListEnabled) {
+ if (blockListHelper.isBlocked(currentDomain, url, isThirdPartyRequest, fanboysAnnoyanceList)) {
+ // Increment the blocked requests counters.
+ blockedRequests++;
+ fanboysAnnoyanceListBlockedRequests++;
+
+ // Update the titles of the blocklist menu items. This must be run from the UI thread.
+ activity.runOnUiThread(() -> {
+ navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
+ blocklistsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
+ fanboysAnnoyanceListMenuItem.setTitle(fanboysAnnoyanceListBlockedRequests + " - " + getString(R.string.fanboys_annoyance_list));
+ });
+
+ // Reset the whitelist results tracker (because otherwise it will sometimes add results to the list due to a race condition).
+ whiteListResultStringArray = null;
+
+ // The resource request was blocked. Return an empty web resource response.
+ return emptyWebResourceResponse;
+ }
+ } else if (fanboysSocialBlockingListEnabled) { // Only check Fanboy’s Social Blocking List if Fanboy’s Annoyance List is disabled.
+ if (blockListHelper.isBlocked(currentDomain, url, isThirdPartyRequest, fanboysSocialList)) {
+ // Increment the blocked requests counters.
+ blockedRequests++;
+ fanboysSocialBlockingListBlockedRequests++;
+
+ // Update the titles of the blocklist menu items. This must be run from the UI thread.
+ activity.runOnUiThread(() -> {
+ navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
+ blocklistsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
+ fanboysSocialBlockingListMenuItem.setTitle(fanboysSocialBlockingListBlockedRequests + " - " + getString(R.string.fanboys_social_blocking_list));
+ });
+
+ // Reset the whitelist results tracker (because otherwise it will sometimes add results to the list due to a race condition).
+ whiteListResultStringArray = null;
+
+ // The resource request was blocked. Return an empty web resource response.
+ return emptyWebResourceResponse;
+ }
+ }
+
+ // Add the request to the log because it hasn't been processed by any of the previous checks.
+ if (whiteListResultStringArray != null) { // The request was processed by a whitelist.
+ resourceRequests.add(whiteListResultStringArray);
+ } else { // The request didn't match any blocklist entry. Log it as a default request.
+ resourceRequests.add(new String[]{String.valueOf(REQUEST_DEFAULT), url});
+ }
+
+ // The resource request has not been blocked. `return null` loads the requested resource.
+ return null;
+ }
+
+ // Handle HTTP authentication requests.
+ @Override
+ public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {
+ // Store `handler` so it can be accessed from `onHttpAuthenticationCancel()` and `onHttpAuthenticationProceed()`.
+ httpAuthHandler = handler;
+
+ // Display the HTTP authentication dialog.
+ DialogFragment httpAuthenticationDialogFragment = HttpAuthenticationDialog.displayDialog(host, realm);
+ httpAuthenticationDialogFragment.show(fragmentManager, getString(R.string.http_authentication));
+ }
+
+ // Update the URL in urlTextBox when the page starts to load.
+ @Override
+ public void onPageStarted(WebView view, String url, Bitmap favicon) {
+ // Set `urlIsLoading` to `true`, so that redirects while loading do not trigger changes in the user agent, which forces another reload of the existing page.
+ // This is also used to determine when to check for pinned mismatches.
+ urlIsLoading = true;
+
+ // Reset the list of host IP addresses.
+ currentHostIpAddresses = "";
+
+ // Reset the list of resource requests.
+ resourceRequests.clear();
+
+ // Initialize the counters for requests blocked by each blocklist.
+ blockedRequests = 0;
+ easyListBlockedRequests = 0;
+ easyPrivacyBlockedRequests = 0;
+ fanboysAnnoyanceListBlockedRequests = 0;
+ fanboysSocialBlockingListBlockedRequests = 0;
+ ultraPrivacyBlockedRequests = 0;
+ thirdPartyBlockedRequests = 0;
+
+ // If night mode is enabled, hide `mainWebView` until after the night mode CSS is applied.
+ if (nightMode) {
+ nestedScrollWebView.setVisibility(View.INVISIBLE);
+ }
+
+ // Hide the keyboard.
+ inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0);
+
+ // Check to see if Privacy Browser is waiting on Orbot.
+ if (!waitingForOrbot) { // Process the URL.
+ // The formatted URL string must be updated at the beginning of the load, so that if the user toggles JavaScript during the load the new website is reloaded.
+ formattedUrlString = url;
+
+ // Display the formatted URL text.
+ urlTextBox.setText(formattedUrlString);
+
+ // Apply text highlighting to `urlTextBox`.
+ highlightUrlText();
+
+ // Get a URI for the current URL.
+ Uri currentUri = Uri.parse(formattedUrlString);
+
+ // Get the IP addresses for the host.
+ new GetHostIpAddresses(activity).execute(currentUri.getHost());
+
+ // Apply any custom domain settings if the URL was loaded by navigating history.
+ if (navigatingHistory) {
+ // Apply the domain settings.
+ boolean userAgentChanged = applyDomainSettings(url, true, false);
+
+ // Reset `navigatingHistory`.
+ navigatingHistory = false;
+
+ // Manually load the URL if the user agent has changed, which will have caused the previous URL to be reloaded.
+ if (userAgentChanged) {
+ loadUrl(formattedUrlString);
+ }
+ }
+
+ // Replace Refresh with Stop if the menu item has been created. (The WebView typically begins loading before the menu items are instantiated.)
+ if (refreshMenuItem != null) {
+ // Set the title.
+ refreshMenuItem.setTitle(R.string.stop);
+
+ // If the icon is displayed in the AppBar, set it according to the theme.
+ if (displayAdditionalAppBarIcons) {
+ if (darkTheme) {
+ refreshMenuItem.setIcon(R.drawable.close_dark);
+ } else {
+ refreshMenuItem.setIcon(R.drawable.close_light);
+ }
+ }
+ }
+ }
+ }
+
+ // It is necessary to update `formattedUrlString` and `urlTextBox` after the page finishes loading because the final URL can change during load.
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ // Reset the wide view port if it has been turned off by the waiting for Orbot message.
+ if (!waitingForOrbot) {
+ // Only use a wide view port if the URL starts with `http`, not for `file://` and `content://`.
+ nestedScrollWebView.getSettings().setUseWideViewPort(url.startsWith("http"));
+ }
+
+ // Flush any cookies to persistent storage. `CookieManager` has become very lazy about flushing cookies in recent versions.
+ if (firstPartyCookiesEnabled && Build.VERSION.SDK_INT >= 21) {
+ cookieManager.flush();
+ }
+
+ // Update the Refresh menu item if it has been created.
+ if (refreshMenuItem != null) {
+ // Reset the Refresh title.
+ refreshMenuItem.setTitle(R.string.refresh);
+
+ // If the icon is displayed in the AppBar, reset it according to the theme.
+ if (displayAdditionalAppBarIcons) {
+ if (darkTheme) {
+ refreshMenuItem.setIcon(R.drawable.refresh_enabled_dark);
+ } else {
+ refreshMenuItem.setIcon(R.drawable.refresh_enabled_light);
+ }
+ }
+ }
+
+
+ // Clear the cache and history if Incognito Mode is enabled.
+ if (incognitoModeEnabled) {
+ // Clear the cache. `true` includes disk files.
+ nestedScrollWebView.clearCache(true);
+
+ // Clear the back/forward history.
+ nestedScrollWebView.clearHistory();
+
+ // Manually delete cache folders.
+ try {
+ // Delete the main cache directory.
+ privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/cache");
+
+ // Delete the secondary `Service Worker` cache directory.
+ // A `String[]` must be used because the directory contains a space and `Runtime.exec` will not escape the string correctly otherwise.
+ privacyBrowserRuntime.exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
+ } catch (IOException e) {
+ // Do nothing if an error is thrown.
+ }
+ }
+
+ // Update the URL text box and apply domain settings if not waiting on Orbot.
+ if (!waitingForOrbot) {
+ // Check to see if `WebView` has set `url` to be `about:blank`.
+ if (url.equals("about:blank")) { // `WebView` is blank, so `formattedUrlString` should be `""` and `urlTextBox` should display a hint.
+ // Set `formattedUrlString` to `""`.
+ formattedUrlString = "";
+
+ urlTextBox.setText(formattedUrlString);
+
+ // Request focus for `urlTextBox`.
+ urlTextBox.requestFocus();
+
+ // Display the keyboard.
+ inputMethodManager.showSoftInput(urlTextBox, 0);
+
+ // Apply the domain settings. This clears any settings from the previous domain.
+ applyDomainSettings(formattedUrlString, true, false);
+ } else { // `WebView` has loaded a webpage.
+ // Set the formatted URL string. Getting the URL from the WebView instead of using the one provided by `onPageFinished` makes websites like YouTube function correctly.
+ formattedUrlString = nestedScrollWebView.getUrl();
+
+ // Only update the URL text box if the user is not typing in it.
+ if (!urlTextBox.hasFocus()) {
+ // Display the formatted URL text.
+ urlTextBox.setText(formattedUrlString);
+
+ // Apply text highlighting to `urlTextBox`.
+ highlightUrlText();
+ }
+ }
+
+ // Store the SSL certificate so it can be accessed from `ViewSslCertificateDialog` and `PinnedMismatchDialog`.
+ sslCertificate = nestedScrollWebView.getCertificate();
+
+ // Check the current website information against any pinned domain information if the current IP addresses have been loaded.
+ if (!gettingIpAddresses) {
+ checkPinnedMismatch();
+ }
+ }
+
+ // Reset `urlIsLoading`, which is used to prevent reloads on redirect if the user agent changes. It is also used to determine when to check for pinned mismatches.
+ urlIsLoading = false;
+ }
+
+ // Handle SSL Certificate errors.
+ @Override
+ public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
+ // Get the current website SSL certificate.
+ SslCertificate currentWebsiteSslCertificate = error.getCertificate();
+
+ // Extract the individual pieces of information from the current website SSL certificate.
+ String currentWebsiteIssuedToCName = currentWebsiteSslCertificate.getIssuedTo().getCName();
+ String currentWebsiteIssuedToOName = currentWebsiteSslCertificate.getIssuedTo().getOName();
+ String currentWebsiteIssuedToUName = currentWebsiteSslCertificate.getIssuedTo().getUName();
+ String currentWebsiteIssuedByCName = currentWebsiteSslCertificate.getIssuedBy().getCName();
+ String currentWebsiteIssuedByOName = currentWebsiteSslCertificate.getIssuedBy().getOName();
+ String currentWebsiteIssuedByUName = currentWebsiteSslCertificate.getIssuedBy().getUName();
+ Date currentWebsiteSslStartDate = currentWebsiteSslCertificate.getValidNotBeforeDate();
+ Date currentWebsiteSslEndDate = currentWebsiteSslCertificate.getValidNotAfterDate();
+
+ // Proceed to the website if the current SSL website certificate matches the pinned domain certificate.
+ if (pinnedSslCertificate &&
+ currentWebsiteIssuedToCName.equals(pinnedSslIssuedToCName) && currentWebsiteIssuedToOName.equals(pinnedSslIssuedToOName) &&
+ currentWebsiteIssuedToUName.equals(pinnedSslIssuedToUName) && currentWebsiteIssuedByCName.equals(pinnedSslIssuedByCName) &&
+ currentWebsiteIssuedByOName.equals(pinnedSslIssuedByOName) && currentWebsiteIssuedByUName.equals(pinnedSslIssuedByUName) &&
+ currentWebsiteSslStartDate.equals(pinnedSslStartDate) && currentWebsiteSslEndDate.equals(pinnedSslEndDate)) {
+
+ // An SSL certificate is pinned and matches the current domain certificate. Proceed to the website without displaying an error.
+ handler.proceed();
+ } else { // Either there isn't a pinned SSL certificate or it doesn't match the current website certificate.
+ // Store `handler` so it can be accesses from `onSslErrorCancel()` and `onSslErrorProceed()`.
+ sslErrorHandler = handler;
+
+ // Display the SSL error `AlertDialog`.
+ DialogFragment sslCertificateErrorDialogFragment = SslCertificateErrorDialog.displayDialog(error);
+ sslCertificateErrorDialogFragment.show(fragmentManager, getString(R.string.ssl_certificate_error));
+ }
+ }
+ });
+
+ // Check to see if this is the first tab.
+ if (tabNumber == 0) {
+ // Set this nested scroll WebView as the current WebView.
+ currentWebView = nestedScrollWebView;
+
+ // Apply the app settings from the shared preferences.
+ applyAppSettings();
+
+ // Load the website if not waiting for Orbot to connect.
+ if (!waitingForOrbot) {
+ loadUrl(formattedUrlString);
+ }