+ // Only highlight the URL text if the box is not currently selected.
+ if (!urlEditText.hasFocus()) {
+ // Get the URL string.
+ String urlString = urlEditText.getText().toString();
+
+ // Highlight the URL according to the protocol.
+ if (urlString.startsWith("file://") || urlString.startsWith("content://")) { // This is a file or content URL.
+ // De-emphasize everything before the file name.
+ urlEditText.getText().setSpan(initialGrayColorSpan, 0, urlString.lastIndexOf("/") + 1,Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+ } else { // This is a web URL.
+ // Get the index of the `/` immediately after the domain name.
+ int endOfDomainName = urlString.indexOf("/", (urlString.indexOf("//") + 2));
+
+ // Create a base URL string.
+ String baseUrl;
+
+ // Get the base URL.
+ if (endOfDomainName > 0) { // There is at least one character after the base URL.
+ // Get the base URL.
+ baseUrl = urlString.substring(0, endOfDomainName);
+ } else { // There are no characters after the base URL.
+ // Set the base URL to be the entire URL string.
+ baseUrl = urlString;
+ }
+
+ // Get the index of the last `.` in the domain.
+ int lastDotIndex = baseUrl.lastIndexOf(".");
+
+ // Get the index of the penultimate `.` in the domain.
+ int penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1);
+
+ // Markup the beginning of the URL.
+ if (urlString.startsWith("http://")) { // Highlight the protocol of connections that are not encrypted.
+ urlEditText.getText().setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+
+ // De-emphasize subdomains.
+ if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name.
+ urlEditText.getText().setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+ }
+ } else if (urlString.startsWith("https://")) { // De-emphasize the protocol of connections that are encrypted.
+ if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name.
+ // De-emphasize the protocol and the additional subdomains.
+ urlEditText.getText().setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+ } else { // There is only one subdomain in the domain name.
+ // De-emphasize only the protocol.
+ urlEditText.getText().setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+ }
+ }
+
+ // De-emphasize the text after the domain name.
+ if (endOfDomainName > 0) {
+ urlEditText.getText().setSpan(finalGrayColorSpan, endOfDomainName, urlString.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+ }
+ }
+ }
+ }
+
+ private void loadBookmarksFolder() {
+ // Update the bookmarks cursor with the contents of the bookmarks database for the current folder.
+ bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
+
+ // Populate the bookmarks cursor adapter. `this` specifies the `Context`. `false` disables `autoRequery`.
+ bookmarksCursorAdapter = new CursorAdapter(this, bookmarksCursor, false) {
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ // Inflate the individual item layout. `false` does not attach it to the root.
+ return getLayoutInflater().inflate(R.layout.bookmarks_drawer_item_linearlayout, parent, false);
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ // Get handles for the views.
+ ImageView bookmarkFavoriteIcon = view.findViewById(R.id.bookmark_favorite_icon);
+ TextView bookmarkNameTextView = view.findViewById(R.id.bookmark_name);
+
+ // Get the favorite icon byte array from the cursor.
+ byte[] favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON));
+
+ // Convert the byte array to a `Bitmap` beginning at the first byte and ending at the last.
+ Bitmap favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.length);
+
+ // Display the bitmap in `bookmarkFavoriteIcon`.
+ bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap);
+
+ // Get the bookmark name from the cursor and display it in `bookmarkNameTextView`.
+ String bookmarkNameString = cursor.getString(cursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
+ bookmarkNameTextView.setText(bookmarkNameString);
+
+ // Make the font bold for folders.
+ if (cursor.getInt(cursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {
+ bookmarkNameTextView.setTypeface(Typeface.DEFAULT_BOLD);
+ } else { // Reset the font to default for normal bookmarks.
+ bookmarkNameTextView.setTypeface(Typeface.DEFAULT);
+ }
+ }
+ };
+
+ // Get a handle for the bookmarks list view.
+ ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
+
+ // Populate the list view with the adapter.
+ bookmarksListView.setAdapter(bookmarksCursorAdapter);
+
+ // Get a handle for the bookmarks title text view.
+ TextView bookmarksTitleTextView = findViewById(R.id.bookmarks_title_textview);
+
+ // Set the bookmarks drawer title.
+ if (currentBookmarksFolder.isEmpty()) {
+ bookmarksTitleTextView.setText(R.string.bookmarks);
+ } else {
+ bookmarksTitleTextView.setText(currentBookmarksFolder);
+ }
+ }
+
+ private void openWithApp(String url) {
+ // Create an open with app intent with `ACTION_VIEW`.
+ Intent openWithAppIntent = new Intent(Intent.ACTION_VIEW);
+
+ // Set the URI but not the MIME type. This should open all available apps.
+ openWithAppIntent.setData(Uri.parse(url));
+
+ // Flag the intent to open in a new task.
+ openWithAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ // Try the intent.
+ try {
+ // Show the chooser.
+ startActivity(openWithAppIntent);
+ } catch (ActivityNotFoundException exception) { // There are no apps available to open the URL.
+ // Show a snackbar with the error.
+ Snackbar.make(currentWebView, getString(R.string.error) + " " + exception, Snackbar.LENGTH_INDEFINITE).show();
+ }
+ }
+
+ private void openWithBrowser(String url) {
+ // Create an open with browser intent with `ACTION_VIEW`.
+ Intent openWithBrowserIntent = new Intent(Intent.ACTION_VIEW);
+
+ // Set the URI and the MIME type. `"text/html"` should load browser options.
+ openWithBrowserIntent.setDataAndType(Uri.parse(url), "text/html");
+
+ // Flag the intent to open in a new task.
+ openWithBrowserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ // Try the intent.
+ try {
+ // Show the chooser.
+ startActivity(openWithBrowserIntent);
+ } catch (ActivityNotFoundException exception) { // There are no browsers available to open the URL.
+ // Show a snackbar with the error.
+ Snackbar.make(currentWebView, getString(R.string.error) + " " + exception, Snackbar.LENGTH_INDEFINITE).show();
+ }
+ }
+
+ private String sanitizeUrl(String url) {
+ // Sanitize Google Analytics.
+ if (sanitizeGoogleAnalytics) {
+ // Remove `?utm_`.
+ if (url.contains("?utm_")) {
+ url = url.substring(0, url.indexOf("?utm_"));
+ }
+
+ // Remove `&utm_`.
+ if (url.contains("&utm_")) {
+ url = url.substring(0, url.indexOf("&utm_"));
+ }
+ }
+
+ // Sanitize Facebook Click IDs.
+ if (sanitizeFacebookClickIds) {
+ // Remove `?fbclid=`.
+ if (url.contains("?fbclid=")) {
+ url = url.substring(0, url.indexOf("?fbclid="));
+ }
+
+ // Remove `&fbclid=`.
+ if (url.contains("&fbclid=")) {
+ url = url.substring(0, url.indexOf("&fbclid="));
+ }
+
+ // Remove `?fbadid=`.
+ if (url.contains("?fbadid=")) {
+ url = url.substring(0, url.indexOf("?fbadid="));
+ }
+
+ // Remove `&fbadid=`.
+ if (url.contains("&fbadid=")) {
+ url = url.substring(0, url.indexOf("&fbadid="));
+ }
+ }
+
+ // Sanitize Twitter AMP redirects.
+ if (sanitizeTwitterAmpRedirects) {
+ // Remove `?amp=1`.
+ if (url.contains("?amp=1")) {
+ url = url.substring(0, url.indexOf("?amp=1"));
+ }
+ }
+
+ // Return the sanitized URL.
+ return url;
+ }
+
+ public void finishedPopulatingBlocklists(ArrayList<ArrayList<List<String[]>>> combinedBlocklists) {
+ // Store the blocklists.
+ easyList = combinedBlocklists.get(0);
+ easyPrivacy = combinedBlocklists.get(1);
+ fanboysAnnoyanceList = combinedBlocklists.get(2);
+ fanboysSocialList = combinedBlocklists.get(3);
+ ultraList = combinedBlocklists.get(4);
+ ultraPrivacy = combinedBlocklists.get(5);
+
+ // Check to see if the activity has been restarted.
+ if ((savedStateArrayList == null) || (savedStateArrayList.size() == 0)) { // The activity has not been restarted or it was restarted on start to force the night theme.
+ // Add the first tab.
+ addNewTab("", true);
+ } else { // The activity has been restarted.
+ // Restore each tab. Once the minimum API >= 24, a `forEach()` command can be used.
+ for (int i = 0; i < savedStateArrayList.size(); i++) {
+ // Add a new tab.
+ tabLayout.addTab(tabLayout.newTab());
+
+ // Get the new tab.
+ TabLayout.Tab newTab = tabLayout.getTabAt(i);
+
+ // Remove the lint warning below that the current tab might be null.
+ assert newTab != null;
+
+ // Set a custom view on the new tab.
+ newTab.setCustomView(R.layout.tab_custom_view);
+
+ // Add the new page.
+ webViewPagerAdapter.restorePage(savedStateArrayList.get(i), savedNestedScrollWebViewStateArrayList.get(i));
+ }
+
+ // Reset the saved state variables.
+ savedStateArrayList = null;
+ savedNestedScrollWebViewStateArrayList = null;
+
+ // Restore the selected tab position.
+ if (savedTabPosition == 0) { // The first tab is selected.
+ // Set the first page as the current WebView.
+ setCurrentWebView(0);
+ } else { // the first tab is not selected.
+ // Move to the selected tab.
+ webViewPager.setCurrentItem(savedTabPosition);
+ }
+
+ // Get the intent that started the app.
+ Intent intent = getIntent();
+
+ // Reset the intent. This prevents a duplicate tab from being created on restart.
+ setIntent(new Intent());
+
+ // Get the information from the intent.
+ String intentAction = intent.getAction();
+ Uri intentUriData = intent.getData();
+
+ // Determine if this is a web search.
+ boolean isWebSearch = ((intentAction != null) && intentAction.equals(Intent.ACTION_WEB_SEARCH));
+
+ // Only process the URI if it contains data or it is a web search. If the user pressed the desktop icon after the app was already running the URI will be null.
+ if (intentUriData != null || isWebSearch) {
+ // Get the shared preferences.
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+
+ // Create a URL string.
+ String url;
+
+ // If the intent action is a web search, perform the search.
+ if (isWebSearch) { // The intent is a web search.
+ // Create an encoded URL string.
+ String encodedUrlString;
+
+ // Sanitize the search input and convert it to a search.
+ try {
+ encodedUrlString = URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8");
+ } catch (UnsupportedEncodingException exception) {
+ encodedUrlString = "";
+ }
+
+ // Add the base search URL.
+ url = searchURL + encodedUrlString;
+ } else { // The intent should contain a URL.
+ // Set the intent data as the url.
+ url = intentUriData.toString();
+ }
+
+ // Add a new tab if specified in the preferences.
+ if (sharedPreferences.getBoolean("open_intents_in_new_tab", true)) { // Load the URL in a new tab.
+ // Set the loading new intent flag.
+ loadingNewIntent = true;
+
+ // Add a new tab.
+ addNewTab(url, true);
+ } else { // Load the URL in the current tab.
+ // Make it so.
+ loadUrl(currentWebView, url);
+ }
+ }
+ }
+ }
+
+ public void addTab(View view) {
+ // Add a new tab with a blank URL.
+ addNewTab("", true);
+ }
+
+ private void addNewTab(String url, boolean moveToTab) {
+ // Get the new page number. The page numbers are 0 indexed, so the new page number will match the current count.
+ int newTabNumber = tabLayout.getTabCount();
+
+ // Add a new tab.
+ tabLayout.addTab(tabLayout.newTab());
+
+ // Get the new tab.
+ TabLayout.Tab newTab = tabLayout.getTabAt(newTabNumber);
+
+ // Remove the lint warning below that the current tab might be null.
+ assert newTab != null;
+
+ // Set a custom view on the new tab.
+ newTab.setCustomView(R.layout.tab_custom_view);
+
+ // Add the new WebView page.
+ webViewPagerAdapter.addPage(newTabNumber, webViewPager, url, moveToTab);
+ }
+
+ public void closeTab(View view) {
+ // Run the command according to the number of tabs.
+ if (tabLayout.getTabCount() > 1) { // There is more than one tab open.
+ // Close the current tab.
+ closeCurrentTab();
+ } else { // There is only one tab open.
+ clearAndExit();
+ }
+ }
+
+ private void closeCurrentTab() {
+ // Pause the current WebView.
+ currentWebView.onPause();
+
+ // Pause the current WebView JavaScript timers.
+ currentWebView.pauseTimers();
+
+ // Get the current tab number.
+ int currentTabNumber = tabLayout.getSelectedTabPosition();
+
+ // Delete the current tab.
+ tabLayout.removeTabAt(currentTabNumber);
+
+ // Delete the current page. If the selected page number did not change during the delete, it will return true, meaning that the current WebView must be reset.
+ if (webViewPagerAdapter.deletePage(currentTabNumber, webViewPager)) {
+ setCurrentWebView(currentTabNumber);
+ }
+
+ // Expand the app bar if it is currently collapsed.
+ appBarLayout.setExpanded(true);
+ }
+
+ private void saveWebpageArchive(String filePath) {
+ // Save the webpage archive.
+ currentWebView.saveWebArchive(filePath);
+
+ // Display a snackbar.
+ Snackbar saveWebpageArchiveSnackbar = Snackbar.make(currentWebView, getString(R.string.file_saved) + " " + filePath, Snackbar.LENGTH_SHORT);
+
+ // Add an open option to the snackbar.
+ saveWebpageArchiveSnackbar.setAction(R.string.open, (View view) -> {
+ // Get a file for the file name string.
+ File file = new File(filePath);
+
+ // Declare a file URI variable.
+ Uri fileUri;
+
+ // Get the URI for the file according to the Android version.
+ if (Build.VERSION.SDK_INT >= 24) { // Use a file provider.
+ fileUri = FileProvider.getUriForFile(this, getString(R.string.file_provider), file);
+ } else { // Get the raw file path URI.
+ fileUri = Uri.fromFile(file);
+ }
+
+ // Get a handle for the content resolver.
+ ContentResolver contentResolver = getContentResolver();
+
+ // Create an open intent with `ACTION_VIEW`.
+ Intent openIntent = new Intent(Intent.ACTION_VIEW);
+
+ // Set the URI and the MIME type.
+ openIntent.setDataAndType(fileUri, contentResolver.getType(fileUri));
+
+ // Allow the app to read the file URI.
+ openIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+
+ // Show the chooser.
+ startActivity(Intent.createChooser(openIntent, getString(R.string.open)));
+ });
+
+ // Show the snackbar.
+ saveWebpageArchiveSnackbar.show();
+ }
+
+ private void clearAndExit() {
+ // Get a handle for the shared preferences.
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+
+ // Close the bookmarks cursor and database.
+ bookmarksCursor.close();
+ bookmarksDatabaseHelper.close();
+
+ // Get the status of the clear everything preference.
+ boolean clearEverything = sharedPreferences.getBoolean("clear_everything", true);
+
+ // Get a handle for the runtime.
+ Runtime runtime = Runtime.getRuntime();
+
+ // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
+ // which links to `/data/data/com.stoutner.privacybrowser.standard`.
+ String privateDataDirectoryString = getApplicationInfo().dataDir;
+
+ // Clear cookies.
+ if (clearEverything || sharedPreferences.getBoolean("clear_cookies", true)) {
+ // The command to remove cookies changed slightly in API 21.
+ if (Build.VERSION.SDK_INT >= 21) {
+ CookieManager.getInstance().removeAllCookies(null);
+ } else {
+ CookieManager.getInstance().removeAllCookie();
+ }
+
+ // Manually delete the cookies database, as `CookieManager` sometimes will not flush its changes to disk before `System.exit(0)` is run.
+ try {
+ // Two commands must be used because `Runtime.exec()` does not like `*`.
+ Process deleteCookiesProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies");
+ Process deleteCookiesJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies-journal");
+
+ // Wait until the processes have finished.
+ deleteCookiesProcess.waitFor();
+ deleteCookiesJournalProcess.waitFor();
+ } catch (Exception exception) {
+ // Do nothing if an error is thrown.
+ }
+ }
+
+ // Clear DOM storage.
+ if (clearEverything || sharedPreferences.getBoolean("clear_dom_storage", true)) {
+ // Ask `WebStorage` to clear the DOM storage.
+ WebStorage webStorage = WebStorage.getInstance();
+ webStorage.deleteAllData();
+
+ // Manually delete the DOM storage files and directories, as `WebStorage` sometimes will not flush its changes to disk before `System.exit(0)` is run.
+ try {
+ // A `String[]` must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
+ Process deleteLocalStorageProcess = runtime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
+
+ // Multiple commands must be used because `Runtime.exec()` does not like `*`.
+ Process deleteIndexProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
+ Process deleteQuotaManagerProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
+ Process deleteQuotaManagerJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
+ Process deleteDatabaseProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
+
+ // Wait until the processes have finished.
+ deleteLocalStorageProcess.waitFor();
+ deleteIndexProcess.waitFor();
+ deleteQuotaManagerProcess.waitFor();
+ deleteQuotaManagerJournalProcess.waitFor();
+ deleteDatabaseProcess.waitFor();
+ } catch (Exception exception) {
+ // Do nothing if an error is thrown.
+ }
+ }
+
+ // Clear form data if the API < 26.
+ if ((Build.VERSION.SDK_INT < 26) && (clearEverything || sharedPreferences.getBoolean("clear_form_data", true))) {
+ WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
+ webViewDatabase.clearFormData();
+
+ // Manually delete the form data database, as `WebViewDatabase` sometimes will not flush its changes to disk before `System.exit(0)` is run.
+ try {
+ // A string array must be used because the database contains a space and `Runtime.exec` will not otherwise escape the string correctly.
+ Process deleteWebDataProcess = runtime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data"});
+ Process deleteWebDataJournalProcess = runtime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data-journal"});
+
+ // Wait until the processes have finished.
+ deleteWebDataProcess.waitFor();
+ deleteWebDataJournalProcess.waitFor();
+ } catch (Exception exception) {
+ // Do nothing if an error is thrown.
+ }
+ }
+
+ // Clear the logcat.
+ if (clearEverything || sharedPreferences.getBoolean(getString(R.string.clear_logcat_key), true)) {
+ try {
+ // Clear the logcat. `-c` clears the logcat. `-b all` clears all the buffers (instead of just crash, main, and system).
+ Process process = Runtime.getRuntime().exec("logcat -b all -c");
+
+ // Wait for the process to finish.
+ process.waitFor();
+ } catch (IOException|InterruptedException exception) {
+ // Do nothing.
+ }
+ }
+
+ // Clear the cache.
+ if (clearEverything || sharedPreferences.getBoolean("clear_cache", true)) {
+ // Clear the cache from each WebView.
+ for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
+ // Get the WebView tab fragment.
+ WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
+
+ // Get the fragment view.
+ View fragmentView = webViewTabFragment.getView();
+
+ // Only clear the cache if the WebView exists.
+ if (fragmentView != null) {
+ // Get the nested scroll WebView from the tab fragment.
+ NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
+
+ // Clear the cache for this WebView.
+ nestedScrollWebView.clearCache(true);
+ }
+ }
+
+ // Manually delete the cache directories.
+ try {
+ // Delete the main cache directory.
+ Process deleteCacheProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/cache");
+
+ // Delete the secondary `Service Worker` cache directory.
+ // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
+ Process deleteServiceWorkerProcess = runtime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
+
+ // Wait until the processes have finished.
+ deleteCacheProcess.waitFor();
+ deleteServiceWorkerProcess.waitFor();
+ } catch (Exception exception) {
+ // Do nothing if an error is thrown.
+ }
+ }
+
+ // Wipe out each WebView.
+ for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
+ // Get the WebView tab fragment.
+ WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
+
+ // Get the fragment view.
+ View fragmentView = webViewTabFragment.getView();
+
+ // Only wipe out the WebView if it exists.
+ if (fragmentView != null) {
+ // Get the nested scroll WebView from the tab fragment.
+ NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
+
+ // Clear SSL certificate preferences for this WebView.
+ nestedScrollWebView.clearSslPreferences();
+
+ // Clear the back/forward history for this WebView.
+ nestedScrollWebView.clearHistory();
+
+ // Destroy the internal state of the WebView.
+ nestedScrollWebView.destroy();
+ }
+ }
+
+ // Clear the custom headers.
+ customHeaders.clear();
+
+ // 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.
+ if (Build.VERSION.SDK_INT >= 21) {
+ finishAndRemoveTask();
+ } else {
+ finish();
+ }
+
+ // 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();
+ }
+ }
+
+ private void setCurrentWebView(int pageNumber) {
+ // Get handles for the URL views.
+ RelativeLayout urlRelativeLayout = findViewById(R.id.url_relativelayout);
+ EditText urlEditText = findViewById(R.id.url_edittext);
+ SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
+
+ // 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 fragmentView = webViewTabFragment.getView();
+
+ // Set the current WebView if the fragment view is not null.
+ if (fragmentView != null) { // The fragment has been populated.
+ // Store the current WebView.
+ currentWebView = fragmentView.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.getY() == 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 first-party cookie status.
+ cookieManager.setAcceptCookie(currentWebView.getAcceptFirstPartyCookies());
+
+ // 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();