]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java
d7e4e69132a9886cfa017710fab1d93ab7e92839
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / activities / MainWebViewActivity.java
1 /*
2  * Copyright © 2015-2019 Soren Stoutner <soren@stoutner.com>.
3  *
4  * Download cookie code contributed 2017 Hendrik Knackstedt.  Copyright assigned to Soren Stoutner <soren@stoutner.com>.
5  *
6  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
7  *
8  * Privacy Browser is free software: you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation, either version 3 of the License, or
11  * (at your option) any later version.
12  *
13  * Privacy Browser is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with Privacy Browser.  If not, see <http://www.gnu.org/licenses/>.
20  */
21
22 package com.stoutner.privacybrowser.activities;
23
24 import android.Manifest;
25 import android.annotation.SuppressLint;
26 import android.app.Activity;
27 import android.app.DownloadManager;
28 import android.app.SearchManager;
29 import android.content.ActivityNotFoundException;
30 import android.content.BroadcastReceiver;
31 import android.content.ClipData;
32 import android.content.ClipboardManager;
33 import android.content.Context;
34 import android.content.Intent;
35 import android.content.IntentFilter;
36 import android.content.SharedPreferences;
37 import android.content.pm.PackageManager;
38 import android.content.res.Configuration;
39 import android.database.Cursor;
40 import android.graphics.Bitmap;
41 import android.graphics.BitmapFactory;
42 import android.graphics.Typeface;
43 import android.graphics.drawable.BitmapDrawable;
44 import android.graphics.drawable.Drawable;
45 import android.net.Uri;
46 import android.net.http.SslCertificate;
47 import android.net.http.SslError;
48 import android.os.Build;
49 import android.os.Bundle;
50 import android.os.Environment;
51 import android.os.Handler;
52 import android.preference.PreferenceManager;
53 import android.print.PrintDocumentAdapter;
54 import android.print.PrintManager;
55 import android.text.Editable;
56 import android.text.Spanned;
57 import android.text.TextWatcher;
58 import android.text.style.ForegroundColorSpan;
59 import android.util.Patterns;
60 import android.view.ContextMenu;
61 import android.view.GestureDetector;
62 import android.view.KeyEvent;
63 import android.view.Menu;
64 import android.view.MenuItem;
65 import android.view.MotionEvent;
66 import android.view.View;
67 import android.view.ViewGroup;
68 import android.view.WindowManager;
69 import android.view.inputmethod.InputMethodManager;
70 import android.webkit.CookieManager;
71 import android.webkit.HttpAuthHandler;
72 import android.webkit.SslErrorHandler;
73 import android.webkit.ValueCallback;
74 import android.webkit.WebBackForwardList;
75 import android.webkit.WebChromeClient;
76 import android.webkit.WebResourceResponse;
77 import android.webkit.WebSettings;
78 import android.webkit.WebStorage;
79 import android.webkit.WebView;
80 import android.webkit.WebViewClient;
81 import android.webkit.WebViewDatabase;
82 import android.widget.ArrayAdapter;
83 import android.widget.CursorAdapter;
84 import android.widget.EditText;
85 import android.widget.FrameLayout;
86 import android.widget.ImageView;
87 import android.widget.LinearLayout;
88 import android.widget.ListView;
89 import android.widget.ProgressBar;
90 import android.widget.RadioButton;
91 import android.widget.RelativeLayout;
92 import android.widget.TextView;
93
94 import androidx.annotation.NonNull;
95 import androidx.appcompat.app.ActionBar;
96 import androidx.appcompat.app.ActionBarDrawerToggle;
97 import androidx.appcompat.app.AppCompatActivity;
98 import androidx.appcompat.widget.Toolbar;
99 import androidx.core.app.ActivityCompat;
100 import androidx.core.content.ContextCompat;
101 import androidx.core.view.GravityCompat;
102 import androidx.drawerlayout.widget.DrawerLayout;
103 import androidx.fragment.app.DialogFragment;
104 import androidx.fragment.app.FragmentManager;
105 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
106 import androidx.viewpager.widget.ViewPager;
107
108 import com.google.android.material.floatingactionbutton.FloatingActionButton;
109 import com.google.android.material.navigation.NavigationView;
110 import com.google.android.material.snackbar.Snackbar;
111 import com.google.android.material.tabs.TabLayout;
112
113 import com.stoutner.privacybrowser.BuildConfig;
114 import com.stoutner.privacybrowser.R;
115 import com.stoutner.privacybrowser.adapters.WebViewPagerAdapter;
116 import com.stoutner.privacybrowser.asynctasks.GetHostIpAddresses;
117 import com.stoutner.privacybrowser.dialogs.AdConsentDialog;
118 import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog;
119 import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog;
120 import com.stoutner.privacybrowser.dialogs.CreateHomeScreenShortcutDialog;
121 import com.stoutner.privacybrowser.dialogs.DownloadFileDialog;
122 import com.stoutner.privacybrowser.dialogs.DownloadImageDialog;
123 import com.stoutner.privacybrowser.dialogs.DownloadLocationPermissionDialog;
124 import com.stoutner.privacybrowser.dialogs.EditBookmarkDialog;
125 import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDialog;
126 import com.stoutner.privacybrowser.dialogs.HttpAuthenticationDialog;
127 import com.stoutner.privacybrowser.dialogs.PinnedMismatchDialog;
128 import com.stoutner.privacybrowser.dialogs.SslCertificateErrorDialog;
129 import com.stoutner.privacybrowser.dialogs.UrlHistoryDialog;
130 import com.stoutner.privacybrowser.dialogs.ViewSslCertificateDialog;
131 import com.stoutner.privacybrowser.fragments.WebViewTabFragment;
132 import com.stoutner.privacybrowser.helpers.AdHelper;
133 import com.stoutner.privacybrowser.helpers.BlockListHelper;
134 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper;
135 import com.stoutner.privacybrowser.helpers.CheckPinnedMismatchHelper;
136 import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper;
137 import com.stoutner.privacybrowser.helpers.OrbotProxyHelper;
138 import com.stoutner.privacybrowser.views.NestedScrollWebView;
139
140 import java.io.ByteArrayInputStream;
141 import java.io.ByteArrayOutputStream;
142 import java.io.File;
143 import java.io.IOException;
144 import java.io.UnsupportedEncodingException;
145 import java.net.MalformedURLException;
146 import java.net.URL;
147 import java.net.URLDecoder;
148 import java.net.URLEncoder;
149 import java.util.ArrayList;
150 import java.util.Date;
151 import java.util.HashMap;
152 import java.util.HashSet;
153 import java.util.List;
154 import java.util.Map;
155 import java.util.Set;
156
157 // TODO.  Store up reloads for tabs that are not visible.
158 // TODO.  New tabs are white in dark mode.
159 // TODO.  Hide the tabs in full screen mode.
160 // TODO.  Find on page.
161 // TODO.  Use TabLayout.setScrollPosition to scroll to new tabs.
162
163 // AppCompatActivity from android.support.v7.app.AppCompatActivity must be used to have access to the SupportActionBar until the minimum API is >= 21.
164 public class MainWebViewActivity extends AppCompatActivity implements CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener,
165         DownloadFileDialog.DownloadFileListener, DownloadImageDialog.DownloadImageListener, DownloadLocationPermissionDialog.DownloadLocationPermissionDialogListener, EditBookmarkDialog.EditBookmarkListener,
166         EditBookmarkFolderDialog.EditBookmarkFolderListener, NavigationView.OnNavigationItemSelectedListener, WebViewTabFragment.NewTabListener, PinnedMismatchDialog.PinnedMismatchListener,
167         UrlHistoryDialog.UrlHistoryListener {
168
169     // `orbotStatus` is public static so it can be accessed from `OrbotProxyHelper`.  It is also used in `onCreate()`, `onResume()`, and `applyProxyThroughOrbot()`.
170     public static String orbotStatus;
171
172     // The WebView pager adapter is accessed from `HttpAuthenticationDialog`, `PinnedMismatchDialog`, and `SslCertificateErrorDialog`.  It is also used in `onCreate()`, `onResume()`, and `addTab()`.
173     public static WebViewPagerAdapter webViewPagerAdapter;
174
175     // The load URL on restart variables are public static so they can be accessed from `BookmarksActivity`.  They are used in `onRestart()`.
176     public static boolean loadUrlOnRestart;
177     public static String urlToLoadOnRestart;
178
179     // `restartFromBookmarksActivity` is public static so it can be accessed from `BookmarksActivity`.  It is also used in `onRestart()`.
180     public static boolean restartFromBookmarksActivity;
181
182     // `currentBookmarksFolder` is public static so it can be accessed from `BookmarksActivity`.  It is also used in `onCreate()`, `onBackPressed()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`,
183     // `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
184     public static String currentBookmarksFolder;
185
186     // The user agent constants are public static so they can be accessed from `SettingsFragment`, `DomainsActivity`, and `DomainSettingsFragment`.
187     public final static int UNRECOGNIZED_USER_AGENT = -1;
188     public final static int SETTINGS_WEBVIEW_DEFAULT_USER_AGENT = 1;
189     public final static int SETTINGS_CUSTOM_USER_AGENT = 12;
190     public final static int DOMAINS_SYSTEM_DEFAULT_USER_AGENT = 0;
191     public final static int DOMAINS_WEBVIEW_DEFAULT_USER_AGENT = 2;
192     public final static int DOMAINS_CUSTOM_USER_AGENT = 13;
193
194
195
196     // The current WebView is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`,
197     // `findNextOnPage()`, `closeFindOnPage()`, `loadUrlFromTextBox()`, `onSslMismatchBack()`, `applyProxyThroughOrbot()`, and `applyDomainSettings()`.
198     private NestedScrollWebView currentWebView;
199
200     // `customHeader` is used in `onCreate()`, `onOptionsItemSelected()`, `onCreateContextMenu()`, and `loadUrl()`.
201     private final Map<String, String> customHeaders = new HashMap<>();
202
203     // The search URL is set in `applyProxyThroughOrbot()` and used in `onCreate()`, `onNewIntent()`, `loadURLFromTextBox()`, and `initializeWebView()`.
204     private String searchURL;
205
206     // The options menu is set in `onCreateOptionsMenu()` and used in `onOptionsItemSelected()`, `updatePrivacyIcons()`, and `initializeWebView()`.
207     private Menu optionsMenu;
208
209     // The blocklists are populated in `onCreate()` and accessed from `initializeWebView()`.
210     private ArrayList<List<String[]>> easyList;
211     private ArrayList<List<String[]>> easyPrivacy;
212     private ArrayList<List<String[]>> fanboysAnnoyanceList;
213     private ArrayList<List<String[]>> fanboysSocialList;
214     private ArrayList<List<String[]>> ultraPrivacy;
215
216     // `webViewDefaultUserAgent` is used in `onCreate()` and `onPrepareOptionsMenu()`.
217     private String webViewDefaultUserAgent;
218
219     // `proxyThroughOrbot` is used in `onRestart()`, `onOptionsItemSelected()`, `applyAppSettings()`, and `applyProxyThroughOrbot()`.
220     private boolean proxyThroughOrbot;
221
222     // The incognito mode is set in `applyAppSettings()` and used in `initializeWebView()`.
223     private boolean incognitoModeEnabled;
224
225     // The full screen browsing mode tracker is set it `applyAppSettings()` and used in `initializeWebView()`.
226     private boolean fullScreenBrowsingModeEnabled;
227
228     // `inFullScreenBrowsingMode` is used in `onCreate()`, `onConfigurationChanged()`, and `applyAppSettings()`.
229     private boolean inFullScreenBrowsingMode;
230
231     // Hide app bar is used in `applyAppSettings()` and `initializeWebView()`.
232     private boolean hideAppBar;
233
234     // `reapplyDomainSettingsOnRestart` is used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, and `onAddDomain()`, .
235     private boolean reapplyDomainSettingsOnRestart;
236
237     // `reapplyAppSettingsOnRestart` is used in `onNavigationItemSelected()` and `onRestart()`.
238     private boolean reapplyAppSettingsOnRestart;
239
240     // `displayingFullScreenVideo` is used in `onCreate()` and `onResume()`.
241     private boolean displayingFullScreenVideo;
242
243     // `orbotStatusBroadcastReceiver` is used in `onCreate()` and `onDestroy()`.
244     private BroadcastReceiver orbotStatusBroadcastReceiver;
245
246     // `waitingForOrbot` is used in `onCreate()`, `onResume()`, and `applyProxyThroughOrbot()`.
247     private boolean waitingForOrbot;
248
249     // The action bar drawer toggle is initialized in `onCreate()` and used in `onResume()`.
250     private ActionBarDrawerToggle actionBarDrawerToggle;
251
252     // The color spans are used in `onCreate()` and `highlightUrlText()`.
253     private ForegroundColorSpan redColorSpan;
254     private ForegroundColorSpan initialGrayColorSpan;
255     private ForegroundColorSpan finalGrayColorSpan;
256
257     // The drawer header padding variables are used in `onCreate()` and `onConfigurationChanged()`.
258     private int drawerHeaderPaddingLeftAndRight;
259     private int drawerHeaderPaddingTop;
260     private int drawerHeaderPaddingBottom;
261
262     // `bookmarksDatabaseHelper` is used in `onCreate()`, `onDestroy`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`,
263     // and `loadBookmarksFolder()`.
264     private BookmarksDatabaseHelper bookmarksDatabaseHelper;
265
266     // `bookmarksCursor` is used in `onDestroy()`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
267     private Cursor bookmarksCursor;
268
269     // `bookmarksCursorAdapter` is used in `onCreateBookmark()`, `onCreateBookmarkFolder()` `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
270     private CursorAdapter bookmarksCursorAdapter;
271
272     // `oldFolderNameString` is used in `onCreate()` and `onSaveEditBookmarkFolder()`.
273     private String oldFolderNameString;
274
275     // `fileChooserCallback` is used in `onCreate()` and `onActivityResult()`.
276     private ValueCallback<Uri[]> fileChooserCallback;
277
278     // The download strings are used in `onCreate()`, `onRequestPermissionResult()` and `initializeWebView()`.
279     private String downloadUrl;
280     private String downloadContentDisposition;
281     private long downloadContentLength;
282
283     // `downloadImageUrl` is used in `onCreateContextMenu()` and `onRequestPermissionResult()`.
284     private String downloadImageUrl;
285
286     // The request codes are used in `onCreate()`, `onCreateContextMenu()`, `onCloseDownloadLocationPermissionDialog()`, `onRequestPermissionResult()`, and `initializeWebView()`.
287     private final int DOWNLOAD_FILE_REQUEST_CODE = 1;
288     private final int DOWNLOAD_IMAGE_REQUEST_CODE = 2;
289
290     @Override
291     // Remove the warning about needing to override `performClick()` when using an `OnTouchListener` with `WebView`.
292     @SuppressLint("ClickableViewAccessibility")
293     protected void onCreate(Bundle savedInstanceState) {
294         // Get a handle for the shared preferences.
295         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
296
297         // Get the theme and screenshot preferences.
298         boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
299         boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
300
301         // Disable screenshots if not allowed.
302         if (!allowScreenshots) {
303             getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
304         }
305
306         // Set the activity theme.
307         if (darkTheme) {
308             setTheme(R.style.PrivacyBrowserDark);
309         } else {
310             setTheme(R.style.PrivacyBrowserLight);
311         }
312
313         // Run the default commands.
314         super.onCreate(savedInstanceState);
315
316         // Set the content view.
317         setContentView(R.layout.main_framelayout);
318
319         // Get a handle for the input method.
320         InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
321
322         // Remove the lint warning below that the input method manager might be null.
323         assert inputMethodManager != null;
324
325         // Get a handle for the toolbar.
326         Toolbar toolbar = findViewById(R.id.toolbar);
327
328         // Set the action bar.  `SupportActionBar` must be used until the minimum API is >= 21.
329         setSupportActionBar(toolbar);
330
331         // Get a handle for the action bar.
332         ActionBar actionBar = getSupportActionBar();
333
334         // This is needed to get rid of the Android Studio warning that the action bar might be null.
335         assert actionBar != null;
336
337         // Add the custom layout, which shows the URL text bar.
338         actionBar.setCustomView(R.layout.url_app_bar);
339         actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
340
341         // Initialize the foreground color spans for highlighting the URLs.  We have to use the deprecated `getColor()` until API >= 23.
342         redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_a700));
343         initialGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
344         finalGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
345
346         // Get handles for the URL views.
347         EditText urlEditText = findViewById(R.id.url_edittext);
348
349         // Remove the formatting from `urlTextBar` when the user is editing the text.
350         urlEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
351             if (hasFocus) {  // The user is editing the URL text box.
352                 // Remove the highlighting.
353                 urlEditText.getText().removeSpan(redColorSpan);
354                 urlEditText.getText().removeSpan(initialGrayColorSpan);
355                 urlEditText.getText().removeSpan(finalGrayColorSpan);
356             } else {  // The user has stopped editing the URL text box.
357                 // Move to the beginning of the string.
358                 urlEditText.setSelection(0);
359
360                 // Reapply the highlighting.
361                 highlightUrlText();
362             }
363         });
364
365         // Set the go button on the keyboard to load the URL in `urlTextBox`.
366         urlEditText.setOnKeyListener((View v, int keyCode, KeyEvent event) -> {
367             // If the event is a key-down event on the `enter` button, load the URL.
368             if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {
369                 // Load the URL into the mainWebView and consume the event.
370                 loadUrlFromTextBox();
371
372                 // If the enter key was pressed, consume the event.
373                 return true;
374             } else {
375                 // If any other key was pressed, do not consume the event.
376                 return false;
377             }
378         });
379
380         // Initialize the Orbot status and the waiting for Orbot trackers.
381         orbotStatus = "unknown";
382         waitingForOrbot = false;
383
384         // Create an Orbot status `BroadcastReceiver`.
385         orbotStatusBroadcastReceiver = new BroadcastReceiver() {
386             @Override
387             public void onReceive(Context context, Intent intent) {
388                 // Store the content of the status message in `orbotStatus`.
389                 orbotStatus = intent.getStringExtra("org.torproject.android.intent.extra.STATUS");
390
391                 // If Privacy Browser is waiting on Orbot, load the website now that Orbot is connected.
392                 if (orbotStatus.equals("ON") && waitingForOrbot) {
393                     // Reset the waiting for Orbot status.
394                     waitingForOrbot = false;
395
396                     // Get the intent that started the app.
397                     Intent launchingIntent = getIntent();
398
399                     // Get the information from the intent.
400                     String launchingIntentAction = launchingIntent.getAction();
401                     Uri launchingIntentUriData = launchingIntent.getData();
402
403                     // If the intent action is a web search, perform the search.
404                     if ((launchingIntentAction != null) && launchingIntentAction.equals(Intent.ACTION_WEB_SEARCH)) {
405                         // Create an encoded URL string.
406                         String encodedUrlString;
407
408                         // Sanitize the search input and convert it to a search.
409                         try {
410                             encodedUrlString = URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8");
411                         } catch (UnsupportedEncodingException exception) {
412                             encodedUrlString = "";
413                         }
414
415                         // Load the completed search URL.
416                         loadUrl(searchURL + encodedUrlString);
417                     } else if (launchingIntentUriData != null){  // Check to see if the intent contains a new URL.
418                         // Load the URL from the intent.
419                         loadUrl(launchingIntentUriData.toString());
420                     } else {  // The is no URL in the intent.
421                         // Select the homepage based on the proxy through Orbot status.
422                         if (proxyThroughOrbot) {
423                             // Load the Tor homepage.
424                             loadUrl(sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value)));
425                         } else {
426                             // Load the normal homepage.
427                             loadUrl(sharedPreferences.getString("homepage", getString(R.string.homepage_default_value)));
428                         }
429                     }
430                 }
431             }
432         };
433
434         // Register `orbotStatusBroadcastReceiver` on `this` context.
435         this.registerReceiver(orbotStatusBroadcastReceiver, new IntentFilter("org.torproject.android.intent.action.STATUS"));
436
437         // Instantiate the blocklist helper.
438         BlockListHelper blockListHelper = new BlockListHelper();
439
440         // Parse the block lists.
441         easyList = blockListHelper.parseBlockList(getAssets(), "blocklists/easylist.txt");
442         easyPrivacy = blockListHelper.parseBlockList(getAssets(), "blocklists/easyprivacy.txt");
443         fanboysAnnoyanceList = blockListHelper.parseBlockList(getAssets(), "blocklists/fanboy-annoyance.txt");
444         fanboysSocialList = blockListHelper.parseBlockList(getAssets(), "blocklists/fanboy-social.txt");
445         ultraPrivacy = blockListHelper.parseBlockList(getAssets(), "blocklists/ultraprivacy.txt");
446
447         // Get handles for views that need to be modified.
448         DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
449         NavigationView navigationView = findViewById(R.id.navigationview);
450         TabLayout tabLayout = findViewById(R.id.tablayout);
451         SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
452         ViewPager webViewPager = findViewById(R.id.webviewpager);
453         ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
454         FloatingActionButton launchBookmarksActivityFab = findViewById(R.id.launch_bookmarks_activity_fab);
455         FloatingActionButton createBookmarkFolderFab = findViewById(R.id.create_bookmark_folder_fab);
456         FloatingActionButton createBookmarkFab = findViewById(R.id.create_bookmark_fab);
457         EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
458
459         // Listen for touches on the navigation menu.
460         navigationView.setNavigationItemSelectedListener(this);
461
462         // Get handles for the navigation menu and the back and forward menu items.  The menu is zero-based.
463         Menu navigationMenu = navigationView.getMenu();
464         MenuItem navigationCloseTabMenuItem = navigationMenu.getItem(0);
465         MenuItem navigationBackMenuItem = navigationMenu.getItem(3);
466         MenuItem navigationForwardMenuItem = navigationMenu.getItem(4);
467         MenuItem navigationHistoryMenuItem = navigationMenu.getItem(5);
468         MenuItem navigationRequestsMenuItem = navigationMenu.getItem(6);
469
470         // Initialize the web view pager adapter.
471         webViewPagerAdapter = new WebViewPagerAdapter(getSupportFragmentManager());
472
473         // Set the pager adapter on the web view pager.
474         webViewPager.setAdapter(webViewPagerAdapter);
475
476         // Store up to 100 tabs in memory.
477         webViewPager.setOffscreenPageLimit(100);
478
479         // Update the web view pager every time a tab is modified.
480         webViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
481             @Override
482             public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
483                 // Do nothing.
484             }
485
486             @Override
487             public void onPageSelected(int position) {
488                 // Set the current WebView.
489                 setCurrentWebView(position);
490
491                 // Select the corresponding tab if it does not match the currently selected page.  This will happen if the page was scrolled via swiping in the view pager.
492                 if (tabLayout.getSelectedTabPosition() != position) {
493                     // Get a handle for the corresponding tab.
494                     TabLayout.Tab correspondingTab = tabLayout.getTabAt(position);
495
496                     // Assert that the corresponding tab is not null.
497                     assert correspondingTab != null;
498
499                     // Select the corresponding tab.
500                     correspondingTab.select();
501                 }
502             }
503
504             @Override
505             public void onPageScrollStateChanged(int state) {
506                 // Do nothing.
507             }
508         });
509
510         // Display the View SSL Certificate dialog when the currently selected tab is reselected.
511         tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
512             @Override
513             public void onTabSelected(TabLayout.Tab tab) {
514                 // Select the same page in the view pager.
515                 webViewPager.setCurrentItem(tab.getPosition());
516             }
517
518             @Override
519             public void onTabUnselected(TabLayout.Tab tab) {
520                 // Do nothing.
521             }
522
523             @Override
524             public void onTabReselected(TabLayout.Tab tab) {
525                 // Instantiate the View SSL Certificate dialog.
526                 DialogFragment viewSslCertificateDialogFragment = ViewSslCertificateDialog.displayDialog(currentWebView.getWebViewFragmentId());
527
528                 // Display the View SSL Certificate dialog.
529                 viewSslCertificateDialogFragment.show(getSupportFragmentManager(), getString(R.string.view_ssl_certificate));
530             }
531         });
532
533         // Add the first tab.
534         addTab(null);
535
536         // Set the bookmarks drawer resources according to the theme.  This can't be done in the layout due to compatibility issues with the `DrawerLayout` support widget.
537         // The deprecated `getResources().getDrawable()` must be used until the minimum API >= 21 and and `getResources().getColor()` must be used until the minimum API >= 23.
538         if (darkTheme) {
539             launchBookmarksActivityFab.setImageDrawable(getResources().getDrawable(R.drawable.bookmarks_dark));
540             createBookmarkFolderFab.setImageDrawable(getResources().getDrawable(R.drawable.create_folder_dark));
541             createBookmarkFab.setImageDrawable(getResources().getDrawable(R.drawable.create_bookmark_dark));
542             bookmarksListView.setBackgroundColor(getResources().getColor(R.color.gray_850));
543         } else {
544             launchBookmarksActivityFab.setImageDrawable(getResources().getDrawable(R.drawable.bookmarks_light));
545             createBookmarkFolderFab.setImageDrawable(getResources().getDrawable(R.drawable.create_folder_light));
546             createBookmarkFab.setImageDrawable(getResources().getDrawable(R.drawable.create_bookmark_light));
547             bookmarksListView.setBackgroundColor(getResources().getColor(R.color.white));
548         }
549
550         // Set the launch bookmarks activity FAB to launch the bookmarks activity.
551         launchBookmarksActivityFab.setOnClickListener(v -> {
552             // Get a copy of the favorite icon bitmap.
553             Bitmap favoriteIconBitmap = currentWebView.getFavoriteOrDefaultIcon();
554
555             // Create a favorite icon byte array output stream.
556             ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
557
558             // Convert the favorite icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
559             favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
560
561             // Convert the favorite icon byte array stream to a byte array.
562             byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
563
564             // Create an intent to launch the bookmarks activity.
565             Intent bookmarksIntent = new Intent(getApplicationContext(), BookmarksActivity.class);
566
567             // Add the extra information to the intent.
568             bookmarksIntent.putExtra("current_url", currentWebView.getUrl());
569             bookmarksIntent.putExtra("current_title", currentWebView.getTitle());
570             bookmarksIntent.putExtra("current_folder", currentBookmarksFolder);
571             bookmarksIntent.putExtra("favorite_icon_byte_array", favoriteIconByteArray);
572
573             // Make it so.
574             startActivity(bookmarksIntent);
575         });
576
577         // Set the create new bookmark folder FAB to display an alert dialog.
578         createBookmarkFolderFab.setOnClickListener(v -> {
579             // Create a create bookmark folder dialog.
580             DialogFragment createBookmarkFolderDialog = CreateBookmarkFolderDialog.createBookmarkFolder(currentWebView.getFavoriteOrDefaultIcon());
581
582             // Show the create bookmark folder dialog.
583             createBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.create_folder));
584         });
585
586         // Set the create new bookmark FAB to display an alert dialog.
587         createBookmarkFab.setOnClickListener(view -> {
588             // Instantiate the create bookmark dialog.
589             DialogFragment createBookmarkDialog = CreateBookmarkDialog.createBookmark(currentWebView.getUrl(), currentWebView.getTitle(), currentWebView.getFavoriteOrDefaultIcon());
590
591             // Display the create bookmark dialog.
592             createBookmarkDialog.show(getSupportFragmentManager(), getString(R.string.create_bookmark));
593         });
594
595         // Search for the string on the page whenever a character changes in the `findOnPageEditText`.
596         findOnPageEditText.addTextChangedListener(new TextWatcher() {
597             @Override
598             public void beforeTextChanged(CharSequence s, int start, int count, int after) {
599                 // Do nothing.
600             }
601
602             @Override
603             public void onTextChanged(CharSequence s, int start, int before, int count) {
604                 // Do nothing.
605             }
606
607             @Override
608             public void afterTextChanged(Editable s) {
609                 // Search for the text in `mainWebView`.
610                 currentWebView.findAllAsync(findOnPageEditText.getText().toString());
611             }
612         });
613
614         // Set the `check mark` button for the `findOnPageEditText` keyboard to close the soft keyboard.
615         findOnPageEditText.setOnKeyListener((v, keyCode, event) -> {
616             if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {  // The `enter` key was pressed.
617                 // Hide the soft keyboard.
618                 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
619
620                 // Consume the event.
621                 return true;
622             } else {  // A different key was pressed.
623                 // Do not consume the event.
624                 return false;
625             }
626         });
627
628         // Implement swipe to refresh.
629         swipeRefreshLayout.setOnRefreshListener(() -> currentWebView.reload());
630
631         // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
632         swipeRefreshLayout.setProgressViewOffset(false, swipeRefreshLayout.getProgressViewStartOffset() - 10, swipeRefreshLayout.getProgressViewEndOffset());
633
634         // Set the swipe to refresh color according to the theme.
635         if (darkTheme) {
636             swipeRefreshLayout.setColorSchemeResources(R.color.blue_800);
637             swipeRefreshLayout.setProgressBackgroundColorSchemeResource(R.color.gray_850);
638         } else {
639             swipeRefreshLayout.setColorSchemeResources(R.color.blue_500);
640         }
641
642         // `DrawerTitle` identifies the `DrawerLayouts` in accessibility mode.
643         drawerLayout.setDrawerTitle(GravityCompat.START, getString(R.string.navigation_drawer));
644         drawerLayout.setDrawerTitle(GravityCompat.END, getString(R.string.bookmarks));
645
646         // Initialize the bookmarks database helper.  The `0` specifies a database version, but that is ignored and set instead using a constant in `BookmarksDatabaseHelper`.
647         bookmarksDatabaseHelper = new BookmarksDatabaseHelper(this, null, null, 0);
648
649         // Initialize `currentBookmarksFolder`.  `""` is the home folder in the database.
650         currentBookmarksFolder = "";
651
652         // Load the home folder, which is `""` in the database.
653         loadBookmarksFolder();
654
655         bookmarksListView.setOnItemClickListener((parent, view, position, id) -> {
656             // Convert the id from long to int to match the format of the bookmarks database.
657             int databaseID = (int) id;
658
659             // Get the bookmark cursor for this ID and move it to the first row.
660             Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseID);
661             bookmarkCursor.moveToFirst();
662
663             // Act upon the bookmark according to the type.
664             if (bookmarkCursor.getInt(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {  // The selected bookmark is a folder.
665                 // Store the new folder name in `currentBookmarksFolder`.
666                 currentBookmarksFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
667
668                 // Load the new folder.
669                 loadBookmarksFolder();
670             } else {  // The selected bookmark is not a folder.
671                 // Load the bookmark URL.
672                 loadUrl(bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_URL)));
673
674                 // Close the bookmarks drawer.
675                 drawerLayout.closeDrawer(GravityCompat.END);
676             }
677
678             // Close the `Cursor`.
679             bookmarkCursor.close();
680         });
681
682         bookmarksListView.setOnItemLongClickListener((parent, view, position, id) -> {
683             // Convert the database ID from `long` to `int`.
684             int databaseId = (int) id;
685
686             // Find out if the selected bookmark is a folder.
687             boolean isFolder = bookmarksDatabaseHelper.isFolder(databaseId);
688
689             if (isFolder) {
690                 // Save the current folder name, which is used in `onSaveEditBookmarkFolder()`.
691                 oldFolderNameString = bookmarksCursor.getString(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
692
693                 // Show the edit bookmark folder `AlertDialog` and name the instance `@string/edit_folder`.
694                 DialogFragment editBookmarkFolderDialog = EditBookmarkFolderDialog.folderDatabaseId(databaseId, currentWebView.getFavoriteOrDefaultIcon());
695                 editBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.edit_folder));
696             } else {
697                 // Show the edit bookmark `AlertDialog` and name the instance `@string/edit_bookmark`.
698                 DialogFragment editBookmarkDialog = EditBookmarkDialog.bookmarkDatabaseId(databaseId, currentWebView.getFavoriteOrDefaultIcon());
699                 editBookmarkDialog.show(getSupportFragmentManager(), getString(R.string.edit_bookmark));
700             }
701
702             // Consume the event.
703             return true;
704         });
705
706         // Get the status bar pixel size.
707         int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
708         int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId);
709
710         // Get the resource density.
711         float screenDensity = getResources().getDisplayMetrics().density;
712
713         // Calculate the drawer header padding.  This is used to move the text in the drawer headers below any cutouts.
714         drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity);
715         drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity);
716         drawerHeaderPaddingBottom = (int) (8 * screenDensity);
717
718         // The drawer listener is used to update the navigation menu.`
719         drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() {
720             @Override
721             public void onDrawerSlide(@NonNull View drawerView, float slideOffset) {
722             }
723
724             @Override
725             public void onDrawerOpened(@NonNull View drawerView) {
726             }
727
728             @Override
729             public void onDrawerClosed(@NonNull View drawerView) {
730             }
731
732             @Override
733             public void onDrawerStateChanged(int newState) {
734                 if ((newState == DrawerLayout.STATE_SETTLING) || (newState == DrawerLayout.STATE_DRAGGING)) {  // A drawer is opening or closing.
735                     // Get handles for the drawer headers.
736                     TextView navigationHeaderTextView = findViewById(R.id.navigationText);
737                     TextView bookmarksHeaderTextView = findViewById(R.id.bookmarks_title_textview);
738
739                     // Apply the navigation header paddings if the view is not null (sometimes it is null if another activity has already started).  This moves the text in the header below any cutouts.
740                     if (navigationHeaderTextView != null) {
741                         navigationHeaderTextView.setPadding(drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingTop, drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingBottom);
742                     }
743
744                     // Apply the bookmarks header paddings if the view is not null (sometimes it is null if another activity has already started).  This moves the text in the header below any cutouts.
745                     if (bookmarksHeaderTextView != null) {
746                         bookmarksHeaderTextView.setPadding(drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingTop, drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingBottom);
747                     }
748
749                     // Update the navigation menu items.
750                     navigationCloseTabMenuItem.setEnabled(tabLayout.getTabCount() > 1);
751                     navigationBackMenuItem.setEnabled(currentWebView.canGoBack());
752                     navigationForwardMenuItem.setEnabled(currentWebView.canGoForward());
753                     navigationHistoryMenuItem.setEnabled((currentWebView.canGoBack() || currentWebView.canGoForward()));
754                     navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
755
756                     // Hide the keyboard (if displayed).
757                     inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
758
759                     // Clear the focus from from the URL text box and the WebView.  This removes any text selection markers and context menus, which otherwise draw above the open drawers.
760                     urlEditText.clearFocus();
761                     currentWebView.clearFocus();
762                 }
763             }
764         });
765
766         // Create the hamburger icon at the start of the AppBar.
767         actionBarDrawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.open_navigation_drawer, R.string.close_navigation_drawer);
768
769         // Replace the header that `WebView` creates for `X-Requested-With` with a null value.  The default value is the application ID (com.stoutner.privacybrowser.standard).
770         customHeaders.put("X-Requested-With", "");
771
772         // Initialize the default preference values the first time the program is run.  `false` keeps this command from resetting any current preferences back to default.
773         PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
774
775         // Inflate a bare WebView to get the default user agent.  It is not used to render content on the screen.
776         @SuppressLint("InflateParams") View webViewLayout = getLayoutInflater().inflate(R.layout.bare_webview, null, false);
777
778         // Get a handle for the WebView.
779         WebView bareWebView = webViewLayout.findViewById(R.id.bare_webview);
780
781         // Store the default user agent.
782         webViewDefaultUserAgent = bareWebView.getSettings().getUserAgentString();
783
784         // Destroy the bare WebView.
785         bareWebView.destroy();
786     }
787
788     @Override
789     protected void onNewIntent(Intent intent) {
790         // Get the information from the intent.
791         String intentAction = intent.getAction();
792         Uri intentUriData = intent.getData();
793
794         // Only process the URI if it contains data.  If the user pressed the desktop icon after the app was already running the URI will be null.
795         if (intentUriData != null) {
796             // Sets the new intent as the activity intent, which replaces the one that originally started the app.
797             setIntent(intent);
798
799             // Add a new tab.
800             addTab(null);
801
802             // Create a URL string.
803             String url;
804
805             // If the intent action is a web search, perform the search.
806             if ((intentAction != null) && intentAction.equals(Intent.ACTION_WEB_SEARCH)) {
807                 // Create an encoded URL string.
808                 String encodedUrlString;
809
810                 // Sanitize the search input and convert it to a search.
811                 try {
812                     encodedUrlString = URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8");
813                 } catch (UnsupportedEncodingException exception) {
814                     encodedUrlString = "";
815                 }
816
817                 // Add the base search URL.
818                 url = searchURL + encodedUrlString;
819             } else {  // The intent should contain a URL.
820                 // Set the intent data as the URL.
821                 url = intentUriData.toString();
822             }
823
824             // Load the URL.
825             loadUrl(url);
826
827             // Get a handle for the drawer layout.
828             DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
829
830             // Close the navigation drawer if it is open.
831             if (drawerLayout.isDrawerVisible(GravityCompat.START)) {
832                 drawerLayout.closeDrawer(GravityCompat.START);
833             }
834
835             // Close the bookmarks drawer if it is open.
836             if (drawerLayout.isDrawerVisible(GravityCompat.END)) {
837                 drawerLayout.closeDrawer(GravityCompat.END);
838             }
839
840             // Clear the keyboard if displayed and remove the focus on the urlTextBar if it has it.
841             currentWebView.requestFocus();
842         }
843     }
844
845     @Override
846     public void onRestart() {
847         // Run the default commands.
848         super.onRestart();
849
850         // Make sure Orbot is running if Privacy Browser is proxying through Orbot.
851         if (proxyThroughOrbot) {
852             // Request Orbot to start.  If Orbot is already running no hard will be caused by this request.
853             Intent orbotIntent = new Intent("org.torproject.android.intent.action.START");
854
855             // Send the intent to the Orbot package.
856             orbotIntent.setPackage("org.torproject.android");
857
858             // Make it so.
859             sendBroadcast(orbotIntent);
860         }
861
862         // Apply the app settings if returning from the Settings activity.
863         if (reapplyAppSettingsOnRestart) {
864             // Reset the reapply app settings on restart tracker.
865             reapplyAppSettingsOnRestart = false;
866
867             // Apply the app settings.
868             applyAppSettings();
869         }
870
871         // Apply the domain settings if returning from the settings or domains activity.
872         if (reapplyDomainSettingsOnRestart) {
873             // Reset the reapply domain settings on restart tracker.
874             reapplyDomainSettingsOnRestart = false;
875
876             // Reapply the domain settings for each tab.
877             for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
878                 // Get the WebView tab fragment.
879                 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
880
881                 // Get the fragment view.
882                 View fragmentView = webViewTabFragment.getView();
883
884                 // Only reload the WebViews if they exist.
885                 if (fragmentView != null) {
886                     // Get the nested scroll WebView from the tab fragment.
887                     NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
888
889                     // Reset the current domain name so the domain settings will be reapplied.
890                     nestedScrollWebView.resetCurrentDomainName();
891
892                     // Reapply the domain settings.
893                     applyDomainSettings(nestedScrollWebView, nestedScrollWebView.getUrl(), false, true);
894                 }
895             }
896         }
897
898         // Load the URL on restart (used when loading a bookmark).
899         if (loadUrlOnRestart) {
900             // Load the specified URL.
901             loadUrl(urlToLoadOnRestart);
902
903             // Reset the load on restart tracker.
904             loadUrlOnRestart = false;
905         }
906
907         // Update the bookmarks drawer if returning from the Bookmarks activity.
908         if (restartFromBookmarksActivity) {
909             // Get a handle for the drawer layout.
910             DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
911
912             // Close the bookmarks drawer.
913             drawerLayout.closeDrawer(GravityCompat.END);
914
915             // Reload the bookmarks drawer.
916             loadBookmarksFolder();
917
918             // Reset `restartFromBookmarksActivity`.
919             restartFromBookmarksActivity = false;
920         }
921
922         // Update the privacy icon.  `true` runs `invalidateOptionsMenu` as the last step.  This can be important if the screen was rotated.
923         updatePrivacyIcons(true);
924     }
925
926     // `onResume()` runs after `onStart()`, which runs after `onCreate()` and `onRestart()`.
927     @Override
928     public void onResume() {
929         // Run the default commands.
930         super.onResume();
931
932         for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
933             // Get the WebView tab fragment.
934             WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
935
936             // Get the fragment view.
937             View fragmentView = webViewTabFragment.getView();
938
939             // Only resume the WebViews if they exist (they won't when the app is first created).
940             if (fragmentView != null) {
941                 // Get the nested scroll WebView from the tab fragment.
942                 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
943
944                 // Resume the nested scroll WebView JavaScript timers.
945                 nestedScrollWebView.resumeTimers();
946
947                 // Resume the nested scroll WebView.
948                 nestedScrollWebView.onResume();
949             }
950         }
951
952         // Display a message to the user if waiting for Orbot.
953         if (waitingForOrbot && !orbotStatus.equals("ON")) {
954             // Disable the wide view port so that the waiting for Orbot text is displayed correctly.
955             currentWebView.getSettings().setUseWideViewPort(false);
956
957             // Load a waiting page.  `null` specifies no encoding, which defaults to ASCII.
958             currentWebView.loadData("<html><body><br/><center><h1>" + getString(R.string.waiting_for_orbot) + "</h1></center></body></html>", "text/html", null);
959         }
960
961         if (displayingFullScreenVideo || inFullScreenBrowsingMode) {
962             // Get a handle for the root frame layouts.
963             FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
964
965             // Remove the translucent status flag.  This is necessary so the root frame layout can fill the entire screen.
966             getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
967
968             /* Hide the system bars.
969              * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
970              * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
971              * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
972              * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
973              */
974             rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
975                     View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
976         } else if (BuildConfig.FLAVOR.contentEquals("free")) {  // Resume the adView for the free flavor.
977             // Resume the ad.
978             AdHelper.resumeAd(findViewById(R.id.adview));
979         }
980     }
981
982     @Override
983     public void onPause() {
984         // Run the default commands.
985         super.onPause();
986
987         for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
988             // Get the WebView tab fragment.
989             WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
990
991             // Get the fragment view.
992             View fragmentView = webViewTabFragment.getView();
993
994             // Only pause the WebViews if they exist (they won't when the app is first created).
995             if (fragmentView != null) {
996                 // Get the nested scroll WebView from the tab fragment.
997                 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
998
999                 // Pause the nested scroll WebView.
1000                 nestedScrollWebView.onPause();
1001
1002                 // Pause the nested scroll WebView JavaScript timers.
1003                 nestedScrollWebView.pauseTimers();
1004             }
1005         }
1006
1007         // Pause the ad or it will continue to consume resources in the background on the free flavor.
1008         if (BuildConfig.FLAVOR.contentEquals("free")) {
1009             // Pause the ad.
1010             AdHelper.pauseAd(findViewById(R.id.adview));
1011         }
1012     }
1013
1014     @Override
1015     public void onDestroy() {
1016         // Unregister the Orbot status broadcast receiver.
1017         this.unregisterReceiver(orbotStatusBroadcastReceiver);
1018
1019         // Close the bookmarks cursor and database.
1020         bookmarksCursor.close();
1021         bookmarksDatabaseHelper.close();
1022
1023         // Run the default commands.
1024         super.onDestroy();
1025     }
1026
1027     @Override
1028     public boolean onCreateOptionsMenu(Menu menu) {
1029         // Inflate the menu.  This adds items to the action bar if it is present.
1030         getMenuInflater().inflate(R.menu.webview_options_menu, menu);
1031
1032         // Store a handle for the options menu so it can be used by `onOptionsItemSelected()` and `updatePrivacyIcons()`.
1033         optionsMenu = menu;
1034
1035         // Set the initial status of the privacy icons.  `false` does not call `invalidateOptionsMenu` as the last step.
1036         updatePrivacyIcons(false);
1037
1038         // Get handles for the menu items.
1039         MenuItem toggleFirstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies);
1040         MenuItem toggleThirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies);
1041         MenuItem toggleDomStorageMenuItem = menu.findItem(R.id.toggle_dom_storage);
1042         MenuItem toggleSaveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data);  // Form data can be removed once the minimum API >= 26.
1043         MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data);  // Form data can be removed once the minimum API >= 26.
1044         MenuItem refreshMenuItem = menu.findItem(R.id.refresh);
1045         MenuItem adConsentMenuItem = menu.findItem(R.id.ad_consent);
1046
1047         // Only display third-party cookies if API >= 21
1048         toggleThirdPartyCookiesMenuItem.setVisible(Build.VERSION.SDK_INT >= 21);
1049
1050         // Only display the form data menu items if the API < 26.
1051         toggleSaveFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
1052         clearFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
1053
1054         // Disable the clear form data menu item if the API >= 26 so that the status of the main Clear Data is calculated correctly.
1055         clearFormDataMenuItem.setEnabled(Build.VERSION.SDK_INT < 26);
1056
1057         // Only show Ad Consent if this is the free flavor.
1058         adConsentMenuItem.setVisible(BuildConfig.FLAVOR.contentEquals("free"));
1059
1060         // Get the shared preference values.
1061         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
1062
1063         // Get the dark theme and app bar preferences..
1064         boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false);
1065         boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
1066
1067         // 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.
1068         if (displayAdditionalAppBarIcons) {
1069             toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
1070             toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
1071             refreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
1072         } else { //Do not display the additional icons.
1073             toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
1074             toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
1075             refreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
1076         }
1077
1078         // Replace Refresh with Stop if a URL is already loading.
1079         if (currentWebView != null && currentWebView.getProgress() != 100) {
1080             // Set the title.
1081             refreshMenuItem.setTitle(R.string.stop);
1082
1083             // If the icon is displayed in the AppBar, set it according to the theme.
1084             if (displayAdditionalAppBarIcons) {
1085                 if (darkTheme) {
1086                     refreshMenuItem.setIcon(R.drawable.close_dark);
1087                 } else {
1088                     refreshMenuItem.setIcon(R.drawable.close_light);
1089                 }
1090             }
1091         }
1092
1093         return true;
1094     }
1095
1096     @Override
1097     public boolean onPrepareOptionsMenu(Menu menu) {
1098         // Get handles for the menu items.
1099         MenuItem addOrEditDomain = menu.findItem(R.id.add_or_edit_domain);
1100         MenuItem firstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies);
1101         MenuItem thirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies);
1102         MenuItem domStorageMenuItem = menu.findItem(R.id.toggle_dom_storage);
1103         MenuItem saveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data);  // Form data can be removed once the minimum API >= 26.
1104         MenuItem clearDataMenuItem = menu.findItem(R.id.clear_data);
1105         MenuItem clearCookiesMenuItem = menu.findItem(R.id.clear_cookies);
1106         MenuItem clearDOMStorageMenuItem = menu.findItem(R.id.clear_dom_storage);
1107         MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data);  // Form data can be removed once the minimum API >= 26.
1108         MenuItem blocklistsMenuItem = menu.findItem(R.id.blocklists);
1109         MenuItem easyListMenuItem = menu.findItem(R.id.easylist);
1110         MenuItem easyPrivacyMenuItem = menu.findItem(R.id.easyprivacy);
1111         MenuItem fanboysAnnoyanceListMenuItem = menu.findItem(R.id.fanboys_annoyance_list);
1112         MenuItem fanboysSocialBlockingListMenuItem = menu.findItem(R.id.fanboys_social_blocking_list);
1113         MenuItem ultraPrivacyMenuItem = menu.findItem(R.id.ultraprivacy);
1114         MenuItem blockAllThirdPartyRequestsMenuItem = menu.findItem(R.id.block_all_third_party_requests);
1115         MenuItem fontSizeMenuItem = menu.findItem(R.id.font_size);
1116         MenuItem swipeToRefreshMenuItem = menu.findItem(R.id.swipe_to_refresh);
1117         MenuItem displayImagesMenuItem = menu.findItem(R.id.display_images);
1118         MenuItem nightModeMenuItem = menu.findItem(R.id.night_mode);
1119         MenuItem proxyThroughOrbotMenuItem = menu.findItem(R.id.proxy_through_orbot);
1120
1121         // Get a handle for the cookie manager.
1122         CookieManager cookieManager = CookieManager.getInstance();
1123
1124         // Initialize the current user agent string and the font size.
1125         String currentUserAgent = getString(R.string.user_agent_privacy_browser);
1126         int fontSize = 100;
1127
1128         // 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.
1129         if (currentWebView != null) {
1130             // Set the add or edit domain text.
1131             if (currentWebView.getDomainSettingsApplied()) {
1132                 addOrEditDomain.setTitle(R.string.edit_domain_settings);
1133             } else {
1134                 addOrEditDomain.setTitle(R.string.add_domain_settings);
1135             }
1136
1137             // Get the current user agent from the WebView.
1138             currentUserAgent = currentWebView.getSettings().getUserAgentString();
1139
1140             // Get the current font size from the
1141             fontSize = currentWebView.getSettings().getTextZoom();
1142
1143             // Set the status of the menu item checkboxes.
1144             domStorageMenuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled());
1145             saveFormDataMenuItem.setChecked(currentWebView.getSettings().getSaveFormData());  // Form data can be removed once the minimum API >= 26.
1146             easyListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST));
1147             easyPrivacyMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY));
1148             fanboysAnnoyanceListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1149             fanboysSocialBlockingListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
1150             ultraPrivacyMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY));
1151             blockAllThirdPartyRequestsMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
1152             swipeToRefreshMenuItem.setChecked(currentWebView.getSwipeToRefresh());
1153             displayImagesMenuItem.setChecked(currentWebView.getSettings().getLoadsImagesAutomatically());
1154             nightModeMenuItem.setChecked(currentWebView.getNightMode());
1155
1156             // Initialize the display names for the blocklists with the number of blocked requests.
1157             blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
1158             easyListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASY_LIST) + " - " + getString(R.string.easylist));
1159             easyPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASY_PRIVACY) + " - " + getString(R.string.easyprivacy));
1160             fanboysAnnoyanceListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST) + " - " + getString(R.string.fanboys_annoyance_list));
1161             fanboysSocialBlockingListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST) + " - " + getString(R.string.fanboys_social_blocking_list));
1162             ultraPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.ULTRA_PRIVACY) + " - " + getString(R.string.ultraprivacy));
1163             blockAllThirdPartyRequestsMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " + getString(R.string.block_all_third_party_requests));
1164
1165             // Only modify third-party cookies if the API >= 21.
1166             if (Build.VERSION.SDK_INT >= 21) {
1167                 // Set the status of the third-party cookies checkbox.
1168                 thirdPartyCookiesMenuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView));
1169
1170                 // Enable third-party cookies if first-party cookies are enabled.
1171                 thirdPartyCookiesMenuItem.setEnabled(cookieManager.acceptCookie());
1172             }
1173
1174             // Enable DOM Storage if JavaScript is enabled.
1175             domStorageMenuItem.setEnabled(currentWebView.getSettings().getJavaScriptEnabled());
1176         }
1177
1178         // Set the status of the menu item checkboxes.
1179         firstPartyCookiesMenuItem.setChecked(cookieManager.acceptCookie());
1180         proxyThroughOrbotMenuItem.setChecked(proxyThroughOrbot);
1181
1182         // Enable Clear Cookies if there are any.
1183         clearCookiesMenuItem.setEnabled(cookieManager.hasCookies());
1184
1185         // 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`.
1186         String privateDataDirectoryString = getApplicationInfo().dataDir;
1187
1188         // Get a count of the number of files in the Local Storage directory.
1189         File localStorageDirectory = new File (privateDataDirectoryString + "/app_webview/Local Storage/");
1190         int localStorageDirectoryNumberOfFiles = 0;
1191         if (localStorageDirectory.exists()) {
1192             localStorageDirectoryNumberOfFiles = localStorageDirectory.list().length;
1193         }
1194
1195         // Get a count of the number of files in the IndexedDB directory.
1196         File indexedDBDirectory = new File (privateDataDirectoryString + "/app_webview/IndexedDB");
1197         int indexedDBDirectoryNumberOfFiles = 0;
1198         if (indexedDBDirectory.exists()) {
1199             indexedDBDirectoryNumberOfFiles = indexedDBDirectory.list().length;
1200         }
1201
1202         // Enable Clear DOM Storage if there is any.
1203         clearDOMStorageMenuItem.setEnabled(localStorageDirectoryNumberOfFiles > 0 || indexedDBDirectoryNumberOfFiles > 0);
1204
1205         // Enable Clear Form Data is there is any.  This can be removed once the minimum API >= 26.
1206         if (Build.VERSION.SDK_INT < 26) {
1207             // Get the WebView database.
1208             WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
1209
1210             // Enable the clear form data menu item if there is anything to clear.
1211             clearFormDataMenuItem.setEnabled(webViewDatabase.hasFormData());
1212         }
1213
1214         // Enable Clear Data if any of the submenu items are enabled.
1215         clearDataMenuItem.setEnabled(clearCookiesMenuItem.isEnabled() || clearDOMStorageMenuItem.isEnabled() || clearFormDataMenuItem.isEnabled());
1216
1217         // Disable Fanboy's Social Blocking List menu item if Fanboy's Annoyance List is checked.
1218         fanboysSocialBlockingListMenuItem.setEnabled(!fanboysAnnoyanceListMenuItem.isChecked());
1219
1220         // Select the current user agent menu item.  A switch statement cannot be used because the user agents are not compile time constants.
1221         if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[0])) {  // Privacy Browser.
1222             menu.findItem(R.id.user_agent_privacy_browser).setChecked(true);
1223         } else if (currentUserAgent.equals(webViewDefaultUserAgent)) {  // WebView Default.
1224             menu.findItem(R.id.user_agent_webview_default).setChecked(true);
1225         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[2])) {  // Firefox on Android.
1226             menu.findItem(R.id.user_agent_firefox_on_android).setChecked(true);
1227         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[3])) {  // Chrome on Android.
1228             menu.findItem(R.id.user_agent_chrome_on_android).setChecked(true);
1229         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[4])) {  // Safari on iOS.
1230             menu.findItem(R.id.user_agent_safari_on_ios).setChecked(true);
1231         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[5])) {  // Firefox on Linux.
1232             menu.findItem(R.id.user_agent_firefox_on_linux).setChecked(true);
1233         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[6])) {  // Chromium on Linux.
1234             menu.findItem(R.id.user_agent_chromium_on_linux).setChecked(true);
1235         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[7])) {  // Firefox on Windows.
1236             menu.findItem(R.id.user_agent_firefox_on_windows).setChecked(true);
1237         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[8])) {  // Chrome on Windows.
1238             menu.findItem(R.id.user_agent_chrome_on_windows).setChecked(true);
1239         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[9])) {  // Edge on Windows.
1240             menu.findItem(R.id.user_agent_edge_on_windows).setChecked(true);
1241         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[10])) {  // Internet Explorer on Windows.
1242             menu.findItem(R.id.user_agent_internet_explorer_on_windows).setChecked(true);
1243         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[11])) {  // Safari on macOS.
1244             menu.findItem(R.id.user_agent_safari_on_macos).setChecked(true);
1245         } else {  // Custom user agent.
1246             menu.findItem(R.id.user_agent_custom).setChecked(true);
1247         }
1248
1249         // Instantiate the font size title and the selected font size menu item.
1250         String fontSizeTitle;
1251         MenuItem selectedFontSizeMenuItem;
1252
1253         // Prepare the font size title and current size menu item.
1254         switch (fontSize) {
1255             case 25:
1256                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.twenty_five_percent);
1257                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_twenty_five_percent);
1258                 break;
1259
1260             case 50:
1261                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.fifty_percent);
1262                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_fifty_percent);
1263                 break;
1264
1265             case 75:
1266                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.seventy_five_percent);
1267                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_seventy_five_percent);
1268                 break;
1269
1270             case 100:
1271                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent);
1272                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent);
1273                 break;
1274
1275             case 125:
1276                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_twenty_five_percent);
1277                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_twenty_five_percent);
1278                 break;
1279
1280             case 150:
1281                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_fifty_percent);
1282                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_fifty_percent);
1283                 break;
1284
1285             case 175:
1286                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_seventy_five_percent);
1287                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_seventy_five_percent);
1288                 break;
1289
1290             case 200:
1291                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.two_hundred_percent);
1292                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_two_hundred_percent);
1293                 break;
1294
1295             default:
1296                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent);
1297                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent);
1298                 break;
1299         }
1300
1301         // Set the font size title and select the current size menu item.
1302         fontSizeMenuItem.setTitle(fontSizeTitle);
1303         selectedFontSizeMenuItem.setChecked(true);
1304
1305         // Run all the other default commands.
1306         super.onPrepareOptionsMenu(menu);
1307
1308         // Display the menu.
1309         return true;
1310     }
1311
1312     @Override
1313     // Remove Android Studio's warning about the dangers of using SetJavaScriptEnabled.
1314     @SuppressLint("SetJavaScriptEnabled")
1315     public boolean onOptionsItemSelected(MenuItem menuItem) {
1316         // Reenter full screen browsing mode if it was interrupted by the options menu.  <https://redmine.stoutner.com/issues/389>
1317         if (inFullScreenBrowsingMode) {
1318             // Remove the translucent status flag.  This is necessary so the root frame layout can fill the entire screen.
1319             getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
1320
1321             FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
1322
1323             /* Hide the system bars.
1324              * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
1325              * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
1326              * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
1327              * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
1328              */
1329             rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
1330                     View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
1331         }
1332
1333         // Get the selected menu item ID.
1334         int menuItemId = menuItem.getItemId();
1335
1336         // Get a handle for the shared preferences.
1337         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
1338
1339         // Get a handle for the cookie manager.
1340         CookieManager cookieManager = CookieManager.getInstance();
1341
1342         // Run the commands that correlate to the selected menu item.
1343         switch (menuItemId) {
1344             case R.id.toggle_javascript:
1345                 // Toggle the JavaScript status.
1346                 currentWebView.getSettings().setJavaScriptEnabled(!currentWebView.getSettings().getJavaScriptEnabled());
1347
1348                 // Update the privacy icon.  `true` runs `invalidateOptionsMenu` as the last step.
1349                 updatePrivacyIcons(true);
1350
1351                 // Display a `Snackbar`.
1352                 if (currentWebView.getSettings().getJavaScriptEnabled()) {  // JavaScrip is enabled.
1353                     Snackbar.make(findViewById(R.id.webviewpager), R.string.javascript_enabled, Snackbar.LENGTH_SHORT).show();
1354                 } else if (cookieManager.acceptCookie()) {  // JavaScript is disabled, but first-party cookies are enabled.
1355                     Snackbar.make(findViewById(R.id.webviewpager), R.string.javascript_disabled, Snackbar.LENGTH_SHORT).show();
1356                 } else {  // Privacy mode.
1357                     Snackbar.make(findViewById(R.id.webviewpager), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
1358                 }
1359
1360                 // Reload the current WebView.
1361                 currentWebView.reload();
1362                 return true;
1363
1364             case R.id.add_or_edit_domain:
1365                 if (currentWebView.getDomainSettingsApplied()) {  // Edit the current domain settings.
1366                     // Reapply the domain settings on returning to `MainWebViewActivity`.
1367                     reapplyDomainSettingsOnRestart = true;
1368
1369                     // Create an intent to launch the domains activity.
1370                     Intent domainsIntent = new Intent(this, DomainsActivity.class);
1371
1372                     // Add the extra information to the intent.
1373                     domainsIntent.putExtra("load_domain", currentWebView.getDomainSettingsDatabaseId());
1374                     domainsIntent.putExtra("close_on_back", true);
1375                     domainsIntent.putExtra("current_url", currentWebView.getUrl());
1376
1377                     // Get the current certificate.
1378                     SslCertificate sslCertificate = currentWebView.getCertificate();
1379
1380                     // Check to see if the SSL certificate is populated.
1381                     if (sslCertificate != null) {
1382                         // Extract the certificate to strings.
1383                         String issuedToCName = sslCertificate.getIssuedTo().getCName();
1384                         String issuedToOName = sslCertificate.getIssuedTo().getOName();
1385                         String issuedToUName = sslCertificate.getIssuedTo().getUName();
1386                         String issuedByCName = sslCertificate.getIssuedBy().getCName();
1387                         String issuedByOName = sslCertificate.getIssuedBy().getOName();
1388                         String issuedByUName = sslCertificate.getIssuedBy().getUName();
1389                         long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
1390                         long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
1391
1392                         // Add the certificate to the intent.
1393                         domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
1394                         domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
1395                         domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
1396                         domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
1397                         domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
1398                         domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
1399                         domainsIntent.putExtra("ssl_start_date", startDateLong);
1400                         domainsIntent.putExtra("ssl_end_date", endDateLong);
1401                     }
1402
1403                     // Check to see if the current IP addresses have been received.
1404                     if (currentWebView.hasCurrentIpAddresses()) {
1405                         // Add the current IP addresses to the intent.
1406                         domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
1407                     }
1408
1409                     // Make it so.
1410                     startActivity(domainsIntent);
1411                 } else {  // Add a new domain.
1412                     // Apply the new domain settings on returning to `MainWebViewActivity`.
1413                     reapplyDomainSettingsOnRestart = true;
1414
1415                     // Get the current domain
1416                     Uri currentUri = Uri.parse(currentWebView.getUrl());
1417                     String currentDomain = currentUri.getHost();
1418
1419                     // Initialize the database handler.  The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
1420                     DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
1421
1422                     // Create the domain and store the database ID.
1423                     int newDomainDatabaseId = domainsDatabaseHelper.addDomain(currentDomain);
1424
1425                     // Create an intent to launch the domains activity.
1426                     Intent domainsIntent = new Intent(this, DomainsActivity.class);
1427
1428                     // Add the extra information to the intent.
1429                     domainsIntent.putExtra("load_domain", newDomainDatabaseId);
1430                     domainsIntent.putExtra("close_on_back", true);
1431                     domainsIntent.putExtra("current_url", currentWebView.getUrl());
1432
1433                     // Get the current certificate.
1434                     SslCertificate sslCertificate = currentWebView.getCertificate();
1435
1436                     // Check to see if the SSL certificate is populated.
1437                     if (sslCertificate != null) {
1438                         // Extract the certificate to strings.
1439                         String issuedToCName = sslCertificate.getIssuedTo().getCName();
1440                         String issuedToOName = sslCertificate.getIssuedTo().getOName();
1441                         String issuedToUName = sslCertificate.getIssuedTo().getUName();
1442                         String issuedByCName = sslCertificate.getIssuedBy().getCName();
1443                         String issuedByOName = sslCertificate.getIssuedBy().getOName();
1444                         String issuedByUName = sslCertificate.getIssuedBy().getUName();
1445                         long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
1446                         long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
1447
1448                         // Add the certificate to the intent.
1449                         domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
1450                         domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
1451                         domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
1452                         domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
1453                         domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
1454                         domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
1455                         domainsIntent.putExtra("ssl_start_date", startDateLong);
1456                         domainsIntent.putExtra("ssl_end_date", endDateLong);
1457                     }
1458
1459                     // Check to see if the current IP addresses have been received.
1460                     if (currentWebView.hasCurrentIpAddresses()) {
1461                         // Add the current IP addresses to the intent.
1462                         domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
1463                     }
1464
1465                     // Make it so.
1466                     startActivity(domainsIntent);
1467                 }
1468                 return true;
1469
1470             case R.id.toggle_first_party_cookies:
1471                 // Switch the first-party cookie status.
1472                 cookieManager.setAcceptCookie(!cookieManager.acceptCookie());
1473
1474                 // Store the first-party cookie status.
1475                 currentWebView.setAcceptFirstPartyCookies(cookieManager.acceptCookie());
1476
1477                 // Update the menu checkbox.
1478                 menuItem.setChecked(cookieManager.acceptCookie());
1479
1480                 // Update the privacy icon.  `true` runs `invalidateOptionsMenu` as the last step.
1481                 updatePrivacyIcons(true);
1482
1483                 // Display a snackbar.
1484                 if (cookieManager.acceptCookie()) {  // First-party cookies are enabled.
1485                     Snackbar.make(findViewById(R.id.webviewpager), R.string.first_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
1486                 } else if (currentWebView.getSettings().getJavaScriptEnabled()) {  // JavaScript is still enabled.
1487                     Snackbar.make(findViewById(R.id.webviewpager), R.string.first_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
1488                 } else {  // Privacy mode.
1489                     Snackbar.make(findViewById(R.id.webviewpager), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
1490                 }
1491
1492                 // Reload the current WebView.
1493                 currentWebView.reload();
1494                 return true;
1495
1496             case R.id.toggle_third_party_cookies:
1497                 if (Build.VERSION.SDK_INT >= 21) {
1498                     // Switch the status of thirdPartyCookiesEnabled.
1499                     cookieManager.setAcceptThirdPartyCookies(currentWebView, !cookieManager.acceptThirdPartyCookies(currentWebView));
1500
1501                     // Update the menu checkbox.
1502                     menuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView));
1503
1504                     // Display a snackbar.
1505                     if (cookieManager.acceptThirdPartyCookies(currentWebView)) {
1506                         Snackbar.make(findViewById(R.id.webviewpager), R.string.third_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
1507                     } else {
1508                         Snackbar.make(findViewById(R.id.webviewpager), R.string.third_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
1509                     }
1510
1511                     // Reload the current WebView.
1512                     currentWebView.reload();
1513                 } // Else do nothing because SDK < 21.
1514                 return true;
1515
1516             case R.id.toggle_dom_storage:
1517                 // Toggle the status of domStorageEnabled.
1518                 currentWebView.getSettings().setDomStorageEnabled(!currentWebView.getSettings().getDomStorageEnabled());
1519
1520                 // Update the menu checkbox.
1521                 menuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled());
1522
1523                 // Update the privacy icon.  `true` refreshes the app bar icons.
1524                 updatePrivacyIcons(true);
1525
1526                 // Display a snackbar.
1527                 if (currentWebView.getSettings().getDomStorageEnabled()) {
1528                     Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_enabled, Snackbar.LENGTH_SHORT).show();
1529                 } else {
1530                     Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_disabled, Snackbar.LENGTH_SHORT).show();
1531                 }
1532
1533                 // Reload the current WebView.
1534                 currentWebView.reload();
1535                 return true;
1536
1537             // Form data can be removed once the minimum API >= 26.
1538             case R.id.toggle_save_form_data:
1539                 // Switch the status of saveFormDataEnabled.
1540                 currentWebView.getSettings().setSaveFormData(!currentWebView.getSettings().getSaveFormData());
1541
1542                 // Update the menu checkbox.
1543                 menuItem.setChecked(currentWebView.getSettings().getSaveFormData());
1544
1545                 // Display a snackbar.
1546                 if (currentWebView.getSettings().getSaveFormData()) {
1547                     Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_enabled, Snackbar.LENGTH_SHORT).show();
1548                 } else {
1549                     Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_disabled, Snackbar.LENGTH_SHORT).show();
1550                 }
1551
1552                 // Update the privacy icon.  `true` runs `invalidateOptionsMenu` as the last step.
1553                 updatePrivacyIcons(true);
1554
1555                 // Reload the current WebView.
1556                 currentWebView.reload();
1557                 return true;
1558
1559             case R.id.clear_cookies:
1560                 Snackbar.make(findViewById(R.id.webviewpager), R.string.cookies_deleted, Snackbar.LENGTH_LONG)
1561                         .setAction(R.string.undo, v -> {
1562                             // Do nothing because everything will be handled by `onDismissed()` below.
1563                         })
1564                         .addCallback(new Snackbar.Callback() {
1565                             @SuppressLint("SwitchIntDef")  // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1566                             @Override
1567                             public void onDismissed(Snackbar snackbar, int event) {
1568                                 if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) {  // The snackbar was dismissed without the undo button being pushed.
1569                                     // Delete the cookies, which command varies by SDK.
1570                                     if (Build.VERSION.SDK_INT < 21) {
1571                                         cookieManager.removeAllCookie();
1572                                     } else {
1573                                         cookieManager.removeAllCookies(null);
1574                                     }
1575                                 }
1576                             }
1577                         })
1578                         .show();
1579                 return true;
1580
1581             case R.id.clear_dom_storage:
1582                 Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_deleted, Snackbar.LENGTH_LONG)
1583                         .setAction(R.string.undo, v -> {
1584                             // Do nothing because everything will be handled by `onDismissed()` below.
1585                         })
1586                         .addCallback(new Snackbar.Callback() {
1587                             @SuppressLint("SwitchIntDef")  // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1588                             @Override
1589                             public void onDismissed(Snackbar snackbar, int event) {
1590                                 if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) {  // The snackbar was dismissed without the undo button being pushed.
1591                                     // Delete the DOM Storage.
1592                                     WebStorage webStorage = WebStorage.getInstance();
1593                                     webStorage.deleteAllData();
1594
1595                                     // Initialize a handler to manually delete the DOM storage files and directories.
1596                                     Handler deleteDomStorageHandler = new Handler();
1597
1598                                     // Setup a runnable to manually delete the DOM storage files and directories.
1599                                     Runnable deleteDomStorageRunnable = () -> {
1600                                         try {
1601                                             // Get a handle for the runtime.
1602                                             Runtime runtime = Runtime.getRuntime();
1603
1604                                             // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
1605                                             // which links to `/data/data/com.stoutner.privacybrowser.standard`.
1606                                             String privateDataDirectoryString = getApplicationInfo().dataDir;
1607
1608                                             // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
1609                                             Process deleteLocalStorageProcess = runtime.exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
1610
1611                                             // Multiple commands must be used because `Runtime.exec()` does not like `*`.
1612                                             Process deleteIndexProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
1613                                             Process deleteQuotaManagerProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
1614                                             Process deleteQuotaManagerJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
1615                                             Process deleteDatabasesProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
1616
1617                                             // Wait for the processes to finish.
1618                                             deleteLocalStorageProcess.waitFor();
1619                                             deleteIndexProcess.waitFor();
1620                                             deleteQuotaManagerProcess.waitFor();
1621                                             deleteQuotaManagerJournalProcess.waitFor();
1622                                             deleteDatabasesProcess.waitFor();
1623                                         } catch (Exception exception) {
1624                                             // Do nothing if an error is thrown.
1625                                         }
1626                                     };
1627
1628                                     // Manually delete the DOM storage files after 200 milliseconds.
1629                                     deleteDomStorageHandler.postDelayed(deleteDomStorageRunnable, 200);
1630                                 }
1631                             }
1632                         })
1633                         .show();
1634                 return true;
1635
1636             // Form data can be remove once the minimum API >= 26.
1637             case R.id.clear_form_data:
1638                 Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_deleted, Snackbar.LENGTH_LONG)
1639                         .setAction(R.string.undo, v -> {
1640                             // Do nothing because everything will be handled by `onDismissed()` below.
1641                         })
1642                         .addCallback(new Snackbar.Callback() {
1643                             @SuppressLint("SwitchIntDef")  // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1644                             @Override
1645                             public void onDismissed(Snackbar snackbar, int event) {
1646                                 if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) {  // The snackbar was dismissed without the undo button being pushed.
1647                                     // Delete the form data.
1648                                     WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(getApplicationContext());
1649                                     mainWebViewDatabase.clearFormData();
1650                                 }
1651                             }
1652                         })
1653                         .show();
1654                 return true;
1655
1656             case R.id.easylist:
1657                 // Toggle the EasyList status.
1658                 currentWebView.enableBlocklist(NestedScrollWebView.EASY_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST));
1659
1660                 // Update the menu checkbox.
1661                 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST));
1662
1663                 // Reload the current WebView.
1664                 currentWebView.reload();
1665                 return true;
1666
1667             case R.id.easyprivacy:
1668                 // Toggle the EasyPrivacy status.
1669                 currentWebView.enableBlocklist(NestedScrollWebView.EASY_PRIVACY, !currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY));
1670
1671                 // Update the menu checkbox.
1672                 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY));
1673
1674                 // Reload the current WebView.
1675                 currentWebView.reload();
1676                 return true;
1677
1678             case R.id.fanboys_annoyance_list:
1679                 // Toggle Fanboy's Annoyance List status.
1680                 currentWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1681
1682                 // Update the menu checkbox.
1683                 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1684
1685                 // Update the staus of Fanboy's Social Blocking List.
1686                 MenuItem fanboysSocialBlockingListMenuItem = optionsMenu.findItem(R.id.fanboys_social_blocking_list);
1687                 fanboysSocialBlockingListMenuItem.setEnabled(!currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1688
1689                 // Reload the current WebView.
1690                 currentWebView.reload();
1691                 return true;
1692
1693             case R.id.fanboys_social_blocking_list:
1694                 // Toggle Fanboy's Social Blocking List status.
1695                 currentWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
1696
1697                 // Update the menu checkbox.
1698                 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
1699
1700                 // Reload the current WebView.
1701                 currentWebView.reload();
1702                 return true;
1703
1704             case R.id.ultraprivacy:
1705                 // Toggle the UltraPrivacy status.
1706                 currentWebView.enableBlocklist(NestedScrollWebView.ULTRA_PRIVACY, !currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY));
1707
1708                 // Update the menu checkbox.
1709                 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY));
1710
1711                 // Reload the current WebView.
1712                 currentWebView.reload();
1713                 return true;
1714
1715             case R.id.block_all_third_party_requests:
1716                 //Toggle the third-party requests blocker status.
1717                 currentWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS, !currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
1718
1719                 // Update the menu checkbox.
1720                 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
1721
1722                 // Reload the current WebView.
1723                 currentWebView.reload();
1724                 return true;
1725
1726             case R.id.user_agent_privacy_browser:
1727                 // Update the user agent.
1728                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[0]);
1729
1730                 // Reload the current WebView.
1731                 currentWebView.reload();
1732                 return true;
1733
1734             case R.id.user_agent_webview_default:
1735                 // Update the user agent.
1736                 currentWebView.getSettings().setUserAgentString("");
1737
1738                 // Reload the current WebView.
1739                 currentWebView.reload();
1740                 return true;
1741
1742             case R.id.user_agent_firefox_on_android:
1743                 // Update the user agent.
1744                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[2]);
1745
1746                 // Reload the current WebView.
1747                 currentWebView.reload();
1748                 return true;
1749
1750             case R.id.user_agent_chrome_on_android:
1751                 // Update the user agent.
1752                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[3]);
1753
1754                 // Reload the current WebView.
1755                 currentWebView.reload();
1756                 return true;
1757
1758             case R.id.user_agent_safari_on_ios:
1759                 // Update the user agent.
1760                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[4]);
1761
1762                 // Reload the current WebView.
1763                 currentWebView.reload();
1764                 return true;
1765
1766             case R.id.user_agent_firefox_on_linux:
1767                 // Update the user agent.
1768                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[5]);
1769
1770                 // Reload the current WebView.
1771                 currentWebView.reload();
1772                 return true;
1773
1774             case R.id.user_agent_chromium_on_linux:
1775                 // Update the user agent.
1776                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[6]);
1777
1778                 // Reload the current WebView.
1779                 currentWebView.reload();
1780                 return true;
1781
1782             case R.id.user_agent_firefox_on_windows:
1783                 // Update the user agent.
1784                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[7]);
1785
1786                 // Reload the current WebView.
1787                 currentWebView.reload();
1788                 return true;
1789
1790             case R.id.user_agent_chrome_on_windows:
1791                 // Update the user agent.
1792                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[8]);
1793
1794                 // Reload the current WebView.
1795                 currentWebView.reload();
1796                 return true;
1797
1798             case R.id.user_agent_edge_on_windows:
1799                 // Update the user agent.
1800                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[9]);
1801
1802                 // Reload the current WebView.
1803                 currentWebView.reload();
1804                 return true;
1805
1806             case R.id.user_agent_internet_explorer_on_windows:
1807                 // Update the user agent.
1808                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[10]);
1809
1810                 // Reload the current WebView.
1811                 currentWebView.reload();
1812                 return true;
1813
1814             case R.id.user_agent_safari_on_macos:
1815                 // Update the user agent.
1816                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[11]);
1817
1818                 // Reload the current WebView.
1819                 currentWebView.reload();
1820                 return true;
1821
1822             case R.id.user_agent_custom:
1823                 // Update the user agent.
1824                 currentWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
1825
1826                 // Reload the current WebView.
1827                 currentWebView.reload();
1828                 return true;
1829
1830             case R.id.font_size_twenty_five_percent:
1831                 currentWebView.getSettings().setTextZoom(25);
1832                 return true;
1833
1834             case R.id.font_size_fifty_percent:
1835                 currentWebView.getSettings().setTextZoom(50);
1836                 return true;
1837
1838             case R.id.font_size_seventy_five_percent:
1839                 currentWebView.getSettings().setTextZoom(75);
1840                 return true;
1841
1842             case R.id.font_size_one_hundred_percent:
1843                 currentWebView.getSettings().setTextZoom(100);
1844                 return true;
1845
1846             case R.id.font_size_one_hundred_twenty_five_percent:
1847                 currentWebView.getSettings().setTextZoom(125);
1848                 return true;
1849
1850             case R.id.font_size_one_hundred_fifty_percent:
1851                 currentWebView.getSettings().setTextZoom(150);
1852                 return true;
1853
1854             case R.id.font_size_one_hundred_seventy_five_percent:
1855                 currentWebView.getSettings().setTextZoom(175);
1856                 return true;
1857
1858             case R.id.font_size_two_hundred_percent:
1859                 currentWebView.getSettings().setTextZoom(200);
1860                 return true;
1861
1862             case R.id.swipe_to_refresh:
1863                 // Toggle the stored status of swipe to refresh.
1864                 currentWebView.setSwipeToRefresh(!currentWebView.getSwipeToRefresh());
1865
1866                 // Get a handle for the swipe refresh layout.
1867                 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
1868
1869                 // Update the swipe refresh layout.
1870                 if (currentWebView.getSwipeToRefresh()) {  // Swipe to refresh is enabled.
1871                     if (Build.VERSION.SDK_INT >= 23) {  // For API >= 23, the status of the scroll refresh listener is continuously updated by the on scroll change listener.
1872                         // Only enable the swipe refresh layout if the WebView is scrolled to the top.
1873                         swipeRefreshLayout.setEnabled(currentWebView.getY() == 0);
1874                     } else {  // For API < 23, the swipe refresh layout is always enabled.
1875                         // Enable the swipe refresh layout.
1876                         swipeRefreshLayout.setEnabled(true);
1877                     }
1878                 } else {  // Swipe to refresh is disabled.
1879                     // Disable the swipe refresh layout.
1880                     swipeRefreshLayout.setEnabled(false);
1881                 }
1882                 return true;
1883
1884             case R.id.display_images:
1885                 if (currentWebView.getSettings().getLoadsImagesAutomatically()) {  // Images are currently loaded automatically.
1886                     // Disable loading of images.
1887                     currentWebView.getSettings().setLoadsImagesAutomatically(false);
1888
1889                     // Reload the website to remove existing images.
1890                     currentWebView.reload();
1891                 } else {  // Images are not currently loaded automatically.
1892                     // Enable loading of images.  Missing images will be loaded without the need for a reload.
1893                     currentWebView.getSettings().setLoadsImagesAutomatically(true);
1894                 }
1895                 return true;
1896
1897             case R.id.night_mode:
1898                 // Toggle night mode.
1899                 currentWebView.setNightMode(!currentWebView.getNightMode());
1900
1901                 // Enable or disable JavaScript according to night mode, the global preference, and any domain settings.
1902                 if (currentWebView.getNightMode()) {  // Night mode is enabled, which requires JavaScript.
1903                     // Enable JavaScript.
1904                     currentWebView.getSettings().setJavaScriptEnabled(true);
1905                 } else if (currentWebView.getDomainSettingsApplied()) {  // Night mode is disabled and domain settings are applied.  Set JavaScript according to the domain settings.
1906                     // Apply the JavaScript preference that was stored the last time domain settings were loaded.
1907                     currentWebView.getSettings().setJavaScriptEnabled(currentWebView.getDomainSettingsJavaScriptEnabled());
1908                 } else {  // Night mode is disabled and domain settings are not applied.  Set JavaScript according to the global preference.
1909                     // Apply the JavaScript preference.
1910                     currentWebView.getSettings().setJavaScriptEnabled(sharedPreferences.getBoolean("javascript", false));
1911                 }
1912
1913                 // Update the privacy icons.
1914                 updatePrivacyIcons(false);
1915
1916                 // Reload the website.
1917                 currentWebView.reload();
1918                 return true;
1919
1920             case R.id.find_on_page:
1921                 // Get a handle for the views.
1922                 Toolbar toolbar = findViewById(R.id.toolbar);
1923                 LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
1924                 EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
1925
1926                 // Hide the toolbar.
1927                 toolbar.setVisibility(View.GONE);
1928
1929                 // Show the find on page linear layout.
1930                 findOnPageLinearLayout.setVisibility(View.VISIBLE);
1931
1932                 // Display the keyboard.  The app must wait 200 ms before running the command to work around a bug in Android.
1933                 // http://stackoverflow.com/questions/5520085/android-show-softkeyboard-with-showsoftinput-is-not-working
1934                 findOnPageEditText.postDelayed(() -> {
1935                     // Set the focus on `findOnPageEditText`.
1936                     findOnPageEditText.requestFocus();
1937
1938                     // Get a handle for the input method manager.
1939                     InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
1940
1941                     // Remove the lint warning below that the input method manager might be null.
1942                     assert inputMethodManager != null;
1943
1944                     // Display the keyboard.  `0` sets no input flags.
1945                     inputMethodManager.showSoftInput(findOnPageEditText, 0);
1946                 }, 200);
1947                 return true;
1948
1949             case R.id.view_source:
1950                 // Create an intent to launch the view source activity.
1951                 Intent viewSourceIntent = new Intent(this, ViewSourceActivity.class);
1952
1953                 // Add the variables to the intent.
1954                 viewSourceIntent.putExtra("user_agent", currentWebView.getSettings().getUserAgentString());
1955                 viewSourceIntent.putExtra("current_url", currentWebView.getUrl());
1956
1957                 // Make it so.
1958                 startActivity(viewSourceIntent);
1959                 return true;
1960
1961             case R.id.share_url:
1962                 // Setup the share string.
1963                 String shareString = currentWebView.getTitle() + " – " + currentWebView.getUrl();
1964
1965                 // Create the share intent.
1966                 Intent shareIntent = new Intent(Intent.ACTION_SEND);
1967                 shareIntent.putExtra(Intent.EXTRA_TEXT, shareString);
1968                 shareIntent.setType("text/plain");
1969
1970                 // Make it so.
1971                 startActivity(Intent.createChooser(shareIntent, getString(R.string.share_url)));
1972                 return true;
1973
1974             case R.id.print:
1975                 // Get a print manager instance.
1976                 PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);
1977
1978                 // Remove the lint error below that print manager might be null.
1979                 assert printManager != null;
1980
1981                 // Create a print document adapter from the current WebView.
1982                 PrintDocumentAdapter printDocumentAdapter = currentWebView.createPrintDocumentAdapter();
1983
1984                 // Print the document.
1985                 printManager.print(getString(R.string.privacy_browser_web_page), printDocumentAdapter, null);
1986                 return true;
1987
1988             case R.id.open_with_app:
1989                 openWithApp(currentWebView.getUrl());
1990                 return true;
1991
1992             case R.id.open_with_browser:
1993                 openWithBrowser(currentWebView.getUrl());
1994                 return true;
1995
1996             case R.id.add_to_homescreen:
1997                 // Instantiate the create home screen shortcut dialog.
1998                 DialogFragment createHomeScreenShortcutDialogFragment = CreateHomeScreenShortcutDialog.createDialog(currentWebView.getTitle(), currentWebView.getUrl(),
1999                         currentWebView.getFavoriteOrDefaultIcon());
2000
2001                 // Show the create home screen shortcut dialog.
2002                 createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getString(R.string.create_shortcut));
2003                 return true;
2004
2005             case R.id.proxy_through_orbot:
2006                 // Toggle the proxy through Orbot variable.
2007                 proxyThroughOrbot = !proxyThroughOrbot;
2008
2009                 // Apply the proxy through Orbot settings.
2010                 applyProxyThroughOrbot(true);
2011                 return true;
2012
2013             case R.id.refresh:
2014                 if (menuItem.getTitle().equals(getString(R.string.refresh))) {  // The refresh button was pushed.
2015                     // Reload the current WebView.
2016                     currentWebView.reload();
2017                 } else {  // The stop button was pushed.
2018                     // Stop the loading of the WebView.
2019                     currentWebView.stopLoading();
2020                 }
2021                 return true;
2022
2023             case R.id.ad_consent:
2024                 // Display the ad consent dialog.
2025                 DialogFragment adConsentDialogFragment = new AdConsentDialog();
2026                 adConsentDialogFragment.show(getSupportFragmentManager(), getString(R.string.ad_consent));
2027                 return true;
2028
2029             default:
2030                 // Don't consume the event.
2031                 return super.onOptionsItemSelected(menuItem);
2032         }
2033     }
2034
2035     // removeAllCookies is deprecated, but it is required for API < 21.
2036     @Override
2037     public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
2038         // Get the menu item ID.
2039         int menuItemId = menuItem.getItemId();
2040
2041         // Get a handle for the shared preferences.
2042         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
2043
2044         // Run the commands that correspond to the selected menu item.
2045         switch (menuItemId) {
2046             case R.id.close_tab:
2047                 // Get a handle for the tab layout and the view pager.
2048                 TabLayout tabLayout = findViewById(R.id.tablayout);
2049                 ViewPager webViewPager = findViewById(R.id.webviewpager);
2050
2051                 // Get the current tab number.
2052                 int currentTabNumber = tabLayout.getSelectedTabPosition();
2053
2054                 // Delete the current tab.
2055                 tabLayout.removeTabAt(currentTabNumber);
2056
2057                 // 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.
2058                 if (webViewPagerAdapter.deletePage(currentTabNumber, webViewPager)) {
2059                     setCurrentWebView(currentTabNumber);
2060                 }
2061                 break;
2062
2063             case R.id.clear_and_exit:
2064                 // Close the bookmarks cursor and database.
2065                 bookmarksCursor.close();
2066                 bookmarksDatabaseHelper.close();
2067
2068                 // Get the status of the clear everything preference.
2069                 boolean clearEverything = sharedPreferences.getBoolean("clear_everything", true);
2070
2071                 // Get a handle for the runtime.
2072                 Runtime runtime = Runtime.getRuntime();
2073
2074                 // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
2075                 // which links to `/data/data/com.stoutner.privacybrowser.standard`.
2076                 String privateDataDirectoryString = getApplicationInfo().dataDir;
2077
2078                 // Clear cookies.
2079                 if (clearEverything || sharedPreferences.getBoolean("clear_cookies", true)) {
2080                     // The command to remove cookies changed slightly in API 21.
2081                     if (Build.VERSION.SDK_INT >= 21) {
2082                         CookieManager.getInstance().removeAllCookies(null);
2083                     } else {
2084                         CookieManager.getInstance().removeAllCookie();
2085                     }
2086
2087                     // Manually delete the cookies database, as `CookieManager` sometimes will not flush its changes to disk before `System.exit(0)` is run.
2088                     try {
2089                         // Two commands must be used because `Runtime.exec()` does not like `*`.
2090                         Process deleteCookiesProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies");
2091                         Process deleteCookiesJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies-journal");
2092
2093                         // Wait until the processes have finished.
2094                         deleteCookiesProcess.waitFor();
2095                         deleteCookiesJournalProcess.waitFor();
2096                     } catch (Exception exception) {
2097                         // Do nothing if an error is thrown.
2098                     }
2099                 }
2100
2101                 // Clear DOM storage.
2102                 if (clearEverything || sharedPreferences.getBoolean("clear_dom_storage", true)) {
2103                     // Ask `WebStorage` to clear the DOM storage.
2104                     WebStorage webStorage = WebStorage.getInstance();
2105                     webStorage.deleteAllData();
2106
2107                     // Manually delete the DOM storage files and directories, as `WebStorage` sometimes will not flush its changes to disk before `System.exit(0)` is run.
2108                     try {
2109                         // A `String[]` must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
2110                         Process deleteLocalStorageProcess = runtime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
2111
2112                         // Multiple commands must be used because `Runtime.exec()` does not like `*`.
2113                         Process deleteIndexProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
2114                         Process deleteQuotaManagerProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
2115                         Process deleteQuotaManagerJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
2116                         Process deleteDatabaseProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
2117
2118                         // Wait until the processes have finished.
2119                         deleteLocalStorageProcess.waitFor();
2120                         deleteIndexProcess.waitFor();
2121                         deleteQuotaManagerProcess.waitFor();
2122                         deleteQuotaManagerJournalProcess.waitFor();
2123                         deleteDatabaseProcess.waitFor();
2124                     } catch (Exception exception) {
2125                         // Do nothing if an error is thrown.
2126                     }
2127                 }
2128
2129                 // Clear form data if the API < 26.
2130                 if ((Build.VERSION.SDK_INT < 26) && (clearEverything || sharedPreferences.getBoolean("clear_form_data", true))) {
2131                     WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
2132                     webViewDatabase.clearFormData();
2133
2134                     // Manually delete the form data database, as `WebViewDatabase` sometimes will not flush its changes to disk before `System.exit(0)` is run.
2135                     try {
2136                         // A string array must be used because the database contains a space and `Runtime.exec` will not otherwise escape the string correctly.
2137                         Process deleteWebDataProcess = runtime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data"});
2138                         Process deleteWebDataJournalProcess = runtime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data-journal"});
2139
2140                         // Wait until the processes have finished.
2141                         deleteWebDataProcess.waitFor();
2142                         deleteWebDataJournalProcess.waitFor();
2143                     } catch (Exception exception) {
2144                         // Do nothing if an error is thrown.
2145                     }
2146                 }
2147
2148                 // Clear the cache.
2149                 if (clearEverything || sharedPreferences.getBoolean("clear_cache", true)) {
2150                     // Clear the cache from each WebView.
2151                     for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
2152                         // Get the WebView tab fragment.
2153                         WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
2154
2155                         // Get the fragment view.
2156                         View fragmentView = webViewTabFragment.getView();
2157
2158                         // Only clear the cache if the WebView exists.
2159                         if (fragmentView != null) {
2160                             // Get the nested scroll WebView from the tab fragment.
2161                             NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
2162
2163                             // Clear the cache for this WebView.
2164                             nestedScrollWebView.clearCache(true);
2165                         }
2166                     }
2167
2168                     // Manually delete the cache directories.
2169                     try {
2170                         // Delete the main cache directory.
2171                         Process deleteCacheProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/cache");
2172
2173                         // Delete the secondary `Service Worker` cache directory.
2174                         // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
2175                         Process deleteServiceWorkerProcess = runtime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
2176
2177                         // Wait until the processes have finished.
2178                         deleteCacheProcess.waitFor();
2179                         deleteServiceWorkerProcess.waitFor();
2180                     } catch (Exception exception) {
2181                         // Do nothing if an error is thrown.
2182                     }
2183                 }
2184
2185                 // Wipe out each WebView.
2186                 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
2187                     // Get the WebView tab fragment.
2188                     WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
2189
2190                     // Get the fragment view.
2191                     View fragmentView = webViewTabFragment.getView();
2192
2193                     // Only wipe out the WebView if it exists.
2194                     if (fragmentView != null) {
2195                         // Get the nested scroll WebView from the tab fragment.
2196                         NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
2197
2198                         // Clear SSL certificate preferences for this WebView.
2199                         nestedScrollWebView.clearSslPreferences();
2200
2201                         // Clear the back/forward history for this WebView.
2202                         nestedScrollWebView.clearHistory();
2203
2204                         // Destroy the internal state of `mainWebView`.
2205                         nestedScrollWebView.destroy();
2206                     }
2207                 }
2208
2209                 // Clear the custom headers.
2210                 customHeaders.clear();
2211
2212                 // Manually delete the `app_webview` folder, which contains the cookies, DOM storage, form data, and `Service Worker` cache.
2213                 // See `https://code.google.com/p/android/issues/detail?id=233826&thanks=233826&ts=1486670530`.
2214                 if (clearEverything) {
2215                     try {
2216                         // Delete the folder.
2217                         Process deleteAppWebviewProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview");
2218
2219                         // Wait until the process has finished.
2220                         deleteAppWebviewProcess.waitFor();
2221                     } catch (Exception exception) {
2222                         // Do nothing if an error is thrown.
2223                     }
2224                 }
2225
2226                 // Close Privacy Browser.  `finishAndRemoveTask` also removes Privacy Browser from the recent app list.
2227                 if (Build.VERSION.SDK_INT >= 21) {
2228                     finishAndRemoveTask();
2229                 } else {
2230                     finish();
2231                 }
2232
2233                 // Remove the terminated program from RAM.  The status code is `0`.
2234                 System.exit(0);
2235                 break;
2236
2237             case R.id.home:
2238                 // Select the homepage based on the proxy through Orbot status.
2239                 if (proxyThroughOrbot) {
2240                     // Load the Tor homepage.
2241                     loadUrl(sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value)));
2242                 } else {
2243                     // Load the normal homepage.
2244                     loadUrl(sharedPreferences.getString("homepage", getString(R.string.homepage_default_value)));
2245                 }
2246                 break;
2247
2248             case R.id.back:
2249                 if (currentWebView.canGoBack()) {
2250                     // Reset the current domain name so that navigation works if third-party requests are blocked.
2251                     currentWebView.resetCurrentDomainName();
2252
2253                     // Set navigating history so that the domain settings are applied when the new URL is loaded.
2254                     currentWebView.setNavigatingHistory(true);
2255
2256                     // Load the previous website in the history.
2257                     currentWebView.goBack();
2258                 }
2259                 break;
2260
2261             case R.id.forward:
2262                 if (currentWebView.canGoForward()) {
2263                     // Reset the current domain name so that navigation works if third-party requests are blocked.
2264                     currentWebView.resetCurrentDomainName();
2265
2266                     // Set navigating history so that the domain settings are applied when the new URL is loaded.
2267                     currentWebView.setNavigatingHistory(true);
2268
2269                     // Load the next website in the history.
2270                     currentWebView.goForward();
2271                 }
2272                 break;
2273
2274             case R.id.history:
2275                 // Get the `WebBackForwardList`.
2276                 WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList();
2277
2278                 // Show the URL history dialog and name this instance `R.string.history`.
2279                 DialogFragment urlHistoryDialogFragment = UrlHistoryDialog.loadBackForwardList(this, webBackForwardList);
2280                 urlHistoryDialogFragment.show(getSupportFragmentManager(), getString(R.string.history));
2281                 break;
2282
2283             case R.id.requests:
2284                 // Populate the resource requests.
2285                 RequestsActivity.resourceRequests = currentWebView.getResourceRequests();
2286
2287                 // Create an intent to launch the Requests activity.
2288                 Intent requestsIntent = new Intent(this, RequestsActivity.class);
2289
2290                 // Add the block third-party requests status to the intent.
2291                 requestsIntent.putExtra("block_all_third_party_requests", currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
2292
2293                 // Make it so.
2294                 startActivity(requestsIntent);
2295                 break;
2296
2297             case R.id.downloads:
2298                 // Launch the system Download Manager.
2299                 Intent downloadManagerIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
2300
2301                 // Launch as a new task so that Download Manager and Privacy Browser show as separate windows in the recent tasks list.
2302                 downloadManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2303
2304                 startActivity(downloadManagerIntent);
2305                 break;
2306
2307             case R.id.domains:
2308                 // Set the flag to reapply the domain settings on restart when returning from Domain Settings.
2309                 reapplyDomainSettingsOnRestart = true;
2310
2311                 // Launch the domains activity.
2312                 Intent domainsIntent = new Intent(this, DomainsActivity.class);
2313
2314                 // Add the extra information to the intent.
2315                 domainsIntent.putExtra("current_url", currentWebView.getUrl());
2316
2317                 // Get the current certificate.
2318                 SslCertificate sslCertificate = currentWebView.getCertificate();
2319
2320                 // Check to see if the SSL certificate is populated.
2321                 if (sslCertificate != null) {
2322                     // Extract the certificate to strings.
2323                     String issuedToCName = sslCertificate.getIssuedTo().getCName();
2324                     String issuedToOName = sslCertificate.getIssuedTo().getOName();
2325                     String issuedToUName = sslCertificate.getIssuedTo().getUName();
2326                     String issuedByCName = sslCertificate.getIssuedBy().getCName();
2327                     String issuedByOName = sslCertificate.getIssuedBy().getOName();
2328                     String issuedByUName = sslCertificate.getIssuedBy().getUName();
2329                     long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
2330                     long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
2331
2332                     // Add the certificate to the intent.
2333                     domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
2334                     domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
2335                     domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
2336                     domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
2337                     domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
2338                     domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
2339                     domainsIntent.putExtra("ssl_start_date", startDateLong);
2340                     domainsIntent.putExtra("ssl_end_date", endDateLong);
2341                 }
2342
2343                 // Check to see if the current IP addresses have been received.
2344                 if (currentWebView.hasCurrentIpAddresses()) {
2345                     // Add the current IP addresses to the intent.
2346                     domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
2347                 }
2348
2349                 // Make it so.
2350                 startActivity(domainsIntent);
2351                 break;
2352
2353             case R.id.settings:
2354                 // Set the flag to reapply app settings on restart when returning from Settings.
2355                 reapplyAppSettingsOnRestart = true;
2356
2357                 // Set the flag to reapply the domain settings on restart when returning from Settings.
2358                 reapplyDomainSettingsOnRestart = true;
2359
2360                 // Launch the settings activity.
2361                 Intent settingsIntent = new Intent(this, SettingsActivity.class);
2362                 startActivity(settingsIntent);
2363                 break;
2364
2365             case R.id.import_export:
2366                 // Launch the import/export activity.
2367                 Intent importExportIntent = new Intent (this, ImportExportActivity.class);
2368                 startActivity(importExportIntent);
2369                 break;
2370
2371             case R.id.logcat:
2372                 // Launch the logcat activity.
2373                 Intent logcatIntent = new Intent(this, LogcatActivity.class);
2374                 startActivity(logcatIntent);
2375                 break;
2376
2377             case R.id.guide:
2378                 // Launch `GuideActivity`.
2379                 Intent guideIntent = new Intent(this, GuideActivity.class);
2380                 startActivity(guideIntent);
2381                 break;
2382
2383             case R.id.about:
2384                 // Create an intent to launch the about activity.
2385                 Intent aboutIntent = new Intent(this, AboutActivity.class);
2386
2387                 // Create a string array for the blocklist versions.
2388                 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],
2389                         ultraPrivacy.get(0).get(0)[0]};
2390
2391                 // Add the blocklist versions to the intent.
2392                 aboutIntent.putExtra("blocklist_versions", blocklistVersions);
2393
2394                 // Make it so.
2395                 startActivity(aboutIntent);
2396                 break;
2397         }
2398
2399         // Get a handle for the drawer layout.
2400         DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
2401
2402         // Close the navigation drawer.
2403         drawerLayout.closeDrawer(GravityCompat.START);
2404         return true;
2405     }
2406
2407     @Override
2408     public void onPostCreate(Bundle savedInstanceState) {
2409         // Run the default commands.
2410         super.onPostCreate(savedInstanceState);
2411
2412         // Sync the state of the DrawerToggle after the default `onRestoreInstanceState()` has finished.  This creates the navigation drawer icon.
2413         actionBarDrawerToggle.syncState();
2414     }
2415
2416     @Override
2417     public void onConfigurationChanged(Configuration newConfig) {
2418         // Run the default commands.
2419         super.onConfigurationChanged(newConfig);
2420
2421         // Get the status bar pixel size.
2422         int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
2423         int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId);
2424
2425         // Get the resource density.
2426         float screenDensity = getResources().getDisplayMetrics().density;
2427
2428         // Recalculate the drawer header padding.
2429         drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity);
2430         drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity);
2431         drawerHeaderPaddingBottom = (int) (8 * screenDensity);
2432
2433         // Reload the ad for the free flavor if not in full screen mode.
2434         if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
2435             // Reload the ad.  The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
2436             AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
2437         }
2438
2439         // `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:
2440         // https://code.google.com/p/android/issues/detail?id=20493#c8
2441         // ActivityCompat.invalidateOptionsMenu(this);
2442     }
2443
2444     @Override
2445     public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
2446         // Store the hit test result.
2447         final WebView.HitTestResult hitTestResult = currentWebView.getHitTestResult();
2448
2449         // Create the URL strings.
2450         final String imageUrl;
2451         final String linkUrl;
2452
2453         // Get handles for the system managers.
2454         final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
2455         FragmentManager fragmentManager = getSupportFragmentManager();
2456         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
2457
2458         // Remove the lint errors below that the clipboard manager might be null.
2459         assert clipboardManager != null;
2460
2461         // Process the link according to the type.
2462         switch (hitTestResult.getType()) {
2463             // `SRC_ANCHOR_TYPE` is a link.
2464             case WebView.HitTestResult.SRC_ANCHOR_TYPE:
2465                 // Get the target URL.
2466                 linkUrl = hitTestResult.getExtra();
2467
2468                 // Set the target URL as the title of the `ContextMenu`.
2469                 menu.setHeaderTitle(linkUrl);
2470
2471                 // Add a Load URL entry.
2472                 menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
2473                     // Add a new tab.
2474                     addTab(null);
2475
2476                     // Load the URL.
2477                     loadUrl(linkUrl);
2478                     return false;
2479                 });
2480
2481                 // Add an Open with App entry.
2482                 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2483                     openWithApp(linkUrl);
2484                     return false;
2485                 });
2486
2487                 // Add an Open with Browser entry.
2488                 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2489                     openWithBrowser(linkUrl);
2490                     return false;
2491                 });
2492
2493                 // Add a Copy URL entry.
2494                 menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
2495                     // Save the link URL in a `ClipData`.
2496                     ClipData srcAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl);
2497
2498                     // Set the `ClipData` as the clipboard's primary clip.
2499                     clipboardManager.setPrimaryClip(srcAnchorTypeClipData);
2500                     return false;
2501                 });
2502
2503                 // Add a Download URL entry.
2504                 menu.add(R.string.download_url).setOnMenuItemClickListener((MenuItem item) -> {
2505                     // Check if the download should be processed by an external app.
2506                     if (sharedPreferences.getBoolean("download_with_external_app", false)) {  // Download with an external app.
2507                         openUrlWithExternalApp(linkUrl);
2508                     } else {  // Download with Android's download manager.
2509                         // Check to see if the storage permission has already been granted.
2510                         if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {  // The storage permission needs to be requested.
2511                             // Store the variables for future use by `onRequestPermissionsResult()`.
2512                             downloadUrl = linkUrl;
2513                             downloadContentDisposition = "none";
2514                             downloadContentLength = -1;
2515
2516                             // Show a dialog if the user has previously denied the permission.
2517                             if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
2518                                 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
2519                                 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE);
2520
2521                                 // Show the download location permission alert dialog.  The permission will be requested when the the dialog is closed.
2522                                 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
2523                             } else {  // Show the permission request directly.
2524                                 // Request the permission.  The download dialog will be launched by `onRequestPermissionResult()`.
2525                                 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
2526                             }
2527                         } else {  // The storage permission has already been granted.
2528                             // Get a handle for the download file alert dialog.
2529                             DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(linkUrl, "none", -1);
2530
2531                             // Show the download file alert dialog.
2532                             downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
2533                         }
2534                     }
2535                     return false;
2536                 });
2537
2538                 // Add a Cancel entry, which by default closes the context menu.
2539                 menu.add(R.string.cancel);
2540                 break;
2541
2542             case WebView.HitTestResult.EMAIL_TYPE:
2543                 // Get the target URL.
2544                 linkUrl = hitTestResult.getExtra();
2545
2546                 // Set the target URL as the title of the `ContextMenu`.
2547                 menu.setHeaderTitle(linkUrl);
2548
2549                 // Add a Write Email entry.
2550                 menu.add(R.string.write_email).setOnMenuItemClickListener(item -> {
2551                     // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
2552                     Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
2553
2554                     // Parse the url and set it as the data for the `Intent`.
2555                     emailIntent.setData(Uri.parse("mailto:" + linkUrl));
2556
2557                     // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser.
2558                     emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2559
2560                     // Make it so.
2561                     startActivity(emailIntent);
2562                     return false;
2563                 });
2564
2565                 // Add a Copy Email Address entry.
2566                 menu.add(R.string.copy_email_address).setOnMenuItemClickListener(item -> {
2567                     // Save the email address in a `ClipData`.
2568                     ClipData srcEmailTypeClipData = ClipData.newPlainText(getString(R.string.email_address), linkUrl);
2569
2570                     // Set the `ClipData` as the clipboard's primary clip.
2571                     clipboardManager.setPrimaryClip(srcEmailTypeClipData);
2572                     return false;
2573                 });
2574
2575                 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
2576                 menu.add(R.string.cancel);
2577                 break;
2578
2579             // `IMAGE_TYPE` is an image.
2580             case WebView.HitTestResult.IMAGE_TYPE:
2581                 // Get the image URL.
2582                 imageUrl = hitTestResult.getExtra();
2583
2584                 // Set the image URL as the title of the `ContextMenu`.
2585                 menu.setHeaderTitle(imageUrl);
2586
2587                 // Add a View Image entry.
2588                 menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
2589                     loadUrl(imageUrl);
2590                     return false;
2591                 });
2592
2593                 // Add a Download Image entry.
2594                 menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
2595                     // Check if the download should be processed by an external app.
2596                     if (sharedPreferences.getBoolean("download_with_external_app", false)) {  // Download with an external app.
2597                         openUrlWithExternalApp(imageUrl);
2598                     } else {  // Download with Android's download manager.
2599                         // Check to see if the storage permission has already been granted.
2600                         if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {  // The storage permission needs to be requested.
2601                             // Store the image URL for use by `onRequestPermissionResult()`.
2602                             downloadImageUrl = imageUrl;
2603
2604                             // Show a dialog if the user has previously denied the permission.
2605                             if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
2606                                 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
2607                                 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
2608
2609                                 // Show the download location permission alert dialog.  The permission will be requested when the dialog is closed.
2610                                 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
2611                             } else {  // Show the permission request directly.
2612                                 // Request the permission.  The download dialog will be launched by `onRequestPermissionResult().
2613                                 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
2614                             }
2615                         } else {  // The storage permission has already been granted.
2616                             // Get a handle for the download image alert dialog.
2617                             DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
2618
2619                             // Show the download image alert dialog.
2620                             downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
2621                         }
2622                     }
2623                     return false;
2624                 });
2625
2626                 // Add a Copy URL entry.
2627                 menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> {
2628                     // Save the image URL in a `ClipData`.
2629                     ClipData srcImageTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
2630
2631                     // Set the `ClipData` as the clipboard's primary clip.
2632                     clipboardManager.setPrimaryClip(srcImageTypeClipData);
2633                     return false;
2634                 });
2635
2636                 // Add an Open with App entry.
2637                 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2638                     openWithApp(imageUrl);
2639                     return false;
2640                 });
2641
2642                 // Add an Open with Browser entry.
2643                 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2644                     openWithBrowser(imageUrl);
2645                     return false;
2646                 });
2647
2648                 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
2649                 menu.add(R.string.cancel);
2650                 break;
2651
2652
2653             // `SRC_IMAGE_ANCHOR_TYPE` is an image that is also a link.
2654             case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
2655                 // Get the image URL.
2656                 imageUrl = hitTestResult.getExtra();
2657
2658                 // Set the image URL as the title of the `ContextMenu`.
2659                 menu.setHeaderTitle(imageUrl);
2660
2661                 // Add a `View Image` entry.
2662                 menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
2663                     loadUrl(imageUrl);
2664                     return false;
2665                 });
2666
2667                 // Add a `Download Image` entry.
2668                 menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
2669                     // Check if the download should be processed by an external app.
2670                     if (sharedPreferences.getBoolean("download_with_external_app", false)) {  // Download with an external app.
2671                         openUrlWithExternalApp(imageUrl);
2672                     } else {  // Download with Android's download manager.
2673                         // Check to see if the storage permission has already been granted.
2674                         if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {  // The storage permission needs to be requested.
2675                             // Store the image URL for use by `onRequestPermissionResult()`.
2676                             downloadImageUrl = imageUrl;
2677
2678                             // Show a dialog if the user has previously denied the permission.
2679                             if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
2680                                 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
2681                                 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
2682
2683                                 // Show the download location permission alert dialog.  The permission will be requested when the dialog is closed.
2684                                 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
2685                             } else {  // Show the permission request directly.
2686                                 // Request the permission.  The download dialog will be launched by `onRequestPermissionResult().
2687                                 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
2688                             }
2689                         } else {  // The storage permission has already been granted.
2690                             // Get a handle for the download image alert dialog.
2691                             DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
2692
2693                             // Show the download image alert dialog.
2694                             downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
2695                         }
2696                     }
2697                     return false;
2698                 });
2699
2700                 // Add a `Copy URL` entry.
2701                 menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> {
2702                     // Save the image URL in a `ClipData`.
2703                     ClipData srcImageAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
2704
2705                     // Set the `ClipData` as the clipboard's primary clip.
2706                     clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData);
2707                     return false;
2708                 });
2709
2710                 // Add an Open with App entry.
2711                 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2712                     openWithApp(imageUrl);
2713                     return false;
2714                 });
2715
2716                 // Add an Open with Browser entry.
2717                 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2718                     openWithBrowser(imageUrl);
2719                     return false;
2720                 });
2721
2722                 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
2723                 menu.add(R.string.cancel);
2724                 break;
2725         }
2726     }
2727
2728     @Override
2729     public void onCreateBookmark(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) {
2730         // Get a handle for the bookmarks list view.
2731         ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
2732
2733         // Get the views from the dialog fragment.
2734         EditText createBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_name_edittext);
2735         EditText createBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_url_edittext);
2736
2737         // Extract the strings from the edit texts.
2738         String bookmarkNameString = createBookmarkNameEditText.getText().toString();
2739         String bookmarkUrlString = createBookmarkUrlEditText.getText().toString();
2740
2741         // Create a favorite icon byte array output stream.
2742         ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
2743
2744         // Convert the favorite icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2745         favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
2746
2747         // Convert the favorite icon byte array stream to a byte array.
2748         byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
2749
2750         // Display the new bookmark below the current items in the (0 indexed) list.
2751         int newBookmarkDisplayOrder = bookmarksListView.getCount();
2752
2753         // Create the bookmark.
2754         bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolder, newBookmarkDisplayOrder, favoriteIconByteArray);
2755
2756         // Update the bookmarks cursor with the current contents of this folder.
2757         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2758
2759         // Update the list view.
2760         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2761
2762         // Scroll to the new bookmark.
2763         bookmarksListView.setSelection(newBookmarkDisplayOrder);
2764     }
2765
2766     @Override
2767     public void onCreateBookmarkFolder(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) {
2768         // Get a handle for the bookmarks list view.
2769         ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
2770
2771         // Get handles for the views in the dialog fragment.
2772         EditText createFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.create_folder_name_edittext);
2773         RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon_radiobutton);
2774         ImageView folderIconImageView = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon);
2775
2776         // Get new folder name string.
2777         String folderNameString = createFolderNameEditText.getText().toString();
2778
2779         // Create a folder icon bitmap.
2780         Bitmap folderIconBitmap;
2781
2782         // Set the folder icon bitmap according to the dialog.
2783         if (defaultFolderIconRadioButton.isChecked()) {  // Use the default folder icon.
2784             // Get the default folder icon drawable.
2785             Drawable folderIconDrawable = folderIconImageView.getDrawable();
2786
2787             // Convert the folder icon drawable to a bitmap drawable.
2788             BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2789
2790             // Convert the folder icon bitmap drawable to a bitmap.
2791             folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2792         } else {  // Use the WebView favorite icon.
2793             // Copy the favorite icon bitmap to the folder icon bitmap.
2794             folderIconBitmap = favoriteIconBitmap;
2795         }
2796
2797         // Create a folder icon byte array output stream.
2798         ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream();
2799
2800         // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2801         folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream);
2802
2803         // Convert the folder icon byte array stream to a byte array.
2804         byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray();
2805
2806         // Move all the bookmarks down one in the display order.
2807         for (int i = 0; i < bookmarksListView.getCount(); i++) {
2808             int databaseId = (int) bookmarksListView.getItemIdAtPosition(i);
2809             bookmarksDatabaseHelper.updateDisplayOrder(databaseId, i + 1);
2810         }
2811
2812         // Create the folder, which will be placed at the top of the `ListView`.
2813         bookmarksDatabaseHelper.createFolder(folderNameString, currentBookmarksFolder, folderIconByteArray);
2814
2815         // Update the bookmarks cursor with the current contents of this folder.
2816         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2817
2818         // Update the `ListView`.
2819         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2820
2821         // Scroll to the new folder.
2822         bookmarksListView.setSelection(0);
2823     }
2824
2825     @Override
2826     public void onSaveBookmark(DialogFragment dialogFragment, int selectedBookmarkDatabaseId, Bitmap favoriteIconBitmap) {
2827         // Get handles for the views from `dialogFragment`.
2828         EditText editBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_name_edittext);
2829         EditText editBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_url_edittext);
2830         RadioButton currentBookmarkIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_current_icon_radiobutton);
2831
2832         // Store the bookmark strings.
2833         String bookmarkNameString = editBookmarkNameEditText.getText().toString();
2834         String bookmarkUrlString = editBookmarkUrlEditText.getText().toString();
2835
2836         // Update the bookmark.
2837         if (currentBookmarkIconRadioButton.isChecked()) {  // Update the bookmark without changing the favorite icon.
2838             bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString);
2839         } else {  // Update the bookmark using the `WebView` favorite icon.
2840             // Create a favorite icon byte array output stream.
2841             ByteArrayOutputStream newFavoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
2842
2843             // Convert the favorite icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2844             favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFavoriteIconByteArrayOutputStream);
2845
2846             // Convert the favorite icon byte array stream to a byte array.
2847             byte[] newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray();
2848
2849             //  Update the bookmark and the favorite icon.
2850             bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, newFavoriteIconByteArray);
2851         }
2852
2853         // Update the bookmarks cursor with the current contents of this folder.
2854         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2855
2856         // Update the list view.
2857         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2858     }
2859
2860     @Override
2861     public void onSaveBookmarkFolder(DialogFragment dialogFragment, int selectedFolderDatabaseId, Bitmap favoriteIconBitmap) {
2862         // Get handles for the views from `dialogFragment`.
2863         EditText editFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_folder_name_edittext);
2864         RadioButton currentFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_current_icon_radiobutton);
2865         RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_radiobutton);
2866         ImageView defaultFolderIconImageView = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_imageview);
2867
2868         // Get the new folder name.
2869         String newFolderNameString = editFolderNameEditText.getText().toString();
2870
2871         // Check if the favorite icon has changed.