+ // Manually delete the `app_webview` folder, which contains the cookies, DOM storage, form data, and `Service Worker` cache.
+ // See `https://code.google.com/p/android/issues/detail?id=233826&thanks=233826&ts=1486670530`.
+ if (clearEverything) {
+ try {
+ // Delete the folder.
+ Process deleteAppWebviewProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview");
+
+ // Wait until the process has finished.
+ deleteAppWebviewProcess.waitFor();
+ } catch (Exception exception) {
+ // Do nothing if an error is thrown.
+ }
+ }
+
+ // Close Privacy Browser. `finishAndRemoveTask` also removes Privacy Browser from the recent app list.
+ finishAndRemoveTask();
+
+ // Remove the terminated program from RAM. The status code is `0`.
+ System.exit(0);
+ }
+
+ public void bookmarksBack(View view) {
+ if (currentBookmarksFolder.isEmpty()) { // The home folder is displayed.
+ // close the bookmarks drawer.
+ drawerLayout.closeDrawer(GravityCompat.END);
+ } else { // A subfolder is displayed.
+ // Place the former parent folder in `currentFolder`.
+ currentBookmarksFolder = bookmarksDatabaseHelper.getParentFolderName(currentBookmarksFolder);
+
+ // Load the new folder.
+ loadBookmarksFolder();
+ }
+ }
+
+ public void toggleBookmarksDrawerPinned(View view) {
+ // Toggle the bookmarks drawer pinned tracker.
+ bookmarksDrawerPinned = !bookmarksDrawerPinned;
+
+ // Update the bookmarks drawer pinned image view.
+ updateBookmarksDrawerPinnedImageView();
+ }
+
+ private void updateBookmarksDrawerPinnedImageView() {
+ // Set the current icon.
+ if (bookmarksDrawerPinned)
+ bookmarksDrawerPinnedImageView.setImageResource(R.drawable.pin_selected);
+ else
+ bookmarksDrawerPinnedImageView.setImageResource(R.drawable.pin);
+ }
+
+ private void setCurrentWebView(int pageNumber) {
+ // Stop the swipe to refresh indicator if it is running
+ swipeRefreshLayout.setRefreshing(false);
+
+ // Get the WebView tab fragment.
+ WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(pageNumber);
+
+ // Get the fragment view.
+ View webViewFragmentView = webViewTabFragment.getView();
+
+ // Set the current WebView if the fragment view is not null.
+ if (webViewFragmentView != null) { // The fragment has been populated.
+ // Store the current WebView.
+ currentWebView = webViewFragmentView.findViewById(R.id.nestedscroll_webview);
+
+ // Update the status of swipe to refresh.
+ if (currentWebView.getSwipeToRefresh()) { // Swipe to refresh is enabled.
+ // Enable the swipe refresh layout if the WebView is scrolled all the way to the top. It is updated every time the scroll changes.
+ swipeRefreshLayout.setEnabled(currentWebView.getScrollY() == 0);
+ } else { // Swipe to refresh is disabled.
+ // Disable the swipe refresh layout.
+ swipeRefreshLayout.setEnabled(false);
+ }
+
+ // Get a handle for the cookie manager.
+ CookieManager cookieManager = CookieManager.getInstance();
+
+ // Set the cookie status.
+ cookieManager.setAcceptCookie(currentWebView.getAcceptCookies());
+
+ // Update the privacy icons. `true` redraws the icons in the app bar.
+ updatePrivacyIcons(true);
+
+ // Get a handle for the input method manager.
+ InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+
+ // Remove the lint warning below that the input method manager might be null.
+ assert inputMethodManager != null;
+
+ // Get the current URL.
+ String url = currentWebView.getUrl();
+
+ // Update the URL edit text if not loading a new intent. Otherwise, this will be handled by `onPageStarted()` (if called) and `onPageFinished()`.
+ if (!loadingNewIntent) { // A new intent is not being loaded.
+ if ((url == null) || url.equals("about:blank")) { // The WebView is blank.
+ // Display the hint in the URL edit text.
+ urlEditText.setText("");
+
+ // Request focus for the URL text box.
+ urlEditText.requestFocus();
+
+ // Display the keyboard.
+ inputMethodManager.showSoftInput(urlEditText, 0);
+ } else { // The WebView has a loaded URL.
+ // Clear the focus from the URL text box.
+ urlEditText.clearFocus();
+
+ // Hide the soft keyboard.
+ inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
+
+ // Display the current URL in the URL text box.
+ urlEditText.setText(url);
+
+ // Highlight the URL syntax.
+ UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan);
+ }
+ } else { // A new intent is being loaded.
+ // Reset the loading new intent tracker.
+ loadingNewIntent = false;
+ }
+
+ // Set the background to indicate the domain settings status.
+ if (currentWebView.getDomainSettingsApplied()) {
+ // Set a background on the URL relative layout to indicate that custom domain settings are being used.
+ urlRelativeLayout.setBackground(ResourcesCompat.getDrawable(getResources(), R.drawable.domain_settings_url_background, null));
+ } else {
+ // Remove any background on the URL relative layout.
+ urlRelativeLayout.setBackground(ResourcesCompat.getDrawable(getResources(), R.color.transparent, null));
+ }
+ } else if (pageNumber == savedTabPosition){ // The app is being restored but the saved tab position fragment has not been populated yet. Try again in 100 milliseconds.
+ // Create a handler to set the current WebView.
+ Handler setCurrentWebViewHandler = new Handler();
+
+ // Create a runnable to set the current WebView.
+ Runnable setCurrentWebWebRunnable = () -> {
+ // Set the current WebView.
+ setCurrentWebView(pageNumber);
+ };
+
+ // Try setting the current WebView again after 100 milliseconds.
+ setCurrentWebViewHandler.postDelayed(setCurrentWebWebRunnable, 100);
+ }
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ @Override
+ public void initializeWebView(@NonNull NestedScrollWebView nestedScrollWebView, int pageNumber, @NonNull ProgressBar progressBar, @NonNull String url, boolean restoringState) {
+ // Get a handle for the shared preferences.
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+
+ // Get the WebView theme.
+ String webViewTheme = sharedPreferences.getString(getString(R.string.webview_theme_key), getString(R.string.webview_theme_default_value));
+
+ // Get the WebView theme entry values string array.
+ String[] webViewThemeEntryValuesStringArray = getResources().getStringArray(R.array.webview_theme_entry_values);
+
+ // Set the WebView theme if device is running API >= 29 and algorithmic darkening is supported.
+ if ((Build.VERSION.SDK_INT >= 29) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {
+ // Set the WebView them. A switch statement cannot be used because the WebView theme entry values string array is not a compile time constant.
+ if (webViewTheme.equals(webViewThemeEntryValuesStringArray[1])) { // The light theme is selected.
+ // Turn off algorithmic darkening.
+ WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.getSettings(), false);
+
+ // Make the WebView visible. The WebView was created invisible in `webview_framelayout` to prevent a white background splash in night mode.
+ // If the system is currently in night mode, showing the WebView will be handled in `onProgressChanged()`.
+ nestedScrollWebView.setVisibility(View.VISIBLE);
+ } else if (webViewTheme.equals(webViewThemeEntryValuesStringArray[2])) { // The dark theme is selected.
+ // Turn on algorithmic darkening.
+ WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.getSettings(), true);
+ } else {
+ // The system default theme is selected.
+ int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
+
+ // Set the algorithmic darkening according to the current system theme status.
+ if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { // The system is in day mode.
+ // Turn off algorithmic darkening.
+ WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.getSettings(), false);
+
+ // Make the WebView visible. The WebView was created invisible in `webview_framelayout` to prevent a white background splash in night mode.
+ // If the system is currently in night mode, showing the WebView will be handled in `onProgressChanged()`.
+ nestedScrollWebView.setVisibility(View.VISIBLE);
+ } else { // The system is in night mode.
+ // Turn on algorithmic darkening.
+ WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.getSettings(), true);
+ }
+ }
+ }
+
+ // Get a handle for the activity
+ Activity activity = this;
+
+ // Get a handle for the input method manager.
+ InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+
+ // Instantiate the blocklist helper.
+ BlocklistHelper blocklistHelper = new BlocklistHelper();
+
+ // Remove the lint warning below that the input method manager might be null.
+ assert inputMethodManager != null;
+
+ // Set the app bar scrolling.
+ nestedScrollWebView.setNestedScrollingEnabled(scrollAppBar);
+
+ // 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.
+ nestedScrollWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW);
+
+ // 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);
+
+ // Allow loading of file:// URLs. This is necessary for opening MHT web archives, which are copies into a temporary cache location.
+ nestedScrollWebView.getSettings().setAllowFileAccess(true);
+
+ // 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) { // The app bar is hidden.
+ // Close the find on page bar if it is visible.
+ closeFindOnPage(null);
+
+ // Hide the tab linear layout.
+ tabsLinearLayout.setVisibility(View.GONE);
+
+ // Hide the action bar.
+ actionBar.hide();
+
+ // Set layout and scrolling parameters according to the position of the app bar.
+ if (bottomAppBar) { // The app bar is at the bottom.
+ // Reset the WebView padding to fill the available space.
+ swipeRefreshLayout.setPadding(0, 0, 0, 0);
+ } else { // The app bar is at the top.
+ // Check to see if the app bar is normally scrolled.
+ if (scrollAppBar) { // The app bar is scrolled when it is displayed.
+ // Get the swipe refresh layout parameters.
+ CoordinatorLayout.LayoutParams swipeRefreshLayoutParams = (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams();
+
+ // Remove the off-screen scrolling layout.
+ swipeRefreshLayoutParams.setBehavior(null);
+ } else { // The app bar is not scrolled when it is displayed.
+ // Remove the padding from the top of the swipe refresh layout.
+ swipeRefreshLayout.setPadding(0, 0, 0, 0);
+
+ // The swipe refresh circle must be moved above the now removed status bar location.
+ swipeRefreshLayout.setProgressViewOffset(false, -200, defaultProgressViewEndOffset);
+ }
+ }
+ } else { // The app bar is not hidden.
+ // Adjust the UI for the bottom app bar.
+ if (bottomAppBar) {
+ // Adjust the UI according to the scrolling of the app bar.
+ if (scrollAppBar) {
+ // Reset the WebView padding to fill the available space.
+ swipeRefreshLayout.setPadding(0, 0, 0, 0);
+ } else {
+ // Move the WebView above the app bar layout.
+ swipeRefreshLayout.setPadding(0, 0, 0, appBarHeight);
+ }
+ }
+ }
+
+ /* 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 if it was hidden.
+ if (hideAppBar) {
+ // Show the tab linear layout.
+ tabsLinearLayout.setVisibility(View.VISIBLE);
+
+ // Show the action bar.
+ actionBar.show();
+ }
+
+ // Set layout and scrolling parameters according to the position of the app bar.
+ if (bottomAppBar) { // The app bar is at the bottom.
+ // Adjust the UI.
+ if (scrollAppBar) {
+ // Reset the WebView padding to fill the available space.
+ swipeRefreshLayout.setPadding(0, 0, 0, 0);
+ } else {
+ // Move the WebView above the app bar layout.
+ swipeRefreshLayout.setPadding(0, 0, 0, appBarHeight);
+ }
+ } else { // The app bar is at the top.
+ // Check to see if the app bar is normally scrolled.
+ if (scrollAppBar) { // The app bar is scrolled when it is displayed.
+ // Get the swipe refresh layout parameters.
+ CoordinatorLayout.LayoutParams swipeRefreshLayoutParams = (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams();
+
+ // Add the off-screen scrolling layout.
+ swipeRefreshLayoutParams.setBehavior(new 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.
+ rootFrameLayout.setSystemUiVisibility(0);
+ }
+
+ // Consume the double-tap.
+ return true;
+ } else { // Do not consume the double-tap because full screen browsing mode is disabled.
+ return false;
+ }
+ }
+
+ @Override
+ public boolean onFling(MotionEvent motionEvent1, MotionEvent motionEvent2, float velocityX, float velocityY) {
+ // Scroll the bottom app bar if enabled.
+ if (bottomAppBar && scrollAppBar && !objectAnimator.isRunning()) {
+ // Calculate the Y change.
+ float motionY = motionEvent2.getY() - motionEvent1.getY();
+
+ // 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", 0);
+ } else if (motionY < -50) {
+ // Animate the bottom app bar off the screen.
+ objectAnimator = ObjectAnimator.ofFloat(appBarLayout, "translationY", appBarLayout.getHeight());
+ }
+
+ // 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 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 downloadUrl, String userAgent, String contentDisposition, String mimetype, long contentLength) -> {
+ // Check the download preference.
+ if (downloadWithExternalApp) { // Download with an external app.
+ downloadUrlWithExternalApp(downloadUrl);
+ } else { // Handle the download inside of Privacy Browser.
+ // Define a formatted file size string.
+ String formattedFileSizeString;
+
+ // Process the content length if it contains data.
+ if (contentLength > 0) { // The content length is greater than 0.
+ // Format the content length as a string.
+ formattedFileSizeString = 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`.
+ formattedFileSizeString = getString(R.string.unknown_size);
+ }
+
+ // Get the file name from the content disposition.
+ String fileNameString = UrlHelper.getFileName(this, contentDisposition, mimetype, downloadUrl);
+
+ // Instantiate the save dialog.
+ DialogFragment saveDialogFragment = SaveDialog.saveUrl(downloadUrl, fileNameString, formattedFileSizeString, userAgent,
+ nestedScrollWebView.getAcceptCookies());
+
+ // 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. It must be named `save_dialog` so that the file picker can update the file name.
+ saveDialogFragment.show(getSupportFragmentManager(), 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(new PendingDialogDataClass(saveDialogFragment, getString(R.string.save_dialog)));
+ }
+ }
+ });
+
+ // 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);