+ // Pause the ad or it will continue to consume resources in the background on the free flavor.
+ if (BuildConfig.FLAVOR.contentEquals("free")) {
+ // Pause the ad.
+ AdHelper.pauseAd(findViewById(R.id.adview));
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ // Unregister the Orbot status broadcast receiver.
+ this.unregisterReceiver(orbotStatusBroadcastReceiver);
+
+ // Close the bookmarks cursor and database.
+ bookmarksCursor.close();
+ bookmarksDatabaseHelper.close();
+
+ // Run the default commands.
+ super.onDestroy();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // Inflate the menu. This adds items to the action bar if it is present.
+ getMenuInflater().inflate(R.menu.webview_options_menu, menu);
+
+ // Store a handle for the options menu so it can be used by `onOptionsItemSelected()` and `updatePrivacyIcons()`.
+ optionsMenu = menu;
+
+ // Set the initial status of the privacy icons. `false` does not call `invalidateOptionsMenu` as the last step.
+ updatePrivacyIcons(false);
+
+ // Get handles for the menu items.
+ MenuItem toggleFirstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies);
+ MenuItem toggleThirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies);
+ MenuItem toggleDomStorageMenuItem = menu.findItem(R.id.toggle_dom_storage);
+ MenuItem toggleSaveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data); // Form data can be removed once the minimum API >= 26.
+ MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data); // Form data can be removed once the minimum API >= 26.
+ MenuItem refreshMenuItem = menu.findItem(R.id.refresh);
+ MenuItem adConsentMenuItem = menu.findItem(R.id.ad_consent);
+
+ // Only display third-party cookies if API >= 21
+ toggleThirdPartyCookiesMenuItem.setVisible(Build.VERSION.SDK_INT >= 21);
+
+ // Only display the form data menu items if the API < 26.
+ toggleSaveFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
+ clearFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
+
+ // Disable the clear form data menu item if the API >= 26 so that the status of the main Clear Data is calculated correctly.
+ clearFormDataMenuItem.setEnabled(Build.VERSION.SDK_INT < 26);
+
+ // Only show Ad Consent if this is the free flavor.
+ adConsentMenuItem.setVisible(BuildConfig.FLAVOR.contentEquals("free"));
+
+ // Get the shared preferences.
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+
+ // Get the dark theme and app bar preferences..
+ boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false);
+ boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
+
+ // Set the status of the additional app bar icons. Setting the refresh menu item to `SHOW_AS_ACTION_ALWAYS` makes it appear even on small devices like phones.
+ if (displayAdditionalAppBarIcons) {
+ toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ refreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+ } else { //Do not display the additional icons.
+ toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
+ toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
+ refreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
+ }
+
+ // Replace Refresh with Stop if a URL is already loading.
+ if (currentWebView != null && currentWebView.getProgress() != 100) {
+ // 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);
+ }
+ }
+ }
+
+ // Done.
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ // Get handles for the menu items.
+ MenuItem addOrEditDomain = menu.findItem(R.id.add_or_edit_domain);
+ MenuItem firstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies);
+ MenuItem thirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies);
+ MenuItem domStorageMenuItem = menu.findItem(R.id.toggle_dom_storage);
+ MenuItem saveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data); // Form data can be removed once the minimum API >= 26.
+ MenuItem clearDataMenuItem = menu.findItem(R.id.clear_data);
+ MenuItem clearCookiesMenuItem = menu.findItem(R.id.clear_cookies);
+ MenuItem clearDOMStorageMenuItem = menu.findItem(R.id.clear_dom_storage);
+ MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data); // Form data can be removed once the minimum API >= 26.
+ MenuItem blocklistsMenuItem = menu.findItem(R.id.blocklists);
+ MenuItem easyListMenuItem = menu.findItem(R.id.easylist);
+ MenuItem easyPrivacyMenuItem = menu.findItem(R.id.easyprivacy);
+ MenuItem fanboysAnnoyanceListMenuItem = menu.findItem(R.id.fanboys_annoyance_list);
+ MenuItem fanboysSocialBlockingListMenuItem = menu.findItem(R.id.fanboys_social_blocking_list);
+ MenuItem ultraPrivacyMenuItem = menu.findItem(R.id.ultraprivacy);
+ MenuItem blockAllThirdPartyRequestsMenuItem = menu.findItem(R.id.block_all_third_party_requests);
+ MenuItem fontSizeMenuItem = menu.findItem(R.id.font_size);
+ MenuItem swipeToRefreshMenuItem = menu.findItem(R.id.swipe_to_refresh);
+ MenuItem wideViewportMenuItem = menu.findItem(R.id.wide_viewport);
+ MenuItem displayImagesMenuItem = menu.findItem(R.id.display_images);
+ MenuItem nightModeMenuItem = menu.findItem(R.id.night_mode);
+ MenuItem proxyThroughOrbotMenuItem = menu.findItem(R.id.proxy_through_orbot);
+
+ // Get a handle for the cookie manager.
+ CookieManager cookieManager = CookieManager.getInstance();
+
+ // Initialize the current user agent string and the font size.
+ String currentUserAgent = getString(R.string.user_agent_privacy_browser);
+ int fontSize = 100;
+
+ // Set items that require the current web view to be populated. It will be null when the program is first opened, as `onPrepareOptionsMenu()` is called before the first WebView is initialized.
+ if (currentWebView != null) {
+ // Set the add or edit domain text.
+ if (currentWebView.getDomainSettingsApplied()) {
+ addOrEditDomain.setTitle(R.string.edit_domain_settings);
+ } else {
+ addOrEditDomain.setTitle(R.string.add_domain_settings);
+ }
+
+ // Get the current user agent from the WebView.
+ currentUserAgent = currentWebView.getSettings().getUserAgentString();
+
+ // Get the current font size from the
+ fontSize = currentWebView.getSettings().getTextZoom();
+
+ // Set the status of the menu item checkboxes.
+ domStorageMenuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled());
+ saveFormDataMenuItem.setChecked(currentWebView.getSettings().getSaveFormData()); // Form data can be removed once the minimum API >= 26.
+ easyListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST));
+ easyPrivacyMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY));
+ fanboysAnnoyanceListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
+ fanboysSocialBlockingListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
+ ultraPrivacyMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY));
+ blockAllThirdPartyRequestsMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
+ swipeToRefreshMenuItem.setChecked(currentWebView.getSwipeToRefresh());
+ wideViewportMenuItem.setChecked(currentWebView.getSettings().getUseWideViewPort());
+ displayImagesMenuItem.setChecked(currentWebView.getSettings().getLoadsImagesAutomatically());
+ nightModeMenuItem.setChecked(currentWebView.getNightMode());
+
+ // Initialize the display names for the blocklists with the number of blocked requests.
+ blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
+ easyListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASY_LIST) + " - " + getString(R.string.easylist));
+ easyPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASY_PRIVACY) + " - " + getString(R.string.easyprivacy));
+ fanboysAnnoyanceListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST) + " - " + getString(R.string.fanboys_annoyance_list));
+ fanboysSocialBlockingListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST) + " - " + getString(R.string.fanboys_social_blocking_list));
+ ultraPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.ULTRA_PRIVACY) + " - " + getString(R.string.ultraprivacy));
+ blockAllThirdPartyRequestsMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " + getString(R.string.block_all_third_party_requests));
+
+ // Only modify third-party cookies if the API >= 21.
+ if (Build.VERSION.SDK_INT >= 21) {
+ // Set the status of the third-party cookies checkbox.
+ thirdPartyCookiesMenuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView));
+
+ // Enable third-party cookies if first-party cookies are enabled.
+ thirdPartyCookiesMenuItem.setEnabled(cookieManager.acceptCookie());
+ }
+
+ // Enable DOM Storage if JavaScript is enabled.
+ domStorageMenuItem.setEnabled(currentWebView.getSettings().getJavaScriptEnabled());
+ }
+
+ // Set the status of the menu item checkboxes.
+ firstPartyCookiesMenuItem.setChecked(cookieManager.acceptCookie());
+ proxyThroughOrbotMenuItem.setChecked(proxyThroughOrbot);
+
+ // Enable Clear Cookies if there are any.
+ clearCookiesMenuItem.setEnabled(cookieManager.hasCookies());
+
+ // 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;
+
+ // Get a count of the number of files in the Local Storage directory.
+ File localStorageDirectory = new File (privateDataDirectoryString + "/app_webview/Local Storage/");
+ int localStorageDirectoryNumberOfFiles = 0;
+ if (localStorageDirectory.exists()) {
+ localStorageDirectoryNumberOfFiles = localStorageDirectory.list().length;
+ }
+
+ // Get a count of the number of files in the IndexedDB directory.
+ File indexedDBDirectory = new File (privateDataDirectoryString + "/app_webview/IndexedDB");
+ int indexedDBDirectoryNumberOfFiles = 0;
+ if (indexedDBDirectory.exists()) {
+ indexedDBDirectoryNumberOfFiles = indexedDBDirectory.list().length;
+ }
+
+ // Enable Clear DOM Storage if there is any.
+ clearDOMStorageMenuItem.setEnabled(localStorageDirectoryNumberOfFiles > 0 || indexedDBDirectoryNumberOfFiles > 0);
+
+ // Enable Clear Form Data is there is any. This can be removed once the minimum API >= 26.
+ if (Build.VERSION.SDK_INT < 26) {
+ // Get the WebView database.
+ WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
+
+ // Enable the clear form data menu item if there is anything to clear.
+ clearFormDataMenuItem.setEnabled(webViewDatabase.hasFormData());
+ }
+
+ // Enable Clear Data if any of the submenu items are enabled.
+ clearDataMenuItem.setEnabled(clearCookiesMenuItem.isEnabled() || clearDOMStorageMenuItem.isEnabled() || clearFormDataMenuItem.isEnabled());
+
+ // Disable Fanboy's Social Blocking List menu item if Fanboy's Annoyance List is checked.
+ fanboysSocialBlockingListMenuItem.setEnabled(!fanboysAnnoyanceListMenuItem.isChecked());
+
+ // Select the current user agent menu item. A switch statement cannot be used because the user agents are not compile time constants.
+ if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[0])) { // Privacy Browser.
+ menu.findItem(R.id.user_agent_privacy_browser).setChecked(true);
+ } else if (currentUserAgent.equals(webViewDefaultUserAgent)) { // WebView Default.
+ menu.findItem(R.id.user_agent_webview_default).setChecked(true);
+ } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[2])) { // Firefox on Android.
+ menu.findItem(R.id.user_agent_firefox_on_android).setChecked(true);
+ } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[3])) { // Chrome on Android.
+ menu.findItem(R.id.user_agent_chrome_on_android).setChecked(true);
+ } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[4])) { // Safari on iOS.
+ menu.findItem(R.id.user_agent_safari_on_ios).setChecked(true);
+ } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[5])) { // Firefox on Linux.
+ menu.findItem(R.id.user_agent_firefox_on_linux).setChecked(true);
+ } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[6])) { // Chromium on Linux.
+ menu.findItem(R.id.user_agent_chromium_on_linux).setChecked(true);
+ } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[7])) { // Firefox on Windows.
+ menu.findItem(R.id.user_agent_firefox_on_windows).setChecked(true);
+ } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[8])) { // Chrome on Windows.
+ menu.findItem(R.id.user_agent_chrome_on_windows).setChecked(true);
+ } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[9])) { // Edge on Windows.
+ menu.findItem(R.id.user_agent_edge_on_windows).setChecked(true);
+ } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[10])) { // Internet Explorer on Windows.
+ menu.findItem(R.id.user_agent_internet_explorer_on_windows).setChecked(true);
+ } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[11])) { // Safari on macOS.
+ menu.findItem(R.id.user_agent_safari_on_macos).setChecked(true);
+ } else { // Custom user agent.
+ menu.findItem(R.id.user_agent_custom).setChecked(true);
+ }
+
+ // Instantiate the font size title and the selected font size menu item.
+ String fontSizeTitle;
+ MenuItem selectedFontSizeMenuItem;
+
+ // Prepare the font size title and current size menu item.
+ switch (fontSize) {
+ case 25:
+ fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.twenty_five_percent);
+ selectedFontSizeMenuItem = menu.findItem(R.id.font_size_twenty_five_percent);
+ break;
+
+ case 50:
+ fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.fifty_percent);
+ selectedFontSizeMenuItem = menu.findItem(R.id.font_size_fifty_percent);
+ break;
+
+ case 75:
+ fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.seventy_five_percent);
+ selectedFontSizeMenuItem = menu.findItem(R.id.font_size_seventy_five_percent);
+ break;
+
+ case 100:
+ fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent);
+ selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent);
+ break;
+
+ case 125:
+ fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_twenty_five_percent);
+ selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_twenty_five_percent);
+ break;
+
+ case 150:
+ fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_fifty_percent);
+ selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_fifty_percent);
+ break;
+
+ case 175:
+ fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_seventy_five_percent);
+ selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_seventy_five_percent);
+ break;
+
+ case 200:
+ fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.two_hundred_percent);
+ selectedFontSizeMenuItem = menu.findItem(R.id.font_size_two_hundred_percent);
+ break;
+
+ default:
+ fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent);
+ selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent);
+ break;
+ }
+
+ // Set the font size title and select the current size menu item.
+ fontSizeMenuItem.setTitle(fontSizeTitle);
+ selectedFontSizeMenuItem.setChecked(true);
+
+ // Run all the other default commands.
+ super.onPrepareOptionsMenu(menu);
+
+ // Display the menu.
+ return true;
+ }
+
+ @Override
+ // Remove Android Studio's warning about the dangers of using SetJavaScriptEnabled.
+ @SuppressLint("SetJavaScriptEnabled")
+ public boolean onOptionsItemSelected(MenuItem menuItem) {
+ // Reenter full screen browsing mode if it was interrupted by the options menu. <https://redmine.stoutner.com/issues/389>
+ if (inFullScreenBrowsingMode) {
+ // 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);
+
+ FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
+
+ /* 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);
+ }
+
+ // Get the selected menu item ID.
+ int menuItemId = menuItem.getItemId();
+
+ // Get a handle for the shared preferences.
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+
+ // Get a handle for the cookie manager.
+ CookieManager cookieManager = CookieManager.getInstance();
+
+ // Run the commands that correlate to the selected menu item.
+ switch (menuItemId) {
+ case R.id.toggle_javascript:
+ // Toggle the JavaScript status.
+ currentWebView.getSettings().setJavaScriptEnabled(!currentWebView.getSettings().getJavaScriptEnabled());
+
+ // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
+ updatePrivacyIcons(true);
+
+ // Display a `Snackbar`.
+ if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScrip is enabled.
+ Snackbar.make(findViewById(R.id.webviewpager), R.string.javascript_enabled, Snackbar.LENGTH_SHORT).show();
+ } else if (cookieManager.acceptCookie()) { // JavaScript is disabled, but first-party cookies are enabled.
+ Snackbar.make(findViewById(R.id.webviewpager), R.string.javascript_disabled, Snackbar.LENGTH_SHORT).show();
+ } else { // Privacy mode.
+ Snackbar.make(findViewById(R.id.webviewpager), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
+ }
+
+ // Reload the current WebView.
+ currentWebView.reload();
+
+ // Consume the event.
+ return true;
+
+ case R.id.add_or_edit_domain:
+ if (currentWebView.getDomainSettingsApplied()) { // Edit the current domain settings.
+ // Reapply the domain settings on returning to `MainWebViewActivity`.
+ reapplyDomainSettingsOnRestart = true;
+
+ // Create an intent to launch the domains activity.
+ Intent domainsIntent = new Intent(this, DomainsActivity.class);
+
+ // Add the extra information to the intent.
+ domainsIntent.putExtra("load_domain", currentWebView.getDomainSettingsDatabaseId());
+ domainsIntent.putExtra("close_on_back", true);
+ domainsIntent.putExtra("current_url", currentWebView.getUrl());
+
+ // Get the current certificate.
+ SslCertificate sslCertificate = currentWebView.getCertificate();
+
+ // Check to see if the SSL certificate is populated.
+ if (sslCertificate != null) {
+ // Extract the certificate to strings.
+ String issuedToCName = sslCertificate.getIssuedTo().getCName();
+ String issuedToOName = sslCertificate.getIssuedTo().getOName();
+ String issuedToUName = sslCertificate.getIssuedTo().getUName();
+ String issuedByCName = sslCertificate.getIssuedBy().getCName();
+ String issuedByOName = sslCertificate.getIssuedBy().getOName();
+ String issuedByUName = sslCertificate.getIssuedBy().getUName();
+ long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
+ long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
+
+ // Add the certificate to the intent.
+ domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
+ domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
+ domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
+ domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
+ domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
+ domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
+ domainsIntent.putExtra("ssl_start_date", startDateLong);
+ domainsIntent.putExtra("ssl_end_date", endDateLong);
+ }
+
+ // Check to see if the current IP addresses have been received.
+ if (currentWebView.hasCurrentIpAddresses()) {
+ // Add the current IP addresses to the intent.
+ domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
+ }
+
+ // Make it so.
+ startActivity(domainsIntent);
+ } else { // Add a new domain.
+ // Apply the new domain settings on returning to `MainWebViewActivity`.
+ reapplyDomainSettingsOnRestart = true;
+
+ // Get the current domain
+ Uri currentUri = Uri.parse(currentWebView.getUrl());
+ String currentDomain = currentUri.getHost();
+
+ // Initialize the database handler. The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
+ DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
+
+ // Create the domain and store the database ID.
+ int newDomainDatabaseId = domainsDatabaseHelper.addDomain(currentDomain);
+
+ // Create an intent to launch the domains activity.
+ Intent domainsIntent = new Intent(this, DomainsActivity.class);
+
+ // Add the extra information to the intent.
+ domainsIntent.putExtra("load_domain", newDomainDatabaseId);
+ domainsIntent.putExtra("close_on_back", true);
+ domainsIntent.putExtra("current_url", currentWebView.getUrl());
+
+ // Get the current certificate.
+ SslCertificate sslCertificate = currentWebView.getCertificate();
+
+ // Check to see if the SSL certificate is populated.
+ if (sslCertificate != null) {
+ // Extract the certificate to strings.
+ String issuedToCName = sslCertificate.getIssuedTo().getCName();
+ String issuedToOName = sslCertificate.getIssuedTo().getOName();
+ String issuedToUName = sslCertificate.getIssuedTo().getUName();
+ String issuedByCName = sslCertificate.getIssuedBy().getCName();
+ String issuedByOName = sslCertificate.getIssuedBy().getOName();
+ String issuedByUName = sslCertificate.getIssuedBy().getUName();
+ long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
+ long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
+
+ // Add the certificate to the intent.
+ domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
+ domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
+ domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
+ domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
+ domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
+ domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
+ domainsIntent.putExtra("ssl_start_date", startDateLong);
+ domainsIntent.putExtra("ssl_end_date", endDateLong);
+ }
+
+ // Check to see if the current IP addresses have been received.
+ if (currentWebView.hasCurrentIpAddresses()) {
+ // Add the current IP addresses to the intent.
+ domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
+ }
+
+ // Make it so.
+ startActivity(domainsIntent);
+ }
+
+ // Consume the event.
+ return true;
+
+ case R.id.toggle_first_party_cookies:
+ // Switch the first-party cookie status.
+ cookieManager.setAcceptCookie(!cookieManager.acceptCookie());
+
+ // Store the first-party cookie status.
+ currentWebView.setAcceptFirstPartyCookies(cookieManager.acceptCookie());
+
+ // Update the menu checkbox.
+ menuItem.setChecked(cookieManager.acceptCookie());
+
+ // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
+ updatePrivacyIcons(true);
+
+ // Display a snackbar.
+ if (cookieManager.acceptCookie()) { // First-party cookies are enabled.
+ Snackbar.make(findViewById(R.id.webviewpager), R.string.first_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
+ } else if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScript is still enabled.
+ Snackbar.make(findViewById(R.id.webviewpager), R.string.first_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
+ } else { // Privacy mode.
+ Snackbar.make(findViewById(R.id.webviewpager), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
+ }
+
+ // Reload the current WebView.
+ currentWebView.reload();
+
+ // Consume the event.
+ return true;
+
+ case R.id.toggle_third_party_cookies:
+ if (Build.VERSION.SDK_INT >= 21) {
+ // Switch the status of thirdPartyCookiesEnabled.
+ cookieManager.setAcceptThirdPartyCookies(currentWebView, !cookieManager.acceptThirdPartyCookies(currentWebView));
+
+ // Update the menu checkbox.
+ menuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView));
+
+ // Display a snackbar.
+ if (cookieManager.acceptThirdPartyCookies(currentWebView)) {
+ Snackbar.make(findViewById(R.id.webviewpager), R.string.third_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
+ } else {
+ Snackbar.make(findViewById(R.id.webviewpager), R.string.third_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
+ }
+
+ // Reload the current WebView.
+ currentWebView.reload();
+ } // Else do nothing because SDK < 21.
+
+ // Consume the event.
+ return true;
+
+ case R.id.toggle_dom_storage:
+ // Toggle the status of domStorageEnabled.
+ currentWebView.getSettings().setDomStorageEnabled(!currentWebView.getSettings().getDomStorageEnabled());
+
+ // Update the menu checkbox.
+ menuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled());
+
+ // Update the privacy icon. `true` refreshes the app bar icons.
+ updatePrivacyIcons(true);
+
+ // Display a snackbar.
+ if (currentWebView.getSettings().getDomStorageEnabled()) {
+ Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_enabled, Snackbar.LENGTH_SHORT).show();
+ } else {
+ Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_disabled, Snackbar.LENGTH_SHORT).show();
+ }
+
+ // Reload the current WebView.
+ currentWebView.reload();
+
+ // Consume the event.
+ return true;
+
+ // Form data can be removed once the minimum API >= 26.
+ case R.id.toggle_save_form_data:
+ // Switch the status of saveFormDataEnabled.
+ currentWebView.getSettings().setSaveFormData(!currentWebView.getSettings().getSaveFormData());
+
+ // Update the menu checkbox.
+ menuItem.setChecked(currentWebView.getSettings().getSaveFormData());
+
+ // Display a snackbar.
+ if (currentWebView.getSettings().getSaveFormData()) {
+ Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_enabled, Snackbar.LENGTH_SHORT).show();
+ } else {
+ Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_disabled, Snackbar.LENGTH_SHORT).show();
+ }
+
+ // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
+ updatePrivacyIcons(true);
+
+ // Reload the current WebView.
+ currentWebView.reload();
+
+ // Consume the event.
+ return true;
+
+ case R.id.clear_cookies:
+ Snackbar.make(findViewById(R.id.webviewpager), R.string.cookies_deleted, Snackbar.LENGTH_LONG)
+ .setAction(R.string.undo, v -> {
+ // Do nothing because everything will be handled by `onDismissed()` below.
+ })
+ .addCallback(new Snackbar.Callback() {
+ @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
+ @Override
+ public void onDismissed(Snackbar snackbar, int event) {
+ if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) { // The snackbar was dismissed without the undo button being pushed.
+ // Delete the cookies, which command varies by SDK.
+ if (Build.VERSION.SDK_INT < 21) {
+ cookieManager.removeAllCookie();
+ } else {
+ cookieManager.removeAllCookies(null);
+ }
+ }
+ }
+ })
+ .show();
+
+ // Consume the event.
+ return true;
+
+ case R.id.clear_dom_storage:
+ Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_deleted, Snackbar.LENGTH_LONG)
+ .setAction(R.string.undo, v -> {
+ // Do nothing because everything will be handled by `onDismissed()` below.
+ })
+ .addCallback(new Snackbar.Callback() {
+ @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
+ @Override
+ public void onDismissed(Snackbar snackbar, int event) {
+ if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) { // The snackbar was dismissed without the undo button being pushed.
+ // Delete the DOM Storage.
+ WebStorage webStorage = WebStorage.getInstance();
+ webStorage.deleteAllData();
+
+ // Initialize a handler to manually delete the DOM storage files and directories.
+ Handler deleteDomStorageHandler = new Handler();
+
+ // Setup a runnable to manually delete the DOM storage files and directories.
+ Runnable deleteDomStorageRunnable = () -> {
+ try {
+ // 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;
+
+ // A string array 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 deleteDatabasesProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
+
+ // Wait for the processes to finish.
+ deleteLocalStorageProcess.waitFor();
+ deleteIndexProcess.waitFor();
+ deleteQuotaManagerProcess.waitFor();
+ deleteQuotaManagerJournalProcess.waitFor();
+ deleteDatabasesProcess.waitFor();
+ } catch (Exception exception) {
+ // Do nothing if an error is thrown.
+ }
+ };
+
+ // Manually delete the DOM storage files after 200 milliseconds.
+ deleteDomStorageHandler.postDelayed(deleteDomStorageRunnable, 200);
+ }
+ }
+ })
+ .show();
+
+ // Consume the event.
+ return true;
+
+ // Form data can be remove once the minimum API >= 26.
+ case R.id.clear_form_data:
+ Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_deleted, Snackbar.LENGTH_LONG)
+ .setAction(R.string.undo, v -> {
+ // Do nothing because everything will be handled by `onDismissed()` below.
+ })
+ .addCallback(new Snackbar.Callback() {
+ @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
+ @Override
+ public void onDismissed(Snackbar snackbar, int event) {
+ if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) { // The snackbar was dismissed without the undo button being pushed.
+ // Delete the form data.
+ WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(getApplicationContext());
+ mainWebViewDatabase.clearFormData();
+ }
+ }
+ })
+ .show();
+
+ // Consume the event.
+ return true;
+
+ case R.id.easylist:
+ // Toggle the EasyList status.
+ currentWebView.enableBlocklist(NestedScrollWebView.EASY_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST));
+
+ // Update the menu checkbox.
+ menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST));
+
+ // Reload the current WebView.
+ currentWebView.reload();
+
+ // Consume the event.
+ return true;
+
+ case R.id.easyprivacy:
+ // Toggle the EasyPrivacy status.
+ currentWebView.enableBlocklist(NestedScrollWebView.EASY_PRIVACY, !currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY));
+
+ // Update the menu checkbox.
+ menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY));
+
+ // Reload the current WebView.
+ currentWebView.reload();
+
+ // Consume the event.
+ return true;
+
+ case R.id.fanboys_annoyance_list:
+ // Toggle Fanboy's Annoyance List status.
+ currentWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
+
+ // Update the menu checkbox.
+ menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
+
+ // Update the staus of Fanboy's Social Blocking List.
+ MenuItem fanboysSocialBlockingListMenuItem = optionsMenu.findItem(R.id.fanboys_social_blocking_list);
+ fanboysSocialBlockingListMenuItem.setEnabled(!currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
+
+ // Reload the current WebView.
+ currentWebView.reload();
+
+ // Consume the event.
+ return true;
+
+ case R.id.fanboys_social_blocking_list:
+ // Toggle Fanboy's Social Blocking List status.
+ currentWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
+
+ // Update the menu checkbox.
+ menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
+
+ // Reload the current WebView.
+ currentWebView.reload();
+
+ // Consume the event.
+ return true;
+
+ case R.id.ultraprivacy:
+ // Toggle the UltraPrivacy status.
+ currentWebView.enableBlocklist(NestedScrollWebView.ULTRA_PRIVACY, !currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY));
+
+ // Update the menu checkbox.
+ menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY));
+
+ // Reload the current WebView.
+ currentWebView.reload();
+
+ // Consume the event.
+ return true;
+
+ case R.id.block_all_third_party_requests:
+ //Toggle the third-party requests blocker status.
+ currentWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS, !currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
+
+ // Update the menu checkbox.
+ menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
+
+ // Reload the current WebView.
+ currentWebView.reload();
+
+ // Consume the event.
+ return true;
+
+ case R.id.user_agent_privacy_browser:
+ // Update the user agent.
+ currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[0]);
+
+ // Reload the current WebView.
+ currentWebView.reload();
+
+ // Consume the event.
+ return true;
+
+ case R.id.user_agent_webview_default:
+ // Update the user agent.
+ currentWebView.getSettings().setUserAgentString("");
+
+ // Reload the current WebView.
+ currentWebView.reload();
+
+ // Consume the event.
+ return true;
+
+ case R.id.user_agent_firefox_on_android:
+ // Update the user agent.
+ currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[2]);
+
+ // Reload the current WebView.
+ currentWebView.reload();
+
+ // Consume the event.
+ return true;
+
+ case R.id.user_agent_chrome_on_android:
+ // Update the user agent.
+ currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[3]);
+
+ // Reload the current WebView.
+ currentWebView.reload();
+
+ // Consume the event.
+ return true;
+
+ case R.id.user_agent_safari_on_ios:
+ // Update the user agent.
+ currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[4]);
+
+ // Reload the current WebView.
+ currentWebView.reload();
+
+ // Consume the event.
+ return true;
+
+ case R.id.user_agent_firefox_on_linux:
+ // Update the user agent.
+ currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[5]);
+
+ // Reload the current WebView.
+ currentWebView.reload();
+
+ // Consume the event.
+ return true;
+
+ case R.id.user_agent_chromium_on_linux:
+ // Update the user agent.
+ currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[6]);
+
+ // Reload the current WebView.
+ currentWebView.reload();
+
+ // Consume the event.
+ return true;
+
+ case R.id.user_agent_firefox_on_windows:
+ // Update the user agent.
+ currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[7]);
+
+ // Reload the current WebView.
+ currentWebView.reload();
+
+ // Consume the event.
+ return true;
+
+ case R.id.user_agent_chrome_on_windows:
+ // Update the user agent.
+ currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[8]);
+
+ // Reload the current WebView.
+ currentWebView.reload();
+
+ // Consume the event.
+ return true;
+
+ case R.id.user_agent_edge_on_windows:
+ // Update the user agent.
+ currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[9]);
+
+ // Reload the current WebView.
+ currentWebView.reload();
+
+ // Consume the event.
+ return true;
+
+ case R.id.user_agent_internet_explorer_on_windows:
+ // Update the user agent.
+ currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[10]);
+
+ // Reload the current WebView.
+ currentWebView.reload();
+
+ // Consume the event.
+ return true;
+
+ case R.id.user_agent_safari_on_macos:
+ // Update the user agent.
+ currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[11]);
+
+ // Reload the current WebView.
+ currentWebView.reload();
+
+ // Consume the event.
+ return true;
+
+ case R.id.user_agent_custom:
+ // Update the user agent.
+ currentWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
+
+ // Reload the current WebView.
+ currentWebView.reload();
+
+ // Consume the event.
+ return true;
+
+ case R.id.font_size_twenty_five_percent:
+ // Set the font size.
+ currentWebView.getSettings().setTextZoom(25);
+
+ // Consume the event.
+ return true;
+
+ case R.id.font_size_fifty_percent:
+ // Set the font size.
+ currentWebView.getSettings().setTextZoom(50);
+
+ // Consume the event.
+ return true;
+
+ case R.id.font_size_seventy_five_percent:
+ // Set the font size.
+ currentWebView.getSettings().setTextZoom(75);
+
+ // Consume the event.
+ return true;
+
+ case R.id.font_size_one_hundred_percent:
+ // Set the font size.
+ currentWebView.getSettings().setTextZoom(100);
+
+ // Consume the event.
+ return true;
+
+ case R.id.font_size_one_hundred_twenty_five_percent:
+ // Set the font size.
+ currentWebView.getSettings().setTextZoom(125);
+
+ // Consume the event.
+ return true;
+
+ case R.id.font_size_one_hundred_fifty_percent:
+ // Set the font size.
+ currentWebView.getSettings().setTextZoom(150);
+
+ // Consume the event.
+ return true;
+
+ case R.id.font_size_one_hundred_seventy_five_percent:
+ // Set the font size.
+ currentWebView.getSettings().setTextZoom(175);
+
+ // Consume the event.
+ return true;
+
+ case R.id.font_size_two_hundred_percent:
+ // Set the font size.
+ currentWebView.getSettings().setTextZoom(200);
+
+ // Consume the event.
+ return true;
+
+ case R.id.swipe_to_refresh:
+ // Toggle the stored status of swipe to refresh.
+ currentWebView.setSwipeToRefresh(!currentWebView.getSwipeToRefresh());
+
+ // Get a handle for the swipe refresh layout.
+ SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
+
+ // Update the swipe refresh layout.
+ if (currentWebView.getSwipeToRefresh()) { // Swipe to refresh is enabled.
+ // Only enable the swipe refresh layout if the WebView is scrolled 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);
+ }
+
+ // Consume the event.
+ return true;
+
+ case R.id.wide_viewport:
+ // Toggle the viewport.
+ currentWebView.getSettings().setUseWideViewPort(!currentWebView.getSettings().getUseWideViewPort());
+
+ // Consume the event.
+ return true;
+
+ case R.id.display_images:
+ if (currentWebView.getSettings().getLoadsImagesAutomatically()) { // Images are currently loaded automatically.
+ // Disable loading of images.
+ currentWebView.getSettings().setLoadsImagesAutomatically(false);
+
+ // Reload the website to remove existing images.
+ currentWebView.reload();
+ } else { // Images are not currently loaded automatically.
+ // Enable loading of images. Missing images will be loaded without the need for a reload.
+ currentWebView.getSettings().setLoadsImagesAutomatically(true);
+ }
+
+ // Consume the event.
+ return true;
+
+ case R.id.night_mode:
+ // Toggle night mode.
+ currentWebView.setNightMode(!currentWebView.getNightMode());
+
+ // Enable or disable JavaScript according to night mode, the global preference, and any domain settings.
+ if (currentWebView.getNightMode()) { // Night mode is enabled, which requires JavaScript.
+ // Enable JavaScript.
+ currentWebView.getSettings().setJavaScriptEnabled(true);
+ } else if (currentWebView.getDomainSettingsApplied()) { // Night mode is disabled and domain settings are applied. Set JavaScript according to the domain settings.
+ // Apply the JavaScript preference that was stored the last time domain settings were loaded.
+ currentWebView.getSettings().setJavaScriptEnabled(currentWebView.getDomainSettingsJavaScriptEnabled());
+ } else { // Night mode is disabled and domain settings are not applied. Set JavaScript according to the global preference.
+ // Apply the JavaScript preference.
+ currentWebView.getSettings().setJavaScriptEnabled(sharedPreferences.getBoolean("javascript", false));
+ }
+
+ // Update the privacy icons.
+ updatePrivacyIcons(false);
+
+ // Reload the website.
+ currentWebView.reload();
+
+ // Consume the event.
+ return true;
+
+ case R.id.find_on_page:
+ // Get a handle for the views.
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
+ EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
+
+ // Set the minimum height of the find on page linear layout to match the toolbar.
+ findOnPageLinearLayout.setMinimumHeight(toolbar.getHeight());
+
+ // Hide the toolbar.
+ toolbar.setVisibility(View.GONE);
+
+ // Show the find on page linear layout.
+ findOnPageLinearLayout.setVisibility(View.VISIBLE);
+
+ // Display the keyboard. The app must wait 200 ms before running the command to work around a bug in Android.
+ // http://stackoverflow.com/questions/5520085/android-show-softkeyboard-with-showsoftinput-is-not-working
+ findOnPageEditText.postDelayed(() -> {
+ // Set the focus on `findOnPageEditText`.
+ findOnPageEditText.requestFocus();
+
+ // 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;
+
+ // Display the keyboard. `0` sets no input flags.
+ inputMethodManager.showSoftInput(findOnPageEditText, 0);
+ }, 200);
+
+ // Consume the event.
+ return true;
+
+ case R.id.print:
+ // Get a print manager instance.
+ PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);
+
+ // Remove the lint error below that print manager might be null.
+ assert printManager != null;
+
+ // Create a print document adapter from the current WebView.
+ PrintDocumentAdapter printDocumentAdapter = currentWebView.createPrintDocumentAdapter();
+
+ // Print the document.
+ printManager.print(getString(R.string.privacy_browser_web_page), printDocumentAdapter, null);
+
+ // Consume the event.
+ return true;
+
+ case R.id.save_as_image:
+ // Instantiate the save webpage image dialog.
+ DialogFragment saveWebpageImageDialogFragment = new SaveWebpageImageDialog();
+
+ // Show the save webpage image dialog.
+ saveWebpageImageDialogFragment.show(getSupportFragmentManager(), getString(R.string.save_as_image));
+
+ // Consume the event.
+ return true;
+
+ case R.id.add_to_homescreen:
+ // Instantiate the create home screen shortcut dialog.
+ DialogFragment createHomeScreenShortcutDialogFragment = CreateHomeScreenShortcutDialog.createDialog(currentWebView.getTitle(), currentWebView.getUrl(),
+ currentWebView.getFavoriteOrDefaultIcon());
+
+ // Show the create home screen shortcut dialog.
+ createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getString(R.string.create_shortcut));
+
+ // Consume the event.
+ return true;
+
+ case R.id.view_source:
+ // Create an intent to launch the view source activity.
+ Intent viewSourceIntent = new Intent(this, ViewSourceActivity.class);
+
+ // Add the variables to the intent.
+ viewSourceIntent.putExtra("user_agent", currentWebView.getSettings().getUserAgentString());
+ viewSourceIntent.putExtra("current_url", currentWebView.getUrl());
+
+ // Make it so.
+ startActivity(viewSourceIntent);
+
+ // Consume the event.
+ return true;
+
+ case R.id.share_url:
+ // Setup the share string.
+ String shareString = currentWebView.getTitle() + " – " + currentWebView.getUrl();
+
+ // Create the share intent.
+ Intent shareIntent = new Intent(Intent.ACTION_SEND);
+ shareIntent.putExtra(Intent.EXTRA_TEXT, shareString);
+ shareIntent.setType("text/plain");
+
+ // Make it so.
+ startActivity(Intent.createChooser(shareIntent, getString(R.string.share_url)));
+
+ // Consume the event.
+ return true;
+
+ case R.id.open_with_app:
+ // Open the URL with an outside app.
+ openWithApp(currentWebView.getUrl());
+
+ // Consume the event.
+ return true;
+
+ case R.id.open_with_browser:
+ // Open the URL with an outside browser.
+ openWithBrowser(currentWebView.getUrl());
+
+ // Consume the event.
+ return true;
+
+ case R.id.proxy_through_orbot:
+ // Toggle the proxy through Orbot variable.
+ proxyThroughOrbot = !proxyThroughOrbot;
+
+ // Apply the proxy through Orbot settings.
+ applyProxyThroughOrbot(true);
+
+ // Consume the event.
+ return true;
+
+ case R.id.refresh:
+ if (menuItem.getTitle().equals(getString(R.string.refresh))) { // The refresh button was pushed.
+ // Reload the current WebView.
+ currentWebView.reload();
+ } else { // The stop button was pushed.
+ // Stop the loading of the WebView.
+ currentWebView.stopLoading();
+ }
+
+ // Consume the event.
+ return true;
+
+ case R.id.ad_consent:
+ // Instantiate the ad consent dialog.
+ DialogFragment adConsentDialogFragment = new AdConsentDialog();
+
+ // Display the ad consent dialog.
+ adConsentDialogFragment.show(getSupportFragmentManager(), getString(R.string.ad_consent));
+
+ // Consume the event.
+ return true;
+
+ default:
+ // Don't consume the event.
+ return super.onOptionsItemSelected(menuItem);
+ }
+ }
+
+ // removeAllCookies is deprecated, but it is required for API < 21.
+ @Override
+ public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
+ // Get the menu item ID.
+ int menuItemId = menuItem.getItemId();
+
+ // Get a handle for the shared preferences.
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+
+ // Run the commands that correspond to the selected menu item.
+ switch (menuItemId) {
+ case R.id.clear_and_exit:
+ // Clear and exit Privacy Browser.
+ clearAndExit();
+ break;
+
+ case R.id.home:
+ // Select the homepage based on the proxy through Orbot status.
+ if (proxyThroughOrbot) {
+ // Load the Tor homepage.
+ loadUrl(sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value)));
+ } else {
+ // Load the normal homepage.
+ loadUrl(sharedPreferences.getString("homepage", getString(R.string.homepage_default_value)));
+ }
+ break;
+
+ case R.id.back:
+ if (currentWebView.canGoBack()) {
+ // Reset the current domain name so that navigation works if third-party requests are blocked.
+ currentWebView.resetCurrentDomainName();
+
+ // Set navigating history so that the domain settings are applied when the new URL is loaded.
+ currentWebView.setNavigatingHistory(true);
+
+ // Load the previous website in the history.
+ currentWebView.goBack();
+ }
+ break;
+
+ case R.id.forward:
+ if (currentWebView.canGoForward()) {
+ // Reset the current domain name so that navigation works if third-party requests are blocked.
+ currentWebView.resetCurrentDomainName();
+
+ // Set navigating history so that the domain settings are applied when the new URL is loaded.
+ currentWebView.setNavigatingHistory(true);
+
+ // Load the next website in the history.
+ currentWebView.goForward();
+ }
+ break;
+
+ case R.id.history:
+ // Instantiate the URL history dialog.
+ DialogFragment urlHistoryDialogFragment = UrlHistoryDialog.loadBackForwardList(currentWebView.getWebViewFragmentId());
+
+ // Show the URL history dialog.
+ urlHistoryDialogFragment.show(getSupportFragmentManager(), getString(R.string.history));
+ break;
+
+ case R.id.requests:
+ // Populate the resource requests.
+ RequestsActivity.resourceRequests = currentWebView.getResourceRequests();
+
+ // Create an intent to launch the Requests activity.
+ Intent requestsIntent = new Intent(this, RequestsActivity.class);
+
+ // Add the block third-party requests status to the intent.
+ requestsIntent.putExtra("block_all_third_party_requests", currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
+
+ // Make it so.
+ startActivity(requestsIntent);
+ break;
+
+ case R.id.downloads:
+ // Launch the system Download Manager.
+ Intent downloadManagerIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
+
+ // Launch as a new task so that Download Manager and Privacy Browser show as separate windows in the recent tasks list.
+ downloadManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ startActivity(downloadManagerIntent);
+ break;
+
+ case R.id.domains:
+ // Set the flag to reapply the domain settings on restart when returning from Domain Settings.
+ reapplyDomainSettingsOnRestart = true;
+
+ // Launch the domains activity.
+ Intent domainsIntent = new Intent(this, DomainsActivity.class);
+
+ // Add the extra information to the intent.
+ domainsIntent.putExtra("current_url", currentWebView.getUrl());
+
+ // Get the current certificate.
+ SslCertificate sslCertificate = currentWebView.getCertificate();
+
+ // Check to see if the SSL certificate is populated.
+ if (sslCertificate != null) {
+ // Extract the certificate to strings.
+ String issuedToCName = sslCertificate.getIssuedTo().getCName();
+ String issuedToOName = sslCertificate.getIssuedTo().getOName();
+ String issuedToUName = sslCertificate.getIssuedTo().getUName();
+ String issuedByCName = sslCertificate.getIssuedBy().getCName();
+ String issuedByOName = sslCertificate.getIssuedBy().getOName();
+ String issuedByUName = sslCertificate.getIssuedBy().getUName();
+ long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
+ long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
+
+ // Add the certificate to the intent.
+ domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
+ domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
+ domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
+ domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
+ domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
+ domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
+ domainsIntent.putExtra("ssl_start_date", startDateLong);
+ domainsIntent.putExtra("ssl_end_date", endDateLong);
+ }
+
+ // Check to see if the current IP addresses have been received.
+ if (currentWebView.hasCurrentIpAddresses()) {
+ // Add the current IP addresses to the intent.
+ domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
+ }
+
+ // Make it so.
+ startActivity(domainsIntent);
+ break;
+
+ case R.id.settings:
+ // Set the flag to reapply app settings on restart when returning from Settings.
+ reapplyAppSettingsOnRestart = true;
+
+ // Set the flag to reapply the domain settings on restart when returning from Settings.
+ reapplyDomainSettingsOnRestart = true;
+
+ // Launch the settings activity.
+ Intent settingsIntent = new Intent(this, SettingsActivity.class);
+ startActivity(settingsIntent);
+ break;
+
+ case R.id.import_export:
+ // Launch the import/export activity.
+ Intent importExportIntent = new Intent (this, ImportExportActivity.class);
+ startActivity(importExportIntent);
+ break;
+
+ case R.id.logcat:
+ // Launch the logcat activity.
+ Intent logcatIntent = new Intent(this, LogcatActivity.class);
+ startActivity(logcatIntent);
+ break;
+
+ case R.id.guide:
+ // Launch `GuideActivity`.
+ Intent guideIntent = new Intent(this, GuideActivity.class);
+ startActivity(guideIntent);
+ break;
+
+ case R.id.about:
+ // Create an intent to launch the about activity.
+ Intent aboutIntent = new Intent(this, AboutActivity.class);
+
+ // Create a string array for the blocklist versions.
+ String[] blocklistVersions = new String[] {easyList.get(0).get(0)[0], easyPrivacy.get(0).get(0)[0], fanboysAnnoyanceList.get(0).get(0)[0], fanboysSocialList.get(0).get(0)[0],
+ ultraPrivacy.get(0).get(0)[0]};
+
+ // Add the blocklist versions to the intent.
+ aboutIntent.putExtra("blocklist_versions", blocklistVersions);
+
+ // Make it so.
+ startActivity(aboutIntent);
+ break;
+ }
+
+ // Get a handle for the drawer layout.
+ DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
+
+ // Close the navigation drawer.
+ drawerLayout.closeDrawer(GravityCompat.START);
+ return true;
+ }
+
+ @Override
+ public void onPostCreate(Bundle savedInstanceState) {
+ // Run the default commands.
+ super.onPostCreate(savedInstanceState);
+
+ // Sync the state of the DrawerToggle after the default `onRestoreInstanceState()` has finished. This creates the navigation drawer icon.
+ actionBarDrawerToggle.syncState();
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ // Run the default commands.
+ super.onConfigurationChanged(newConfig);
+
+ // Get the status bar pixel size.
+ int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
+ int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId);
+
+ // Get the resource density.
+ float screenDensity = getResources().getDisplayMetrics().density;
+
+ // Recalculate the drawer header padding.
+ drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity);
+ drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity);
+ drawerHeaderPaddingBottom = (int) (8 * screenDensity);
+
+ // Reload the ad for the free flavor if not in full screen mode.
+ if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
+ // Reload the ad. The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
+ AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
+ }
+
+ // `invalidateOptionsMenu` should recalculate the number of action buttons from the menu to display on the app bar, but it doesn't because of the this bug:
+ // https://code.google.com/p/android/issues/detail?id=20493#c8
+ // ActivityCompat.invalidateOptionsMenu(this);
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ // Store the hit test result.
+ final WebView.HitTestResult hitTestResult = currentWebView.getHitTestResult();
+
+ // Define the URL strings.
+ final String imageUrl;
+ final String linkUrl;
+
+ // Get handles for the system managers.
+ final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
+ FragmentManager fragmentManager = getSupportFragmentManager();
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+
+ // Remove the lint errors below that the clipboard manager might be null.
+ assert clipboardManager != null;
+
+ // Process the link according to the type.
+ switch (hitTestResult.getType()) {
+ // `SRC_ANCHOR_TYPE` is a link.
+ case WebView.HitTestResult.SRC_ANCHOR_TYPE:
+ // Get the target URL.
+ linkUrl = hitTestResult.getExtra();
+
+ // Set the target URL as the title of the `ContextMenu`.
+ menu.setHeaderTitle(linkUrl);
+
+ // Add an Open in New Tab entry.
+ menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
+ // Load the link URL in a new tab.
+ addNewTab(linkUrl);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add an Open with App entry.
+ menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
+ openWithApp(linkUrl);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add an Open with Browser entry.
+ menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
+ openWithBrowser(linkUrl);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add a Copy URL entry.
+ menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
+ // Save the link URL in a `ClipData`.
+ ClipData srcAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl);
+
+ // Set the `ClipData` as the clipboard's primary clip.
+ clipboardManager.setPrimaryClip(srcAnchorTypeClipData);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add a Download URL entry.
+ menu.add(R.string.download_url).setOnMenuItemClickListener((MenuItem item) -> {
+ // Check if the download should be processed by an external app.
+ if (sharedPreferences.getBoolean("download_with_external_app", false)) { // Download with an external app.
+ openUrlWithExternalApp(linkUrl);
+ } else { // Download with Android's download manager.
+ // Check to see if the storage permission has already been granted.
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
+ // Store the variables for future use by `onRequestPermissionsResult()`.
+ downloadUrl = linkUrl;
+ downloadContentDisposition = "none";
+ downloadContentLength = -1;
+
+ // Show a dialog if the user has previously denied the permission.
+ if (ActivityCompat.shouldShowRequestPermissionRationale(this, 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(this, 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(linkUrl, "none", -1);
+
+ // Show the download file alert dialog.
+ downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
+ }
+ }
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add a Cancel entry, which by default closes the context menu.
+ menu.add(R.string.cancel);
+ break;
+
+ case WebView.HitTestResult.EMAIL_TYPE:
+ // Get the target URL.
+ linkUrl = hitTestResult.getExtra();
+
+ // Set the target URL as the title of the `ContextMenu`.
+ menu.setHeaderTitle(linkUrl);
+
+ // Add a Write Email entry.
+ menu.add(R.string.write_email).setOnMenuItemClickListener(item -> {
+ // 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("mailto:" + linkUrl));
+
+ // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser.
+ emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ // Make it so.
+ startActivity(emailIntent);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add a Copy Email Address entry.
+ menu.add(R.string.copy_email_address).setOnMenuItemClickListener(item -> {
+ // Save the email address in a `ClipData`.
+ ClipData srcEmailTypeClipData = ClipData.newPlainText(getString(R.string.email_address), linkUrl);
+
+ // Set the `ClipData` as the clipboard's primary clip.
+ clipboardManager.setPrimaryClip(srcEmailTypeClipData);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add a `Cancel` entry, which by default closes the `ContextMenu`.
+ menu.add(R.string.cancel);
+ break;
+
+ // `IMAGE_TYPE` is an image.
+ case WebView.HitTestResult.IMAGE_TYPE:
+ // Get the image URL.
+ imageUrl = hitTestResult.getExtra();
+
+ // Set the image URL as the title of the context menu.
+ menu.setHeaderTitle(imageUrl);
+
+ // Add an Open in New Tab entry.
+ menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
+ // Load the image URL in a new tab.
+ addNewTab(imageUrl);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add a View Image entry.
+ menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
+ // Load the image in the current tab.
+ loadUrl(imageUrl);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add a Download Image entry.
+ menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
+ // Check if the download should be processed by an external app.
+ if (sharedPreferences.getBoolean("download_with_external_app", false)) { // Download with an external app.
+ openUrlWithExternalApp(imageUrl);
+ } else { // Download with Android's download manager.
+ // Check to see if the storage permission has already been granted.
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
+ // Store the image URL for use by `onRequestPermissionResult()`.
+ downloadImageUrl = imageUrl;
+
+ // Show a dialog if the user has previously denied the permission.
+ if (ActivityCompat.shouldShowRequestPermissionRationale(this, 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_IMAGE.
+ DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
+
+ // Show the download location permission alert dialog. The permission will be requested when 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(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
+ }
+ } else { // The storage permission has already been granted.
+ // Get a handle for the download image alert dialog.
+ DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
+
+ // Show the download image alert dialog.
+ downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
+ }
+ }
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add a Copy URL entry.
+ menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
+ // Save the image URL in a clip data.
+ ClipData imageTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
+
+ // Set the clip data as the clipboard's primary clip.
+ clipboardManager.setPrimaryClip(imageTypeClipData);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add an Open with App entry.
+ menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
+ // Open the image URL with an external app.
+ openWithApp(imageUrl);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add an Open with Browser entry.
+ menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
+ // Open the image URL with an external browser.
+ openWithBrowser(imageUrl);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add a Cancel entry, which by default closes the context menu.
+ menu.add(R.string.cancel);
+ break;
+
+ // `SRC_IMAGE_ANCHOR_TYPE` is an image that is also a link.
+ case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
+ // Get the image URL.
+ imageUrl = hitTestResult.getExtra();
+
+ // Instantiate a handler.
+ Handler handler = new Handler();
+
+ // Get a message from the handler.
+ Message message = handler.obtainMessage();
+
+ // Request the image details from the last touched node be returned in the message.
+ currentWebView.requestFocusNodeHref(message);
+
+ // Get the link URL from the message data.
+ linkUrl = message.getData().getString("url");
+
+ // Set the link URL as the title of the context menu.
+ menu.setHeaderTitle(linkUrl);
+
+ // Add an Open in New Tab entry.
+ menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
+ // Load the link URL in a new tab.
+ addNewTab(linkUrl);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add a View Image entry.
+ menu.add(R.string.view_image).setOnMenuItemClickListener((MenuItem item) -> {
+ // View the image in the current tab.
+ loadUrl(imageUrl);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add a Download Image entry.
+ menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
+ // Check if the download should be processed by an external app.
+ if (sharedPreferences.getBoolean("download_with_external_app", false)) { // Download with an external app.
+ openUrlWithExternalApp(imageUrl);
+ } else { // Download with Android's download manager.
+ // Check to see if the storage permission has already been granted.
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
+ // Store the image URL for use by `onRequestPermissionResult()`.
+ downloadImageUrl = imageUrl;
+
+ // Show a dialog if the user has previously denied the permission.
+ if (ActivityCompat.shouldShowRequestPermissionRationale(this, 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_IMAGE.
+ DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
+
+ // Show the download location permission alert dialog. The permission will be requested when 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(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
+ }
+ } else { // The storage permission has already been granted.
+ // Get a handle for the download image alert dialog.
+ DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
+
+ // Show the download image alert dialog.
+ downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
+ }
+ }
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add a Copy URL entry.
+ menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
+ // Save the link URL in a clip data.
+ ClipData srcImageAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl);
+
+ // Set the clip data as the clipboard's primary clip.
+ clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add an Open with App entry.
+ menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
+ // Open the link URL with an external app.
+ openWithApp(linkUrl);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add an Open with Browser entry.
+ menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
+ // Open the link URL with an external browser.
+ openWithBrowser(linkUrl);
+
+ // Consume the event.
+ return true;
+ });
+
+ // Add a cancel entry, which by default closes the context menu.
+ menu.add(R.string.cancel);
+ break;
+ }
+ }
+
+ @Override
+ public void onCreateBookmark(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) {
+ // Get a handle for the bookmarks list view.
+ ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
+
+ // Get the views from the dialog fragment.
+ EditText createBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_name_edittext);
+ EditText createBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_url_edittext);
+
+ // Extract the strings from the edit texts.
+ String bookmarkNameString = createBookmarkNameEditText.getText().toString();
+ String bookmarkUrlString = createBookmarkUrlEditText.getText().toString();
+
+ // Create a favorite icon byte array output stream.
+ ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
+
+ // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
+ favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
+
+ // Convert the favorite icon byte array stream to a byte array.
+ byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
+
+ // Display the new bookmark below the current items in the (0 indexed) list.
+ int newBookmarkDisplayOrder = bookmarksListView.getCount();
+
+ // Create the bookmark.
+ bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolder, newBookmarkDisplayOrder, favoriteIconByteArray);
+
+ // Update the bookmarks cursor with the current contents of this folder.
+ bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
+
+ // Update the list view.
+ bookmarksCursorAdapter.changeCursor(bookmarksCursor);
+
+ // Scroll to the new bookmark.
+ bookmarksListView.setSelection(newBookmarkDisplayOrder);
+ }
+
+ @Override
+ public void onCreateBookmarkFolder(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) {
+ // Get a handle for the bookmarks list view.
+ ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
+
+ // Get handles for the views in the dialog fragment.
+ EditText createFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.create_folder_name_edittext);
+ RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon_radiobutton);
+ ImageView folderIconImageView = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon);
+
+ // Get new folder name string.
+ String folderNameString = createFolderNameEditText.getText().toString();
+
+ // Create a folder icon bitmap.
+ Bitmap folderIconBitmap;
+
+ // Set the folder icon bitmap according to the dialog.
+ if (defaultFolderIconRadioButton.isChecked()) { // Use the default folder icon.
+ // Get the default folder icon drawable.
+ Drawable folderIconDrawable = folderIconImageView.getDrawable();
+
+ // Convert the folder icon drawable to a bitmap drawable.
+ BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
+
+ // Convert the folder icon bitmap drawable to a bitmap.
+ folderIconBitmap = folderIconBitmapDrawable.getBitmap();
+ } else { // Use the WebView favorite icon.
+ // Copy the favorite icon bitmap to the folder icon bitmap.
+ folderIconBitmap = favoriteIconBitmap;
+ }
+
+ // Create a folder icon byte array output stream.
+ ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream();
+
+ // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
+ folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream);
+
+ // Convert the folder icon byte array stream to a byte array.
+ byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray();
+
+ // Move all the bookmarks down one in the display order.
+ for (int i = 0; i < bookmarksListView.getCount(); i++) {
+ int databaseId = (int) bookmarksListView.getItemIdAtPosition(i);
+ bookmarksDatabaseHelper.updateDisplayOrder(databaseId, i + 1);
+ }
+
+ // Create the folder, which will be placed at the top of the `ListView`.
+ bookmarksDatabaseHelper.createFolder(folderNameString, currentBookmarksFolder, folderIconByteArray);
+
+ // Update the bookmarks cursor with the current contents of this folder.
+ bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
+
+ // Update the `ListView`.
+ bookmarksCursorAdapter.changeCursor(bookmarksCursor);
+
+ // Scroll to the new folder.
+ bookmarksListView.setSelection(0);
+ }
+
+ @Override
+ public void onSaveBookmark(DialogFragment dialogFragment, int selectedBookmarkDatabaseId, Bitmap favoriteIconBitmap) {
+ // Get handles for the views from `dialogFragment`.
+ EditText editBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_name_edittext);
+ EditText editBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_url_edittext);
+ RadioButton currentBookmarkIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_current_icon_radiobutton);
+
+ // Store the bookmark strings.
+ String bookmarkNameString = editBookmarkNameEditText.getText().toString();
+ String bookmarkUrlString = editBookmarkUrlEditText.getText().toString();
+
+ // Update the bookmark.
+ if (currentBookmarkIconRadioButton.isChecked()) { // Update the bookmark without changing the favorite icon.
+ bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString);
+ } else { // Update the bookmark using the `WebView` favorite icon.
+ // Create a favorite icon byte array output stream.
+ ByteArrayOutputStream newFavoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
+
+ // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
+ favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFavoriteIconByteArrayOutputStream);
+
+ // Convert the favorite icon byte array stream to a byte array.
+ byte[] newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray();
+
+ // Update the bookmark and the favorite icon.
+ bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, newFavoriteIconByteArray);
+ }
+
+ // Update the bookmarks cursor with the current contents of this folder.
+ bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
+
+ // Update the list view.
+ bookmarksCursorAdapter.changeCursor(bookmarksCursor);
+ }
+
+ @Override
+ public void onSaveBookmarkFolder(DialogFragment dialogFragment, int selectedFolderDatabaseId, Bitmap favoriteIconBitmap) {
+ // Get handles for the views from `dialogFragment`.
+ EditText editFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_folder_name_edittext);
+ RadioButton currentFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_current_icon_radiobutton);
+ RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_radiobutton);
+ ImageView defaultFolderIconImageView = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_imageview);
+
+ // Get the new folder name.
+ String newFolderNameString = editFolderNameEditText.getText().toString();
+
+ // Check if the favorite icon has changed.
+ if (currentFolderIconRadioButton.isChecked()) { // Only the name has changed.
+ // Update the name in the database.
+ bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString);
+ } else if (!currentFolderIconRadioButton.isChecked() && newFolderNameString.equals(oldFolderNameString)) { // Only the icon has changed.
+ // Create the new folder icon Bitmap.
+ Bitmap folderIconBitmap;
+
+ // Populate the new folder icon bitmap.
+ if (defaultFolderIconRadioButton.isChecked()) {
+ // Get the default folder icon drawable.
+ Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
+
+ // Convert the folder icon drawable to a bitmap drawable.
+ BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
+
+ // Convert the folder icon bitmap drawable to a bitmap.
+ folderIconBitmap = folderIconBitmapDrawable.getBitmap();
+ } else { // Use the `WebView` favorite icon.
+ // Copy the favorite icon bitmap to the folder icon bitmap.
+ folderIconBitmap = favoriteIconBitmap;
+ }
+
+ // Create a folder icon byte array output stream.
+ ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
+
+ // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
+ folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
+
+ // Convert the folder icon byte array stream to a byte array.
+ byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
+
+ // Update the folder icon in the database.
+ bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderIconByteArray);
+ } else { // The folder icon and the name have changed.
+ // Get the new folder icon `Bitmap`.
+ Bitmap folderIconBitmap;
+ if (defaultFolderIconRadioButton.isChecked()) {
+ // Get the default folder icon drawable.
+ Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
+
+ // Convert the folder icon drawable to a bitmap drawable.
+ BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
+
+ // Convert the folder icon bitmap drawable to a bitmap.
+ folderIconBitmap = folderIconBitmapDrawable.getBitmap();
+ } else { // Use the `WebView` favorite icon.
+ // Copy the favorite icon bitmap to the folder icon bitmap.
+ folderIconBitmap = favoriteIconBitmap;
+ }
+
+ // Create a folder icon byte array output stream.
+ ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
+
+ // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
+ folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
+
+ // Convert the folder icon byte array stream to a byte array.
+ byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
+
+ // Update the folder name and icon in the database.
+ bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, newFolderIconByteArray);
+ }
+
+ // Update the bookmarks cursor with the current contents of this folder.
+ bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
+
+ // Update the `ListView`.
+ bookmarksCursorAdapter.changeCursor(bookmarksCursor);
+ }
+
+ @Override
+ public void onCloseDownloadLocationPermissionDialog(int downloadType) {
+ switch (downloadType) {
+ case DownloadLocationPermissionDialog.DOWNLOAD_FILE:
+ // Request the WRITE_EXTERNAL_STORAGE permission with a file request code.
+ ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
+ break;
+
+ case DownloadLocationPermissionDialog.DOWNLOAD_IMAGE:
+ // Request the WRITE_EXTERNAL_STORAGE permission with an image request code.
+ ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
+ break;
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ // Get a handle for the fragment manager.
+ FragmentManager fragmentManager = getSupportFragmentManager();
+
+ switch (requestCode) {
+ case DOWNLOAD_FILE_REQUEST_CODE:
+ // Show the download file alert dialog. When the dialog closes, the correct command will be used based on the permission status.
+ DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(downloadUrl, downloadContentDisposition, downloadContentLength);
+
+ // On API 23, displaying the fragment must be delayed or the app will crash.
+ if (Build.VERSION.SDK_INT == 23) {
+ new Handler().postDelayed(() -> downloadFileDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
+ } else {
+ downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
+ }
+
+ // Reset the download variables.
+ downloadUrl = "";
+ downloadContentDisposition = "";
+ downloadContentLength = 0;
+ break;
+
+ case DOWNLOAD_IMAGE_REQUEST_CODE:
+ // Show the download image alert dialog. When the dialog closes, the correct command will be used based on the permission status.
+ DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(downloadImageUrl);
+
+ // On API 23, displaying the fragment must be delayed or the app will crash.
+ if (Build.VERSION.SDK_INT == 23) {
+ new Handler().postDelayed(() -> downloadImageDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
+ } else {
+ downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
+ }
+
+ // Reset the image URL variable.
+ downloadImageUrl = "";
+ break;
+
+ case SAVE_WEBPAGE_IMAGE_REQUEST_CODE:
+ // Check to see if the storage permission was granted. If the dialog was canceled the grant result will be empty.
+ if ((grantResults.length > 0) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) { // The storage permission was granted.
+ // Save the webpage image.
+ new SaveWebpageImage(this, currentWebView).execute(saveWebsiteImageFilePath);
+ } else { // The storage permission was not granted.
+ // Display an error snackbar.
+ Snackbar.make(currentWebView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show();
+ }
+
+ // Reset the save website image file path.
+ saveWebsiteImageFilePath = "";
+ break;
+ }
+ }
+
+ @Override
+ public void onDownloadImage(DialogFragment dialogFragment, String imageUrl) {
+ // Download the image if it has an HTTP or HTTPS URI.
+ if (imageUrl.startsWith("http")) {
+ // Get a handle for the system `DOWNLOAD_SERVICE`.
+ DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
+
+ // Parse `imageUrl`.
+ DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(imageUrl));
+
+ // Get a handle for the cookie manager.
+ CookieManager cookieManager = CookieManager.getInstance();
+
+ // Pass cookies to download manager if cookies are enabled. This is required to download images from websites that require a login.
+ // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
+ if (cookieManager.acceptCookie()) {
+ // Get the cookies for `imageUrl`.
+ String cookies = cookieManager.getCookie(imageUrl);
+
+ // Add the cookies to `downloadRequest`. In the HTTP request header, cookies are named `Cookie`.
+ downloadRequest.addRequestHeader("Cookie", cookies);
+ }
+
+ // Get the file name from the dialog fragment.
+ EditText downloadImageNameEditText = dialogFragment.getDialog().findViewById(R.id.download_image_name);
+ String imageName = downloadImageNameEditText.getText().toString();
+
+ // Specify the download location.
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // External write permission granted.
+ // Download to the public download directory.
+ downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, imageName);
+ } else { // External write permission denied.
+ // Download to the app's external download directory.
+ downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, imageName);
+ }
+
+ // Allow `MediaScanner` to index the download if it is a media file.
+ downloadRequest.allowScanningByMediaScanner();
+
+ // Add the URL as the description for the download.
+ downloadRequest.setDescription(imageUrl);
+
+ // Show the download notification after the download is completed.
+ downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
+
+ // Remove the lint warning below that `downloadManager` might be `null`.
+ assert downloadManager != null;
+
+ // Initiate the download.
+ downloadManager.enqueue(downloadRequest);
+ } else { // The image is not an HTTP or HTTPS URI.
+ Snackbar.make(currentWebView, R.string.cannot_download_image, Snackbar.LENGTH_INDEFINITE).show();
+ }
+ }
+
+ @Override
+ public void onDownloadFile(DialogFragment dialogFragment, String downloadUrl) {
+ // Download the file if it has an HTTP or HTTPS URI.
+ if (downloadUrl.startsWith("http")) {
+ // Get a handle for the system `DOWNLOAD_SERVICE`.
+ DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
+
+ // Parse `downloadUrl`.
+ DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(downloadUrl));
+
+ // Get a handle for the cookie manager.
+ CookieManager cookieManager = CookieManager.getInstance();
+
+ // Pass cookies to download manager if cookies are enabled. This is required to download files from websites that require a login.
+ // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
+ if (cookieManager.acceptCookie()) {
+ // Get the cookies for `downloadUrl`.
+ String cookies = cookieManager.getCookie(downloadUrl);
+
+ // Add the cookies to `downloadRequest`. In the HTTP request header, cookies are named `Cookie`.
+ downloadRequest.addRequestHeader("Cookie", cookies);
+ }
+
+ // Get the file name from the dialog fragment.
+ EditText downloadFileNameEditText = dialogFragment.getDialog().findViewById(R.id.download_file_name);
+ String fileName = downloadFileNameEditText.getText().toString();
+
+ // Specify the download location.
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // External write permission granted.
+ // Download to the public download directory.
+ downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
+ } else { // External write permission denied.
+ // Download to the app's external download directory.
+ downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, fileName);
+ }
+
+ // Allow `MediaScanner` to index the download if it is a media file.
+ downloadRequest.allowScanningByMediaScanner();
+
+ // Add the URL as the description for the download.
+ downloadRequest.setDescription(downloadUrl);
+
+ // Show the download notification after the download is completed.
+ downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
+
+ // Remove the lint warning below that `downloadManager` might be `null`.
+ assert downloadManager != null;
+
+ // Initiate the download.
+ downloadManager.enqueue(downloadRequest);
+ } else { // The download is not an HTTP or HTTPS URI.
+ Snackbar.make(currentWebView, R.string.cannot_download_file, Snackbar.LENGTH_INDEFINITE).show();
+ }
+ }
+
+ // Override `onBackPressed` to handle the navigation drawer and and the WebView.
+ @Override
+ public void onBackPressed() {
+ // Get a handle for the drawer layout and the tab layout.
+ DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
+ TabLayout tabLayout = findViewById(R.id.tablayout);
+
+ if (drawerLayout.isDrawerVisible(GravityCompat.START)) { // The navigation drawer is open.
+ // Close the navigation drawer.
+ drawerLayout.closeDrawer(GravityCompat.START);
+ } else if (drawerLayout.isDrawerVisible(GravityCompat.END)){ // The bookmarks drawer is open.
+ 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);