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