+ // Reset the proxy to default. The host is `""` and the port is `"0"`.
+ OrbotProxyHelper.setProxy(getApplicationContext(), this, "", "0");
+
+ // Set the default app bar layout background.
+ if (darkTheme) {
+ appBarLayout.setBackgroundResource(R.color.gray_900);
+ } else {
+ appBarLayout.setBackgroundResource(R.color.gray_100);
+ }
+
+ // Reset `waitingForOrbot.
+ waitingForOrbot = false;
+
+ // Reload the WebViews if requested.
+ if (reloadWebsite) {
+ // Reload the WebViews.
+ 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 reload the WebViews if they exist.
+ if (fragmentView != null) {
+ // Get the nested scroll WebView from the tab fragment.
+ NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
+
+ // Reload the WebView.
+ nestedScrollWebView.reload();
+ }
+ }
+ }
+ }
+ }
+
+ private void updatePrivacyIcons(boolean runInvalidateOptionsMenu) {
+ // Only update the privacy icons if the options menu and the current WebView have already been populated.
+ if ((optionsMenu != null) && (currentWebView != null)) {
+ // Get a handle for the shared preferences.
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+
+ // Get the theme and screenshot preferences.
+ boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
+
+ // Get handles for the menu items.
+ MenuItem privacyMenuItem = optionsMenu.findItem(R.id.toggle_javascript);
+ MenuItem firstPartyCookiesMenuItem = optionsMenu.findItem(R.id.toggle_first_party_cookies);
+ MenuItem domStorageMenuItem = optionsMenu.findItem(R.id.toggle_dom_storage);
+ MenuItem refreshMenuItem = optionsMenu.findItem(R.id.refresh);
+
+ // Update the privacy icon.
+ if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScript is enabled.
+ privacyMenuItem.setIcon(R.drawable.javascript_enabled);
+ } else if (currentWebView.getAcceptFirstPartyCookies()) { // JavaScript is disabled but cookies are enabled.
+ privacyMenuItem.setIcon(R.drawable.warning);
+ } else { // All the dangerous features are disabled.
+ privacyMenuItem.setIcon(R.drawable.privacy_mode);
+ }
+
+ // Update the first-party cookies icon.
+ if (currentWebView.getAcceptFirstPartyCookies()) { // First-party cookies are enabled.
+ firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_enabled);
+ } else { // First-party cookies are disabled.
+ if (darkTheme) {
+ firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_dark);
+ } else {
+ firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_light);
+ }
+ }
+
+ // Update the DOM storage icon.
+ if (currentWebView.getSettings().getJavaScriptEnabled() && currentWebView.getSettings().getDomStorageEnabled()) { // Both JavaScript and DOM storage are enabled.
+ domStorageMenuItem.setIcon(R.drawable.dom_storage_enabled);
+ } else if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScript is enabled but DOM storage is disabled.
+ if (darkTheme) {
+ domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_dark);
+ } else {
+ domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_light);
+ }
+ } else { // JavaScript is disabled, so DOM storage is ghosted.
+ if (darkTheme) {
+ domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_dark);
+ } else {
+ domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_light);
+ }
+ }
+
+ // Update the refresh icon.
+ if (darkTheme) {
+ refreshMenuItem.setIcon(R.drawable.refresh_enabled_dark);
+ } else {
+ refreshMenuItem.setIcon(R.drawable.refresh_enabled_light);
+ }
+
+ // `invalidateOptionsMenu` calls `onPrepareOptionsMenu()` and redraws the icons in the `AppBar`.
+ if (runInvalidateOptionsMenu) {
+ invalidateOptionsMenu();
+ }
+ }
+ }
+
+ private void openUrlWithExternalApp(String url) {
+ // 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)));
+ }
+
+ private void highlightUrlText() {
+ // Get a handle for the URL edit text.
+ EditText urlEditText = findViewById(R.id.url_edittext);
+
+ // 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://")) { // This is a file URL.
+ // De-emphasize only the protocol.
+ urlEditText.getText().setSpan(initialGrayColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+ } else if (urlString.startsWith("content://")) {
+ // De-emphasize only the protocol.
+ urlEditText.getText().setSpan(initialGrayColorSpan, 0, 10, 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 the open with 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);
+
+ // Show the chooser.
+ startActivity(openWithAppIntent);
+ }
+
+ private void openWithBrowser(String url) {
+ // Create the open with 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);
+
+ // Show the chooser.
+ startActivity(openWithBrowserIntent);
+ }
+
+ 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="));
+ }
+ }
+
+ // 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 addTab(View view) {
+ // Add a new tab with a blank URL.
+ addNewTab("");
+ }
+
+ private void addNewTab(String url) {
+ // Sanitize the URL.
+ url = sanitizeUrl(url);
+
+ // Get a handle for the tab layout and the view pager.
+ TabLayout tabLayout = findViewById(R.id.tablayout);
+ ViewPager webViewPager = findViewById(R.id.webviewpager);
+
+ // 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);
+ }
+
+ public void closeTab(View view) {
+ // Get a handle for the tab layout.
+ TabLayout tabLayout = findViewById(R.id.tablayout);
+
+ // 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() {
+ // Get handles for the views.
+ AppBarLayout appBarLayout = findViewById(R.id.appbar_layout);
+ TabLayout tabLayout = findViewById(R.id.tablayout);
+ ViewPager webViewPager = findViewById(R.id.webviewpager);
+
+ // 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 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 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 `mainWebView`.
+ 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);
+ }
+
+ private void setCurrentWebView(int pageNumber) {
+ // Get a handle for the shared preferences.
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+
+ // Get the theme preference.
+ boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
+
+ // 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();
+
+ // Hide the soft keyboard.
+ inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
+
+ // Display the current URL in the URL text box.
+ urlEditText.setText(url);
+
+ // Highlight the URL text.
+ highlightUrlText();
+ }
+ } 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 green background on the URL relative layout to indicate that custom domain settings are being used. The deprecated `.getDrawable()` must be used until the minimum API >= 21.
+ if (darkTheme) {
+ urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_dark_blue));
+ } else {
+ urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_light_green));
+ }
+ } else {
+ urlRelativeLayout.setBackground(getResources().getDrawable(R.color.transparent));
+ }
+ } else { // The fragment has not been populated. 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);
+ }
+ }
+
+ @Override
+ public void initializeWebView(NestedScrollWebView nestedScrollWebView, int pageNumber, ProgressBar progressBar, String url) {
+ // Get handles for the activity views.
+ FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
+ DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
+ RelativeLayout mainContentRelativeLayout = findViewById(R.id.main_content_relativelayout);
+ ActionBar actionBar = getSupportActionBar();
+ LinearLayout tabsLinearLayout = findViewById(R.id.tabs_linearlayout);
+ EditText urlEditText = findViewById(R.id.url_edittext);
+ TabLayout tabLayout = findViewById(R.id.tablayout);
+ SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
+
+ // Remove the incorrect lint warning below that the action bar might be null.
+ assert actionBar != null;
+
+ // 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;
+
+ // 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);
+
+ // Initialize the favorite icon.
+ nestedScrollWebView.initializeFavoriteIcon();
+
+ // Set the app bar scrolling.
+ nestedScrollWebView.setNestedScrollingEnabled(sharedPreferences.getBoolean("scroll_app_bar", true));
+
+ // 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 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.
+ // Store the swipe refresh layout top padding.
+ swipeRefreshLayoutPaddingTop = swipeRefreshLayout.getPaddingTop();
+
+ // Hide the app bar if specified.
+ if (hideAppBar) {
+ // 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();
+
+ // Check to see if app bar scrolling is disabled.
+ if (!scrollAppBar) {
+ // Remove the padding from the top of the swipe refresh layout.
+ swipeRefreshLayout.setPadding(0, 0, 0, 0);
+ }
+ }
+
+ // 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 tab linear layout.
+ tabsLinearLayout.setVisibility(View.VISIBLE);
+
+ // Show the action bar.
+ actionBar.show();
+
+ // Check to see if app bar scrolling is disabled.
+ if (!scrollAppBar) {
+ // Add the padding from the top of the swipe refresh layout.
+ swipeRefreshLayout.setPadding(0, swipeRefreshLayoutPaddingTop, 0, 0);
+ }
+
+ // 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();