Use FQDNs for some file names. https://redmine.stoutner.com/issues/680
[PrivacyBrowser.git] / app / src / main / java / com / stoutner / privacybrowser / activities / MainWebViewActivity.java
1 /*
2  * Copyright © 2015-2021 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.annotation.SuppressLint;
25 import android.app.Activity;
26 import android.app.Dialog;
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.AsyncTask;
49 import android.os.Build;
50 import android.os.Bundle;
51 import android.os.Handler;
52 import android.os.Message;
53 import android.preference.PreferenceManager;
54 import android.print.PrintDocumentAdapter;
55 import android.print.PrintManager;
56 import android.text.Editable;
57 import android.text.Spanned;
58 import android.text.TextWatcher;
59 import android.text.style.ForegroundColorSpan;
60 import android.util.Patterns;
61 import android.util.TypedValue;
62 import android.view.ContextMenu;
63 import android.view.GestureDetector;
64 import android.view.KeyEvent;
65 import android.view.Menu;
66 import android.view.MenuItem;
67 import android.view.MotionEvent;
68 import android.view.View;
69 import android.view.ViewGroup;
70 import android.view.WindowManager;
71 import android.view.inputmethod.InputMethodManager;
72 import android.webkit.CookieManager;
73 import android.webkit.HttpAuthHandler;
74 import android.webkit.SslErrorHandler;
75 import android.webkit.ValueCallback;
76 import android.webkit.WebBackForwardList;
77 import android.webkit.WebChromeClient;
78 import android.webkit.WebResourceResponse;
79 import android.webkit.WebSettings;
80 import android.webkit.WebStorage;
81 import android.webkit.WebView;
82 import android.webkit.WebViewClient;
83 import android.webkit.WebViewDatabase;
84 import android.widget.ArrayAdapter;
85 import android.widget.CheckBox;
86 import android.widget.CursorAdapter;
87 import android.widget.EditText;
88 import android.widget.FrameLayout;
89 import android.widget.ImageView;
90 import android.widget.LinearLayout;
91 import android.widget.ListView;
92 import android.widget.ProgressBar;
93 import android.widget.RadioButton;
94 import android.widget.RelativeLayout;
95 import android.widget.TextView;
96
97 import androidx.annotation.NonNull;
98 import androidx.appcompat.app.ActionBar;
99 import androidx.appcompat.app.ActionBarDrawerToggle;
100 import androidx.appcompat.app.AppCompatActivity;
101 import androidx.appcompat.app.AppCompatDelegate;
102 import androidx.appcompat.widget.Toolbar;
103 import androidx.coordinatorlayout.widget.CoordinatorLayout;
104 import androidx.core.content.res.ResourcesCompat;
105 import androidx.core.view.GravityCompat;
106 import androidx.drawerlayout.widget.DrawerLayout;
107 import androidx.fragment.app.DialogFragment;
108 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
109 import androidx.viewpager.widget.ViewPager;
110 import androidx.webkit.WebSettingsCompat;
111 import androidx.webkit.WebViewFeature;
112
113 import com.google.android.material.appbar.AppBarLayout;
114 import com.google.android.material.floatingactionbutton.FloatingActionButton;
115 import com.google.android.material.navigation.NavigationView;
116 import com.google.android.material.snackbar.Snackbar;
117 import com.google.android.material.tabs.TabLayout;
118
119 import com.stoutner.privacybrowser.BuildConfig;
120 import com.stoutner.privacybrowser.R;
121 import com.stoutner.privacybrowser.adapters.WebViewPagerAdapter;
122 import com.stoutner.privacybrowser.asynctasks.GetHostIpAddresses;
123 import com.stoutner.privacybrowser.asynctasks.PopulateBlocklists;
124 import com.stoutner.privacybrowser.asynctasks.PrepareSaveDialog;
125 import com.stoutner.privacybrowser.asynctasks.SaveUrl;
126 import com.stoutner.privacybrowser.asynctasks.SaveWebpageImage;
127 import com.stoutner.privacybrowser.dialogs.AdConsentDialog;
128 import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog;
129 import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog;
130 import com.stoutner.privacybrowser.dialogs.CreateHomeScreenShortcutDialog;
131 import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDialog;
132 import com.stoutner.privacybrowser.dialogs.FontSizeDialog;
133 import com.stoutner.privacybrowser.dialogs.HttpAuthenticationDialog;
134 import com.stoutner.privacybrowser.dialogs.OpenDialog;
135 import com.stoutner.privacybrowser.dialogs.ProxyNotInstalledDialog;
136 import com.stoutner.privacybrowser.dialogs.PinnedMismatchDialog;
137 import com.stoutner.privacybrowser.dialogs.SaveWebpageDialog;
138 import com.stoutner.privacybrowser.dialogs.SslCertificateErrorDialog;
139 import com.stoutner.privacybrowser.dialogs.UrlHistoryDialog;
140 import com.stoutner.privacybrowser.dialogs.ViewSslCertificateDialog;
141 import com.stoutner.privacybrowser.dialogs.WaitingForProxyDialog;
142 import com.stoutner.privacybrowser.fragments.WebViewTabFragment;
143 import com.stoutner.privacybrowser.helpers.AdHelper;
144 import com.stoutner.privacybrowser.helpers.BlocklistHelper;
145 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper;
146 import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper;
147 import com.stoutner.privacybrowser.helpers.ProxyHelper;
148 import com.stoutner.privacybrowser.views.NestedScrollWebView;
149
150 import java.io.ByteArrayInputStream;
151 import java.io.ByteArrayOutputStream;
152 import java.io.File;
153 import java.io.FileInputStream;
154 import java.io.FileOutputStream;
155 import java.io.IOException;
156 import java.io.InputStream;
157 import java.io.OutputStream;
158 import java.io.UnsupportedEncodingException;
159 import java.net.MalformedURLException;
160 import java.net.URL;
161 import java.net.URLDecoder;
162 import java.net.URLEncoder;
163 import java.text.NumberFormat;
164 import java.util.ArrayList;
165 import java.util.Date;
166 import java.util.HashMap;
167 import java.util.HashSet;
168 import java.util.List;
169 import java.util.Map;
170 import java.util.Objects;
171 import java.util.Set;
172 import java.util.concurrent.ExecutorService;
173 import java.util.concurrent.Executors;
174
175 public class MainWebViewActivity extends AppCompatActivity implements CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener,
176         EditBookmarkFolderDialog.EditBookmarkFolderListener, FontSizeDialog.UpdateFontSizeListener, NavigationView.OnNavigationItemSelectedListener, OpenDialog.OpenListener,
177         PinnedMismatchDialog.PinnedMismatchListener, PopulateBlocklists.PopulateBlocklistsListener, SaveWebpageDialog.SaveWebpageListener, UrlHistoryDialog.NavigateHistoryListener,
178         WebViewTabFragment.NewTabListener {
179
180     // The executor service handles background tasks.  It is accessed from `ViewSourceActivity`.
181     public static ExecutorService executorService = Executors.newFixedThreadPool(4);
182
183     // `orbotStatus` is public static so it can be accessed from `OrbotProxyHelper`.  It is also used in `onCreate()`, `onResume()`, and `applyProxy()`.
184     public static String orbotStatus = "unknown";
185
186     // The WebView pager adapter is accessed from `HttpAuthenticationDialog`, `PinnedMismatchDialog`, and `SslCertificateErrorDialog`.  It is also used in `onCreate()`, `onResume()`, and `addTab()`.
187     public static WebViewPagerAdapter webViewPagerAdapter;
188
189     // `restartFromBookmarksActivity` is public static so it can be accessed from `BookmarksActivity`.  It is also used in `onRestart()`.
190     public static boolean restartFromBookmarksActivity;
191
192     // `currentBookmarksFolder` is public static so it can be accessed from `BookmarksActivity`.  It is also used in `onCreate()`, `onBackPressed()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`,
193     // `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
194     public static String currentBookmarksFolder;
195
196     // The user agent constants are public static so they can be accessed from `SettingsFragment`, `DomainsActivity`, and `DomainSettingsFragment`.
197     public final static int UNRECOGNIZED_USER_AGENT = -1;
198     public final static int SETTINGS_WEBVIEW_DEFAULT_USER_AGENT = 1;
199     public final static int SETTINGS_CUSTOM_USER_AGENT = 12;
200     public final static int DOMAINS_SYSTEM_DEFAULT_USER_AGENT = 0;
201     public final static int DOMAINS_WEBVIEW_DEFAULT_USER_AGENT = 2;
202     public final static int DOMAINS_CUSTOM_USER_AGENT = 13;
203
204     // Define the start activity for result request codes.  The public static entries are accessed from `OpenDialog()` and `SaveWebpageDialog()`.
205     private final int BROWSE_FILE_UPLOAD_REQUEST_CODE = 0;
206     public final static int BROWSE_OPEN_REQUEST_CODE = 1;
207     public final static int BROWSE_SAVE_WEBPAGE_REQUEST_CODE = 2;
208
209     // The proxy mode is public static so it can be accessed from `ProxyHelper()`.
210     // It is also used in `onRestart()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `applyAppSettings()`, and `applyProxy()`.
211     // It will be updated in `applyAppSettings()`, but it needs to be initialized here or the first run of `onPrepareOptionsMenu()` crashes.
212     public static String proxyMode = ProxyHelper.NONE;
213
214     // Define the saved instance state constants.
215     private final String SAVED_STATE_ARRAY_LIST = "saved_state_array_list";
216     private final String SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST = "saved_nested_scroll_webview_state_array_list";
217     private final String SAVED_TAB_POSITION = "saved_tab_position";
218     private final String PROXY_MODE = "proxy_mode";
219
220     // Define the saved instance state variables.
221     private ArrayList<Bundle> savedStateArrayList;
222     private ArrayList<Bundle> savedNestedScrollWebViewStateArrayList;
223     private int savedTabPosition;
224     private String savedProxyMode;
225
226     // Define the class variables.
227     @SuppressWarnings("rawtypes")
228     AsyncTask populateBlocklists;
229
230     // The current WebView is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`,
231     // `findNextOnPage()`, `closeFindOnPage()`, `loadUrlFromTextBox()`, `onSslMismatchBack()`, `applyProxy()`, and `applyDomainSettings()`.
232     private NestedScrollWebView currentWebView;
233
234     // `customHeader` is used in `onCreate()`, `onOptionsItemSelected()`, `onCreateContextMenu()`, and `loadUrl()`.
235     private final Map<String, String> customHeaders = new HashMap<>();
236
237     // The search URL is set in `applyAppSettings()` and used in `onNewIntent()`, `loadUrlFromTextBox()`, `initializeApp()`, and `initializeWebView()`.
238     private String searchURL;
239
240     // The blocklists are populated in `finishedPopulatingBlocklists()` and accessed from `initializeWebView()`.
241     private ArrayList<List<String[]>> easyList;
242     private ArrayList<List<String[]>> easyPrivacy;
243     private ArrayList<List<String[]>> fanboysAnnoyanceList;
244     private ArrayList<List<String[]>> fanboysSocialList;
245     private ArrayList<List<String[]>> ultraList;
246     private ArrayList<List<String[]>> ultraPrivacy;
247
248     // `webViewDefaultUserAgent` is used in `onCreate()` and `onPrepareOptionsMenu()`.
249     private String webViewDefaultUserAgent;
250
251     // The incognito mode is set in `applyAppSettings()` and used in `initializeWebView()`.
252     private boolean incognitoModeEnabled;
253
254     // The full screen browsing mode tracker is set it `applyAppSettings()` and used in `initializeWebView()`.
255     private boolean fullScreenBrowsingModeEnabled;
256
257     // `inFullScreenBrowsingMode` is used in `onCreate()`, `onConfigurationChanged()`, and `applyAppSettings()`.
258     private boolean inFullScreenBrowsingMode;
259
260     // The app bar trackers are set in `applyAppSettings()` and used in `initializeWebView()`.
261     private boolean hideAppBar;
262     private boolean scrollAppBar;
263
264     // The loading new intent tracker is set in `onNewIntent()` and used in `setCurrentWebView()`.
265     private boolean loadingNewIntent;
266
267     // `reapplyDomainSettingsOnRestart` is used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, and `onAddDomain()`, .
268     private boolean reapplyDomainSettingsOnRestart;
269
270     // `reapplyAppSettingsOnRestart` is used in `onNavigationItemSelected()` and `onRestart()`.
271     private boolean reapplyAppSettingsOnRestart;
272
273     // `displayingFullScreenVideo` is used in `onCreate()` and `onResume()`.
274     private boolean displayingFullScreenVideo;
275
276     // `orbotStatusBroadcastReceiver` is used in `onCreate()` and `onDestroy()`.
277     private BroadcastReceiver orbotStatusBroadcastReceiver;
278
279     // The waiting for proxy boolean is used in `onResume()`, `initializeApp()` and `applyProxy()`.
280     private boolean waitingForProxy = false;
281
282     // The action bar drawer toggle is initialized in `onCreate()` and used in `onResume()`.
283     private ActionBarDrawerToggle actionBarDrawerToggle;
284
285     // The color spans are used in `onCreate()` and `highlightUrlText()`.
286     private ForegroundColorSpan redColorSpan;
287     private ForegroundColorSpan initialGrayColorSpan;
288     private ForegroundColorSpan finalGrayColorSpan;
289
290     // `bookmarksDatabaseHelper` is used in `onCreate()`, `onDestroy`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`,
291     // and `loadBookmarksFolder()`.
292     private BookmarksDatabaseHelper bookmarksDatabaseHelper;
293
294     // `bookmarksCursor` is used in `onDestroy()`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
295     private Cursor bookmarksCursor;
296
297     // `bookmarksCursorAdapter` is used in `onCreateBookmark()`, `onCreateBookmarkFolder()` `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
298     private CursorAdapter bookmarksCursorAdapter;
299
300     // `oldFolderNameString` is used in `onCreate()` and `onSaveEditBookmarkFolder()`.
301     private String oldFolderNameString;
302
303     // `fileChooserCallback` is used in `onCreate()` and `onActivityResult()`.
304     private ValueCallback<Uri[]> fileChooserCallback;
305
306     // The default progress view offsets are set in `onCreate()` and used in `initializeWebView()`.
307     private int appBarHeight;
308     private int defaultProgressViewStartOffset;
309     private int defaultProgressViewEndOffset;
310
311     // The URL sanitizers are set in `applyAppSettings()` and used in `sanitizeUrl()`.
312     private boolean sanitizeGoogleAnalytics;
313     private boolean sanitizeFacebookClickIds;
314     private boolean sanitizeTwitterAmpRedirects;
315
316     // Declare the class views.
317     private FrameLayout rootFrameLayout;
318     private DrawerLayout drawerLayout;
319     private RelativeLayout mainContentRelativeLayout;
320     private AppBarLayout appBarLayout;
321     private Toolbar toolbar;
322     private RelativeLayout urlRelativeLayout;
323     private EditText urlEditText;
324     private ActionBar actionBar;
325     private LinearLayout findOnPageLinearLayout;
326     private LinearLayout tabsLinearLayout;
327     private TabLayout tabLayout;
328     private SwipeRefreshLayout swipeRefreshLayout;
329     private ViewPager webViewPager;
330     private FrameLayout fullScreenVideoFrameLayout;
331
332     // Declare the class menus.
333     private Menu optionsMenu;
334
335     // Declare the class menu items.
336     private MenuItem navigationBackMenuItem;
337     private MenuItem navigationForwardMenuItem;
338     private MenuItem navigationHistoryMenuItem;
339     private MenuItem navigationRequestsMenuItem;
340     private MenuItem optionsPrivacyMenuItem;
341     private MenuItem optionsRefreshMenuItem;
342     private MenuItem optionsFirstPartyCookiesMenuItem;
343     private MenuItem optionsThirdPartyCookiesMenuItem;
344     private MenuItem optionsDomStorageMenuItem;
345     private MenuItem optionsSaveFormDataMenuItem;
346     private MenuItem optionsClearDataMenuItem;
347     private MenuItem optionsClearCookiesMenuItem;
348     private MenuItem optionsClearDomStorageMenuItem;
349     private MenuItem optionsClearFormDataMenuItem;
350     private MenuItem optionsBlocklistsMenuItem;
351     private MenuItem optionsEasyListMenuItem;
352     private MenuItem optionsEasyPrivacyMenuItem;
353     private MenuItem optionsFanboysAnnoyanceListMenuItem;
354     private MenuItem optionsFanboysSocialBlockingListMenuItem;
355     private MenuItem optionsUltraListMenuItem;
356     private MenuItem optionsUltraPrivacyMenuItem;
357     private MenuItem optionsBlockAllThirdPartyRequestsMenuItem;
358     private MenuItem optionsProxyMenuItem;
359     private MenuItem optionsProxyNoneMenuItem;
360     private MenuItem optionsProxyTorMenuItem;
361     private MenuItem optionsProxyI2pMenuItem;
362     private MenuItem optionsProxyCustomMenuItem;
363     private MenuItem optionsUserAgentMenuItem;
364     private MenuItem optionsUserAgentPrivacyBrowserMenuItem;
365     private MenuItem optionsUserAgentWebViewDefaultMenuItem;
366     private MenuItem optionsUserAgentFirefoxOnAndroidMenuItem;
367     private MenuItem optionsUserAgentChromeOnAndroidMenuItem;
368     private MenuItem optionsUserAgentSafariOnIosMenuItem;
369     private MenuItem optionsUserAgentFirefoxOnLinuxMenuItem;
370     private MenuItem optionsUserAgentChromiumOnLinuxMenuItem;
371     private MenuItem optionsUserAgentFirefoxOnWindowsMenuItem;
372     private MenuItem optionsUserAgentChromeOnWindowsMenuItem;
373     private MenuItem optionsUserAgentEdgeOnWindowsMenuItem;
374     private MenuItem optionsUserAgentInternetExplorerOnWindowsMenuItem;
375     private MenuItem optionsUserAgentSafariOnMacosMenuItem;
376     private MenuItem optionsUserAgentCustomMenuItem;
377     private MenuItem optionsSwipeToRefreshMenuItem;
378     private MenuItem optionsWideViewportMenuItem;
379     private MenuItem optionsDisplayImagesMenuItem;
380     private MenuItem optionsDarkWebViewMenuItem;
381     private MenuItem optionsFontSizeMenuItem;
382     private MenuItem optionsAddOrEditDomainMenuItem;
383
384     @Override
385     // Remove the warning about needing to override `performClick()` when using an `OnTouchListener` with `WebView`.
386     @SuppressLint("ClickableViewAccessibility")
387     protected void onCreate(Bundle savedInstanceState) {
388         // Run the default commands.
389         super.onCreate(savedInstanceState);
390
391         // Check to see if the activity has been restarted.
392         if (savedInstanceState != null) {
393             // Store the saved instance state variables.
394             savedStateArrayList = savedInstanceState.getParcelableArrayList(SAVED_STATE_ARRAY_LIST);
395             savedNestedScrollWebViewStateArrayList = savedInstanceState.getParcelableArrayList(SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST);
396             savedTabPosition = savedInstanceState.getInt(SAVED_TAB_POSITION);
397             savedProxyMode = savedInstanceState.getString(PROXY_MODE);
398         }
399
400         // Initialize the default preference values the first time the program is run.  `false` keeps this command from resetting any current preferences back to default.
401         PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
402
403         // Get a handle for the shared preferences.
404         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
405
406         // Get the screenshot preference.
407         String appTheme = sharedPreferences.getString("app_theme", getString(R.string.app_theme_default_value));
408         boolean allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false);
409
410         // Get the theme entry values string array.
411         String[] appThemeEntryValuesStringArray = getResources().getStringArray(R.array.app_theme_entry_values);
412
413         // Set the app theme according to the preference.  A switch statement cannot be used because the theme entry values string array is not a compile time constant.
414         if (appTheme.equals(appThemeEntryValuesStringArray[1])) {  // The light theme is selected.
415             // Apply the light theme.
416             AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
417         } else if (appTheme.equals(appThemeEntryValuesStringArray[2])) {  // The dark theme is selected.
418             // Apply the dark theme.
419             AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
420         } else {  // The system default theme is selected.
421             if (Build.VERSION.SDK_INT >= 28) {  // The system default theme is supported.
422                 // Follow the system default theme.
423                 AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
424             } else {  // The system default theme is not supported.
425                 // Follow the battery saver mode.
426                 AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY);
427             }
428         }
429
430         // Disable screenshots if not allowed.
431         if (!allowScreenshots) {
432             getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
433         }
434
435         // Enable the drawing of the entire webpage.  This makes it possible to save a website image.  This must be done before anything else happens with the WebView.
436         if (Build.VERSION.SDK_INT >= 21) {
437             WebView.enableSlowWholeDocumentDraw();
438         }
439
440         // Set the theme.
441         setTheme(R.style.PrivacyBrowser);
442
443         // Set the content view.
444         setContentView(R.layout.main_framelayout);
445
446         // Get handles for the views.
447         rootFrameLayout = findViewById(R.id.root_framelayout);
448         drawerLayout = findViewById(R.id.drawerlayout);
449         mainContentRelativeLayout = findViewById(R.id.main_content_relativelayout);
450         appBarLayout = findViewById(R.id.appbar_layout);
451         toolbar = findViewById(R.id.toolbar);
452         findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
453         tabsLinearLayout = findViewById(R.id.tabs_linearlayout);
454         tabLayout = findViewById(R.id.tablayout);
455         swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
456         webViewPager = findViewById(R.id.webviewpager);
457         fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout);
458
459         // Get a handle for the navigation view.
460         NavigationView navigationView = findViewById(R.id.navigationview);
461
462         // Get a handle for the navigation menu.
463         Menu navigationMenu = navigationView.getMenu();
464
465         // Get handles for the navigation menu items.
466         navigationBackMenuItem = navigationMenu.findItem(R.id.back);
467         navigationForwardMenuItem = navigationMenu.findItem(R.id.forward);
468         navigationHistoryMenuItem = navigationMenu.findItem(R.id.history);
469         navigationRequestsMenuItem = navigationMenu.findItem(R.id.requests);
470
471         // Listen for touches on the navigation menu.
472         navigationView.setNavigationItemSelectedListener(this);
473
474         // Get a handle for the app compat delegate.
475         AppCompatDelegate appCompatDelegate = getDelegate();
476
477         // Set the support action bar.
478         appCompatDelegate.setSupportActionBar(toolbar);
479
480         // Get a handle for the action bar.
481         actionBar = appCompatDelegate.getSupportActionBar();
482
483         // Remove the incorrect lint warning below that the action bar might be null.
484         assert actionBar != null;
485
486         // Add the custom layout, which shows the URL text bar.
487         actionBar.setCustomView(R.layout.url_app_bar);
488         actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
489
490         // Get handles for the views in the URL app bar.
491         urlRelativeLayout = findViewById(R.id.url_relativelayout);
492         urlEditText = findViewById(R.id.url_edittext);
493
494         // Create the hamburger icon at the start of the AppBar.
495         actionBarDrawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.open_navigation_drawer, R.string.close_navigation_drawer);
496
497         // Initially disable the sliding drawers.  They will be enabled once the blocklists are loaded.
498         drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
499
500         // Initialize the web view pager adapter.
501         webViewPagerAdapter = new WebViewPagerAdapter(getSupportFragmentManager());
502
503         // Set the pager adapter on the web view pager.
504         webViewPager.setAdapter(webViewPagerAdapter);
505
506         // Store up to 100 tabs in memory.
507         webViewPager.setOffscreenPageLimit(100);
508
509         // Initialize the app.
510         initializeApp();
511
512         // Apply the app settings from the shared preferences.
513         applyAppSettings();
514
515         // Populate the blocklists.
516         populateBlocklists = new PopulateBlocklists(this, this).execute();
517     }
518
519     @Override
520     protected void onNewIntent(Intent intent) {
521         // Run the default commands.
522         super.onNewIntent(intent);
523
524         // Replace the intent that started the app with this one.
525         setIntent(intent);
526
527         // Check to see if the app is being restarted from a saved state.
528         if (savedStateArrayList == null || savedStateArrayList.size() == 0) {  // The activity is not being restarted from a saved state.
529             // Get the information from the intent.
530             String intentAction = intent.getAction();
531             Uri intentUriData = intent.getData();
532             String intentStringExtra = intent.getStringExtra(Intent.EXTRA_TEXT);
533
534             // Determine if this is a web search.
535             boolean isWebSearch = ((intentAction != null) && intentAction.equals(Intent.ACTION_WEB_SEARCH));
536
537             // Only process the URI if it contains data or it is a web search.  If the user pressed the desktop icon after the app was already running the URI will be null.
538             if (intentUriData != null || intentStringExtra != null || isWebSearch) {
539                 // Get the shared preferences.
540                 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
541
542                 // Create a URL string.
543                 String url;
544
545                 // If the intent action is a web search, perform the search.
546                 if (isWebSearch) {  // The intent is a web search.
547                     // Create an encoded URL string.
548                     String encodedUrlString;
549
550                     // Sanitize the search input and convert it to a search.
551                     try {
552                         encodedUrlString = URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8");
553                     } catch (UnsupportedEncodingException exception) {
554                         encodedUrlString = "";
555                     }
556
557                     // Add the base search URL.
558                     url = searchURL + encodedUrlString;
559                 } else if (intentUriData != null) {  // The intent contains a URL formatted as a URI.
560                     // Set the intent data as the URL.
561                     url = intentUriData.toString();
562                 } else {  // The intent contains a string, which might be a URL.
563                     // Set the intent string as the URL.
564                     url = intentStringExtra;
565                 }
566
567                 // Add a new tab if specified in the preferences.
568                 if (sharedPreferences.getBoolean("open_intents_in_new_tab", true)) {  // Load the URL in a new tab.
569                     // Set the loading new intent flag.
570                     loadingNewIntent = true;
571
572                     // Add a new tab.
573                     addNewTab(url, true);
574                 } else {  // Load the URL in the current tab.
575                     // Make it so.
576                     loadUrl(currentWebView, url);
577                 }
578
579                 // Close the navigation drawer if it is open.
580                 if (drawerLayout.isDrawerVisible(GravityCompat.START)) {
581                     drawerLayout.closeDrawer(GravityCompat.START);
582                 }
583
584                 // Close the bookmarks drawer if it is open.
585                 if (drawerLayout.isDrawerVisible(GravityCompat.END)) {
586                     drawerLayout.closeDrawer(GravityCompat.END);
587                 }
588             }
589         }
590     }
591
592     @Override
593     public void onRestart() {
594         // Run the default commands.
595         super.onRestart();
596
597         // Apply the app settings if returning from the Settings activity.
598         if (reapplyAppSettingsOnRestart) {
599             // Reset the reapply app settings on restart tracker.
600             reapplyAppSettingsOnRestart = false;
601
602             // Apply the app settings.
603             applyAppSettings();
604         }
605
606         // Apply the domain settings if returning from the settings or domains activity.
607         if (reapplyDomainSettingsOnRestart) {
608             // Reset the reapply domain settings on restart tracker.
609             reapplyDomainSettingsOnRestart = false;
610
611             // Reapply the domain settings for each tab.
612             for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
613                 // Get the WebView tab fragment.
614                 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
615
616                 // Get the fragment view.
617                 View fragmentView = webViewTabFragment.getView();
618
619                 // Only reload the WebViews if they exist.
620                 if (fragmentView != null) {
621                     // Get the nested scroll WebView from the tab fragment.
622                     NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
623
624                     // Reset the current domain name so the domain settings will be reapplied.
625                     nestedScrollWebView.resetCurrentDomainName();
626
627                     // Reapply the domain settings if the URL is not null, which can happen if an empty tab is active when returning from settings.
628                     if (nestedScrollWebView.getUrl() != null) {
629                         applyDomainSettings(nestedScrollWebView, nestedScrollWebView.getUrl(), false, true, false);
630                     }
631                 }
632             }
633         }
634
635         // Update the bookmarks drawer if returning from the Bookmarks activity.
636         if (restartFromBookmarksActivity) {
637             // Close the bookmarks drawer.
638             drawerLayout.closeDrawer(GravityCompat.END);
639
640             // Reload the bookmarks drawer.
641             loadBookmarksFolder();
642
643             // Reset `restartFromBookmarksActivity`.
644             restartFromBookmarksActivity = false;
645         }
646
647         // Update the privacy icon.  `true` runs `invalidateOptionsMenu` as the last step.  This can be important if the screen was rotated.
648         updatePrivacyIcons(true);
649     }
650
651     // `onStart()` runs after `onCreate()` or `onRestart()`.  This is used instead of `onResume()` so the commands aren't called every time the screen is partially hidden.
652     @Override
653     public void onStart() {
654         // Run the default commands.
655         super.onStart();
656
657         // Resume any WebViews.
658         for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
659             // Get the WebView tab fragment.
660             WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
661
662             // Get the fragment view.
663             View fragmentView = webViewTabFragment.getView();
664
665             // Only resume the WebViews if they exist (they won't when the app is first created).
666             if (fragmentView != null) {
667                 // Get the nested scroll WebView from the tab fragment.
668                 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
669
670                 // Resume the nested scroll WebView.
671                 nestedScrollWebView.onResume();
672             }
673         }
674
675         // Resume the nested scroll WebView JavaScript timers.  This is a global command that resumes JavaScript timers on all WebViews.
676         if (currentWebView != null) {
677             currentWebView.resumeTimers();
678         }
679
680         // Reapply the proxy settings if the system is using a proxy.  This redisplays the appropriate alert dialog.
681         if (!proxyMode.equals(ProxyHelper.NONE)) {
682             applyProxy(false);
683         }
684
685         // Reapply any system UI flags and the ad in the free flavor.
686         if (displayingFullScreenVideo || inFullScreenBrowsingMode) {  // The system is displaying a website or a video in full screen mode.
687             /* Hide the system bars.
688              * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
689              * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
690              * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
691              * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
692              */
693             rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
694                     View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
695         } else if (BuildConfig.FLAVOR.contentEquals("free")) {  // The system in not in full screen mode.
696             // Get a handle for the ad view.  This cannot be a class variable because it changes with each ad load.
697             View adView = findViewById(R.id.adview);
698
699             // Resume the ad.
700             AdHelper.resumeAd(adView);
701         }
702     }
703
704     // `onStop()` runs after `onPause()`.  It is used instead of `onPause()` so the commands are not called every time the screen is partially hidden.
705     @Override
706     public void onStop() {
707         // Run the default commands.
708         super.onStop();
709
710         for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
711             // Get the WebView tab fragment.
712             WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
713
714             // Get the fragment view.
715             View fragmentView = webViewTabFragment.getView();
716
717             // Only pause the WebViews if they exist (they won't when the app is first created).
718             if (fragmentView != null) {
719                 // Get the nested scroll WebView from the tab fragment.
720                 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
721
722                 // Pause the nested scroll WebView.
723                 nestedScrollWebView.onPause();
724             }
725         }
726
727         // Pause the WebView JavaScript timers.  This is a global command that pauses JavaScript on all WebViews.
728         if (currentWebView != null) {
729             currentWebView.pauseTimers();
730         }
731
732         // Pause the ad or it will continue to consume resources in the background on the free flavor.
733         if (BuildConfig.FLAVOR.contentEquals("free")) {
734             // Get a handle for the ad view.  This cannot be a class variable because it changes with each ad load.
735             View adView = findViewById(R.id.adview);
736
737             // Pause the ad.
738             AdHelper.pauseAd(adView);
739         }
740     }
741
742     @Override
743     public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
744         // Run the default commands.
745         super.onSaveInstanceState(savedInstanceState);
746
747         // Create the saved state array lists.
748         ArrayList<Bundle> savedStateArrayList = new ArrayList<>();
749         ArrayList<Bundle> savedNestedScrollWebViewStateArrayList = new ArrayList<>();
750
751         // Get the URLs from each tab.
752         for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
753             // Get the WebView tab fragment.
754             WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
755
756             // Get the fragment view.
757             View fragmentView = webViewTabFragment.getView();
758
759             if (fragmentView != null) {
760                 // Get the nested scroll WebView from the tab fragment.
761                 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
762
763                 // Create saved state bundle.
764                 Bundle savedStateBundle = new Bundle();
765
766                 // Get the current states.
767                 nestedScrollWebView.saveState(savedStateBundle);
768                 Bundle savedNestedScrollWebViewStateBundle = nestedScrollWebView.saveNestedScrollWebViewState();
769
770                 // Store the saved states in the array lists.
771                 savedStateArrayList.add(savedStateBundle);
772                 savedNestedScrollWebViewStateArrayList.add(savedNestedScrollWebViewStateBundle);
773             }
774         }
775
776         // Get the current tab position.
777         int currentTabPosition = tabLayout.getSelectedTabPosition();
778
779         // Store the saved states in the bundle.
780         savedInstanceState.putParcelableArrayList(SAVED_STATE_ARRAY_LIST, savedStateArrayList);
781         savedInstanceState.putParcelableArrayList(SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST, savedNestedScrollWebViewStateArrayList);
782         savedInstanceState.putInt(SAVED_TAB_POSITION, currentTabPosition);
783         savedInstanceState.putString(PROXY_MODE, proxyMode);
784     }
785
786     @Override
787     public void onDestroy() {
788         // Unregister the orbot status broadcast receiver if it exists.
789         if (orbotStatusBroadcastReceiver != null) {
790             this.unregisterReceiver(orbotStatusBroadcastReceiver);
791         }
792
793         // Close the bookmarks cursor if it exists.
794         if (bookmarksCursor != null) {
795             bookmarksCursor.close();
796         }
797
798         // Close the bookmarks database if it exists.
799         if (bookmarksDatabaseHelper != null) {
800             bookmarksDatabaseHelper.close();
801         }
802
803         // Stop populating the blocklists if the AsyncTask is running in the background.
804         if (populateBlocklists != null) {
805             populateBlocklists.cancel(true);
806         }
807
808         // Run the default commands.
809         super.onDestroy();
810     }
811
812     @Override
813     public boolean onCreateOptionsMenu(Menu menu) {
814         // Inflate the menu.  This adds items to the action bar if it is present.
815         getMenuInflater().inflate(R.menu.webview_options_menu, menu);
816
817         // Store a handle for the options menu so it can be used by `onOptionsItemSelected()` and `updatePrivacyIcons()`.
818         optionsMenu = menu;
819
820         // Get handles for the class menu items.
821         optionsPrivacyMenuItem = menu.findItem(R.id.javascript);
822         optionsRefreshMenuItem = menu.findItem(R.id.refresh);
823         optionsFirstPartyCookiesMenuItem = menu.findItem(R.id.first_party_cookies);
824         optionsThirdPartyCookiesMenuItem = menu.findItem(R.id.third_party_cookies);
825         optionsDomStorageMenuItem = menu.findItem(R.id.dom_storage);
826         optionsSaveFormDataMenuItem = menu.findItem(R.id.save_form_data);  // Form data can be removed once the minimum API >= 26.
827         optionsClearDataMenuItem = menu.findItem(R.id.clear_data);
828         optionsClearCookiesMenuItem = menu.findItem(R.id.clear_cookies);
829         optionsClearDomStorageMenuItem = menu.findItem(R.id.clear_dom_storage);
830         optionsClearFormDataMenuItem = menu.findItem(R.id.clear_form_data);  // Form data can be removed once the minimum API >= 26.
831         optionsBlocklistsMenuItem = menu.findItem(R.id.blocklists);
832         optionsEasyListMenuItem = menu.findItem(R.id.easylist);
833         optionsEasyPrivacyMenuItem = menu.findItem(R.id.easyprivacy);
834         optionsFanboysAnnoyanceListMenuItem = menu.findItem(R.id.fanboys_annoyance_list);
835         optionsFanboysSocialBlockingListMenuItem = menu.findItem(R.id.fanboys_social_blocking_list);
836         optionsUltraListMenuItem = menu.findItem(R.id.ultralist);
837         optionsUltraPrivacyMenuItem = menu.findItem(R.id.ultraprivacy);
838         optionsBlockAllThirdPartyRequestsMenuItem = menu.findItem(R.id.block_all_third_party_requests);
839         optionsProxyMenuItem = menu.findItem(R.id.proxy);
840         optionsProxyNoneMenuItem = menu.findItem(R.id.proxy_none);
841         optionsProxyTorMenuItem = menu.findItem(R.id.proxy_tor);
842         optionsProxyI2pMenuItem = menu.findItem(R.id.proxy_i2p);
843         optionsProxyCustomMenuItem = menu.findItem(R.id.proxy_custom);
844         optionsUserAgentMenuItem = menu.findItem(R.id.user_agent);
845         optionsUserAgentPrivacyBrowserMenuItem = menu.findItem(R.id.user_agent_privacy_browser);
846         optionsUserAgentWebViewDefaultMenuItem = menu.findItem(R.id.user_agent_webview_default);
847         optionsUserAgentFirefoxOnAndroidMenuItem = menu.findItem(R.id.user_agent_firefox_on_android);
848         optionsUserAgentChromeOnAndroidMenuItem = menu.findItem(R.id.user_agent_chrome_on_android);
849         optionsUserAgentSafariOnIosMenuItem = menu.findItem(R.id.user_agent_safari_on_ios);
850         optionsUserAgentFirefoxOnLinuxMenuItem = menu.findItem(R.id.user_agent_firefox_on_linux);
851         optionsUserAgentChromiumOnLinuxMenuItem = menu.findItem(R.id.user_agent_chromium_on_linux);
852         optionsUserAgentFirefoxOnWindowsMenuItem = menu.findItem(R.id.user_agent_firefox_on_windows);
853         optionsUserAgentChromeOnWindowsMenuItem = menu.findItem(R.id.user_agent_chrome_on_windows);
854         optionsUserAgentEdgeOnWindowsMenuItem = menu.findItem(R.id.user_agent_edge_on_windows);
855         optionsUserAgentInternetExplorerOnWindowsMenuItem = menu.findItem(R.id.user_agent_internet_explorer_on_windows);
856         optionsUserAgentSafariOnMacosMenuItem = menu.findItem(R.id.user_agent_safari_on_macos);
857         optionsUserAgentCustomMenuItem = menu.findItem(R.id.user_agent_custom);
858         optionsSwipeToRefreshMenuItem = menu.findItem(R.id.swipe_to_refresh);
859         optionsWideViewportMenuItem = menu.findItem(R.id.wide_viewport);
860         optionsDisplayImagesMenuItem = menu.findItem(R.id.display_images);
861         optionsDarkWebViewMenuItem = menu.findItem(R.id.dark_webview);
862         optionsFontSizeMenuItem = menu.findItem(R.id.font_size);
863         optionsAddOrEditDomainMenuItem = menu.findItem(R.id.add_or_edit_domain);
864
865         // Get handles for the method menu items.
866         MenuItem bookmarksMenuItem = menu.findItem(R.id.bookmarks);
867         MenuItem adConsentMenuItem = menu.findItem(R.id.ad_consent);
868
869         // Set the initial status of the privacy icons.  `false` does not call `invalidateOptionsMenu` as the last step.
870         updatePrivacyIcons(false);
871
872         // Only display third-party cookies if API >= 21
873         optionsThirdPartyCookiesMenuItem.setVisible(Build.VERSION.SDK_INT >= 21);
874
875         // Only display the form data menu items if the API < 26.
876         optionsSaveFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
877         optionsClearFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
878
879         // Disable the clear form data menu item if the API >= 26 so that the status of the main Clear Data is calculated correctly.
880         optionsClearFormDataMenuItem.setEnabled(Build.VERSION.SDK_INT < 26);
881
882         // Only display the dark WebView menu item if API >= 21.
883         optionsDarkWebViewMenuItem.setVisible(Build.VERSION.SDK_INT >= 21);
884
885         // Only show Ad Consent if this is the free flavor.
886         adConsentMenuItem.setVisible(BuildConfig.FLAVOR.contentEquals("free"));
887
888         // Get the shared preferences.
889         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
890
891         // Get the dark theme and app bar preferences.
892         boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean(getString(R.string.display_additional_app_bar_icons_key), false);
893
894         // 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.
895         if (displayAdditionalAppBarIcons) {
896             optionsRefreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
897             bookmarksMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
898             optionsFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
899         } else { //Do not display the additional icons.
900             optionsRefreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
901             bookmarksMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
902             optionsFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
903         }
904
905         // Replace Refresh with Stop if a URL is already loading.
906         if (currentWebView != null && currentWebView.getProgress() != 100) {
907             // Set the title.
908             optionsRefreshMenuItem.setTitle(R.string.stop);
909
910             // Set the icon if it is displayed in the app bar.
911             if (displayAdditionalAppBarIcons) {
912                 // Get the current theme status.
913                 int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
914
915                 // Set the icon according to the current theme status.
916                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
917                     optionsRefreshMenuItem.setIcon(R.drawable.close_blue_day);
918                 } else {
919                     optionsRefreshMenuItem.setIcon(R.drawable.close_blue_night);
920                 }
921             }
922         }
923
924         // Done.
925         return true;
926     }
927
928     @Override
929     public boolean onPrepareOptionsMenu(Menu menu) {
930         // Get a handle for the cookie manager.
931         CookieManager cookieManager = CookieManager.getInstance();
932
933         // Initialize the current user agent string and the font size.
934         String currentUserAgent = getString(R.string.user_agent_privacy_browser);
935         int fontSize = 100;
936
937         // 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.
938         if (currentWebView != null) {
939             // Set the add or edit domain text.
940             if (currentWebView.getDomainSettingsApplied()) {
941                 optionsAddOrEditDomainMenuItem.setTitle(R.string.edit_domain_settings);
942             } else {
943                 optionsAddOrEditDomainMenuItem.setTitle(R.string.add_domain_settings);
944             }
945
946             // Get the current user agent from the WebView.
947             currentUserAgent = currentWebView.getSettings().getUserAgentString();
948
949             // Get the current font size from the
950             fontSize = currentWebView.getSettings().getTextZoom();
951
952             // Set the status of the menu item checkboxes.
953             optionsDomStorageMenuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled());
954             optionsSaveFormDataMenuItem.setChecked(currentWebView.getSettings().getSaveFormData());  // Form data can be removed once the minimum API >= 26.
955             optionsEasyListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASYLIST));
956             optionsEasyPrivacyMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASYPRIVACY));
957             optionsFanboysAnnoyanceListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
958             optionsFanboysSocialBlockingListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
959             optionsUltraListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRALIST));
960             optionsUltraPrivacyMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRAPRIVACY));
961             optionsBlockAllThirdPartyRequestsMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
962             optionsSwipeToRefreshMenuItem.setChecked(currentWebView.getSwipeToRefresh());
963             optionsWideViewportMenuItem.setChecked(currentWebView.getSettings().getUseWideViewPort());
964             optionsDisplayImagesMenuItem.setChecked(currentWebView.getSettings().getLoadsImagesAutomatically());
965
966             // Initialize the display names for the blocklists with the number of blocked requests.
967             optionsBlocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
968             optionsEasyListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASYLIST) + " - " + getString(R.string.easylist));
969             optionsEasyPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASYPRIVACY) + " - " + getString(R.string.easyprivacy));
970             optionsFanboysAnnoyanceListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST) + " - " + getString(R.string.fanboys_annoyance_list));
971             optionsFanboysSocialBlockingListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST) + " - " + getString(R.string.fanboys_social_blocking_list));
972             optionsUltraListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.ULTRALIST) + " - " + getString(R.string.ultralist));
973             optionsUltraPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.ULTRAPRIVACY) + " - " + getString(R.string.ultraprivacy));
974             optionsBlockAllThirdPartyRequestsMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " + getString(R.string.block_all_third_party_requests));
975
976             // Only modify third-party cookies if the API >= 21.
977             if (Build.VERSION.SDK_INT >= 21) {
978                 // Set the status of the third-party cookies checkbox.
979                 optionsThirdPartyCookiesMenuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView));
980
981                 // Enable third-party cookies if first-party cookies are enabled.
982                 optionsThirdPartyCookiesMenuItem.setEnabled(cookieManager.acceptCookie());
983             }
984
985             // Enable DOM Storage if JavaScript is enabled.
986             optionsDomStorageMenuItem.setEnabled(currentWebView.getSettings().getJavaScriptEnabled());
987
988             // Set the checkbox status for dark WebView if the WebView supports it.
989             if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
990                 optionsDarkWebViewMenuItem.setChecked(WebSettingsCompat.getForceDark(currentWebView.getSettings()) == WebSettingsCompat.FORCE_DARK_ON);
991             }
992         }
993
994         // Set the checked status of the first party cookies menu item.
995         optionsFirstPartyCookiesMenuItem.setChecked(cookieManager.acceptCookie());
996
997         // Enable Clear Cookies if there are any.
998         optionsClearCookiesMenuItem.setEnabled(cookieManager.hasCookies());
999
1000         // 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`.
1001         String privateDataDirectoryString = getApplicationInfo().dataDir;
1002
1003         // Get a count of the number of files in the Local Storage directory.
1004         File localStorageDirectory = new File (privateDataDirectoryString + "/app_webview/Local Storage/");
1005         int localStorageDirectoryNumberOfFiles = 0;
1006         if (localStorageDirectory.exists()) {
1007             // `Objects.requireNonNull` removes a lint warning that `localStorageDirectory.list` might produce a null pointed exception if it is dereferenced.
1008             localStorageDirectoryNumberOfFiles = Objects.requireNonNull(localStorageDirectory.list()).length;
1009         }
1010
1011         // Get a count of the number of files in the IndexedDB directory.
1012         File indexedDBDirectory = new File (privateDataDirectoryString + "/app_webview/IndexedDB");
1013         int indexedDBDirectoryNumberOfFiles = 0;
1014         if (indexedDBDirectory.exists()) {
1015             // `Objects.requireNonNull` removes a lint warning that `indexedDBDirectory.list` might produce a null pointed exception if it is dereferenced.
1016             indexedDBDirectoryNumberOfFiles = Objects.requireNonNull(indexedDBDirectory.list()).length;
1017         }
1018
1019         // Enable Clear DOM Storage if there is any.
1020         optionsClearDomStorageMenuItem.setEnabled(localStorageDirectoryNumberOfFiles > 0 || indexedDBDirectoryNumberOfFiles > 0);
1021
1022         // Enable Clear Form Data is there is any.  This can be removed once the minimum API >= 26.
1023         if (Build.VERSION.SDK_INT < 26) {
1024             // Get the WebView database.
1025             WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
1026
1027             // Enable the clear form data menu item if there is anything to clear.
1028             optionsClearFormDataMenuItem.setEnabled(webViewDatabase.hasFormData());
1029         }
1030
1031         // Enable Clear Data if any of the submenu items are enabled.
1032         optionsClearDataMenuItem.setEnabled(optionsClearCookiesMenuItem.isEnabled() || optionsClearDomStorageMenuItem.isEnabled() || optionsClearFormDataMenuItem.isEnabled());
1033
1034         // Disable Fanboy's Social Blocking List menu item if Fanboy's Annoyance List is checked.
1035         optionsFanboysSocialBlockingListMenuItem.setEnabled(!optionsFanboysAnnoyanceListMenuItem.isChecked());
1036
1037         // Set the proxy title and check the applied proxy.
1038         switch (proxyMode) {
1039             case ProxyHelper.NONE:
1040                 // Set the proxy title.
1041                 optionsProxyMenuItem.setTitle(getString(R.string.proxy) + " - " + getString(R.string.proxy_none));
1042
1043                 // Check the proxy None radio button.
1044                 optionsProxyNoneMenuItem.setChecked(true);
1045                 break;
1046
1047             case ProxyHelper.TOR:
1048                 // Set the proxy title.
1049                 optionsProxyMenuItem.setTitle(getString(R.string.proxy) + " - " + getString(R.string.proxy_tor));
1050
1051                 // Check the proxy Tor radio button.
1052                 optionsProxyTorMenuItem.setChecked(true);
1053                 break;
1054
1055             case ProxyHelper.I2P:
1056                 // Set the proxy title.
1057                 optionsProxyMenuItem.setTitle(getString(R.string.proxy) + " - " + getString(R.string.proxy_i2p));
1058
1059                 // Check the proxy I2P radio button.
1060                 optionsProxyI2pMenuItem.setChecked(true);
1061                 break;
1062
1063             case ProxyHelper.CUSTOM:
1064                 // Set the proxy title.
1065                 optionsProxyMenuItem.setTitle(getString(R.string.proxy) + " - " + getString(R.string.proxy_custom));
1066
1067                 // Check the proxy Custom radio button.
1068                 optionsProxyCustomMenuItem.setChecked(true);
1069                 break;
1070         }
1071
1072         // Select the current user agent menu item.  A switch statement cannot be used because the user agents are not compile time constants.
1073         if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[0])) {  // Privacy Browser.
1074             // Update the user agent menu item title.
1075             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_privacy_browser));
1076
1077             // Select the Privacy Browser radio box.
1078             optionsUserAgentPrivacyBrowserMenuItem.setChecked(true);
1079         } else if (currentUserAgent.equals(webViewDefaultUserAgent)) {  // WebView Default.
1080             // Update the user agent menu item title.
1081             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_webview_default));
1082
1083             // Select the WebView Default radio box.
1084             optionsUserAgentWebViewDefaultMenuItem.setChecked(true);
1085         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[2])) {  // Firefox on Android.
1086             // Update the user agent menu item title.
1087             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_firefox_on_android));
1088
1089             // Select the Firefox on Android radio box.
1090             optionsUserAgentFirefoxOnAndroidMenuItem.setChecked(true);
1091         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[3])) {  // Chrome on Android.
1092             // Update the user agent menu item title.
1093             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_chrome_on_android));
1094
1095             // Select the Chrome on Android radio box.
1096             optionsUserAgentChromeOnAndroidMenuItem.setChecked(true);
1097         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[4])) {  // Safari on iOS.
1098             // Update the user agent menu item title.
1099             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_safari_on_ios));
1100
1101             // Select the Safari on iOS radio box.
1102             optionsUserAgentSafariOnIosMenuItem.setChecked(true);
1103         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[5])) {  // Firefox on Linux.
1104             // Update the user agent menu item title.
1105             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_firefox_on_linux));
1106
1107             // Select the Firefox on Linux radio box.
1108             optionsUserAgentFirefoxOnLinuxMenuItem.setChecked(true);
1109         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[6])) {  // Chromium on Linux.
1110             // Update the user agent menu item title.
1111             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_chromium_on_linux));
1112
1113             // Select the Chromium on Linux radio box.
1114             optionsUserAgentChromiumOnLinuxMenuItem.setChecked(true);
1115         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[7])) {  // Firefox on Windows.
1116             // Update the user agent menu item title.
1117             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_firefox_on_windows));
1118
1119             // Select the Firefox on Windows radio box.
1120             optionsUserAgentFirefoxOnWindowsMenuItem.setChecked(true);
1121         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[8])) {  // Chrome on Windows.
1122             // Update the user agent menu item title.
1123             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_chrome_on_windows));
1124
1125             // Select the Chrome on Windows radio box.
1126             optionsUserAgentChromeOnWindowsMenuItem.setChecked(true);
1127         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[9])) {  // Edge on Windows.
1128             // Update the user agent menu item title.
1129             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_edge_on_windows));
1130
1131             // Select the Edge on Windows radio box.
1132             optionsUserAgentEdgeOnWindowsMenuItem.setChecked(true);
1133         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[10])) {  // Internet Explorer on Windows.
1134             // Update the user agent menu item title.
1135             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_internet_explorer_on_windows));
1136
1137             // Select the Internet on Windows radio box.
1138             optionsUserAgentInternetExplorerOnWindowsMenuItem.setChecked(true);
1139         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[11])) {  // Safari on macOS.
1140             // Update the user agent menu item title.
1141             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_safari_on_macos));
1142
1143             // Select the Safari on macOS radio box.
1144             optionsUserAgentSafariOnMacosMenuItem.setChecked(true);
1145         } else {  // Custom user agent.
1146             // Update the user agent menu item title.
1147             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_custom));
1148
1149             // Select the Custom radio box.
1150             optionsUserAgentCustomMenuItem.setChecked(true);
1151         }
1152
1153         // Set the font size title.
1154         optionsFontSizeMenuItem.setTitle(getString(R.string.font_size) + " - " + fontSize + "%");
1155
1156         // Run all the other default commands.
1157         super.onPrepareOptionsMenu(menu);
1158
1159         // Display the menu.
1160         return true;
1161     }
1162
1163     @Override
1164     public boolean onOptionsItemSelected(MenuItem menuItem) {
1165         // Get a handle for the shared preferences.
1166         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
1167
1168         // Get a handle for the cookie manager.
1169         CookieManager cookieManager = CookieManager.getInstance();
1170
1171         // Get the selected menu item ID.
1172         int menuItemId = menuItem.getItemId();
1173
1174         // Run the commands that correlate to the selected menu item.
1175         if (menuItemId == R.id.javascript) {  // JavaScript.
1176             // Toggle the JavaScript status.
1177             currentWebView.getSettings().setJavaScriptEnabled(!currentWebView.getSettings().getJavaScriptEnabled());
1178
1179             // Update the privacy icon.
1180             updatePrivacyIcons(true);
1181
1182             // Display a `Snackbar`.
1183             if (currentWebView.getSettings().getJavaScriptEnabled()) {  // JavaScrip is enabled.
1184                 Snackbar.make(webViewPager, R.string.javascript_enabled, Snackbar.LENGTH_SHORT).show();
1185             } else if (cookieManager.acceptCookie()) {  // JavaScript is disabled, but first-party cookies are enabled.
1186                 Snackbar.make(webViewPager, R.string.javascript_disabled, Snackbar.LENGTH_SHORT).show();
1187             } else {  // Privacy mode.
1188                 Snackbar.make(webViewPager, R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
1189             }
1190
1191             // Reload the current WebView.
1192             currentWebView.reload();
1193
1194             // Consume the event.
1195             return true;
1196         } else if (menuItemId == R.id.refresh) {  // Refresh.
1197             // Run the command that correlates to the current status of the menu item.
1198             if (menuItem.getTitle().equals(getString(R.string.refresh))) {  // The refresh button was pushed.
1199                 // Reload the current WebView.
1200                 currentWebView.reload();
1201             } else {  // The stop button was pushed.
1202                 // Stop the loading of the WebView.
1203                 currentWebView.stopLoading();
1204             }
1205
1206             // Consume the event.
1207             return true;
1208         } else if (menuItemId == R.id.bookmarks) {  // Bookmarks.
1209             // Open the bookmarks drawer.
1210             drawerLayout.openDrawer(GravityCompat.END);
1211
1212             // Consume the event.
1213             return true;
1214         } else if (menuItemId == R.id.first_party_cookies) {  // First-party cookies.
1215             // Switch the first-party cookie status.
1216             cookieManager.setAcceptCookie(!cookieManager.acceptCookie());
1217
1218             // Store the first-party cookie status.
1219             currentWebView.setAcceptFirstPartyCookies(cookieManager.acceptCookie());
1220
1221             // Update the menu checkbox.
1222             menuItem.setChecked(cookieManager.acceptCookie());
1223
1224             // Update the privacy icon.
1225             updatePrivacyIcons(true);
1226
1227             // Display a snackbar.
1228             if (cookieManager.acceptCookie()) {  // First-party cookies are enabled.
1229                 Snackbar.make(webViewPager, R.string.first_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
1230             } else if (currentWebView.getSettings().getJavaScriptEnabled()) {  // JavaScript is still enabled.
1231                 Snackbar.make(webViewPager, R.string.first_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
1232             } else {  // Privacy mode.
1233                 Snackbar.make(webViewPager, R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
1234             }
1235
1236             // Reload the current WebView.
1237             currentWebView.reload();
1238
1239             // Consume the event.
1240             return true;
1241         } else if (menuItemId == R.id.third_party_cookies) {  // Third-party cookies.
1242             // Only act if the API >= 21.  Otherwise, there are no third-party cookie controls.
1243             if (Build.VERSION.SDK_INT >= 21) {
1244                 // Toggle the status of thirdPartyCookiesEnabled.
1245                 cookieManager.setAcceptThirdPartyCookies(currentWebView, !cookieManager.acceptThirdPartyCookies(currentWebView));
1246
1247                 // Update the menu checkbox.
1248                 menuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView));
1249
1250                 // Display a snackbar.
1251                 if (cookieManager.acceptThirdPartyCookies(currentWebView)) {
1252                     Snackbar.make(webViewPager, R.string.third_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
1253                 } else {
1254                     Snackbar.make(webViewPager, R.string.third_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
1255                 }
1256
1257                 // Reload the current WebView.
1258                 currentWebView.reload();
1259             }
1260
1261             // Consume the event.
1262             return true;
1263         } else if (menuItemId == R.id.dom_storage) {  // DOM storage.
1264             // Toggle the status of domStorageEnabled.
1265             currentWebView.getSettings().setDomStorageEnabled(!currentWebView.getSettings().getDomStorageEnabled());
1266
1267             // Update the menu checkbox.
1268             menuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled());
1269
1270             // Update the privacy icon.
1271             updatePrivacyIcons(true);
1272
1273             // Display a snackbar.
1274             if (currentWebView.getSettings().getDomStorageEnabled()) {
1275                 Snackbar.make(webViewPager, R.string.dom_storage_enabled, Snackbar.LENGTH_SHORT).show();
1276             } else {
1277                 Snackbar.make(webViewPager, R.string.dom_storage_disabled, Snackbar.LENGTH_SHORT).show();
1278             }
1279
1280             // Reload the current WebView.
1281             currentWebView.reload();
1282
1283             // Consume the event.
1284             return true;
1285         } else if (menuItemId == R.id.save_form_data) {  // Form data.  This can be removed once the minimum API >= 26.
1286             // Switch the status of saveFormDataEnabled.
1287             currentWebView.getSettings().setSaveFormData(!currentWebView.getSettings().getSaveFormData());
1288
1289             // Update the menu checkbox.
1290             menuItem.setChecked(currentWebView.getSettings().getSaveFormData());
1291
1292             // Display a snackbar.
1293             if (currentWebView.getSettings().getSaveFormData()) {
1294                 Snackbar.make(webViewPager, R.string.form_data_enabled, Snackbar.LENGTH_SHORT).show();
1295             } else {
1296                 Snackbar.make(webViewPager, R.string.form_data_disabled, Snackbar.LENGTH_SHORT).show();
1297             }
1298
1299             // Update the privacy icon.
1300             updatePrivacyIcons(true);
1301
1302             // Reload the current WebView.
1303             currentWebView.reload();
1304
1305             // Consume the event.
1306             return true;
1307         } else if (menuItemId == R.id.clear_cookies) {  // Clear cookies.
1308             // Create a snackbar.
1309             Snackbar.make(webViewPager, R.string.cookies_deleted, Snackbar.LENGTH_LONG)
1310                     .setAction(R.string.undo, v -> {
1311                         // Do nothing because everything will be handled by `onDismissed()` below.
1312                     })
1313                     .addCallback(new Snackbar.Callback() {
1314                         @Override
1315                         public void onDismissed(Snackbar snackbar, int event) {
1316                             if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) {  // The snackbar was dismissed without the undo button being pushed.
1317                                 // Delete the cookies, which command varies by SDK.
1318                                 if (Build.VERSION.SDK_INT < 21) {
1319                                     cookieManager.removeAllCookie();
1320                                 } else {
1321                                     cookieManager.removeAllCookies(null);
1322                                 }
1323                             }
1324                         }
1325                     })
1326                     .show();
1327
1328             // Consume the event.
1329             return true;
1330         } else if (menuItemId == R.id.clear_dom_storage) {  // Clear DOM storage.
1331             // Create a snackbar.
1332             Snackbar.make(webViewPager, R.string.dom_storage_deleted, Snackbar.LENGTH_LONG)
1333                     .setAction(R.string.undo, v -> {
1334                         // Do nothing because everything will be handled by `onDismissed()` below.
1335                     })
1336                     .addCallback(new Snackbar.Callback() {
1337                         @Override
1338                         public void onDismissed(Snackbar snackbar, int event) {
1339                             if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) {  // The snackbar was dismissed without the undo button being pushed.
1340                                 // Delete the DOM Storage.
1341                                 WebStorage webStorage = WebStorage.getInstance();
1342                                 webStorage.deleteAllData();
1343
1344                                 // Initialize a handler to manually delete the DOM storage files and directories.
1345                                 Handler deleteDomStorageHandler = new Handler();
1346
1347                                 // Setup a runnable to manually delete the DOM storage files and directories.
1348                                 Runnable deleteDomStorageRunnable = () -> {
1349                                     try {
1350                                         // Get a handle for the runtime.
1351                                         Runtime runtime = Runtime.getRuntime();
1352
1353                                         // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
1354                                         // which links to `/data/data/com.stoutner.privacybrowser.standard`.
1355                                         String privateDataDirectoryString = getApplicationInfo().dataDir;
1356
1357                                         // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
1358                                         Process deleteLocalStorageProcess = runtime.exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
1359
1360                                         // Multiple commands must be used because `Runtime.exec()` does not like `*`.
1361                                         Process deleteIndexProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
1362                                         Process deleteQuotaManagerProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
1363                                         Process deleteQuotaManagerJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
1364                                         Process deleteDatabasesProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
1365
1366                                         // Wait for the processes to finish.
1367                                         deleteLocalStorageProcess.waitFor();
1368                                         deleteIndexProcess.waitFor();
1369                                         deleteQuotaManagerProcess.waitFor();
1370                                         deleteQuotaManagerJournalProcess.waitFor();
1371                                         deleteDatabasesProcess.waitFor();
1372                                     } catch (Exception exception) {
1373                                         // Do nothing if an error is thrown.
1374                                     }
1375                                 };
1376
1377                                 // Manually delete the DOM storage files after 200 milliseconds.
1378                                 deleteDomStorageHandler.postDelayed(deleteDomStorageRunnable, 200);
1379                             }
1380                         }
1381                     })
1382                     .show();
1383
1384             // Consume the event.
1385             return true;
1386         } else if (menuItemId == R.id.clear_form_data) {  // Clear form data.  This can be remove once the minimum API >= 26.
1387             // Create a snackbar.
1388             Snackbar.make(webViewPager, R.string.form_data_deleted, Snackbar.LENGTH_LONG)
1389                     .setAction(R.string.undo, v -> {
1390                         // Do nothing because everything will be handled by `onDismissed()` below.
1391                     })
1392                     .addCallback(new Snackbar.Callback() {
1393                         @Override
1394                         public void onDismissed(Snackbar snackbar, int event) {
1395                             if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) {  // The snackbar was dismissed without the undo button being pushed.
1396                                 // Get a handle for the webView database.
1397                                 WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(getApplicationContext());
1398
1399                                 // Delete the form data.
1400                                 webViewDatabase.clearFormData();
1401                             }
1402                         }
1403                     })
1404                     .show();
1405
1406             // Consume the event.
1407             return true;
1408         } else if (menuItemId == R.id.easylist) {  // EasyList.
1409             // Toggle the EasyList status.
1410             currentWebView.enableBlocklist(NestedScrollWebView.EASYLIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.EASYLIST));
1411
1412             // Update the menu checkbox.
1413             menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASYLIST));
1414
1415             // Reload the current WebView.
1416             currentWebView.reload();
1417
1418             // Consume the event.
1419             return true;
1420         } else if (menuItemId == R.id.easyprivacy) {  // EasyPrivacy.
1421             // Toggle the EasyPrivacy status.
1422             currentWebView.enableBlocklist(NestedScrollWebView.EASYPRIVACY, !currentWebView.isBlocklistEnabled(NestedScrollWebView.EASYPRIVACY));
1423
1424             // Update the menu checkbox.
1425             menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASYPRIVACY));
1426
1427             // Reload the current WebView.
1428             currentWebView.reload();
1429
1430             // Consume the event.
1431             return true;
1432         } else if (menuItemId == R.id.fanboys_annoyance_list) {  // Fanboy's Annoyance List.
1433             // Toggle Fanboy's Annoyance List status.
1434             currentWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1435
1436             // Update the menu checkbox.
1437             menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1438
1439             // Update the staus of Fanboy's Social Blocking List.
1440             optionsFanboysSocialBlockingListMenuItem.setEnabled(!currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1441
1442             // Reload the current WebView.
1443             currentWebView.reload();
1444
1445             // Consume the event.
1446             return true;
1447         } else if (menuItemId == R.id.fanboys_social_blocking_list) {  // Fanboy's Social Blocking List.
1448             // Toggle Fanboy's Social Blocking List status.
1449             currentWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
1450
1451             // Update the menu checkbox.
1452             menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
1453
1454             // Reload the current WebView.
1455             currentWebView.reload();
1456
1457             // Consume the event.
1458             return true;
1459         } else if (menuItemId == R.id.ultralist) {  // UltraList.
1460             // Toggle the UltraList status.
1461             currentWebView.enableBlocklist(NestedScrollWebView.ULTRALIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRALIST));
1462
1463             // Update the menu checkbox.
1464             menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRALIST));
1465
1466             // Reload the current WebView.
1467             currentWebView.reload();
1468
1469             // Consume the event.
1470             return true;
1471         } else if (menuItemId == R.id.ultraprivacy) {  // UltraPrivacy.
1472             // Toggle the UltraPrivacy status.
1473             currentWebView.enableBlocklist(NestedScrollWebView.ULTRAPRIVACY, !currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRAPRIVACY));
1474
1475             // Update the menu checkbox.
1476             menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRAPRIVACY));
1477
1478             // Reload the current WebView.
1479             currentWebView.reload();
1480
1481             // Consume the event.
1482             return true;
1483         } else if (menuItemId == R.id.block_all_third_party_requests) {  // Block all third-party requests.
1484             //Toggle the third-party requests blocker status.
1485             currentWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS, !currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
1486
1487             // Update the menu checkbox.
1488             menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
1489
1490             // Reload the current WebView.
1491             currentWebView.reload();
1492
1493             // Consume the event.
1494             return true;
1495         } else if (menuItemId == R.id.proxy_none) {  // Proxy - None.
1496             // Update the proxy mode.
1497             proxyMode = ProxyHelper.NONE;
1498
1499             // Apply the proxy mode.
1500             applyProxy(true);
1501
1502             // Consume the event.
1503             return true;
1504         } else if (menuItemId == R.id.proxy_tor) {  // Proxy - Tor.
1505             // Update the proxy mode.
1506             proxyMode = ProxyHelper.TOR;
1507
1508             // Apply the proxy mode.
1509             applyProxy(true);
1510
1511             // Consume the event.
1512             return true;
1513         } else if (menuItemId == R.id.proxy_i2p) {  // Proxy - I2P.
1514             // Update the proxy mode.
1515             proxyMode = ProxyHelper.I2P;
1516
1517             // Apply the proxy mode.
1518             applyProxy(true);
1519
1520             // Consume the event.
1521             return true;
1522         } else if (menuItemId == R.id.proxy_custom) {  // Proxy - Custom.
1523             // Update the proxy mode.
1524             proxyMode = ProxyHelper.CUSTOM;
1525
1526             // Apply the proxy mode.
1527             applyProxy(true);
1528
1529             // Consume the event.
1530             return true;
1531         } else if (menuItemId == R.id.user_agent_privacy_browser) {  // User Agent - Privacy Browser.
1532             // Update the user agent.
1533             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[0]);
1534
1535             // Reload the current WebView.
1536             currentWebView.reload();
1537
1538             // Consume the event.
1539             return true;
1540         } else if (menuItemId == R.id.user_agent_webview_default) {  // User Agent - WebView Default.
1541             // Update the user agent.
1542             currentWebView.getSettings().setUserAgentString("");
1543
1544             // Reload the current WebView.
1545             currentWebView.reload();
1546
1547             // Consume the event.
1548             return true;
1549         } else if (menuItemId == R.id.user_agent_firefox_on_android) {  // User Agent - Firefox on Android.
1550             // Update the user agent.
1551             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[2]);
1552
1553             // Reload the current WebView.
1554             currentWebView.reload();
1555
1556             // Consume the event.
1557             return true;
1558         } else if (menuItemId == R.id.user_agent_chrome_on_android) {  // User Agent - Chrome on Android.
1559             // Update the user agent.
1560             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[3]);
1561
1562             // Reload the current WebView.
1563             currentWebView.reload();
1564
1565             // Consume the event.
1566             return true;
1567         } else if (menuItemId == R.id.user_agent_safari_on_ios) {  // User Agent - Safari on iOS.
1568             // Update the user agent.
1569             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[4]);
1570
1571             // Reload the current WebView.
1572             currentWebView.reload();
1573
1574             // Consume the event.
1575             return true;
1576         } else if (menuItemId == R.id.user_agent_firefox_on_linux) {  // User Agent - Firefox on Linux.
1577             // Update the user agent.
1578             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[5]);
1579
1580             // Reload the current WebView.
1581             currentWebView.reload();
1582
1583             // Consume the event.
1584             return true;
1585         } else if (menuItemId == R.id.user_agent_chromium_on_linux) {  // User Agent - Chromium on Linux.
1586             // Update the user agent.
1587             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[6]);
1588
1589             // Reload the current WebView.
1590             currentWebView.reload();
1591
1592             // Consume the event.
1593             return true;
1594         } else if (menuItemId == R.id.user_agent_firefox_on_windows) {  // User Agent - Firefox on Windows.
1595             // Update the user agent.
1596             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[7]);
1597
1598             // Reload the current WebView.
1599             currentWebView.reload();
1600
1601             // Consume the event.
1602             return true;
1603         } else if (menuItemId == R.id.user_agent_chrome_on_windows) {  // User Agent - Chrome on Windows.
1604             // Update the user agent.
1605             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[8]);
1606
1607             // Reload the current WebView.
1608             currentWebView.reload();
1609
1610             // Consume the event.
1611             return true;
1612         } else if (menuItemId == R.id.user_agent_edge_on_windows) {  // User Agent - Edge on Windows.
1613             // Update the user agent.
1614             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[9]);
1615
1616             // Reload the current WebView.
1617             currentWebView.reload();
1618
1619             // Consume the event.
1620             return true;
1621         } else if (menuItemId == R.id.user_agent_internet_explorer_on_windows) {  // User Agent - Internet Explorer on Windows.
1622             // Update the user agent.
1623             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[10]);
1624
1625             // Reload the current WebView.
1626             currentWebView.reload();
1627
1628             // Consume the event.
1629             return true;
1630         } else if (menuItemId == R.id.user_agent_safari_on_macos) {  // User Agent - Safari on macOS.
1631             // Update the user agent.
1632             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[11]);
1633
1634             // Reload the current WebView.
1635             currentWebView.reload();
1636
1637             // Consume the event.
1638             return true;
1639         } else if (menuItemId == R.id.user_agent_custom) {  // User Agent - Custom.
1640             // Update the user agent.
1641             currentWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
1642
1643             // Reload the current WebView.
1644             currentWebView.reload();
1645
1646             // Consume the event.
1647             return true;
1648         } else if (menuItemId == R.id.font_size) {  // Font size.
1649             // Instantiate the font size dialog.
1650             DialogFragment fontSizeDialogFragment = FontSizeDialog.displayDialog(currentWebView.getSettings().getTextZoom());
1651
1652             // Show the font size dialog.
1653             fontSizeDialogFragment.show(getSupportFragmentManager(), getString(R.string.font_size));
1654
1655             // Consume the event.
1656             return true;
1657         } else if (menuItemId == R.id.swipe_to_refresh) {  // Swipe to refresh.
1658             // Toggle the stored status of swipe to refresh.
1659             currentWebView.setSwipeToRefresh(!currentWebView.getSwipeToRefresh());
1660
1661             // Update the swipe refresh layout.
1662             if (currentWebView.getSwipeToRefresh()) {  // Swipe to refresh is enabled.
1663                 // Only enable the swipe refresh layout if the WebView is scrolled to the top.  It is updated every time the scroll changes.
1664                 swipeRefreshLayout.setEnabled(currentWebView.getScrollY() == 0);
1665             } else {  // Swipe to refresh is disabled.
1666                 // Disable the swipe refresh layout.
1667                 swipeRefreshLayout.setEnabled(false);
1668             }
1669
1670             // Consume the event.
1671             return true;
1672         } else if (menuItemId == R.id.wide_viewport) {  // Wide viewport.
1673             // Toggle the viewport.
1674             currentWebView.getSettings().setUseWideViewPort(!currentWebView.getSettings().getUseWideViewPort());
1675
1676             // Consume the event.
1677             return true;
1678         } else if (menuItemId == R.id.display_images) {  // Display images.
1679             // Toggle the displaying of images.
1680             if (currentWebView.getSettings().getLoadsImagesAutomatically()) {  // Images are currently loaded automatically.
1681                 // Disable loading of images.
1682                 currentWebView.getSettings().setLoadsImagesAutomatically(false);
1683
1684                 // Reload the website to remove existing images.
1685                 currentWebView.reload();
1686             } else {  // Images are not currently loaded automatically.
1687                 // Enable loading of images.  Missing images will be loaded without the need for a reload.
1688                 currentWebView.getSettings().setLoadsImagesAutomatically(true);
1689             }
1690
1691             // Consume the event.
1692             return true;
1693         } else if (menuItemId == R.id.dark_webview) {  // Dark WebView.
1694             // Check to see if dark WebView is supported by this WebView.
1695             if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
1696                 // Toggle the dark WebView setting.
1697                 if (WebSettingsCompat.getForceDark(currentWebView.getSettings()) == WebSettingsCompat.FORCE_DARK_ON) {  // Dark WebView is currently enabled.
1698                     // Turn off dark WebView.
1699                     WebSettingsCompat.setForceDark(currentWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
1700                 } else {  // Dark WebView is currently disabled.
1701                     // Turn on dark WebView.
1702                     WebSettingsCompat.setForceDark(currentWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
1703                 }
1704             }
1705
1706             // Consume the event.
1707             return true;
1708         } else if (menuItemId == R.id.find_on_page) {  // Find on page.
1709             // Get a handle for the views.
1710             Toolbar toolbar = findViewById(R.id.toolbar);
1711             LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
1712             EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
1713
1714             // Set the minimum height of the find on page linear layout to match the toolbar.
1715             findOnPageLinearLayout.setMinimumHeight(toolbar.getHeight());
1716
1717             // Hide the toolbar.
1718             toolbar.setVisibility(View.GONE);
1719
1720             // Show the find on page linear layout.
1721             findOnPageLinearLayout.setVisibility(View.VISIBLE);
1722
1723             // Display the keyboard.  The app must wait 200 ms before running the command to work around a bug in Android.
1724             // http://stackoverflow.com/questions/5520085/android-show-softkeyboard-with-showsoftinput-is-not-working
1725             findOnPageEditText.postDelayed(() -> {
1726                 // Set the focus on the find on page edit text.
1727                 findOnPageEditText.requestFocus();
1728
1729                 // Get a handle for the input method manager.
1730                 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
1731
1732                 // Remove the lint warning below that the input method manager might be null.
1733                 assert inputMethodManager != null;
1734
1735                 // Display the keyboard.  `0` sets no input flags.
1736                 inputMethodManager.showSoftInput(findOnPageEditText, 0);
1737             }, 200);
1738
1739             // Consume the event.
1740             return true;
1741         } else if (menuItemId == R.id.print) {  // Print.
1742             // Get a print manager instance.
1743             PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);
1744
1745             // Remove the lint error below that print manager might be null.
1746             assert printManager != null;
1747
1748             // Create a print document adapter from the current WebView.
1749             PrintDocumentAdapter printDocumentAdapter = currentWebView.createPrintDocumentAdapter();
1750
1751             // Print the document.
1752             printManager.print(getString(R.string.privacy_browser_webpage), printDocumentAdapter, null);
1753
1754             // Consume the event.
1755             return true;
1756         } else if (menuItemId == R.id.save_url) {  // Save URL.
1757             // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
1758             new PrepareSaveDialog(this, this, getSupportFragmentManager(), SaveWebpageDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
1759                     currentWebView.getAcceptFirstPartyCookies()).execute(currentWebView.getCurrentUrl());
1760
1761             // Consume the event.
1762             return true;
1763         } else if (menuItemId == R.id.save_archive) {
1764             // Instantiate the save dialog.
1765             DialogFragment saveArchiveFragment = SaveWebpageDialog.saveWebpage(SaveWebpageDialog.SAVE_ARCHIVE, currentWebView.getCurrentUrl(), null, null, null,
1766                     false);
1767
1768             // Show the save dialog.  It must be named `save_dialog` so that the file picker can update the file name.
1769             saveArchiveFragment.show(getSupportFragmentManager(), getString(R.string.save_dialog));
1770
1771             return true;
1772         } else if (menuItemId == R.id.save_image) {  // Save image.
1773             // Instantiate the save dialog.
1774             DialogFragment saveImageFragment = SaveWebpageDialog.saveWebpage(SaveWebpageDialog.SAVE_IMAGE, currentWebView.getCurrentUrl(), null, null, null,
1775                     false);
1776
1777             // Show the save dialog.  It must be named `save_dialog` so that the file picker can update the file name.
1778             saveImageFragment.show(getSupportFragmentManager(), getString(R.string.save_dialog));
1779
1780             // Consume the event.
1781             return true;
1782         } else if (menuItemId == R.id.add_to_homescreen) {  // Add to homescreen.
1783             // Instantiate the create home screen shortcut dialog.
1784             DialogFragment createHomeScreenShortcutDialogFragment = CreateHomeScreenShortcutDialog.createDialog(currentWebView.getTitle(), currentWebView.getUrl(),
1785                     currentWebView.getFavoriteOrDefaultIcon());
1786
1787             // Show the create home screen shortcut dialog.
1788             createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getString(R.string.create_shortcut));
1789
1790             // Consume the event.
1791             return true;
1792         } else if (menuItemId == R.id.view_source) {  // View source.
1793             // Create an intent to launch the view source activity.
1794             Intent viewSourceIntent = new Intent(this, ViewSourceActivity.class);
1795
1796             // Add the variables to the intent.
1797             viewSourceIntent.putExtra(ViewSourceActivityKt.CURRENT_URL, currentWebView.getUrl());
1798             viewSourceIntent.putExtra(ViewSourceActivityKt.USER_AGENT, currentWebView.getSettings().getUserAgentString());
1799
1800             // Make it so.
1801             startActivity(viewSourceIntent);
1802
1803             // Consume the event.
1804             return true;
1805         } else if (menuItemId == R.id.share_url) {  // Share URL.
1806             // Setup the share string.
1807             String shareString = currentWebView.getTitle() + " – " + currentWebView.getUrl();
1808
1809             // Create the share intent.
1810             Intent shareIntent = new Intent(Intent.ACTION_SEND);
1811
1812             // Add the share string to the intent.
1813             shareIntent.putExtra(Intent.EXTRA_TEXT, shareString);
1814
1815             // Set the MIME type.
1816             shareIntent.setType("text/plain");
1817
1818             // Set the intent to open in a new task.
1819             shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1820
1821             // Make it so.
1822             startActivity(Intent.createChooser(shareIntent, getString(R.string.share_url)));
1823
1824             // Consume the event.
1825             return true;
1826         } else if (menuItemId == R.id.open_with_app) {  // Open with app.
1827             // Open the URL with an outside app.
1828             openWithApp(currentWebView.getUrl());
1829
1830             // Consume the event.
1831             return true;
1832         } else if (menuItemId == R.id.open_with_browser) {  // Open with browser.
1833             // Open the URL with an outside browser.
1834             openWithBrowser(currentWebView.getUrl());
1835
1836             // Consume the event.
1837             return true;
1838         } else if (menuItemId == R.id.add_or_edit_domain) {  // Add or edit domain.
1839             // Check if domain settings currently exist.
1840             if (currentWebView.getDomainSettingsApplied()) {  // Edit the current domain settings.
1841                 // Reapply the domain settings on returning to `MainWebViewActivity`.
1842                 reapplyDomainSettingsOnRestart = true;
1843
1844                 // Create an intent to launch the domains activity.
1845                 Intent domainsIntent = new Intent(this, DomainsActivity.class);
1846
1847                 // Add the extra information to the intent.
1848                 domainsIntent.putExtra("load_domain", currentWebView.getDomainSettingsDatabaseId());
1849                 domainsIntent.putExtra("close_on_back", true);
1850                 domainsIntent.putExtra("current_url", currentWebView.getUrl());
1851
1852                 // Get the current certificate.
1853                 SslCertificate sslCertificate = currentWebView.getCertificate();
1854
1855                 // Check to see if the SSL certificate is populated.
1856                 if (sslCertificate != null) {
1857                     // Extract the certificate to strings.
1858                     String issuedToCName = sslCertificate.getIssuedTo().getCName();
1859                     String issuedToOName = sslCertificate.getIssuedTo().getOName();
1860                     String issuedToUName = sslCertificate.getIssuedTo().getUName();
1861                     String issuedByCName = sslCertificate.getIssuedBy().getCName();
1862                     String issuedByOName = sslCertificate.getIssuedBy().getOName();
1863                     String issuedByUName = sslCertificate.getIssuedBy().getUName();
1864                     long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
1865                     long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
1866
1867                     // Add the certificate to the intent.
1868                     domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
1869                     domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
1870                     domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
1871                     domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
1872                     domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
1873                     domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
1874                     domainsIntent.putExtra("ssl_start_date", startDateLong);
1875                     domainsIntent.putExtra("ssl_end_date", endDateLong);
1876                 }
1877
1878                 // Check to see if the current IP addresses have been received.
1879                 if (currentWebView.hasCurrentIpAddresses()) {
1880                     // Add the current IP addresses to the intent.
1881                     domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
1882                 }
1883
1884                 // Make it so.
1885                 startActivity(domainsIntent);
1886             } else {  // Add a new domain.
1887                 // Apply the new domain settings on returning to `MainWebViewActivity`.
1888                 reapplyDomainSettingsOnRestart = true;
1889
1890                 // Get the current domain
1891                 Uri currentUri = Uri.parse(currentWebView.getUrl());
1892                 String currentDomain = currentUri.getHost();
1893
1894                 // Initialize the database handler.  The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
1895                 DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
1896
1897                 // Create the domain and store the database ID.
1898                 int newDomainDatabaseId = domainsDatabaseHelper.addDomain(currentDomain);
1899
1900                 // Create an intent to launch the domains activity.
1901                 Intent domainsIntent = new Intent(this, DomainsActivity.class);
1902
1903                 // Add the extra information to the intent.
1904                 domainsIntent.putExtra("load_domain", newDomainDatabaseId);
1905                 domainsIntent.putExtra("close_on_back", true);
1906                 domainsIntent.putExtra("current_url", currentWebView.getUrl());
1907
1908                 // Get the current certificate.
1909                 SslCertificate sslCertificate = currentWebView.getCertificate();
1910
1911                 // Check to see if the SSL certificate is populated.
1912                 if (sslCertificate != null) {
1913                     // Extract the certificate to strings.
1914                     String issuedToCName = sslCertificate.getIssuedTo().getCName();
1915                     String issuedToOName = sslCertificate.getIssuedTo().getOName();
1916                     String issuedToUName = sslCertificate.getIssuedTo().getUName();
1917                     String issuedByCName = sslCertificate.getIssuedBy().getCName();
1918                     String issuedByOName = sslCertificate.getIssuedBy().getOName();
1919                     String issuedByUName = sslCertificate.getIssuedBy().getUName();
1920                     long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
1921                     long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
1922
1923                     // Add the certificate to the intent.
1924                     domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
1925                     domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
1926                     domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
1927                     domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
1928                     domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
1929                     domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
1930                     domainsIntent.putExtra("ssl_start_date", startDateLong);
1931                     domainsIntent.putExtra("ssl_end_date", endDateLong);
1932                 }
1933
1934                 // Check to see if the current IP addresses have been received.
1935                 if (currentWebView.hasCurrentIpAddresses()) {
1936                     // Add the current IP addresses to the intent.
1937                     domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
1938                 }
1939
1940                 // Make it so.
1941                 startActivity(domainsIntent);
1942             }
1943
1944             // Consume the event.
1945             return true;
1946         } else if (menuItemId == R.id.ad_consent) {  // Ad consent.
1947             // Instantiate the ad consent dialog.
1948             DialogFragment adConsentDialogFragment = new AdConsentDialog();
1949
1950             // Display the ad consent dialog.
1951             adConsentDialogFragment.show(getSupportFragmentManager(), getString(R.string.ad_consent));
1952
1953             // Consume the event.
1954             return true;
1955         } else {  // There is no match with the options menu.  Pass the event up to the parent method.
1956             // Don't consume the event.
1957             return super.onOptionsItemSelected(menuItem);
1958         }
1959     }
1960
1961     // removeAllCookies is deprecated, but it is required for API < 21.
1962     @Override
1963     public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
1964         // Get a handle for the shared preferences.
1965         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
1966
1967         // Get the menu item ID.
1968         int menuItemId = menuItem.getItemId();
1969
1970         // Run the commands that correspond to the selected menu item.
1971         if (menuItemId == R.id.clear_and_exit) {  // Clear and exit.
1972             // Clear and exit Privacy Browser.
1973             clearAndExit();
1974         } else if (menuItemId == R.id.home) {  // Home.
1975             // Load the homepage.
1976             loadUrl(currentWebView, sharedPreferences.getString("homepage", getString(R.string.homepage_default_value)));
1977         } else if (menuItemId == R.id.back) {  // Back.
1978             // Check if the WebView can go back.
1979             if (currentWebView.canGoBack()) {
1980                 // Get the current web back forward list.
1981                 WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList();
1982
1983                 // Get the previous entry URL.
1984                 String previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.getCurrentIndex() - 1).getUrl();
1985
1986                 // Apply the domain settings.
1987                 applyDomainSettings(currentWebView, previousUrl, false, false, false);
1988
1989                 // Load the previous website in the history.
1990                 currentWebView.goBack();
1991             }
1992         } else if (menuItemId == R.id.forward) {  // Forward.
1993             // Check if the WebView can go forward.
1994             if (currentWebView.canGoForward()) {
1995                 // Get the current web back forward list.
1996                 WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList();
1997
1998                 // Get the next entry URL.
1999                 String nextUrl = webBackForwardList.getItemAtIndex(webBackForwardList.getCurrentIndex() + 1).getUrl();
2000
2001                 // Apply the domain settings.
2002                 applyDomainSettings(currentWebView, nextUrl, false, false, false);
2003
2004                 // Load the next website in the history.
2005                 currentWebView.goForward();
2006             }
2007         } else if (menuItemId == R.id.history) {  // History.
2008             // Instantiate the URL history dialog.
2009             DialogFragment urlHistoryDialogFragment = UrlHistoryDialog.loadBackForwardList(currentWebView.getWebViewFragmentId());
2010
2011             // Show the URL history dialog.
2012             urlHistoryDialogFragment.show(getSupportFragmentManager(), getString(R.string.history));
2013         } else if (menuItemId == R.id.open) {  // Open.
2014             // Instantiate the open file dialog.
2015             DialogFragment openDialogFragment = new OpenDialog();
2016
2017             // Show the open file dialog.
2018             openDialogFragment.show(getSupportFragmentManager(), getString(R.string.open));
2019         } else if (menuItemId == R.id.requests) {  // Requests.
2020             // Populate the resource requests.
2021             RequestsActivity.resourceRequests = currentWebView.getResourceRequests();
2022
2023             // Create an intent to launch the Requests activity.
2024             Intent requestsIntent = new Intent(this, RequestsActivity.class);
2025
2026             // Add the block third-party requests status to the intent.
2027             requestsIntent.putExtra("block_all_third_party_requests", currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
2028
2029             // Make it so.
2030             startActivity(requestsIntent);
2031         } else if (menuItemId == R.id.downloads) {  // Downloads.
2032             // Launch the system Download Manager.
2033             Intent downloadManagerIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
2034
2035             // Launch as a new task so that Download Manager and Privacy Browser show as separate windows in the recent tasks list.
2036             downloadManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2037
2038             // Make it so.
2039             startActivity(downloadManagerIntent);
2040         } else if (menuItemId == R.id.domains) {  // Domains.
2041             // Set the flag to reapply the domain settings on restart when returning from Domain Settings.
2042             reapplyDomainSettingsOnRestart = true;
2043
2044             // Launch the domains activity.
2045             Intent domainsIntent = new Intent(this, DomainsActivity.class);
2046
2047             // Add the extra information to the intent.
2048             domainsIntent.putExtra("current_url", currentWebView.getUrl());
2049
2050             // Get the current certificate.
2051             SslCertificate sslCertificate = currentWebView.getCertificate();
2052
2053             // Check to see if the SSL certificate is populated.
2054             if (sslCertificate != null) {
2055                 // Extract the certificate to strings.
2056                 String issuedToCName = sslCertificate.getIssuedTo().getCName();
2057                 String issuedToOName = sslCertificate.getIssuedTo().getOName();
2058                 String issuedToUName = sslCertificate.getIssuedTo().getUName();
2059                 String issuedByCName = sslCertificate.getIssuedBy().getCName();
2060                 String issuedByOName = sslCertificate.getIssuedBy().getOName();
2061                 String issuedByUName = sslCertificate.getIssuedBy().getUName();
2062                 long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
2063                 long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
2064
2065                 // Add the certificate to the intent.
2066                 domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
2067                 domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
2068                 domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
2069                 domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
2070                 domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
2071                 domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
2072                 domainsIntent.putExtra("ssl_start_date", startDateLong);
2073                 domainsIntent.putExtra("ssl_end_date", endDateLong);
2074             }
2075
2076             // Check to see if the current IP addresses have been received.
2077             if (currentWebView.hasCurrentIpAddresses()) {
2078                 // Add the current IP addresses to the intent.
2079                 domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
2080             }
2081
2082             // Make it so.
2083             startActivity(domainsIntent);
2084         } else if (menuItemId == R.id.settings) {  // Settings.
2085             // Set the flag to reapply app settings on restart when returning from Settings.
2086             reapplyAppSettingsOnRestart = true;
2087
2088             // Set the flag to reapply the domain settings on restart when returning from Settings.
2089             reapplyDomainSettingsOnRestart = true;
2090
2091             // Launch the settings activity.
2092             Intent settingsIntent = new Intent(this, SettingsActivity.class);
2093             startActivity(settingsIntent);
2094         } else if (menuItemId == R.id.import_export) { // Import/Export.
2095             // Create an intent to launch the import/export activity.
2096             Intent importExportIntent = new Intent(this, ImportExportActivity.class);
2097
2098             // Make it so.
2099             startActivity(importExportIntent);
2100         } else if (menuItemId == R.id.logcat) {  // Logcat.
2101             // Create an intent to launch the logcat activity.
2102             Intent logcatIntent = new Intent(this, LogcatActivity.class);
2103
2104             // Make it so.
2105             startActivity(logcatIntent);
2106         } else if (menuItemId == R.id.guide) {  // Guide.
2107             // Create an intent to launch the guide activity.
2108             Intent guideIntent = new Intent(this, GuideActivity.class);
2109
2110             // Make it so.
2111             startActivity(guideIntent);
2112         } else if (menuItemId == R.id.about) {  // About
2113             // Create an intent to launch the about activity.
2114             Intent aboutIntent = new Intent(this, AboutActivity.class);
2115
2116             // Create a string array for the blocklist versions.
2117             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],
2118                     ultraList.get(0).get(0)[0], ultraPrivacy.get(0).get(0)[0]};
2119
2120             // Add the blocklist versions to the intent.
2121             aboutIntent.putExtra("blocklist_versions", blocklistVersions);
2122
2123             // Make it so.
2124             startActivity(aboutIntent);
2125         }
2126
2127         // Close the navigation drawer.
2128         drawerLayout.closeDrawer(GravityCompat.START);
2129         return true;
2130     }
2131
2132     @Override
2133     public void onPostCreate(Bundle savedInstanceState) {
2134         // Run the default commands.
2135         super.onPostCreate(savedInstanceState);
2136
2137         // Sync the state of the DrawerToggle after the default `onRestoreInstanceState()` has finished.  This creates the navigation drawer icon.
2138         actionBarDrawerToggle.syncState();
2139     }
2140
2141     @Override
2142     public void onConfigurationChanged(@NonNull Configuration newConfig) {
2143         // Run the default commands.
2144         super.onConfigurationChanged(newConfig);
2145
2146         // Reload the ad for the free flavor if not in full screen mode.
2147         if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
2148             // Get a handle for the ad view.  This cannot be a class variable because it changes with each ad load.
2149             View adView = findViewById(R.id.adview);
2150
2151             // Reload the ad.  The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
2152             // `getContext()` can be used instead of `getActivity.getApplicationContext()` once the minimum API >= 23.
2153             AdHelper.loadAd(adView, getApplicationContext(), this, getString(R.string.ad_unit_id));
2154         }
2155
2156         // `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:
2157         // https://code.google.com/p/android/issues/detail?id=20493#c8
2158         // ActivityCompat.invalidateOptionsMenu(this);
2159     }
2160
2161     @Override
2162     public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
2163         // Get the hit test result.
2164         final WebView.HitTestResult hitTestResult = currentWebView.getHitTestResult();
2165
2166         // Define the URL strings.
2167         final String imageUrl;
2168         final String linkUrl;
2169
2170         // Get handles for the system managers.
2171         final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
2172
2173         // Remove the lint errors below that the clipboard manager might be null.
2174         assert clipboardManager != null;
2175
2176         // Process the link according to the type.
2177         switch (hitTestResult.getType()) {
2178             // `SRC_ANCHOR_TYPE` is a link.
2179             case WebView.HitTestResult.SRC_ANCHOR_TYPE:
2180                 // Get the target URL.
2181                 linkUrl = hitTestResult.getExtra();
2182
2183                 // Set the target URL as the title of the `ContextMenu`.
2184                 menu.setHeaderTitle(linkUrl);
2185
2186                 // Add an Open in New Tab entry.
2187                 menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
2188                     // Load the link URL in a new tab and move to it.
2189                     addNewTab(linkUrl, true);
2190
2191                     // Consume the event.
2192                     return true;
2193                 });
2194
2195                 // Add an Open in Background entry.
2196                 menu.add(R.string.open_in_background).setOnMenuItemClickListener((MenuItem item) -> {
2197                     // Load the link URL in a new tab but do not move to it.
2198                     addNewTab(linkUrl, false);
2199
2200                     // Consume the event.
2201                     return true;
2202                 });
2203
2204                 // Add an Open with App entry.
2205                 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2206                     openWithApp(linkUrl);
2207
2208                     // Consume the event.
2209                     return true;
2210                 });
2211
2212                 // Add an Open with Browser entry.
2213                 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2214                     openWithBrowser(linkUrl);
2215
2216                     // Consume the event.
2217                     return true;
2218                 });
2219
2220                 // Add a Copy URL entry.
2221                 menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
2222                     // Save the link URL in a `ClipData`.
2223                     ClipData srcAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl);
2224
2225                     // Set the `ClipData` as the clipboard's primary clip.
2226                     clipboardManager.setPrimaryClip(srcAnchorTypeClipData);
2227
2228                     // Consume the event.
2229                     return true;
2230                 });
2231
2232                 // Add a Save URL entry.
2233                 menu.add(R.string.save_url).setOnMenuItemClickListener((MenuItem item) -> {
2234                     // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
2235                     new PrepareSaveDialog(this, this, getSupportFragmentManager(), SaveWebpageDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
2236                             currentWebView.getAcceptFirstPartyCookies()).execute(linkUrl);
2237
2238                     // Consume the event.
2239                     return true;
2240                 });
2241
2242                 // Add an empty Cancel entry, which by default closes the context menu.
2243                 menu.add(R.string.cancel);
2244                 break;
2245
2246             // `IMAGE_TYPE` is an image.
2247             case WebView.HitTestResult.IMAGE_TYPE:
2248                 // Get the image URL.
2249                 imageUrl = hitTestResult.getExtra();
2250
2251                 // Remove the incorrect lint warning below that the image URL might be null.
2252                 assert imageUrl != null;
2253
2254                 // Set the context menu title.
2255                 if (imageUrl.startsWith("data:")) {  // The image data is contained in within the URL, making it exceedingly long.
2256                     // Truncate the image URL before making it the title.
2257                     menu.setHeaderTitle(imageUrl.substring(0, 100));
2258                 } else {  // The image URL does not contain the full image data.
2259                     // Set the image URL as the title of the context menu.
2260                     menu.setHeaderTitle(imageUrl);
2261                 }
2262
2263                 // Add an Open in New Tab entry.
2264                 menu.add(R.string.open_image_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
2265                     // Load the image in a new tab.
2266                     addNewTab(imageUrl, true);
2267
2268                     // Consume the event.
2269                     return true;
2270                 });
2271
2272                 // Add an Open with App entry.
2273                 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2274                     // Open the image URL with an external app.
2275                     openWithApp(imageUrl);
2276
2277                     // Consume the event.
2278                     return true;
2279                 });
2280
2281                 // Add an Open with Browser entry.
2282                 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2283                     // Open the image URL with an external browser.
2284                     openWithBrowser(imageUrl);
2285
2286                     // Consume the event.
2287                     return true;
2288                 });
2289
2290                 // Add a View Image entry.
2291                 menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
2292                     // Load the image in the current tab.
2293                     loadUrl(currentWebView, imageUrl);
2294
2295                     // Consume the event.
2296                     return true;
2297                 });
2298
2299                 // Add a Save Image entry.
2300                 menu.add(R.string.save_image).setOnMenuItemClickListener((MenuItem item) -> {
2301                    // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
2302                     new PrepareSaveDialog(this, this, getSupportFragmentManager(), SaveWebpageDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
2303                             currentWebView.getAcceptFirstPartyCookies()).execute(imageUrl);
2304
2305                     // Consume the event.
2306                     return true;
2307                 });
2308
2309                 // Add a Copy URL entry.
2310                 menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
2311                     // Save the image URL in a clip data.
2312                     ClipData imageTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
2313
2314                     // Set the clip data as the clipboard's primary clip.
2315                     clipboardManager.setPrimaryClip(imageTypeClipData);
2316
2317                     // Consume the event.
2318                     return true;
2319                 });
2320
2321                 // Add an empty Cancel entry, which by default closes the context menu.
2322                 menu.add(R.string.cancel);
2323                 break;
2324
2325             // `SRC_IMAGE_ANCHOR_TYPE` is an image that is also a link.
2326             case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
2327                 // Get the image URL.
2328                 imageUrl = hitTestResult.getExtra();
2329
2330                 // Instantiate a handler.
2331                 Handler handler = new Handler();
2332
2333                 // Get a message from the handler.
2334                 Message message = handler.obtainMessage();
2335
2336                 // Request the image details from the last touched node be returned in the message.
2337                 currentWebView.requestFocusNodeHref(message);
2338
2339                 // Get the link URL from the message data.
2340                 linkUrl = message.getData().getString("url");
2341
2342                 // Set the link URL as the title of the context menu.
2343                 menu.setHeaderTitle(linkUrl);
2344
2345                 // Add an Open in New Tab entry.
2346                 menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
2347                     // Load the link URL in a new tab and move to it.
2348                     addNewTab(linkUrl, true);
2349
2350                     // Consume the event.
2351                     return true;
2352                 });
2353
2354                 // Add an Open in Background entry.
2355                 menu.add(R.string.open_in_background).setOnMenuItemClickListener((MenuItem item) -> {
2356                     // Lod the link URL in a new tab but do not move to it.
2357                     addNewTab(linkUrl, false);
2358
2359                     // Consume the event.
2360                     return true;
2361                 });
2362
2363                 // Add an Open Image in New Tab entry.
2364                 menu.add(R.string.open_image_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
2365                     // Load the image in a new tab and move to it.
2366                     addNewTab(imageUrl, true);
2367
2368                     // Consume the event.
2369                     return true;
2370                 });
2371
2372                 // Add an Open with App entry.
2373                 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2374                     // Open the link URL with an external app.
2375                     openWithApp(linkUrl);
2376
2377                     // Consume the event.
2378                     return true;
2379                 });
2380
2381                 // Add an Open with Browser entry.
2382                 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2383                     // Open the link URL with an external browser.
2384                     openWithBrowser(linkUrl);
2385
2386                     // Consume the event.
2387                     return true;
2388                 });
2389
2390                 // Add a View Image entry.
2391                 menu.add(R.string.view_image).setOnMenuItemClickListener((MenuItem item) -> {
2392                    // View the image in the current tab.
2393                    loadUrl(currentWebView, imageUrl);
2394
2395                    // Consume the event.
2396                    return true;
2397                 });
2398
2399                 // Add a Save Image entry.
2400                 menu.add(R.string.save_image).setOnMenuItemClickListener((MenuItem item) -> {
2401                     // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
2402                     new PrepareSaveDialog(this, this, getSupportFragmentManager(), SaveWebpageDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
2403                             currentWebView.getAcceptFirstPartyCookies()).execute(imageUrl);
2404
2405                     // Consume the event.
2406                     return true;
2407                 });
2408
2409                 // Add a Copy URL entry.
2410                 menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
2411                     // Save the link URL in a clip data.
2412                     ClipData srcImageAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl);
2413
2414                     // Set the clip data as the clipboard's primary clip.
2415                     clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData);
2416
2417                     // Consume the event.
2418                     return true;
2419                 });
2420
2421                 // Add a Save URL entry.
2422                 menu.add(R.string.save_url).setOnMenuItemClickListener((MenuItem item) -> {
2423                     // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
2424                     new PrepareSaveDialog(this, this, getSupportFragmentManager(), SaveWebpageDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
2425                             currentWebView.getAcceptFirstPartyCookies()).execute(linkUrl);
2426
2427                     // Consume the event.
2428                     return true;
2429                 });
2430
2431                 // Add an empty Cancel entry, which by default closes the context menu.
2432                 menu.add(R.string.cancel);
2433                 break;
2434
2435             case WebView.HitTestResult.EMAIL_TYPE:
2436                 // Get the target URL.
2437                 linkUrl = hitTestResult.getExtra();
2438
2439                 // Set the target URL as the title of the `ContextMenu`.
2440                 menu.setHeaderTitle(linkUrl);
2441
2442                 // Add a Write Email entry.
2443                 menu.add(R.string.write_email).setOnMenuItemClickListener(item -> {
2444                     // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
2445                     Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
2446
2447                     // Parse the url and set it as the data for the `Intent`.
2448                     emailIntent.setData(Uri.parse("mailto:" + linkUrl));
2449
2450                     // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser.
2451                     emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2452
2453                     try {
2454                         // Make it so.
2455                         startActivity(emailIntent);
2456                     } catch (ActivityNotFoundException exception) {
2457                         // Display a snackbar.
2458                         Snackbar.make(currentWebView, getString(R.string.error) + "  " + exception, Snackbar.LENGTH_INDEFINITE).show();
2459                     }
2460
2461                     // Consume the event.
2462                     return true;
2463                 });
2464
2465                 // Add a Copy Email Address entry.
2466                 menu.add(R.string.copy_email_address).setOnMenuItemClickListener(item -> {
2467                     // Save the email address in a `ClipData`.
2468                     ClipData srcEmailTypeClipData = ClipData.newPlainText(getString(R.string.email_address), linkUrl);
2469
2470                     // Set the `ClipData` as the clipboard's primary clip.
2471                     clipboardManager.setPrimaryClip(srcEmailTypeClipData);
2472
2473                     // Consume the event.
2474                     return true;
2475                 });
2476
2477                 // Add an empty Cancel entry, which by default closes the context menu.
2478                 menu.add(R.string.cancel);
2479                 break;
2480         }
2481     }
2482
2483     @Override
2484     public void onCreateBookmark(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) {
2485         // Get a handle for the bookmarks list view.
2486         ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
2487
2488         // Get the dialog.
2489         Dialog dialog = dialogFragment.getDialog();
2490
2491         // Remove the incorrect lint warning below that the dialog might be null.
2492         assert dialog != null;
2493
2494         // Get the views from the dialog fragment.
2495         EditText createBookmarkNameEditText = dialog.findViewById(R.id.create_bookmark_name_edittext);
2496         EditText createBookmarkUrlEditText = dialog.findViewById(R.id.create_bookmark_url_edittext);
2497
2498         // Extract the strings from the edit texts.
2499         String bookmarkNameString = createBookmarkNameEditText.getText().toString();
2500         String bookmarkUrlString = createBookmarkUrlEditText.getText().toString();
2501
2502         // Create a favorite icon byte array output stream.
2503         ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
2504
2505         // Convert the favorite icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2506         favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
2507
2508         // Convert the favorite icon byte array stream to a byte array.
2509         byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
2510
2511         // Display the new bookmark below the current items in the (0 indexed) list.
2512         int newBookmarkDisplayOrder = bookmarksListView.getCount();
2513
2514         // Create the bookmark.
2515         bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolder, newBookmarkDisplayOrder, favoriteIconByteArray);
2516
2517         // Update the bookmarks cursor with the current contents of this folder.
2518         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2519
2520         // Update the list view.
2521         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2522
2523         // Scroll to the new bookmark.
2524         bookmarksListView.setSelection(newBookmarkDisplayOrder);
2525     }
2526
2527     @Override
2528     public void onCreateBookmarkFolder(DialogFragment dialogFragment, @NonNull Bitmap favoriteIconBitmap) {
2529         // Get a handle for the bookmarks list view.
2530         ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
2531
2532         // Get the dialog.
2533         Dialog dialog = dialogFragment.getDialog();
2534
2535         // Remove the incorrect lint warning below that the dialog might be null.
2536         assert dialog != null;
2537
2538         // Get handles for the views in the dialog fragment.
2539         EditText folderNameEditText = dialog.findViewById(R.id.folder_name_edittext);
2540         RadioButton defaultIconRadioButton = dialog.findViewById(R.id.default_icon_radiobutton);
2541         ImageView defaultIconImageView = dialog.findViewById(R.id.default_icon_imageview);
2542
2543         // Get new folder name string.
2544         String folderNameString = folderNameEditText.getText().toString();
2545
2546         // Create a folder icon bitmap.
2547         Bitmap folderIconBitmap;
2548
2549         // Set the folder icon bitmap according to the dialog.
2550         if (defaultIconRadioButton.isChecked()) {  // Use the default folder icon.
2551             // Get the default folder icon drawable.
2552             Drawable folderIconDrawable = defaultIconImageView.getDrawable();
2553
2554             // Convert the folder icon drawable to a bitmap drawable.
2555             BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2556
2557             // Convert the folder icon bitmap drawable to a bitmap.
2558             folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2559         } else {  // Use the WebView favorite icon.
2560             // Copy the favorite icon bitmap to the folder icon bitmap.
2561             folderIconBitmap = favoriteIconBitmap;
2562         }
2563
2564         // Create a folder icon byte array output stream.
2565         ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream();
2566
2567         // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2568         folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream);
2569
2570         // Convert the folder icon byte array stream to a byte array.
2571         byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray();
2572
2573         // Move all the bookmarks down one in the display order.
2574         for (int i = 0; i < bookmarksListView.getCount(); i++) {
2575             int databaseId = (int) bookmarksListView.getItemIdAtPosition(i);
2576             bookmarksDatabaseHelper.updateDisplayOrder(databaseId, i + 1);
2577         }
2578
2579         // Create the folder, which will be placed at the top of the `ListView`.
2580         bookmarksDatabaseHelper.createFolder(folderNameString, currentBookmarksFolder, folderIconByteArray);
2581
2582         // Update the bookmarks cursor with the current contents of this folder.
2583         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2584
2585         // Update the `ListView`.
2586         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2587
2588         // Scroll to the new folder.
2589         bookmarksListView.setSelection(0);
2590     }
2591
2592     @Override
2593     public void onSaveBookmarkFolder(DialogFragment dialogFragment, int selectedFolderDatabaseId, @NonNull Bitmap favoriteIconBitmap) {
2594         // Remove the incorrect lint warning below that the dialog fragment might be null.
2595         assert dialogFragment != null;
2596
2597         // Get the dialog.
2598         Dialog dialog = dialogFragment.getDialog();
2599
2600         // Remove the incorrect lint warning below that the dialog might be null.
2601         assert dialog != null;
2602
2603         // Get handles for the views from the dialog.
2604         RadioButton currentFolderIconRadioButton = dialog.findViewById(R.id.current_icon_radiobutton);
2605         RadioButton defaultFolderIconRadioButton = dialog.findViewById(R.id.default_icon_radiobutton);
2606         ImageView defaultFolderIconImageView = dialog.findViewById(R.id.default_icon_imageview);
2607         EditText editFolderNameEditText = dialog.findViewById(R.id.folder_name_edittext);
2608
2609         // Get the new folder name.
2610         String newFolderNameString = editFolderNameEditText.getText().toString();
2611
2612         // Check if the favorite icon has changed.
2613         if (currentFolderIconRadioButton.isChecked()) {  // Only the name has changed.
2614             // Update the name in the database.
2615             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString);
2616         } else if (!currentFolderIconRadioButton.isChecked() && newFolderNameString.equals(oldFolderNameString)) {  // Only the icon has changed.
2617             // Create the new folder icon Bitmap.
2618             Bitmap folderIconBitmap;
2619
2620             // Populate the new folder icon bitmap.
2621             if (defaultFolderIconRadioButton.isChecked()) {
2622                 // Get the default folder icon drawable.
2623                 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
2624
2625                 // Convert the folder icon drawable to a bitmap drawable.
2626                 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2627
2628                 // Convert the folder icon bitmap drawable to a bitmap.
2629                 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2630             } else {  // Use the `WebView` favorite icon.
2631                 // Copy the favorite icon bitmap to the folder icon bitmap.
2632                 folderIconBitmap = favoriteIconBitmap;
2633             }
2634
2635             // Create a folder icon byte array output stream.
2636             ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
2637
2638             // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2639             folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
2640
2641             // Convert the folder icon byte array stream to a byte array.
2642             byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
2643
2644             // Update the folder icon in the database.
2645             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderIconByteArray);
2646         } else {  // The folder icon and the name have changed.
2647             // Get the new folder icon bitmap.
2648             Bitmap folderIconBitmap;
2649             if (defaultFolderIconRadioButton.isChecked()) {
2650                 // Get the default folder icon drawable.
2651                 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
2652
2653                 // Convert the folder icon drawable to a bitmap drawable.
2654                 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2655
2656                 // Convert the folder icon bitmap drawable to a bitmap.
2657                 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2658             } else {  // Use the `WebView` favorite icon.
2659                 // Copy the favorite icon bitmap to the folder icon bitmap.
2660                 folderIconBitmap = favoriteIconBitmap;
2661             }
2662
2663             // Create a folder icon byte array output stream.
2664             ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
2665
2666             // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2667             folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
2668
2669             // Convert the folder icon byte array stream to a byte array.
2670             byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
2671
2672             // Update the folder name and icon in the database.
2673             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, newFolderIconByteArray);
2674         }
2675
2676         // Update the bookmarks cursor with the current contents of this folder.
2677         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2678
2679         // Update the `ListView`.
2680         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2681     }
2682