]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java
Add fallbacks to open alternate download managers. https://redmine.stoutner.com/issue...
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / activities / MainWebViewActivity.java
1 /*
2  * Copyright © 2015-2021 Soren Stoutner <soren@stoutner.com>.
3  *
4  * Download cookie code contributed 2017 Hendrik Knackstedt.  Copyright assigned to Soren Stoutner <soren@stoutner.com>.
5  *
6  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
7  *
8  * Privacy Browser is free software: you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation, either version 3 of the License, or
11  * (at your option) any later version.
12  *
13  * Privacy Browser is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with Privacy Browser.  If not, see <http://www.gnu.org/licenses/>.
20  */
21
22 package com.stoutner.privacybrowser.activities;
23
24 import android.annotation.SuppressLint;
25 import android.app.Activity;
26 import android.app.Dialog;
27 import android.app.DownloadManager;
28 import android.app.SearchManager;
29 import android.content.ActivityNotFoundException;
30 import android.content.BroadcastReceiver;
31 import android.content.ClipData;
32 import android.content.ClipboardManager;
33 import android.content.Context;
34 import android.content.Intent;
35 import android.content.IntentFilter;
36 import android.content.SharedPreferences;
37 import android.content.pm.PackageManager;
38 import android.content.res.Configuration;
39 import android.database.Cursor;
40 import android.graphics.Bitmap;
41 import android.graphics.BitmapFactory;
42 import android.graphics.Typeface;
43 import android.graphics.drawable.BitmapDrawable;
44 import android.graphics.drawable.Drawable;
45 import android.net.Uri;
46 import android.net.http.SslCertificate;
47 import android.net.http.SslError;
48 import android.os.AsyncTask;
49 import android.os.Build;
50 import android.os.Bundle;
51 import android.os.Environment;
52 import android.os.Handler;
53 import android.os.Message;
54 import android.preference.PreferenceManager;
55 import android.print.PrintDocumentAdapter;
56 import android.print.PrintManager;
57 import android.provider.DocumentsContract;
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.CheckBox;
88 import android.widget.CursorAdapter;
89 import android.widget.EditText;
90 import android.widget.FrameLayout;
91 import android.widget.ImageView;
92 import android.widget.LinearLayout;
93 import android.widget.ListView;
94 import android.widget.ProgressBar;
95 import android.widget.RadioButton;
96 import android.widget.RelativeLayout;
97 import android.widget.TextView;
98
99 import androidx.annotation.NonNull;
100 import androidx.appcompat.app.ActionBar;
101 import androidx.appcompat.app.ActionBarDrawerToggle;
102 import androidx.appcompat.app.AppCompatActivity;
103 import androidx.appcompat.app.AppCompatDelegate;
104 import androidx.appcompat.widget.Toolbar;
105 import androidx.coordinatorlayout.widget.CoordinatorLayout;
106 import androidx.core.content.res.ResourcesCompat;
107 import androidx.core.view.GravityCompat;
108 import androidx.drawerlayout.widget.DrawerLayout;
109 import androidx.fragment.app.DialogFragment;
110 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
111 import androidx.viewpager.widget.ViewPager;
112 import androidx.webkit.WebSettingsCompat;
113 import androidx.webkit.WebViewFeature;
114
115 import com.google.android.material.appbar.AppBarLayout;
116 import com.google.android.material.floatingactionbutton.FloatingActionButton;
117 import com.google.android.material.navigation.NavigationView;
118 import com.google.android.material.snackbar.Snackbar;
119 import com.google.android.material.tabs.TabLayout;
120
121 import com.stoutner.privacybrowser.BuildConfig;
122 import com.stoutner.privacybrowser.R;
123 import com.stoutner.privacybrowser.adapters.WebViewPagerAdapter;
124 import com.stoutner.privacybrowser.asynctasks.GetHostIpAddresses;
125 import com.stoutner.privacybrowser.asynctasks.PopulateBlocklists;
126 import com.stoutner.privacybrowser.asynctasks.PrepareSaveDialog;
127 import com.stoutner.privacybrowser.asynctasks.SaveUrl;
128 import com.stoutner.privacybrowser.asynctasks.SaveWebpageImage;
129 import com.stoutner.privacybrowser.dialogs.AdConsentDialog;
130 import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog;
131 import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog;
132 import com.stoutner.privacybrowser.dialogs.CreateHomeScreenShortcutDialog;
133 import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDialog;
134 import com.stoutner.privacybrowser.dialogs.FontSizeDialog;
135 import com.stoutner.privacybrowser.dialogs.HttpAuthenticationDialog;
136 import com.stoutner.privacybrowser.dialogs.OpenDialog;
137 import com.stoutner.privacybrowser.dialogs.ProxyNotInstalledDialog;
138 import com.stoutner.privacybrowser.dialogs.PinnedMismatchDialog;
139 import com.stoutner.privacybrowser.dialogs.SaveWebpageDialog;
140 import com.stoutner.privacybrowser.dialogs.SslCertificateErrorDialog;
141 import com.stoutner.privacybrowser.dialogs.UrlHistoryDialog;
142 import com.stoutner.privacybrowser.dialogs.ViewSslCertificateDialog;
143 import com.stoutner.privacybrowser.dialogs.WaitingForProxyDialog;
144 import com.stoutner.privacybrowser.fragments.WebViewTabFragment;
145 import com.stoutner.privacybrowser.helpers.AdHelper;
146 import com.stoutner.privacybrowser.helpers.BlocklistHelper;
147 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper;
148 import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper;
149 import com.stoutner.privacybrowser.helpers.ProxyHelper;
150 import com.stoutner.privacybrowser.views.NestedScrollWebView;
151
152 import java.io.ByteArrayInputStream;
153 import java.io.ByteArrayOutputStream;
154 import java.io.File;
155 import java.io.FileInputStream;
156 import java.io.FileOutputStream;
157 import java.io.IOException;
158 import java.io.InputStream;
159 import java.io.OutputStream;
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, UrlHistoryDialog.NavigateHistoryListener,
180         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     // Define the start activity for result request codes.  The public static entries are accessed from `OpenDialog()` and `SaveWebpageDialog()`.
207     private final int BROWSE_FILE_UPLOAD_REQUEST_CODE = 0;
208     public final static int BROWSE_OPEN_REQUEST_CODE = 1;
209     public final static int BROWSE_SAVE_WEBPAGE_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     // Declare the class views.
319     private FrameLayout rootFrameLayout;
320     private DrawerLayout drawerLayout;
321     private RelativeLayout mainContentRelativeLayout;
322     private AppBarLayout appBarLayout;
323     private Toolbar toolbar;
324     private RelativeLayout urlRelativeLayout;
325     private EditText urlEditText;
326     private ActionBar actionBar;
327     private LinearLayout findOnPageLinearLayout;
328     private LinearLayout tabsLinearLayout;
329     private TabLayout tabLayout;
330     private SwipeRefreshLayout swipeRefreshLayout;
331     private ViewPager webViewPager;
332     private FrameLayout fullScreenVideoFrameLayout;
333
334     // Declare the class menus.
335     private Menu optionsMenu;
336
337     // Declare the class menu items.
338     private MenuItem navigationBackMenuItem;
339     private MenuItem navigationForwardMenuItem;
340     private MenuItem navigationHistoryMenuItem;
341     private MenuItem navigationRequestsMenuItem;
342     private MenuItem optionsPrivacyMenuItem;
343     private MenuItem optionsRefreshMenuItem;
344     private MenuItem optionsFirstPartyCookiesMenuItem;
345     private MenuItem optionsThirdPartyCookiesMenuItem;
346     private MenuItem optionsDomStorageMenuItem;
347     private MenuItem optionsSaveFormDataMenuItem;
348     private MenuItem optionsClearDataMenuItem;
349     private MenuItem optionsClearCookiesMenuItem;
350     private MenuItem optionsClearDomStorageMenuItem;
351     private MenuItem optionsClearFormDataMenuItem;
352     private MenuItem optionsBlocklistsMenuItem;
353     private MenuItem optionsEasyListMenuItem;
354     private MenuItem optionsEasyPrivacyMenuItem;
355     private MenuItem optionsFanboysAnnoyanceListMenuItem;
356     private MenuItem optionsFanboysSocialBlockingListMenuItem;
357     private MenuItem optionsUltraListMenuItem;
358     private MenuItem optionsUltraPrivacyMenuItem;
359     private MenuItem optionsBlockAllThirdPartyRequestsMenuItem;
360     private MenuItem optionsProxyMenuItem;
361     private MenuItem optionsProxyNoneMenuItem;
362     private MenuItem optionsProxyTorMenuItem;
363     private MenuItem optionsProxyI2pMenuItem;
364     private MenuItem optionsProxyCustomMenuItem;
365     private MenuItem optionsUserAgentMenuItem;
366     private MenuItem optionsUserAgentPrivacyBrowserMenuItem;
367     private MenuItem optionsUserAgentWebViewDefaultMenuItem;
368     private MenuItem optionsUserAgentFirefoxOnAndroidMenuItem;
369     private MenuItem optionsUserAgentChromeOnAndroidMenuItem;
370     private MenuItem optionsUserAgentSafariOnIosMenuItem;
371     private MenuItem optionsUserAgentFirefoxOnLinuxMenuItem;
372     private MenuItem optionsUserAgentChromiumOnLinuxMenuItem;
373     private MenuItem optionsUserAgentFirefoxOnWindowsMenuItem;
374     private MenuItem optionsUserAgentChromeOnWindowsMenuItem;
375     private MenuItem optionsUserAgentEdgeOnWindowsMenuItem;
376     private MenuItem optionsUserAgentInternetExplorerOnWindowsMenuItem;
377     private MenuItem optionsUserAgentSafariOnMacosMenuItem;
378     private MenuItem optionsUserAgentCustomMenuItem;
379     private MenuItem optionsSwipeToRefreshMenuItem;
380     private MenuItem optionsWideViewportMenuItem;
381     private MenuItem optionsDisplayImagesMenuItem;
382     private MenuItem optionsDarkWebViewMenuItem;
383     private MenuItem optionsFontSizeMenuItem;
384     private MenuItem optionsAddOrEditDomainMenuItem;
385
386     @Override
387     // Remove the warning about needing to override `performClick()` when using an `OnTouchListener` with `WebView`.
388     @SuppressLint("ClickableViewAccessibility")
389     protected void onCreate(Bundle savedInstanceState) {
390         // Run the default commands.
391         super.onCreate(savedInstanceState);
392
393         // Check to see if the activity has been restarted.
394         if (savedInstanceState != null) {
395             // Store the saved instance state variables.
396             savedStateArrayList = savedInstanceState.getParcelableArrayList(SAVED_STATE_ARRAY_LIST);
397             savedNestedScrollWebViewStateArrayList = savedInstanceState.getParcelableArrayList(SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST);
398             savedTabPosition = savedInstanceState.getInt(SAVED_TAB_POSITION);
399             savedProxyMode = savedInstanceState.getString(PROXY_MODE);
400         }
401
402         // Initialize the default preference values the first time the program is run.  `false` keeps this command from resetting any current preferences back to default.
403         PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
404
405         // Get a handle for the shared preferences.
406         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
407
408         // Get the screenshot preference.
409         String appTheme = sharedPreferences.getString("app_theme", getString(R.string.app_theme_default_value));
410         boolean allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false);
411
412         // Get the theme entry values string array.
413         String[] appThemeEntryValuesStringArray = getResources().getStringArray(R.array.app_theme_entry_values);
414
415         // 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.
416         if (appTheme.equals(appThemeEntryValuesStringArray[1])) {  // The light theme is selected.
417             // Apply the light theme.
418             AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
419         } else if (appTheme.equals(appThemeEntryValuesStringArray[2])) {  // The dark theme is selected.
420             // Apply the dark theme.
421             AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
422         } else {  // The system default theme is selected.
423             if (Build.VERSION.SDK_INT >= 28) {  // The system default theme is supported.
424                 // Follow the system default theme.
425                 AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
426             } else {  // The system default theme is not supported.
427                 // Follow the battery saver mode.
428                 AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY);
429             }
430         }
431
432         // Disable screenshots if not allowed.
433         if (!allowScreenshots) {
434             getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
435         }
436
437         // 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.
438         if (Build.VERSION.SDK_INT >= 21) {
439             WebView.enableSlowWholeDocumentDraw();
440         }
441
442         // Set the theme.
443         setTheme(R.style.PrivacyBrowser);
444
445         // Set the content view.
446         setContentView(R.layout.main_framelayout);
447
448         // Get handles for the views.
449         rootFrameLayout = findViewById(R.id.root_framelayout);
450         drawerLayout = findViewById(R.id.drawerlayout);
451         mainContentRelativeLayout = findViewById(R.id.main_content_relativelayout);
452         appBarLayout = findViewById(R.id.appbar_layout);
453         toolbar = findViewById(R.id.toolbar);
454         findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
455         tabsLinearLayout = findViewById(R.id.tabs_linearlayout);
456         tabLayout = findViewById(R.id.tablayout);
457         swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
458         webViewPager = findViewById(R.id.webviewpager);
459         fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout);
460
461         // Get a handle for the navigation view.
462         NavigationView navigationView = findViewById(R.id.navigationview);
463
464         // Get a handle for the navigation menu.
465         Menu navigationMenu = navigationView.getMenu();
466
467         // Get handles for the navigation menu items.
468         navigationBackMenuItem = navigationMenu.findItem(R.id.back);
469         navigationForwardMenuItem = navigationMenu.findItem(R.id.forward);
470         navigationHistoryMenuItem = navigationMenu.findItem(R.id.history);
471         navigationRequestsMenuItem = navigationMenu.findItem(R.id.requests);
472
473         // Listen for touches on the navigation menu.
474         navigationView.setNavigationItemSelectedListener(this);
475
476         // Get a handle for the app compat delegate.
477         AppCompatDelegate appCompatDelegate = getDelegate();
478
479         // Set the support action bar.
480         appCompatDelegate.setSupportActionBar(toolbar);
481
482         // Get a handle for the action bar.
483         actionBar = appCompatDelegate.getSupportActionBar();
484
485         // Remove the incorrect lint warning below that the action bar might be null.
486         assert actionBar != null;
487
488         // Add the custom layout, which shows the URL text bar.
489         actionBar.setCustomView(R.layout.url_app_bar);
490         actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
491
492         // Get handles for the views in the URL app bar.
493         urlRelativeLayout = findViewById(R.id.url_relativelayout);
494         urlEditText = findViewById(R.id.url_edittext);
495
496         // Create the hamburger icon at the start of the AppBar.
497         actionBarDrawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.open_navigation_drawer, R.string.close_navigation_drawer);
498
499         // Initially disable the sliding drawers.  They will be enabled once the blocklists are loaded.
500         drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
501
502         // Initialize the web view pager adapter.
503         webViewPagerAdapter = new WebViewPagerAdapter(getSupportFragmentManager());
504
505         // Set the pager adapter on the web view pager.
506         webViewPager.setAdapter(webViewPagerAdapter);
507
508         // Store up to 100 tabs in memory.
509         webViewPager.setOffscreenPageLimit(100);
510
511         // Initialize the app.
512         initializeApp();
513
514         // Apply the app settings from the shared preferences.
515         applyAppSettings();
516
517         // Populate the blocklists.
518         populateBlocklists = new PopulateBlocklists(this, this).execute();
519     }
520
521     @Override
522     protected void onNewIntent(Intent intent) {
523         // Run the default commands.
524         super.onNewIntent(intent);
525
526         // Replace the intent that started the app with this one.
527         setIntent(intent);
528
529         // Check to see if the app is being restarted from a saved state.
530         if (savedStateArrayList == null || savedStateArrayList.size() == 0) {  // The activity is not being restarted from a saved state.
531             // Get the information from the intent.
532             String intentAction = intent.getAction();
533             Uri intentUriData = intent.getData();
534             String intentStringExtra = intent.getStringExtra(Intent.EXTRA_TEXT);
535
536             // Determine if this is a web search.
537             boolean isWebSearch = ((intentAction != null) && intentAction.equals(Intent.ACTION_WEB_SEARCH));
538
539             // 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.
540             if (intentUriData != null || intentStringExtra != null || isWebSearch) {
541                 // Get the shared preferences.
542                 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
543
544                 // Create a URL string.
545                 String url;
546
547                 // If the intent action is a web search, perform the search.
548                 if (isWebSearch) {  // The intent is a web search.
549                     // Create an encoded URL string.
550                     String encodedUrlString;
551
552                     // Sanitize the search input and convert it to a search.
553                     try {
554                         encodedUrlString = URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8");
555                     } catch (UnsupportedEncodingException exception) {
556                         encodedUrlString = "";
557                     }
558
559                     // Add the base search URL.
560                     url = searchURL + encodedUrlString;
561                 } else if (intentUriData != null) {  // The intent contains a URL formatted as a URI.
562                     // Set the intent data as the URL.
563                     url = intentUriData.toString();
564                 } else {  // The intent contains a string, which might be a URL.
565                     // Set the intent string as the URL.
566                     url = intentStringExtra;
567                 }
568
569                 // Add a new tab if specified in the preferences.
570                 if (sharedPreferences.getBoolean("open_intents_in_new_tab", true)) {  // Load the URL in a new tab.
571                     // Set the loading new intent flag.
572                     loadingNewIntent = true;
573
574                     // Add a new tab.
575                     addNewTab(url, true);
576                 } else {  // Load the URL in the current tab.
577                     // Make it so.
578                     loadUrl(currentWebView, url);
579                 }
580
581                 // Close the navigation drawer if it is open.
582                 if (drawerLayout.isDrawerVisible(GravityCompat.START)) {
583                     drawerLayout.closeDrawer(GravityCompat.START);
584                 }
585
586                 // Close the bookmarks drawer if it is open.
587                 if (drawerLayout.isDrawerVisible(GravityCompat.END)) {
588                     drawerLayout.closeDrawer(GravityCompat.END);
589                 }
590             }
591         }
592     }
593
594     @Override
595     public void onRestart() {
596         // Run the default commands.
597         super.onRestart();
598
599         // Apply the app settings if returning from the Settings activity.
600         if (reapplyAppSettingsOnRestart) {
601             // Reset the reapply app settings on restart tracker.
602             reapplyAppSettingsOnRestart = false;
603
604             // Apply the app settings.
605             applyAppSettings();
606         }
607
608         // Apply the domain settings if returning from the settings or domains activity.
609         if (reapplyDomainSettingsOnRestart) {
610             // Reset the reapply domain settings on restart tracker.
611             reapplyDomainSettingsOnRestart = false;
612
613             // Reapply the domain settings for each tab.
614             for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
615                 // Get the WebView tab fragment.
616                 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
617
618                 // Get the fragment view.
619                 View fragmentView = webViewTabFragment.getView();
620
621                 // Only reload the WebViews if they exist.
622                 if (fragmentView != null) {
623                     // Get the nested scroll WebView from the tab fragment.
624                     NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
625
626                     // Reset the current domain name so the domain settings will be reapplied.
627                     nestedScrollWebView.resetCurrentDomainName();
628
629                     // Reapply the domain settings if the URL is not null, which can happen if an empty tab is active when returning from settings.
630                     if (nestedScrollWebView.getUrl() != null) {
631                         applyDomainSettings(nestedScrollWebView, nestedScrollWebView.getUrl(), false, true, false);
632                     }
633                 }
634             }
635         }
636
637         // Update the bookmarks drawer if returning from the Bookmarks activity.
638         if (restartFromBookmarksActivity) {
639             // Close the bookmarks drawer.
640             drawerLayout.closeDrawer(GravityCompat.END);
641
642             // Reload the bookmarks drawer.
643             loadBookmarksFolder();
644
645             // Reset `restartFromBookmarksActivity`.
646             restartFromBookmarksActivity = false;
647         }
648
649         // Update the privacy icon.  `true` runs `invalidateOptionsMenu` as the last step.  This can be important if the screen was rotated.
650         updatePrivacyIcons(true);
651     }
652
653     // `onStart()` runs after `onCreate()` or `onRestart()`.  This is used instead of `onResume()` so the commands aren't called every time the screen is partially hidden.
654     @Override
655     public void onStart() {
656         // Run the default commands.
657         super.onStart();
658
659         // Resume any WebViews.
660         for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
661             // Get the WebView tab fragment.
662             WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
663
664             // Get the fragment view.
665             View fragmentView = webViewTabFragment.getView();
666
667             // Only resume the WebViews if they exist (they won't when the app is first created).
668             if (fragmentView != null) {
669                 // Get the nested scroll WebView from the tab fragment.
670                 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
671
672                 // Resume the nested scroll WebView.
673                 nestedScrollWebView.onResume();
674             }
675         }
676
677         // Resume the nested scroll WebView JavaScript timers.  This is a global command that resumes JavaScript timers on all WebViews.
678         if (currentWebView != null) {
679             currentWebView.resumeTimers();
680         }
681
682         // Reapply the proxy settings if the system is using a proxy.  This redisplays the appropriate alert dialog.
683         if (!proxyMode.equals(ProxyHelper.NONE)) {
684             applyProxy(false);
685         }
686
687         // Reapply any system UI flags and the ad in the free flavor.
688         if (displayingFullScreenVideo || inFullScreenBrowsingMode) {  // The system is displaying a website or a video in full screen mode.
689             /* Hide the system bars.
690              * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
691              * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
692              * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
693              * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
694              */
695             rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
696                     View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
697         } else if (BuildConfig.FLAVOR.contentEquals("free")) {  // The system in not in full screen mode.
698             // Get a handle for the ad view.  This cannot be a class variable because it changes with each ad load.
699             View adView = findViewById(R.id.adview);
700
701             // Resume the ad.
702             AdHelper.resumeAd(adView);
703         }
704     }
705
706     // `onStop()` runs after `onPause()`.  It is used instead of `onPause()` so the commands are not called every time the screen is partially hidden.
707     @Override
708     public void onStop() {
709         // Run the default commands.
710         super.onStop();
711
712         for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
713             // Get the WebView tab fragment.
714             WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
715
716             // Get the fragment view.
717             View fragmentView = webViewTabFragment.getView();
718
719             // Only pause the WebViews if they exist (they won't when the app is first created).
720             if (fragmentView != null) {
721                 // Get the nested scroll WebView from the tab fragment.
722                 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
723
724                 // Pause the nested scroll WebView.
725                 nestedScrollWebView.onPause();
726             }
727         }
728
729         // Pause the WebView JavaScript timers.  This is a global command that pauses JavaScript on all WebViews.
730         if (currentWebView != null) {
731             currentWebView.pauseTimers();
732         }
733
734         // Pause the ad or it will continue to consume resources in the background on the free flavor.
735         if (BuildConfig.FLAVOR.contentEquals("free")) {
736             // Get a handle for the ad view.  This cannot be a class variable because it changes with each ad load.
737             View adView = findViewById(R.id.adview);
738
739             // Pause the ad.
740             AdHelper.pauseAd(adView);
741         }
742     }
743
744     @Override
745     public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
746         // Run the default commands.
747         super.onSaveInstanceState(savedInstanceState);
748
749         // Create the saved state array lists.
750         ArrayList<Bundle> savedStateArrayList = new ArrayList<>();
751         ArrayList<Bundle> savedNestedScrollWebViewStateArrayList = new ArrayList<>();
752
753         // Get the URLs from each tab.
754         for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
755             // Get the WebView tab fragment.
756             WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
757
758             // Get the fragment view.
759             View fragmentView = webViewTabFragment.getView();
760
761             if (fragmentView != null) {
762                 // Get the nested scroll WebView from the tab fragment.
763                 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
764
765                 // Create saved state bundle.
766                 Bundle savedStateBundle = new Bundle();
767
768                 // Get the current states.
769                 nestedScrollWebView.saveState(savedStateBundle);
770                 Bundle savedNestedScrollWebViewStateBundle = nestedScrollWebView.saveNestedScrollWebViewState();
771
772                 // Store the saved states in the array lists.
773                 savedStateArrayList.add(savedStateBundle);
774                 savedNestedScrollWebViewStateArrayList.add(savedNestedScrollWebViewStateBundle);
775             }
776         }
777
778         // Get the current tab position.
779         int currentTabPosition = tabLayout.getSelectedTabPosition();
780
781         // Store the saved states in the bundle.
782         savedInstanceState.putParcelableArrayList(SAVED_STATE_ARRAY_LIST, savedStateArrayList);
783         savedInstanceState.putParcelableArrayList(SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST, savedNestedScrollWebViewStateArrayList);
784         savedInstanceState.putInt(SAVED_TAB_POSITION, currentTabPosition);
785         savedInstanceState.putString(PROXY_MODE, proxyMode);
786     }
787
788     @Override
789     public void onDestroy() {
790         // Unregister the orbot status broadcast receiver if it exists.
791         if (orbotStatusBroadcastReceiver != null) {
792             this.unregisterReceiver(orbotStatusBroadcastReceiver);
793         }
794
795         // Close the bookmarks cursor if it exists.
796         if (bookmarksCursor != null) {
797             bookmarksCursor.close();
798         }
799
800         // Close the bookmarks database if it exists.
801         if (bookmarksDatabaseHelper != null) {
802             bookmarksDatabaseHelper.close();
803         }
804
805         // Stop populating the blocklists if the AsyncTask is running in the background.
806         if (populateBlocklists != null) {
807             populateBlocklists.cancel(true);
808         }
809
810         // Run the default commands.
811         super.onDestroy();
812     }
813
814     @Override
815     public boolean onCreateOptionsMenu(Menu menu) {
816         // Inflate the menu.  This adds items to the action bar if it is present.
817         getMenuInflater().inflate(R.menu.webview_options_menu, menu);
818
819         // Store a handle for the options menu so it can be used by `onOptionsItemSelected()` and `updatePrivacyIcons()`.
820         optionsMenu = menu;
821
822         // Get handles for the class menu items.
823         optionsPrivacyMenuItem = menu.findItem(R.id.javascript);
824         optionsRefreshMenuItem = menu.findItem(R.id.refresh);
825         optionsFirstPartyCookiesMenuItem = menu.findItem(R.id.first_party_cookies);
826         optionsThirdPartyCookiesMenuItem = menu.findItem(R.id.third_party_cookies);
827         optionsDomStorageMenuItem = menu.findItem(R.id.dom_storage);
828         optionsSaveFormDataMenuItem = menu.findItem(R.id.save_form_data);  // Form data can be removed once the minimum API >= 26.
829         optionsClearDataMenuItem = menu.findItem(R.id.clear_data);
830         optionsClearCookiesMenuItem = menu.findItem(R.id.clear_cookies);
831         optionsClearDomStorageMenuItem = menu.findItem(R.id.clear_dom_storage);
832         optionsClearFormDataMenuItem = menu.findItem(R.id.clear_form_data);  // Form data can be removed once the minimum API >= 26.
833         optionsBlocklistsMenuItem = menu.findItem(R.id.blocklists);
834         optionsEasyListMenuItem = menu.findItem(R.id.easylist);
835         optionsEasyPrivacyMenuItem = menu.findItem(R.id.easyprivacy);
836         optionsFanboysAnnoyanceListMenuItem = menu.findItem(R.id.fanboys_annoyance_list);
837         optionsFanboysSocialBlockingListMenuItem = menu.findItem(R.id.fanboys_social_blocking_list);
838         optionsUltraListMenuItem = menu.findItem(R.id.ultralist);
839         optionsUltraPrivacyMenuItem = menu.findItem(R.id.ultraprivacy);
840         optionsBlockAllThirdPartyRequestsMenuItem = menu.findItem(R.id.block_all_third_party_requests);
841         optionsProxyMenuItem = menu.findItem(R.id.proxy);
842         optionsProxyNoneMenuItem = menu.findItem(R.id.proxy_none);
843         optionsProxyTorMenuItem = menu.findItem(R.id.proxy_tor);
844         optionsProxyI2pMenuItem = menu.findItem(R.id.proxy_i2p);
845         optionsProxyCustomMenuItem = menu.findItem(R.id.proxy_custom);
846         optionsUserAgentMenuItem = menu.findItem(R.id.user_agent);
847         optionsUserAgentPrivacyBrowserMenuItem = menu.findItem(R.id.user_agent_privacy_browser);
848         optionsUserAgentWebViewDefaultMenuItem = menu.findItem(R.id.user_agent_webview_default);
849         optionsUserAgentFirefoxOnAndroidMenuItem = menu.findItem(R.id.user_agent_firefox_on_android);
850         optionsUserAgentChromeOnAndroidMenuItem = menu.findItem(R.id.user_agent_chrome_on_android);
851         optionsUserAgentSafariOnIosMenuItem = menu.findItem(R.id.user_agent_safari_on_ios);
852         optionsUserAgentFirefoxOnLinuxMenuItem = menu.findItem(R.id.user_agent_firefox_on_linux);
853         optionsUserAgentChromiumOnLinuxMenuItem = menu.findItem(R.id.user_agent_chromium_on_linux);
854         optionsUserAgentFirefoxOnWindowsMenuItem = menu.findItem(R.id.user_agent_firefox_on_windows);
855         optionsUserAgentChromeOnWindowsMenuItem = menu.findItem(R.id.user_agent_chrome_on_windows);
856         optionsUserAgentEdgeOnWindowsMenuItem = menu.findItem(R.id.user_agent_edge_on_windows);
857         optionsUserAgentInternetExplorerOnWindowsMenuItem = menu.findItem(R.id.user_agent_internet_explorer_on_windows);
858         optionsUserAgentSafariOnMacosMenuItem = menu.findItem(R.id.user_agent_safari_on_macos);
859         optionsUserAgentCustomMenuItem = menu.findItem(R.id.user_agent_custom);
860         optionsSwipeToRefreshMenuItem = menu.findItem(R.id.swipe_to_refresh);
861         optionsWideViewportMenuItem = menu.findItem(R.id.wide_viewport);
862         optionsDisplayImagesMenuItem = menu.findItem(R.id.display_images);
863         optionsDarkWebViewMenuItem = menu.findItem(R.id.dark_webview);
864         optionsFontSizeMenuItem = menu.findItem(R.id.font_size);
865         optionsAddOrEditDomainMenuItem = menu.findItem(R.id.add_or_edit_domain);
866
867         // Get handles for the method menu items.
868         MenuItem bookmarksMenuItem = menu.findItem(R.id.bookmarks);
869         MenuItem adConsentMenuItem = menu.findItem(R.id.ad_consent);
870
871         // Set the initial status of the privacy icons.  `false` does not call `invalidateOptionsMenu` as the last step.
872         updatePrivacyIcons(false);
873
874         // Only display third-party cookies if API >= 21
875         optionsThirdPartyCookiesMenuItem.setVisible(Build.VERSION.SDK_INT >= 21);
876
877         // Only display the form data menu items if the API < 26.
878         optionsSaveFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
879         optionsClearFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
880
881         // Disable the clear form data menu item if the API >= 26 so that the status of the main Clear Data is calculated correctly.
882         optionsClearFormDataMenuItem.setEnabled(Build.VERSION.SDK_INT < 26);
883
884         // Only display the dark WebView menu item if API >= 21.
885         optionsDarkWebViewMenuItem.setVisible(Build.VERSION.SDK_INT >= 21);
886
887         // Only show Ad Consent if this is the free flavor.
888         adConsentMenuItem.setVisible(BuildConfig.FLAVOR.contentEquals("free"));
889
890         // Get the shared preferences.
891         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
892
893         // Get the dark theme and app bar preferences.
894         boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean(getString(R.string.display_additional_app_bar_icons_key), false);
895
896         // 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.
897         if (displayAdditionalAppBarIcons) {
898             optionsRefreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
899             bookmarksMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
900             optionsFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
901         } else { //Do not display the additional icons.
902             optionsRefreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
903             bookmarksMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
904             optionsFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
905         }
906
907         // Replace Refresh with Stop if a URL is already loading.
908         if (currentWebView != null && currentWebView.getProgress() != 100) {
909             // Set the title.
910             optionsRefreshMenuItem.setTitle(R.string.stop);
911
912             // Set the icon if it is displayed in the app bar.
913             if (displayAdditionalAppBarIcons) {
914                 // Get the current theme status.
915                 int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
916
917                 // Set the icon according to the current theme status.
918                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
919                     optionsRefreshMenuItem.setIcon(R.drawable.close_blue_day);
920                 } else {
921                     optionsRefreshMenuItem.setIcon(R.drawable.close_blue_night);
922                 }
923             }
924         }
925
926         // Done.
927         return true;
928     }
929
930     @Override
931     public boolean onPrepareOptionsMenu(Menu menu) {
932         // Get a handle for the cookie manager.
933         CookieManager cookieManager = CookieManager.getInstance();
934
935         // Initialize the current user agent string and the font size.
936         String currentUserAgent = getString(R.string.user_agent_privacy_browser);
937         int fontSize = 100;
938
939         // 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.
940         if (currentWebView != null) {
941             // Set the add or edit domain text.
942             if (currentWebView.getDomainSettingsApplied()) {
943                 optionsAddOrEditDomainMenuItem.setTitle(R.string.edit_domain_settings);
944             } else {
945                 optionsAddOrEditDomainMenuItem.setTitle(R.string.add_domain_settings);
946             }
947
948             // Get the current user agent from the WebView.
949             currentUserAgent = currentWebView.getSettings().getUserAgentString();
950
951             // Get the current font size from the
952             fontSize = currentWebView.getSettings().getTextZoom();
953
954             // Set the status of the menu item checkboxes.
955             optionsDomStorageMenuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled());
956             optionsSaveFormDataMenuItem.setChecked(currentWebView.getSettings().getSaveFormData());  // Form data can be removed once the minimum API >= 26.
957             optionsEasyListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASYLIST));
958             optionsEasyPrivacyMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASYPRIVACY));
959             optionsFanboysAnnoyanceListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
960             optionsFanboysSocialBlockingListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
961             optionsUltraListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRALIST));
962             optionsUltraPrivacyMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRAPRIVACY));
963             optionsBlockAllThirdPartyRequestsMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
964             optionsSwipeToRefreshMenuItem.setChecked(currentWebView.getSwipeToRefresh());
965             optionsWideViewportMenuItem.setChecked(currentWebView.getSettings().getUseWideViewPort());
966             optionsDisplayImagesMenuItem.setChecked(currentWebView.getSettings().getLoadsImagesAutomatically());
967
968             // Initialize the display names for the blocklists with the number of blocked requests.
969             optionsBlocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
970             optionsEasyListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASYLIST) + " - " + getString(R.string.easylist));
971             optionsEasyPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASYPRIVACY) + " - " + getString(R.string.easyprivacy));
972             optionsFanboysAnnoyanceListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST) + " - " + getString(R.string.fanboys_annoyance_list));
973             optionsFanboysSocialBlockingListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST) + " - " + getString(R.string.fanboys_social_blocking_list));
974             optionsUltraListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.ULTRALIST) + " - " + getString(R.string.ultralist));
975             optionsUltraPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.ULTRAPRIVACY) + " - " + getString(R.string.ultraprivacy));
976             optionsBlockAllThirdPartyRequestsMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " + getString(R.string.block_all_third_party_requests));
977
978             // Only modify third-party cookies if the API >= 21.
979             if (Build.VERSION.SDK_INT >= 21) {
980                 // Set the status of the third-party cookies checkbox.
981                 optionsThirdPartyCookiesMenuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView));
982
983                 // Enable third-party cookies if first-party cookies are enabled.
984                 optionsThirdPartyCookiesMenuItem.setEnabled(cookieManager.acceptCookie());
985             }
986
987             // Enable DOM Storage if JavaScript is enabled.
988             optionsDomStorageMenuItem.setEnabled(currentWebView.getSettings().getJavaScriptEnabled());
989
990             // Set the checkbox status for dark WebView if the WebView supports it.
991             if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
992                 optionsDarkWebViewMenuItem.setChecked(WebSettingsCompat.getForceDark(currentWebView.getSettings()) == WebSettingsCompat.FORCE_DARK_ON);
993             }
994         }
995
996         // Set the checked status of the first party cookies menu item.
997         optionsFirstPartyCookiesMenuItem.setChecked(cookieManager.acceptCookie());
998
999         // Enable Clear Cookies if there are any.
1000         optionsClearCookiesMenuItem.setEnabled(cookieManager.hasCookies());
1001
1002         // 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`.
1003         String privateDataDirectoryString = getApplicationInfo().dataDir;
1004
1005         // Get a count of the number of files in the Local Storage directory.
1006         File localStorageDirectory = new File (privateDataDirectoryString + "/app_webview/Local Storage/");
1007         int localStorageDirectoryNumberOfFiles = 0;
1008         if (localStorageDirectory.exists()) {
1009             // `Objects.requireNonNull` removes a lint warning that `localStorageDirectory.list` might produce a null pointed exception if it is dereferenced.
1010             localStorageDirectoryNumberOfFiles = Objects.requireNonNull(localStorageDirectory.list()).length;
1011         }
1012
1013         // Get a count of the number of files in the IndexedDB directory.
1014         File indexedDBDirectory = new File (privateDataDirectoryString + "/app_webview/IndexedDB");
1015         int indexedDBDirectoryNumberOfFiles = 0;
1016         if (indexedDBDirectory.exists()) {
1017             // `Objects.requireNonNull` removes a lint warning that `indexedDBDirectory.list` might produce a null pointed exception if it is dereferenced.
1018             indexedDBDirectoryNumberOfFiles = Objects.requireNonNull(indexedDBDirectory.list()).length;
1019         }
1020
1021         // Enable Clear DOM Storage if there is any.
1022         optionsClearDomStorageMenuItem.setEnabled(localStorageDirectoryNumberOfFiles > 0 || indexedDBDirectoryNumberOfFiles > 0);
1023
1024         // Enable Clear Form Data is there is any.  This can be removed once the minimum API >= 26.
1025         if (Build.VERSION.SDK_INT < 26) {
1026             // Get the WebView database.
1027             WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
1028
1029             // Enable the clear form data menu item if there is anything to clear.
1030             optionsClearFormDataMenuItem.setEnabled(webViewDatabase.hasFormData());
1031         }
1032
1033         // Enable Clear Data if any of the submenu items are enabled.
1034         optionsClearDataMenuItem.setEnabled(optionsClearCookiesMenuItem.isEnabled() || optionsClearDomStorageMenuItem.isEnabled() || optionsClearFormDataMenuItem.isEnabled());
1035
1036         // Disable Fanboy's Social Blocking List menu item if Fanboy's Annoyance List is checked.
1037         optionsFanboysSocialBlockingListMenuItem.setEnabled(!optionsFanboysAnnoyanceListMenuItem.isChecked());
1038
1039         // Set the proxy title and check the applied proxy.
1040         switch (proxyMode) {
1041             case ProxyHelper.NONE:
1042                 // Set the proxy title.
1043                 optionsProxyMenuItem.setTitle(getString(R.string.proxy) + " - " + getString(R.string.proxy_none));
1044
1045                 // Check the proxy None radio button.
1046                 optionsProxyNoneMenuItem.setChecked(true);
1047                 break;
1048
1049             case ProxyHelper.TOR:
1050                 // Set the proxy title.
1051                 optionsProxyMenuItem.setTitle(getString(R.string.proxy) + " - " + getString(R.string.proxy_tor));
1052
1053                 // Check the proxy Tor radio button.
1054                 optionsProxyTorMenuItem.setChecked(true);
1055                 break;
1056
1057             case ProxyHelper.I2P:
1058                 // Set the proxy title.
1059                 optionsProxyMenuItem.setTitle(getString(R.string.proxy) + " - " + getString(R.string.proxy_i2p));
1060
1061                 // Check the proxy I2P radio button.
1062                 optionsProxyI2pMenuItem.setChecked(true);
1063                 break;
1064
1065             case ProxyHelper.CUSTOM:
1066                 // Set the proxy title.
1067                 optionsProxyMenuItem.setTitle(getString(R.string.proxy) + " - " + getString(R.string.proxy_custom));
1068
1069                 // Check the proxy Custom radio button.
1070                 optionsProxyCustomMenuItem.setChecked(true);
1071                 break;
1072         }
1073
1074         // Select the current user agent menu item.  A switch statement cannot be used because the user agents are not compile time constants.
1075         if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[0])) {  // Privacy Browser.
1076             // Update the user agent menu item title.
1077             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_privacy_browser));
1078
1079             // Select the Privacy Browser radio box.
1080             optionsUserAgentPrivacyBrowserMenuItem.setChecked(true);
1081         } else if (currentUserAgent.equals(webViewDefaultUserAgent)) {  // WebView Default.
1082             // Update the user agent menu item title.
1083             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_webview_default));
1084
1085             // Select the WebView Default radio box.
1086             optionsUserAgentWebViewDefaultMenuItem.setChecked(true);
1087         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[2])) {  // Firefox on Android.
1088             // Update the user agent menu item title.
1089             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_firefox_on_android));
1090
1091             // Select the Firefox on Android radio box.
1092             optionsUserAgentFirefoxOnAndroidMenuItem.setChecked(true);
1093         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[3])) {  // Chrome on Android.
1094             // Update the user agent menu item title.
1095             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_chrome_on_android));
1096
1097             // Select the Chrome on Android radio box.
1098             optionsUserAgentChromeOnAndroidMenuItem.setChecked(true);
1099         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[4])) {  // Safari on iOS.
1100             // Update the user agent menu item title.
1101             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_safari_on_ios));
1102
1103             // Select the Safari on iOS radio box.
1104             optionsUserAgentSafariOnIosMenuItem.setChecked(true);
1105         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[5])) {  // Firefox on Linux.
1106             // Update the user agent menu item title.
1107             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_firefox_on_linux));
1108
1109             // Select the Firefox on Linux radio box.
1110             optionsUserAgentFirefoxOnLinuxMenuItem.setChecked(true);
1111         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[6])) {  // Chromium on Linux.
1112             // Update the user agent menu item title.
1113             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_chromium_on_linux));
1114
1115             // Select the Chromium on Linux radio box.
1116             optionsUserAgentChromiumOnLinuxMenuItem.setChecked(true);
1117         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[7])) {  // Firefox on Windows.
1118             // Update the user agent menu item title.
1119             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_firefox_on_windows));
1120
1121             // Select the Firefox on Windows radio box.
1122             optionsUserAgentFirefoxOnWindowsMenuItem.setChecked(true);
1123         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[8])) {  // Chrome on Windows.
1124             // Update the user agent menu item title.
1125             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_chrome_on_windows));
1126
1127             // Select the Chrome on Windows radio box.
1128             optionsUserAgentChromeOnWindowsMenuItem.setChecked(true);
1129         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[9])) {  // Edge on Windows.
1130             // Update the user agent menu item title.
1131             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_edge_on_windows));
1132
1133             // Select the Edge on Windows radio box.
1134             optionsUserAgentEdgeOnWindowsMenuItem.setChecked(true);
1135         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[10])) {  // Internet Explorer on Windows.
1136             // Update the user agent menu item title.
1137             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_internet_explorer_on_windows));
1138
1139             // Select the Internet on Windows radio box.
1140             optionsUserAgentInternetExplorerOnWindowsMenuItem.setChecked(true);
1141         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[11])) {  // Safari on macOS.
1142             // Update the user agent menu item title.
1143             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_safari_on_macos));
1144
1145             // Select the Safari on macOS radio box.
1146             optionsUserAgentSafariOnMacosMenuItem.setChecked(true);
1147         } else {  // Custom user agent.
1148             // Update the user agent menu item title.
1149             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_custom));
1150
1151             // Select the Custom radio box.
1152             optionsUserAgentCustomMenuItem.setChecked(true);
1153         }
1154
1155         // Set the font size title.
1156         optionsFontSizeMenuItem.setTitle(getString(R.string.font_size) + " - " + fontSize + "%");
1157
1158         // Run all the other default commands.
1159         super.onPrepareOptionsMenu(menu);
1160
1161         // Display the menu.
1162         return true;
1163     }
1164
1165     @Override
1166     public boolean onOptionsItemSelected(MenuItem menuItem) {
1167         // Get a handle for the shared preferences.
1168         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
1169
1170         // Get a handle for the cookie manager.
1171         CookieManager cookieManager = CookieManager.getInstance();
1172
1173         // Get the selected menu item ID.
1174         int menuItemId = menuItem.getItemId();
1175
1176         // Run the commands that correlate to the selected menu item.
1177         if (menuItemId == R.id.javascript) {  // JavaScript.
1178             // Toggle the JavaScript status.
1179             currentWebView.getSettings().setJavaScriptEnabled(!currentWebView.getSettings().getJavaScriptEnabled());
1180
1181             // Update the privacy icon.
1182             updatePrivacyIcons(true);
1183
1184             // Display a `Snackbar`.
1185             if (currentWebView.getSettings().getJavaScriptEnabled()) {  // JavaScrip is enabled.
1186                 Snackbar.make(webViewPager, R.string.javascript_enabled, Snackbar.LENGTH_SHORT).show();
1187             } else if (cookieManager.acceptCookie()) {  // JavaScript is disabled, but first-party cookies are enabled.
1188                 Snackbar.make(webViewPager, R.string.javascript_disabled, Snackbar.LENGTH_SHORT).show();
1189             } else {  // Privacy mode.
1190                 Snackbar.make(webViewPager, R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
1191             }
1192
1193             // Reload the current WebView.
1194             currentWebView.reload();
1195
1196             // Consume the event.
1197             return true;
1198         } else if (menuItemId == R.id.refresh) {  // Refresh.
1199             // Run the command that correlates to the current status of the menu item.
1200             if (menuItem.getTitle().equals(getString(R.string.refresh))) {  // The refresh button was pushed.
1201                 // Reload the current WebView.
1202                 currentWebView.reload();
1203             } else {  // The stop button was pushed.
1204                 // Stop the loading of the WebView.
1205                 currentWebView.stopLoading();
1206             }
1207
1208             // Consume the event.
1209             return true;
1210         } else if (menuItemId == R.id.bookmarks) {  // Bookmarks.
1211             // Open the bookmarks drawer.
1212             drawerLayout.openDrawer(GravityCompat.END);
1213
1214             // Consume the event.
1215             return true;
1216         } else if (menuItemId == R.id.first_party_cookies) {  // First-party cookies.
1217             // Switch the first-party cookie status.
1218             cookieManager.setAcceptCookie(!cookieManager.acceptCookie());
1219
1220             // Store the first-party cookie status.
1221             currentWebView.setAcceptFirstPartyCookies(cookieManager.acceptCookie());
1222
1223             // Update the menu checkbox.
1224             menuItem.setChecked(cookieManager.acceptCookie());
1225
1226             // Update the privacy icon.
1227             updatePrivacyIcons(true);
1228
1229             // Display a snackbar.
1230             if (cookieManager.acceptCookie()) {  // First-party cookies are enabled.
1231                 Snackbar.make(webViewPager, R.string.first_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
1232             } else if (currentWebView.getSettings().getJavaScriptEnabled()) {  // JavaScript is still enabled.
1233                 Snackbar.make(webViewPager, R.string.first_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
1234             } else {  // Privacy mode.
1235                 Snackbar.make(webViewPager, R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
1236             }
1237
1238             // Reload the current WebView.
1239             currentWebView.reload();
1240
1241             // Consume the event.
1242             return true;
1243         } else if (menuItemId == R.id.third_party_cookies) {  // Third-party cookies.
1244             // Only act if the API >= 21.  Otherwise, there are no third-party cookie controls.
1245             if (Build.VERSION.SDK_INT >= 21) {
1246                 // Toggle the status of thirdPartyCookiesEnabled.
1247                 cookieManager.setAcceptThirdPartyCookies(currentWebView, !cookieManager.acceptThirdPartyCookies(currentWebView));
1248
1249                 // Update the menu checkbox.
1250                 menuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView));
1251
1252                 // Display a snackbar.
1253                 if (cookieManager.acceptThirdPartyCookies(currentWebView)) {
1254                     Snackbar.make(webViewPager, R.string.third_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
1255                 } else {
1256                     Snackbar.make(webViewPager, R.string.third_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
1257                 }
1258
1259                 // Reload the current WebView.
1260                 currentWebView.reload();
1261             }
1262
1263             // Consume the event.
1264             return true;
1265         } else if (menuItemId == R.id.dom_storage) {  // DOM storage.
1266             // Toggle the status of domStorageEnabled.
1267             currentWebView.getSettings().setDomStorageEnabled(!currentWebView.getSettings().getDomStorageEnabled());
1268
1269             // Update the menu checkbox.
1270             menuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled());
1271
1272             // Update the privacy icon.
1273             updatePrivacyIcons(true);
1274
1275             // Display a snackbar.
1276             if (currentWebView.getSettings().getDomStorageEnabled()) {
1277                 Snackbar.make(webViewPager, R.string.dom_storage_enabled, Snackbar.LENGTH_SHORT).show();
1278             } else {
1279                 Snackbar.make(webViewPager, R.string.dom_storage_disabled, Snackbar.LENGTH_SHORT).show();
1280             }
1281
1282             // Reload the current WebView.
1283             currentWebView.reload();
1284
1285             // Consume the event.
1286             return true;
1287         } else if (menuItemId == R.id.save_form_data) {  // Form data.  This can be removed once the minimum API >= 26.
1288             // Switch the status of saveFormDataEnabled.
1289             currentWebView.getSettings().setSaveFormData(!currentWebView.getSettings().getSaveFormData());
1290
1291             // Update the menu checkbox.
1292             menuItem.setChecked(currentWebView.getSettings().getSaveFormData());
1293
1294             // Display a snackbar.
1295             if (currentWebView.getSettings().getSaveFormData()) {
1296                 Snackbar.make(webViewPager, R.string.form_data_enabled, Snackbar.LENGTH_SHORT).show();
1297             } else {
1298                 Snackbar.make(webViewPager, R.string.form_data_disabled, Snackbar.LENGTH_SHORT).show();
1299             }
1300
1301             // Update the privacy icon.
1302             updatePrivacyIcons(true);
1303
1304             // Reload the current WebView.
1305             currentWebView.reload();
1306
1307             // Consume the event.
1308             return true;
1309         } else if (menuItemId == R.id.clear_cookies) {  // Clear cookies.
1310             // Create a snackbar.
1311             Snackbar.make(webViewPager, R.string.cookies_deleted, Snackbar.LENGTH_LONG)
1312                     .setAction(R.string.undo, v -> {
1313                         // Do nothing because everything will be handled by `onDismissed()` below.
1314                     })
1315                     .addCallback(new Snackbar.Callback() {
1316                         @Override
1317                         public void onDismissed(Snackbar snackbar, int event) {
1318                             if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) {  // The snackbar was dismissed without the undo button being pushed.
1319                                 // Delete the cookies, which command varies by SDK.
1320                                 if (Build.VERSION.SDK_INT < 21) {
1321                                     cookieManager.removeAllCookie();
1322                                 } else {
1323                                     cookieManager.removeAllCookies(null);
1324                                 }
1325                             }
1326                         }
1327                     })
1328                     .show();
1329
1330             // Consume the event.
1331             return true;
1332         } else if (menuItemId == R.id.clear_dom_storage) {  // Clear DOM storage.
1333             // Create a snackbar.
1334             Snackbar.make(webViewPager, R.string.dom_storage_deleted, Snackbar.LENGTH_LONG)
1335                     .setAction(R.string.undo, v -> {
1336                         // Do nothing because everything will be handled by `onDismissed()` below.
1337                     })
1338                     .addCallback(new Snackbar.Callback() {
1339                         @Override
1340                         public void onDismissed(Snackbar snackbar, int event) {
1341                             if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) {  // The snackbar was dismissed without the undo button being pushed.
1342                                 // Delete the DOM Storage.
1343                                 WebStorage webStorage = WebStorage.getInstance();
1344                                 webStorage.deleteAllData();
1345
1346                                 // Initialize a handler to manually delete the DOM storage files and directories.
1347                                 Handler deleteDomStorageHandler = new Handler();
1348
1349                                 // Setup a runnable to manually delete the DOM storage files and directories.
1350                                 Runnable deleteDomStorageRunnable = () -> {
1351                                     try {
1352                                         // Get a handle for the runtime.
1353                                         Runtime runtime = Runtime.getRuntime();
1354
1355                                         // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
1356                                         // which links to `/data/data/com.stoutner.privacybrowser.standard`.
1357                                         String privateDataDirectoryString = getApplicationInfo().dataDir;
1358
1359                                         // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
1360                                         Process deleteLocalStorageProcess = runtime.exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
1361
1362                                         // Multiple commands must be used because `Runtime.exec()` does not like `*`.
1363                                         Process deleteIndexProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
1364                                         Process deleteQuotaManagerProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
1365                                         Process deleteQuotaManagerJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
1366                                         Process deleteDatabasesProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
1367
1368                                         // Wait for the processes to finish.
1369                                         deleteLocalStorageProcess.waitFor();
1370                                         deleteIndexProcess.waitFor();
1371                                         deleteQuotaManagerProcess.waitFor();
1372                                         deleteQuotaManagerJournalProcess.waitFor();
1373                                         deleteDatabasesProcess.waitFor();
1374                                     } catch (Exception exception) {
1375                                         // Do nothing if an error is thrown.
1376                                     }
1377                                 };
1378
1379                                 // Manually delete the DOM storage files after 200 milliseconds.
1380                                 deleteDomStorageHandler.postDelayed(deleteDomStorageRunnable, 200);
1381                             }
1382                         }
1383                     })
1384                     .show();
1385
1386             // Consume the event.
1387             return true;
1388         } else if (menuItemId == R.id.clear_form_data) {  // Clear form data.  This can be remove once the minimum API >= 26.
1389             // Create a snackbar.
1390             Snackbar.make(webViewPager, R.string.form_data_deleted, Snackbar.LENGTH_LONG)
1391                     .setAction(R.string.undo, v -> {
1392                         // Do nothing because everything will be handled by `onDismissed()` below.
1393                     })
1394                     .addCallback(new Snackbar.Callback() {
1395                         @Override
1396                         public void onDismissed(Snackbar snackbar, int event) {
1397                             if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) {  // The snackbar was dismissed without the undo button being pushed.
1398                                 // Get a handle for the webView database.
1399                                 WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(getApplicationContext());
1400
1401                                 // Delete the form data.
1402                                 webViewDatabase.clearFormData();
1403                             }
1404                         }
1405                     })
1406                     .show();
1407
1408             // Consume the event.
1409             return true;
1410         } else if (menuItemId == R.id.easylist) {  // EasyList.
1411             // Toggle the EasyList status.
1412             currentWebView.enableBlocklist(NestedScrollWebView.EASYLIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.EASYLIST));
1413
1414             // Update the menu checkbox.
1415             menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASYLIST));
1416
1417             // Reload the current WebView.
1418             currentWebView.reload();
1419
1420             // Consume the event.
1421             return true;
1422         } else if (menuItemId == R.id.easyprivacy) {  // EasyPrivacy.
1423             // Toggle the EasyPrivacy status.
1424             currentWebView.enableBlocklist(NestedScrollWebView.EASYPRIVACY, !currentWebView.isBlocklistEnabled(NestedScrollWebView.EASYPRIVACY));
1425
1426             // Update the menu checkbox.
1427             menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASYPRIVACY));
1428
1429             // Reload the current WebView.
1430             currentWebView.reload();
1431
1432             // Consume the event.
1433             return true;
1434         } else if (menuItemId == R.id.fanboys_annoyance_list) {  // Fanboy's Annoyance List.
1435             // Toggle Fanboy's Annoyance List status.
1436             currentWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1437
1438             // Update the menu checkbox.
1439             menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1440
1441             // Update the staus of Fanboy's Social Blocking List.
1442             optionsFanboysSocialBlockingListMenuItem.setEnabled(!currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1443
1444             // Reload the current WebView.
1445             currentWebView.reload();
1446
1447             // Consume the event.
1448             return true;
1449         } else if (menuItemId == R.id.fanboys_social_blocking_list) {  // Fanboy's Social Blocking List.
1450             // Toggle Fanboy's Social Blocking List status.
1451             currentWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
1452
1453             // Update the menu checkbox.
1454             menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
1455
1456             // Reload the current WebView.
1457             currentWebView.reload();
1458
1459             // Consume the event.
1460             return true;
1461         } else if (menuItemId == R.id.ultralist) {  // UltraList.
1462             // Toggle the UltraList status.
1463             currentWebView.enableBlocklist(NestedScrollWebView.ULTRALIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRALIST));
1464
1465             // Update the menu checkbox.
1466             menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRALIST));
1467
1468             // Reload the current WebView.
1469             currentWebView.reload();
1470
1471             // Consume the event.
1472             return true;
1473         } else if (menuItemId == R.id.ultraprivacy) {  // UltraPrivacy.
1474             // Toggle the UltraPrivacy status.
1475             currentWebView.enableBlocklist(NestedScrollWebView.ULTRAPRIVACY, !currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRAPRIVACY));
1476
1477             // Update the menu checkbox.
1478             menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRAPRIVACY));
1479
1480             // Reload the current WebView.
1481             currentWebView.reload();
1482
1483             // Consume the event.
1484             return true;
1485         } else if (menuItemId == R.id.block_all_third_party_requests) {  // Block all third-party requests.
1486             //Toggle the third-party requests blocker status.
1487             currentWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS, !currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
1488
1489             // Update the menu checkbox.
1490             menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
1491
1492             // Reload the current WebView.
1493             currentWebView.reload();
1494
1495             // Consume the event.
1496             return true;
1497         } else if (menuItemId == R.id.proxy_none) {  // Proxy - None.
1498             // Update the proxy mode.
1499             proxyMode = ProxyHelper.NONE;
1500
1501             // Apply the proxy mode.
1502             applyProxy(true);
1503
1504             // Consume the event.
1505             return true;
1506         } else if (menuItemId == R.id.proxy_tor) {  // Proxy - Tor.
1507             // Update the proxy mode.
1508             proxyMode = ProxyHelper.TOR;
1509
1510             // Apply the proxy mode.
1511             applyProxy(true);
1512
1513             // Consume the event.
1514             return true;
1515         } else if (menuItemId == R.id.proxy_i2p) {  // Proxy - I2P.
1516             // Update the proxy mode.
1517             proxyMode = ProxyHelper.I2P;
1518
1519             // Apply the proxy mode.
1520             applyProxy(true);
1521
1522             // Consume the event.
1523             return true;
1524         } else if (menuItemId == R.id.proxy_custom) {  // Proxy - Custom.
1525             // Update the proxy mode.
1526             proxyMode = ProxyHelper.CUSTOM;
1527
1528             // Apply the proxy mode.
1529             applyProxy(true);
1530
1531             // Consume the event.
1532             return true;
1533         } else if (menuItemId == R.id.user_agent_privacy_browser) {  // User Agent - Privacy Browser.
1534             // Update the user agent.
1535             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[0]);
1536
1537             // Reload the current WebView.
1538             currentWebView.reload();
1539
1540             // Consume the event.
1541             return true;
1542         } else if (menuItemId == R.id.user_agent_webview_default) {  // User Agent - WebView Default.
1543             // Update the user agent.
1544             currentWebView.getSettings().setUserAgentString("");
1545
1546             // Reload the current WebView.
1547             currentWebView.reload();
1548
1549             // Consume the event.
1550             return true;
1551         } else if (menuItemId == R.id.user_agent_firefox_on_android) {  // User Agent - Firefox on Android.
1552             // Update the user agent.
1553             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[2]);
1554
1555             // Reload the current WebView.
1556             currentWebView.reload();
1557
1558             // Consume the event.
1559             return true;
1560         } else if (menuItemId == R.id.user_agent_chrome_on_android) {  // User Agent - Chrome on Android.
1561             // Update the user agent.
1562             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[3]);
1563
1564             // Reload the current WebView.
1565             currentWebView.reload();
1566
1567             // Consume the event.
1568             return true;
1569         } else if (menuItemId == R.id.user_agent_safari_on_ios) {  // User Agent - Safari on iOS.
1570             // Update the user agent.
1571             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[4]);
1572
1573             // Reload the current WebView.
1574             currentWebView.reload();
1575
1576             // Consume the event.
1577             return true;
1578         } else if (menuItemId == R.id.user_agent_firefox_on_linux) {  // User Agent - Firefox on Linux.
1579             // Update the user agent.
1580             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[5]);
1581
1582             // Reload the current WebView.
1583             currentWebView.reload();
1584
1585             // Consume the event.
1586             return true;
1587         } else if (menuItemId == R.id.user_agent_chromium_on_linux) {  // User Agent - Chromium on Linux.
1588             // Update the user agent.
1589             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[6]);
1590
1591             // Reload the current WebView.
1592             currentWebView.reload();
1593
1594             // Consume the event.
1595             return true;
1596         } else if (menuItemId == R.id.user_agent_firefox_on_windows) {  // User Agent - Firefox on Windows.
1597             // Update the user agent.
1598             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[7]);
1599
1600             // Reload the current WebView.
1601             currentWebView.reload();
1602
1603             // Consume the event.
1604             return true;
1605         } else if (menuItemId == R.id.user_agent_chrome_on_windows) {  // User Agent - Chrome on Windows.
1606             // Update the user agent.
1607             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[8]);
1608
1609             // Reload the current WebView.
1610             currentWebView.reload();
1611
1612             // Consume the event.
1613             return true;
1614         } else if (menuItemId == R.id.user_agent_edge_on_windows) {  // User Agent - Edge on Windows.
1615             // Update the user agent.
1616             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[9]);
1617
1618             // Reload the current WebView.
1619             currentWebView.reload();
1620
1621             // Consume the event.
1622             return true;
1623         } else if (menuItemId == R.id.user_agent_internet_explorer_on_windows) {  // User Agent - Internet Explorer on Windows.
1624             // Update the user agent.
1625             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[10]);
1626
1627             // Reload the current WebView.
1628             currentWebView.reload();
1629
1630             // Consume the event.
1631             return true;
1632         } else if (menuItemId == R.id.user_agent_safari_on_macos) {  // User Agent - Safari on macOS.
1633             // Update the user agent.
1634             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[11]);
1635
1636             // Reload the current WebView.
1637             currentWebView.reload();
1638
1639             // Consume the event.
1640             return true;
1641         } else if (menuItemId == R.id.user_agent_custom) {  // User Agent - Custom.
1642             // Update the user agent.
1643             currentWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
1644
1645             // Reload the current WebView.
1646             currentWebView.reload();
1647
1648             // Consume the event.
1649             return true;
1650         } else if (menuItemId == R.id.font_size) {  // Font size.
1651             // Instantiate the font size dialog.
1652             DialogFragment fontSizeDialogFragment = FontSizeDialog.displayDialog(currentWebView.getSettings().getTextZoom());
1653
1654             // Show the font size dialog.
1655             fontSizeDialogFragment.show(getSupportFragmentManager(), getString(R.string.font_size));
1656
1657             // Consume the event.
1658             return true;
1659         } else if (menuItemId == R.id.swipe_to_refresh) {  // Swipe to refresh.
1660             // Toggle the stored status of swipe to refresh.
1661             currentWebView.setSwipeToRefresh(!currentWebView.getSwipeToRefresh());
1662
1663             // Update the swipe refresh layout.
1664             if (currentWebView.getSwipeToRefresh()) {  // Swipe to refresh is enabled.
1665                 // Only enable the swipe refresh layout if the WebView is scrolled to the top.  It is updated every time the scroll changes.
1666                 swipeRefreshLayout.setEnabled(currentWebView.getScrollY() == 0);
1667             } else {  // Swipe to refresh is disabled.
1668                 // Disable the swipe refresh layout.
1669                 swipeRefreshLayout.setEnabled(false);
1670             }
1671
1672             // Consume the event.
1673             return true;
1674         } else if (menuItemId == R.id.wide_viewport) {  // Wide viewport.
1675             // Toggle the viewport.
1676             currentWebView.getSettings().setUseWideViewPort(!currentWebView.getSettings().getUseWideViewPort());
1677
1678             // Consume the event.
1679             return true;
1680         } else if (menuItemId == R.id.display_images) {  // Display images.
1681             // Toggle the displaying of images.
1682             if (currentWebView.getSettings().getLoadsImagesAutomatically()) {  // Images are currently loaded automatically.
1683                 // Disable loading of images.
1684                 currentWebView.getSettings().setLoadsImagesAutomatically(false);
1685
1686                 // Reload the website to remove existing images.
1687                 currentWebView.reload();
1688             } else {  // Images are not currently loaded automatically.
1689                 // Enable loading of images.  Missing images will be loaded without the need for a reload.
1690                 currentWebView.getSettings().setLoadsImagesAutomatically(true);
1691             }
1692
1693             // Consume the event.
1694             return true;
1695         } else if (menuItemId == R.id.dark_webview) {  // Dark WebView.
1696             // Check to see if dark WebView is supported by this WebView.
1697             if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
1698                 // Toggle the dark WebView setting.
1699                 if (WebSettingsCompat.getForceDark(currentWebView.getSettings()) == WebSettingsCompat.FORCE_DARK_ON) {  // Dark WebView is currently enabled.
1700                     // Turn off dark WebView.
1701                     WebSettingsCompat.setForceDark(currentWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
1702                 } else {  // Dark WebView is currently disabled.
1703                     // Turn on dark WebView.
1704                     WebSettingsCompat.setForceDark(currentWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
1705                 }
1706             }
1707
1708             // Consume the event.
1709             return true;
1710         } else if (menuItemId == R.id.find_on_page) {  // Find on page.
1711             // Get a handle for the views.
1712             Toolbar toolbar = findViewById(R.id.toolbar);
1713             LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
1714             EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
1715
1716             // Set the minimum height of the find on page linear layout to match the toolbar.
1717             findOnPageLinearLayout.setMinimumHeight(toolbar.getHeight());
1718
1719             // Hide the toolbar.
1720             toolbar.setVisibility(View.GONE);
1721
1722             // Show the find on page linear layout.
1723             findOnPageLinearLayout.setVisibility(View.VISIBLE);
1724
1725             // Display the keyboard.  The app must wait 200 ms before running the command to work around a bug in Android.
1726             // http://stackoverflow.com/questions/5520085/android-show-softkeyboard-with-showsoftinput-is-not-working
1727             findOnPageEditText.postDelayed(() -> {
1728                 // Set the focus on the find on page edit text.
1729                 findOnPageEditText.requestFocus();
1730
1731                 // Get a handle for the input method manager.
1732                 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
1733
1734                 // Remove the lint warning below that the input method manager might be null.
1735                 assert inputMethodManager != null;
1736
1737                 // Display the keyboard.  `0` sets no input flags.
1738                 inputMethodManager.showSoftInput(findOnPageEditText, 0);
1739             }, 200);
1740
1741             // Consume the event.
1742             return true;
1743         } else if (menuItemId == R.id.print) {  // Print.
1744             // Get a print manager instance.
1745             PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);
1746
1747             // Remove the lint error below that print manager might be null.
1748             assert printManager != null;
1749
1750             // Create a print document adapter from the current WebView.
1751             PrintDocumentAdapter printDocumentAdapter = currentWebView.createPrintDocumentAdapter();
1752
1753             // Print the document.
1754             printManager.print(getString(R.string.privacy_browser_webpage), printDocumentAdapter, null);
1755
1756             // Consume the event.
1757             return true;
1758         } else if (menuItemId == R.id.save_url) {  // Save URL.
1759             // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
1760             new PrepareSaveDialog(this, this, getSupportFragmentManager(), SaveWebpageDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
1761                     currentWebView.getAcceptFirstPartyCookies()).execute(currentWebView.getCurrentUrl());
1762
1763             // Consume the event.
1764             return true;
1765         } else if (menuItemId == R.id.save_archive) {
1766             // Instantiate the save dialog.
1767             DialogFragment saveArchiveFragment = SaveWebpageDialog.saveWebpage(SaveWebpageDialog.SAVE_ARCHIVE, currentWebView.getCurrentUrl(), null, null, null,
1768                     false);
1769
1770             // Show the save dialog.  It must be named `save_dialog` so that the file picker can update the file name.
1771             saveArchiveFragment.show(getSupportFragmentManager(), getString(R.string.save_dialog));
1772
1773             return true;
1774         } else if (menuItemId == R.id.save_image) {  // Save image.
1775             // Instantiate the save dialog.
1776             DialogFragment saveImageFragment = SaveWebpageDialog.saveWebpage(SaveWebpageDialog.SAVE_IMAGE, currentWebView.getCurrentUrl(), null, null, null,
1777                     false);
1778
1779             // Show the save dialog.  It must be named `save_dialog` so that the file picker can update the file name.
1780             saveImageFragment.show(getSupportFragmentManager(), getString(R.string.save_dialog));
1781
1782             // Consume the event.
1783             return true;
1784         } else if (menuItemId == R.id.add_to_homescreen) {  // Add to homescreen.
1785             // Instantiate the create home screen shortcut dialog.
1786             DialogFragment createHomeScreenShortcutDialogFragment = CreateHomeScreenShortcutDialog.createDialog(currentWebView.getTitle(), currentWebView.getUrl(),
1787                     currentWebView.getFavoriteOrDefaultIcon());
1788
1789             // Show the create home screen shortcut dialog.
1790             createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getString(R.string.create_shortcut));
1791
1792             // Consume the event.
1793             return true;
1794         } else if (menuItemId == R.id.view_source) {  // View source.
1795             // Create an intent to launch the view source activity.
1796             Intent viewSourceIntent = new Intent(this, ViewSourceActivity.class);
1797
1798             // Add the variables to the intent.
1799             viewSourceIntent.putExtra(ViewSourceActivityKt.CURRENT_URL, currentWebView.getUrl());
1800             viewSourceIntent.putExtra(ViewSourceActivityKt.USER_AGENT, currentWebView.getSettings().getUserAgentString());
1801
1802             // Make it so.
1803             startActivity(viewSourceIntent);
1804
1805             // Consume the event.
1806             return true;
1807         } else if (menuItemId == R.id.share_url) {  // Share URL.
1808             // Setup the share string.
1809             String shareString = currentWebView.getTitle() + " – " + currentWebView.getUrl();
1810
1811             // Create the share intent.
1812             Intent shareIntent = new Intent(Intent.ACTION_SEND);
1813
1814             // Add the share string to the intent.
1815             shareIntent.putExtra(Intent.EXTRA_TEXT, shareString);
1816
1817             // Set the MIME type.
1818             shareIntent.setType("text/plain");
1819
1820             // Set the intent to open in a new task.
1821             shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1822
1823             // Make it so.
1824             startActivity(Intent.createChooser(shareIntent, getString(R.string.share_url)));
1825
1826             // Consume the event.
1827             return true;
1828         } else if (menuItemId == R.id.open_with_app) {  // Open with app.
1829             // Open the URL with an outside app.
1830             openWithApp(currentWebView.getUrl());
1831
1832             // Consume the event.
1833             return true;
1834         } else if (menuItemId == R.id.open_with_browser) {  // Open with browser.
1835             // Open the URL with an outside browser.
1836             openWithBrowser(currentWebView.getUrl());
1837
1838             // Consume the event.
1839             return true;
1840         } else if (menuItemId == R.id.add_or_edit_domain) {  // Add or edit domain.
1841             // Check if domain settings currently exist.
1842             if (currentWebView.getDomainSettingsApplied()) {  // Edit the current domain settings.
1843                 // Reapply the domain settings on returning to `MainWebViewActivity`.
1844                 reapplyDomainSettingsOnRestart = true;
1845
1846                 // Create an intent to launch the domains activity.
1847                 Intent domainsIntent = new Intent(this, DomainsActivity.class);
1848
1849                 // Add the extra information to the intent.
1850                 domainsIntent.putExtra("load_domain", currentWebView.getDomainSettingsDatabaseId());
1851                 domainsIntent.putExtra("close_on_back", true);
1852                 domainsIntent.putExtra("current_url", currentWebView.getUrl());
1853
1854                 // Get the current certificate.
1855                 SslCertificate sslCertificate = currentWebView.getCertificate();
1856
1857                 // Check to see if the SSL certificate is populated.
1858                 if (sslCertificate != null) {
1859                     // Extract the certificate to strings.
1860                     String issuedToCName = sslCertificate.getIssuedTo().getCName();
1861                     String issuedToOName = sslCertificate.getIssuedTo().getOName();
1862                     String issuedToUName = sslCertificate.getIssuedTo().getUName();
1863                     String issuedByCName = sslCertificate.getIssuedBy().getCName();
1864                     String issuedByOName = sslCertificate.getIssuedBy().getOName();
1865                     String issuedByUName = sslCertificate.getIssuedBy().getUName();
1866                     long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
1867                     long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
1868
1869                     // Add the certificate to the intent.
1870                     domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
1871                     domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
1872                     domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
1873                     domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
1874                     domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
1875                     domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
1876                     domainsIntent.putExtra("ssl_start_date", startDateLong);
1877                     domainsIntent.putExtra("ssl_end_date", endDateLong);
1878                 }
1879
1880                 // Check to see if the current IP addresses have been received.
1881                 if (currentWebView.hasCurrentIpAddresses()) {
1882                     // Add the current IP addresses to the intent.
1883                     domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
1884                 }
1885
1886                 // Make it so.
1887                 startActivity(domainsIntent);
1888             } else {  // Add a new domain.
1889                 // Apply the new domain settings on returning to `MainWebViewActivity`.
1890                 reapplyDomainSettingsOnRestart = true;
1891
1892                 // Get the current domain
1893                 Uri currentUri = Uri.parse(currentWebView.getUrl());
1894                 String currentDomain = currentUri.getHost();
1895
1896                 // Initialize the database handler.  The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
1897                 DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
1898
1899                 // Create the domain and store the database ID.
1900                 int newDomainDatabaseId = domainsDatabaseHelper.addDomain(currentDomain);
1901
1902                 // Create an intent to launch the domains activity.
1903                 Intent domainsIntent = new Intent(this, DomainsActivity.class);
1904
1905                 // Add the extra information to the intent.
1906                 domainsIntent.putExtra("load_domain", newDomainDatabaseId);
1907                 domainsIntent.putExtra("close_on_back", true);
1908                 domainsIntent.putExtra("current_url", currentWebView.getUrl());
1909
1910                 // Get the current certificate.
1911                 SslCertificate sslCertificate = currentWebView.getCertificate();
1912
1913                 // Check to see if the SSL certificate is populated.
1914                 if (sslCertificate != null) {
1915                     // Extract the certificate to strings.
1916                     String issuedToCName = sslCertificate.getIssuedTo().getCName();
1917                     String issuedToOName = sslCertificate.getIssuedTo().getOName();
1918                     String issuedToUName = sslCertificate.getIssuedTo().getUName();
1919                     String issuedByCName = sslCertificate.getIssuedBy().getCName();
1920                     String issuedByOName = sslCertificate.getIssuedBy().getOName();
1921                     String issuedByUName = sslCertificate.getIssuedBy().getUName();
1922                     long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
1923                     long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
1924
1925                     // Add the certificate to the intent.
1926                     domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
1927                     domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
1928                     domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
1929                     domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
1930                     domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
1931                     domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
1932                     domainsIntent.putExtra("ssl_start_date", startDateLong);
1933                     domainsIntent.putExtra("ssl_end_date", endDateLong);
1934                 }
1935
1936                 // Check to see if the current IP addresses have been received.
1937                 if (currentWebView.hasCurrentIpAddresses()) {
1938                     // Add the current IP addresses to the intent.
1939                     domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
1940                 }
1941
1942                 // Make it so.
1943                 startActivity(domainsIntent);
1944             }
1945
1946             // Consume the event.
1947             return true;
1948         } else if (menuItemId == R.id.ad_consent) {  // Ad consent.
1949             // Instantiate the ad consent dialog.
1950             DialogFragment adConsentDialogFragment = new AdConsentDialog();
1951
1952             // Display the ad consent dialog.
1953             adConsentDialogFragment.show(getSupportFragmentManager(), getString(R.string.ad_consent));
1954
1955             // Consume the event.
1956             return true;
1957         } else {  // There is no match with the options menu.  Pass the event up to the parent method.
1958             // Don't consume the event.
1959             return super.onOptionsItemSelected(menuItem);
1960         }
1961     }
1962
1963     // removeAllCookies is deprecated, but it is required for API < 21.
1964     @Override
1965     public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
1966         // Get a handle for the shared preferences.
1967         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
1968
1969         // Get the menu item ID.
1970         int menuItemId = menuItem.getItemId();
1971
1972         // Run the commands that correspond to the selected menu item.
1973         if (menuItemId == R.id.clear_and_exit) {  // Clear and exit.
1974             // Clear and exit Privacy Browser.
1975             clearAndExit();
1976         } else if (menuItemId == R.id.home) {  // Home.
1977             // Load the homepage.
1978             loadUrl(currentWebView, sharedPreferences.getString("homepage", getString(R.string.homepage_default_value)));
1979         } else if (menuItemId == R.id.back) {  // Back.
1980             // Check if the WebView can go back.
1981             if (currentWebView.canGoBack()) {
1982                 // Get the current web back forward list.
1983                 WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList();
1984
1985                 // Get the previous entry URL.
1986                 String previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.getCurrentIndex() - 1).getUrl();
1987
1988                 // Apply the domain settings.
1989                 applyDomainSettings(currentWebView, previousUrl, false, false, false);
1990
1991                 // Load the previous website in the history.
1992                 currentWebView.goBack();
1993             }
1994         } else if (menuItemId == R.id.forward) {  // Forward.
1995             // Check if the WebView can go forward.
1996             if (currentWebView.canGoForward()) {
1997                 // Get the current web back forward list.
1998                 WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList();
1999
2000                 // Get the next entry URL.
2001                 String nextUrl = webBackForwardList.getItemAtIndex(webBackForwardList.getCurrentIndex() + 1).getUrl();
2002
2003                 // Apply the domain settings.
2004                 applyDomainSettings(currentWebView, nextUrl, false, false, false);
2005
2006                 // Load the next website in the history.
2007                 currentWebView.goForward();
2008             }
2009         } else if (menuItemId == R.id.history) {  // History.
2010             // Instantiate the URL history dialog.
2011             DialogFragment urlHistoryDialogFragment = UrlHistoryDialog.loadBackForwardList(currentWebView.getWebViewFragmentId());
2012
2013             // Show the URL history dialog.
2014             urlHistoryDialogFragment.show(getSupportFragmentManager(), getString(R.string.history));
2015         } else if (menuItemId == R.id.open) {  // Open.
2016             // Instantiate the open file dialog.
2017             DialogFragment openDialogFragment = new OpenDialog();
2018
2019             // Show the open file dialog.
2020             openDialogFragment.show(getSupportFragmentManager(), getString(R.string.open));
2021         } else if (menuItemId == R.id.requests) {  // Requests.
2022             // Populate the resource requests.
2023             RequestsActivity.resourceRequests = currentWebView.getResourceRequests();
2024
2025             // Create an intent to launch the Requests activity.
2026             Intent requestsIntent = new Intent(this, RequestsActivity.class);
2027
2028             // Add the block third-party requests status to the intent.
2029             requestsIntent.putExtra("block_all_third_party_requests", currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
2030
2031             // Make it so.
2032             startActivity(requestsIntent);
2033         } else if (menuItemId == R.id.downloads) {  // Downloads.
2034             // Try the default system download manager.
2035             try {
2036                 // Launch the default system Download Manager.
2037                 Intent defaultDownloadManagerIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
2038
2039                 // Launch as a new task so that the download manager and Privacy Browser show as separate windows in the recent tasks list.
2040                 defaultDownloadManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2041
2042                 // Make it so.
2043                 startActivity(defaultDownloadManagerIntent);
2044             } catch (Exception defaultDownloadManagerException) {
2045                 // Try a generic file manager.
2046                 try {
2047                     // Create a generic file manager intent.
2048                     Intent genericFileManagerIntent = new Intent(Intent.ACTION_VIEW);
2049
2050                     // Open the download directory.
2051                     genericFileManagerIntent.setDataAndType(Uri.parse(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString()), DocumentsContract.Document.MIME_TYPE_DIR);
2052
2053                     // Launch as a new task so that the file manager and Privacy Browser show as separate windows in the recent tasks list.
2054                     genericFileManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2055
2056                     // Make it so.
2057                     startActivity(genericFileManagerIntent);
2058                 } catch (Exception gengericFileManagerException) {
2059                     // Try an alternate file manager.
2060                     try {
2061                         // Create an alternate file manager intent.
2062                         Intent alternateFileManagerIntent = new Intent(Intent.ACTION_VIEW);
2063
2064                         // Open the download directory.
2065                         alternateFileManagerIntent.setDataAndType(Uri.parse(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString()), "resource/folder");
2066
2067                         // Launch as a new task so that the file manager and Privacy Browser show as separate windows in the recent tasks list.
2068                         alternateFileManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2069
2070                         // Open the alternate file manager.
2071                         startActivity(alternateFileManagerIntent);
2072                     } catch (Exception alternateFileManagerException) {
2073                         // Display a snackbar.
2074                         Snackbar.make(currentWebView, R.string.no_file_manager_detected, Snackbar.LENGTH_INDEFINITE).show();
2075                     }
2076                 }
2077             }
2078         } else if (menuItemId == R.id.domains) {  // Domains.
2079             // Set the flag to reapply the domain settings on restart when returning from Domain Settings.
2080             reapplyDomainSettingsOnRestart = true;
2081
2082             // Launch the domains activity.
2083             Intent domainsIntent = new Intent(this, DomainsActivity.class);
2084
2085             // Add the extra information to the intent.
2086             domainsIntent.putExtra("current_url", currentWebView.getUrl());
2087
2088             // Get the current certificate.
2089             SslCertificate sslCertificate = currentWebView.getCertificate();
2090
2091             // Check to see if the SSL certificate is populated.
2092             if (sslCertificate != null) {
2093                 // Extract the certificate to strings.
2094                 String issuedToCName = sslCertificate.getIssuedTo().getCName();
2095                 String issuedToOName = sslCertificate.getIssuedTo().getOName();
2096                 String issuedToUName = sslCertificate.getIssuedTo().getUName();
2097                 String issuedByCName = sslCertificate.getIssuedBy().getCName();
2098                 String issuedByOName = sslCertificate.getIssuedBy().getOName();
2099                 String issuedByUName = sslCertificate.getIssuedBy().getUName();
2100                 long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
2101                 long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
2102
2103                 // Add the certificate to the intent.
2104                 domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
2105                 domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
2106                 domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
2107                 domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
2108                 domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
2109                 domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
2110                 domainsIntent.putExtra("ssl_start_date", startDateLong);
2111                 domainsIntent.putExtra("ssl_end_date", endDateLong);
2112             }
2113
2114             // Check to see if the current IP addresses have been received.
2115             if (currentWebView.hasCurrentIpAddresses()) {
2116                 // Add the current IP addresses to the intent.
2117                 domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
2118             }
2119
2120             // Make it so.
2121             startActivity(domainsIntent);
2122         } else if (menuItemId == R.id.settings) {  // Settings.
2123             // Set the flag to reapply app settings on restart when returning from Settings.
2124             reapplyAppSettingsOnRestart = true;
2125
2126             // Set the flag to reapply the domain settings on restart when returning from Settings.
2127             reapplyDomainSettingsOnRestart = true;
2128
2129             // Launch the settings activity.
2130             Intent settingsIntent = new Intent(this, SettingsActivity.class);
2131             startActivity(settingsIntent);
2132         } else if (menuItemId == R.id.import_export) { // Import/Export.
2133             // Create an intent to launch the import/export activity.
2134             Intent importExportIntent = new Intent(this, ImportExportActivity.class);
2135
2136             // Make it so.
2137             startActivity(importExportIntent);
2138         } else if (menuItemId == R.id.logcat) {  // Logcat.
2139             // Create an intent to launch the logcat activity.
2140             Intent logcatIntent = new Intent(this, LogcatActivity.class);
2141
2142             // Make it so.
2143             startActivity(logcatIntent);
2144         } else if (menuItemId == R.id.guide) {  // Guide.
2145             // Create an intent to launch the guide activity.
2146             Intent guideIntent = new Intent(this, GuideActivity.class);
2147
2148             // Make it so.
2149             startActivity(guideIntent);
2150         } else if (menuItemId == R.id.about) {  // About
2151             // Create an intent to launch the about activity.
2152             Intent aboutIntent = new Intent(this, AboutActivity.class);
2153
2154             // Create a string array for the blocklist versions.
2155             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],
2156                     ultraList.get(0).get(0)[0], ultraPrivacy.get(0).get(0)[0]};
2157
2158             // Add the blocklist versions to the intent.
2159             aboutIntent.putExtra("blocklist_versions", blocklistVersions);
2160
2161             // Make it so.
2162             startActivity(aboutIntent);
2163         }
2164
2165         // Close the navigation drawer.
2166         drawerLayout.closeDrawer(GravityCompat.START);
2167         return true;
2168     }
2169
2170     @Override
2171     public void onPostCreate(Bundle savedInstanceState) {
2172         // Run the default commands.
2173         super.onPostCreate(savedInstanceState);
2174
2175         // Sync the state of the DrawerToggle after the default `onRestoreInstanceState()` has finished.  This creates the navigation drawer icon.
2176         actionBarDrawerToggle.syncState();
2177     }
2178
2179     @Override
2180     public void onConfigurationChanged(@NonNull Configuration newConfig) {
2181         // Run the default commands.
2182         super.onConfigurationChanged(newConfig);
2183
2184         // Reload the ad for the free flavor if not in full screen mode.
2185         if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
2186             // Get a handle for the ad view.  This cannot be a class variable because it changes with each ad load.
2187             View adView = findViewById(R.id.adview);
2188
2189             // Reload the ad.  The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
2190             // `getContext()` can be used instead of `getActivity.getApplicationContext()` once the minimum API >= 23.
2191             AdHelper.loadAd(adView, getApplicationContext(), this, getString(R.string.ad_unit_id));
2192         }
2193
2194         // `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:
2195         // https://code.google.com/p/android/issues/detail?id=20493#c8
2196         // ActivityCompat.invalidateOptionsMenu(this);
2197     }
2198
2199     @Override
2200     public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
2201         // Get the hit test result.
2202         final WebView.HitTestResult hitTestResult = currentWebView.getHitTestResult();
2203
2204         // Define the URL strings.
2205         final String imageUrl;
2206         final String linkUrl;
2207
2208         // Get handles for the system managers.
2209         final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
2210
2211         // Remove the lint errors below that the clipboard manager might be null.
2212         assert clipboardManager != null;
2213
2214         // Process the link according to the type.
2215         switch (hitTestResult.getType()) {
2216             // `SRC_ANCHOR_TYPE` is a link.
2217             case WebView.HitTestResult.SRC_ANCHOR_TYPE:
2218                 // Get the target URL.
2219                 linkUrl = hitTestResult.getExtra();
2220
2221                 // Set the target URL as the title of the `ContextMenu`.
2222                 menu.setHeaderTitle(linkUrl);
2223
2224                 // Add an Open in New Tab entry.
2225                 menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
2226                     // Load the link URL in a new tab and move to it.
2227                     addNewTab(linkUrl, true);
2228
2229                     // Consume the event.
2230                     return true;
2231                 });
2232
2233                 // Add an Open in Background entry.
2234                 menu.add(R.string.open_in_background).setOnMenuItemClickListener((MenuItem item) -> {
2235                     // Load the link URL in a new tab but do not move to it.
2236                     addNewTab(linkUrl, false);
2237
2238                     // Consume the event.
2239                     return true;
2240                 });
2241
2242                 // Add an Open with App entry.
2243                 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2244                     openWithApp(linkUrl);
2245
2246                     // Consume the event.
2247                     return true;
2248                 });
2249
2250                 // Add an Open with Browser entry.
2251                 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2252                     openWithBrowser(linkUrl);
2253
2254                     // Consume the event.
2255                     return true;
2256                 });
2257
2258                 // Add a Copy URL entry.
2259                 menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
2260                     // Save the link URL in a `ClipData`.
2261                     ClipData srcAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl);
2262
2263                     // Set the `ClipData` as the clipboard's primary clip.
2264                     clipboardManager.setPrimaryClip(srcAnchorTypeClipData);
2265
2266                     // Consume the event.
2267                     return true;
2268                 });
2269
2270                 // Add a Save URL entry.
2271                 menu.add(R.string.save_url).setOnMenuItemClickListener((MenuItem item) -> {
2272                     // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
2273                     new PrepareSaveDialog(this, this, getSupportFragmentManager(), SaveWebpageDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
2274                             currentWebView.getAcceptFirstPartyCookies()).execute(linkUrl);
2275
2276                     // Consume the event.
2277                     return true;
2278                 });
2279
2280                 // Add an empty Cancel entry, which by default closes the context menu.
2281                 menu.add(R.string.cancel);
2282                 break;
2283
2284             // `IMAGE_TYPE` is an image.
2285             case WebView.HitTestResult.IMAGE_TYPE:
2286                 // Get the image URL.
2287                 imageUrl = hitTestResult.getExtra();
2288
2289                 // Remove the incorrect lint warning below that the image URL might be null.
2290                 assert imageUrl != null;
2291
2292                 // Set the context menu title.
2293                 if (imageUrl.startsWith("data:")) {  // The image data is contained in within the URL, making it exceedingly long.
2294                     // Truncate the image URL before making it the title.
2295                     menu.setHeaderTitle(imageUrl.substring(0, 100));
2296                 } else {  // The image URL does not contain the full image data.
2297                     // Set the image URL as the title of the context menu.
2298                     menu.setHeaderTitle(imageUrl);
2299                 }
2300
2301                 // Add an Open in New Tab entry.
2302                 menu.add(R.string.open_image_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
2303                     // Load the image in a new tab.
2304                     addNewTab(imageUrl, true);
2305
2306                     // Consume the event.
2307                     return true;
2308                 });
2309
2310                 // Add an Open with App entry.
2311                 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2312                     // Open the image URL with an external app.
2313                     openWithApp(imageUrl);
2314
2315                     // Consume the event.
2316                     return true;
2317                 });
2318
2319                 // Add an Open with Browser entry.
2320                 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2321                     // Open the image URL with an external browser.
2322                     openWithBrowser(imageUrl);
2323
2324                     // Consume the event.
2325                     return true;
2326                 });
2327
2328                 // Add a View Image entry.
2329                 menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
2330                     // Load the image in the current tab.
2331                     loadUrl(currentWebView, imageUrl);
2332
2333                     // Consume the event.
2334                     return true;
2335                 });
2336
2337                 // Add a Save Image entry.
2338                 menu.add(R.string.save_image).setOnMenuItemClickListener((MenuItem item) -> {
2339                    // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
2340                     new PrepareSaveDialog(this, this, getSupportFragmentManager(), SaveWebpageDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
2341                             currentWebView.getAcceptFirstPartyCookies()).execute(imageUrl);
2342
2343                     // Consume the event.
2344                     return true;
2345                 });
2346
2347                 // Add a Copy URL entry.
2348                 menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
2349                     // Save the image URL in a clip data.
2350                     ClipData imageTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
2351
2352                     // Set the clip data as the clipboard's primary clip.
2353                     clipboardManager.setPrimaryClip(imageTypeClipData);
2354
2355                     // Consume the event.
2356                     return true;
2357                 });
2358
2359                 // Add an empty Cancel entry, which by default closes the context menu.
2360                 menu.add(R.string.cancel);
2361                 break;
2362
2363             // `SRC_IMAGE_ANCHOR_TYPE` is an image that is also a link.
2364             case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
2365                 // Get the image URL.
2366                 imageUrl = hitTestResult.getExtra();
2367
2368                 // Instantiate a handler.
2369                 Handler handler = new Handler();
2370
2371                 // Get a message from the handler.
2372                 Message message = handler.obtainMessage();
2373
2374                 // Request the image details from the last touched node be returned in the message.
2375                 currentWebView.requestFocusNodeHref(message);
2376
2377                 // Get the link URL from the message data.
2378                 linkUrl = message.getData().getString("url");
2379
2380                 // Set the link URL as the title of the context menu.
2381                 menu.setHeaderTitle(linkUrl);
2382
2383                 // Add an Open in New Tab entry.
2384                 menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
2385                     // Load the link URL in a new tab and move to it.
2386                     addNewTab(linkUrl, true);
2387
2388                     // Consume the event.
2389                     return true;
2390                 });
2391
2392                 // Add an Open in Background entry.
2393                 menu.add(R.string.open_in_background).setOnMenuItemClickListener((MenuItem item) -> {
2394                     // Lod the link URL in a new tab but do not move to it.
2395                     addNewTab(linkUrl, false);
2396
2397                     // Consume the event.
2398                     return true;
2399                 });
2400
2401                 // Add an Open Image in New Tab entry.
2402                 menu.add(R.string.open_image_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
2403                     // Load the image in a new tab and move to it.
2404                     addNewTab(imageUrl, true);
2405
2406                     // Consume the event.
2407                     return true;
2408                 });
2409
2410                 // Add an Open with App entry.
2411                 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2412                     // Open the link URL with an external app.
2413                     openWithApp(linkUrl);
2414
2415                     // Consume the event.
2416                     return true;
2417                 });
2418
2419                 // Add an Open with Browser entry.
2420                 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2421                     // Open the link URL with an external browser.
2422                     openWithBrowser(linkUrl);
2423
2424                     // Consume the event.
2425                     return true;
2426                 });
2427
2428                 // Add a View Image entry.
2429                 menu.add(R.string.view_image).setOnMenuItemClickListener((MenuItem item) -> {
2430                    // View the image in the current tab.
2431                    loadUrl(currentWebView, imageUrl);
2432
2433                    // Consume the event.
2434                    return true;
2435                 });
2436
2437                 // Add a Save Image entry.
2438                 menu.add(R.string.save_image).setOnMenuItemClickListener((MenuItem item) -> {
2439                     // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
2440                     new PrepareSaveDialog(this, this, getSupportFragmentManager(), SaveWebpageDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
2441                             currentWebView.getAcceptFirstPartyCookies()).execute(imageUrl);
2442
2443                     // Consume the event.
2444                     return true;
2445                 });
2446
2447                 // Add a Copy URL entry.
2448                 menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
2449                     // Save the link URL in a clip data.
2450                     ClipData srcImageAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl);
2451
2452                     // Set the clip data as the clipboard's primary clip.
2453                     clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData);
2454
2455                     // Consume the event.
2456                     return true;
2457                 });
2458
2459                 // Add a Save URL entry.
2460                 menu.add(R.string.save_url).setOnMenuItemClickListener((MenuItem item) -> {
2461                     // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
2462                     new PrepareSaveDialog(this, this, getSupportFragmentManager(), SaveWebpageDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
2463                             currentWebView.getAcceptFirstPartyCookies()).execute(linkUrl);
2464
2465                     // Consume the event.
2466                     return true;
2467                 });
2468
2469                 // Add an empty Cancel entry, which by default closes the context menu.
2470                 menu.add(R.string.cancel);
2471                 break;
2472
2473             case WebView.HitTestResult.EMAIL_TYPE:
2474                 // Get the target URL.
2475                 linkUrl = hitTestResult.getExtra();
2476
2477                 // Set the target URL as the title of the `ContextMenu`.
2478                 menu.setHeaderTitle(linkUrl);
2479
2480                 // Add a Write Email entry.
2481                 menu.add(R.string.write_email).setOnMenuItemClickListener(item -> {
2482                     // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
2483                     Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
2484
2485                     // Parse the url and set it as the data for the `Intent`.
2486                     emailIntent.setData(Uri.parse("mailto:" + linkUrl));
2487
2488                     // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser.
2489                     emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2490
2491                     try {
2492                         // Make it so.
2493                         startActivity(emailIntent);
2494                     } catch (ActivityNotFoundException exception) {
2495                         // Display a snackbar.
2496                         Snackbar.make(currentWebView, getString(R.string.error) + "  " + exception, Snackbar.LENGTH_INDEFINITE).show();
2497                     }
2498
2499                     // Consume the event.
2500                     return true;
2501                 });
2502
2503                 // Add a Copy Email Address entry.
2504                 menu.add(R.string.copy_email_address).setOnMenuItemClickListener(item -> {
2505                     // Save the email address in a `ClipData`.
2506                     ClipData srcEmailTypeClipData = ClipData.newPlainText(getString(R.string.email_address), linkUrl);
2507
2508                     // Set the `ClipData` as the clipboard's primary clip.
2509                     clipboardManager.setPrimaryClip(srcEmailTypeClipData);
2510
2511                     // Consume the event.
2512                     return true;
2513                 });
2514
2515                 // Add an empty Cancel entry, which by default closes the context menu.
2516                 menu.add(R.string.cancel);
2517                 break;
2518         }
2519     }
2520
2521     @Override
2522     public void onCreateBookmark(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) {
2523         // Get a handle for the bookmarks list view.
2524         ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
2525
2526         // Get the dialog.
2527         Dialog dialog = dialogFragment.getDialog();
2528
2529         // Remove the incorrect lint warning below that the dialog might be null.
2530         assert dialog != null;
2531
2532         // Get the views from the dialog fragment.
2533         EditText createBookmarkNameEditText = dialog.findViewById(R.id.create_bookmark_name_edittext);
2534         EditText createBookmarkUrlEditText = dialog.findViewById(R.id.create_bookmark_url_edittext);
2535
2536         // Extract the strings from the edit texts.
2537         String bookmarkNameString = createBookmarkNameEditText.getText().toString();
2538         String bookmarkUrlString = createBookmarkUrlEditText.getText().toString();
2539
2540         // Create a favorite icon byte array output stream.
2541         ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
2542
2543         // Convert the favorite icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2544         favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
2545
2546         // Convert the favorite icon byte array stream to a byte array.
2547         byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
2548
2549         // Display the new bookmark below the current items in the (0 indexed) list.
2550         int newBookmarkDisplayOrder = bookmarksListView.getCount();
2551
2552         // Create the bookmark.
2553         bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolder, newBookmarkDisplayOrder, favoriteIconByteArray);
2554
2555         // Update the bookmarks cursor with the current contents of this folder.
2556         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2557
2558         // Update the list view.
2559         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2560
2561         // Scroll to the new bookmark.
2562         bookmarksListView.setSelection(newBookmarkDisplayOrder);
2563     }
2564
2565     @Override
2566     public void onCreateBookmarkFolder(DialogFragment dialogFragment, @NonNull Bitmap favoriteIconBitmap) {
2567         // Get a handle for the bookmarks list view.
2568         ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
2569
2570         // Get the dialog.
2571         Dialog dialog = dialogFragment.getDialog();
2572
2573         // Remove the incorrect lint warning below that the dialog might be null.
2574         assert dialog != null;
2575
2576         // Get handles for the views in the dialog fragment.
2577         EditText folderNameEditText = dialog.findViewById(R.id.folder_name_edittext);
2578         RadioButton defaultIconRadioButton = dialog.findViewById(R.id.default_icon_radiobutton);
2579         ImageView defaultIconImageView = dialog.findViewById(R.id.default_icon_imageview);
2580
2581         // Get new folder name string.
2582         String folderNameString = folderNameEditText.getText().toString();
2583
2584         // Create a folder icon bitmap.
2585         Bitmap folderIconBitmap;
2586
2587         // Set the folder icon bitmap according to the dialog.
2588         if (defaultIconRadioButton.isChecked()) {  // Use the default folder icon.
2589             // Get the default folder icon drawable.
2590             Drawable folderIconDrawable = defaultIconImageView.getDrawable();
2591
2592             // Convert the folder icon drawable to a bitmap drawable.
2593             BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2594
2595             // Convert the folder icon bitmap drawable to a bitmap.
2596             folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2597         } else {  // Use the WebView favorite icon.
2598             // Copy the favorite icon bitmap to the folder icon bitmap.
2599             folderIconBitmap = favoriteIconBitmap;
2600         }
2601
2602         // Create a folder icon byte array output stream.
2603         ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream();
2604
2605         // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2606         folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream);
2607
2608         // Convert the folder icon byte array stream to a byte array.
2609         byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray();
2610
2611         // Move all the bookmarks down one in the display order.
2612         for (int i = 0; i < bookmarksListView.getCount(); i++) {
2613             int databaseId = (int) bookmarksListView.getItemIdAtPosition(i);
2614             bookmarksDatabaseHelper.updateDisplayOrder(databaseId, i + 1);
2615         }
2616
2617         // Create the folder, which will be placed at the top of the `ListView`.
2618         bookmarksDatabaseHelper.createFolder(folderNameString, currentBookmarksFolder, folderIconByteArray);
2619
2620         // Update the bookmarks cursor with the current contents of this folder.
2621         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2622
2623         // Update the `ListView`.
2624         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2625
2626         // Scroll to the new folder.
2627         bookmarksListView.setSelection(0);
2628     }
2629
2630     @Override
2631     public void onSaveBookmarkFolder(DialogFragment dialogFragment, int selectedFolderDatabaseId, @NonNull Bitmap favoriteIconBitmap) {
2632         // Remove the incorrect lint warning below that the dialog fragment might be null.
2633         assert dialogFragment != null;
2634
2635         // Get the dialog.
2636         Dialog dialog = dialogFragment.getDialog();
2637
2638         // Remove the incorrect lint warning below that the dialog might be null.
2639         assert dialog != null;
2640
2641         // Get handles for the views from the dialog.
2642         RadioButton currentFolderIconRadioButton = dialog.findViewById(R.id.current_icon_radiobutton);
2643         RadioButton defaultFolderIconRadioButton = dialog.findViewById(R.id.default_icon_radiobutton);
2644         ImageView defaultFolderIconImageView = dialog.findViewById(R.id.default_icon_imageview);
2645         EditText editFolderNameEditText = dialog.findViewById(R.id.folder_name_edittext);
2646
2647         // Get the new folder name.
2648         String newFolderNameString = editFolderNameEditText.getText().toString();
2649
2650         // Check if the favorite icon has changed.
2651         if (currentFolderIconRadioButton.isChecked()) {  // Only the name has changed.
2652             // Update the name in the database.
2653             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString);
2654         } else if (!currentFolderIconRadioButton.isChecked() && newFolderNameString.equals(oldFolderNameString)) {  // Only the icon has changed.
2655             // Create the new folder icon Bitmap.
2656             Bitmap folderIconBitmap;
2657
2658             // Populate the new folder icon bitmap.
2659             if (defaultFolderIconRadioButton.isChecked()) {
2660                 // Get the default folder icon drawable.
2661                 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
2662
2663                 // Convert the folder icon drawable to a bitmap drawable.
2664                 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2665
2666                 // Convert the folder icon bitmap drawable to a bitmap.
2667                 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2668             } else {  // Use the `WebView` favorite icon.
2669                 // Copy the favorite icon bitmap to the folder icon bitmap.
2670                 folderIconBitmap = favoriteIconBitmap;
2671             }
2672
2673             // Create a folder icon byte array output stream.
2674             ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
2675
2676             // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2677             folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
2678
2679             // Convert the folder icon byte array stream to a byte array.
2680             byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
2681
2682             // Update the folder icon in the database.
2683             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderIconByteArray);
2684         } else {  // The folder icon and the name have changed.
2685             // Get the new folder icon bitmap.
2686             Bitmap folderIconBitmap;
2687             if (defaultFolderIconRadioButton.isChecked()) {
2688                 // Get the default folder icon drawable.
2689                 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
2690
2691                 // Convert the folder icon drawable to a bitmap drawable.
2692                 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2693
2694                 // Convert the folder icon bitmap drawable to a bitmap.
2695                 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2696             } else {  // Use the `WebView` favorite icon.
2697                 // Copy the favorite icon bitmap to the folder icon bitmap.
2698                 folderIconBitmap = favoriteIconBitmap;
2699             }
2700
2701             // Create a folder icon byte array output stream.
2702             ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
2703
2704             // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2705             folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
2706
2707             // Convert the folder icon byte array stream to a byte array.
2708             byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
2709
2710             // Update the folder name and icon in the database.
2711             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, newFolderIconByteArray);
2712         }
2713
2714         // Update the bookmarks cursor with the current contents of this folder.
2715         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2716
2717         // Update the `ListView`.
2718         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2719     }
2720
2721     // Override `onBackPressed()` to handle the navigation drawer and and the WebViews.
2722     @Override
2723     public void onBackPressed() {
2724         // Check the different options for processing `back`.
2725         if (drawerLayout.isDrawerVisible(GravityCompat.START)) {  // The navigation drawer is open.
2726             // Close the navigation drawer.
2727             drawerLayout.closeDrawer(GravityCompat.START);
2728         } else if (drawerLayout.isDrawerVisible(GravityCompat.END)){  // The bookmarks drawer is open.
2729             // close the bookmarks drawer.
2730             drawerLayout.closeDrawer(GravityCompat.END);
2731         } else if (displayingFullScreenVideo) {  // A full screen video is shown.
2732             // Re-enable the screen timeout.
2733             fullScreenVideoFrameLayout.setKeepScreenOn(false);
2734
2735             // Unset the full screen video flag.
2736             displayingFullScreenVideo = false;
2737
2738             // Remove all the views from the full screen video frame layout.
2739             fullScreenVideoFrameLayout.removeAllViews();
2740
2741             // Hide the full screen video frame layout.
2742             fullScreenVideoFrameLayout.setVisibility(View.GONE);
2743
2744             // Enable the sliding drawers.
2745             drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
2746
2747             // Show the main content relative layout.
2748             mainContentRelativeLayout.setVisibility(View.VISIBLE);
2749
2750             // Apply the appropriate full screen mode flags.
2751             if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) {  // Privacy Browser is currently in full screen browsing mode.
2752                 // Hide the banner ad in the free flavor.
2753                 if (BuildConfig.FLAVOR.contentEquals("free")) {
2754                     // Get a handle for the ad view.  This cannot be a class variable because it changes with each ad load.
2755                     View adView = findViewById(R.id.adview);
2756
2757                     // Hide the banner ad.
2758                     AdHelper.hideAd(adView);
2759                 }
2760
2761                 /* Hide the system bars.
2762                  * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
2763                  * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
2764                  * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
2765                  * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
2766                  */
2767                 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
2768                         View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
2769
2770                 // Reload the website if the app bar is hidden.  Otherwise, there is some bug in Android that causes the WebView to be entirely black.
2771                 if (hideAppBar) {
2772                     // Reload the WebView.
2773                     currentWebView.reload();
2774                 }
2775             } else {  // Switch to normal viewing mode.
2776                 // Remove the `SYSTEM_UI` flags from the root frame layout.
2777                 rootFrameLayout.setSystemUiVisibility(0);
2778             }
2779
2780             // Reload the ad for the free flavor if not in full screen mode.
2781             if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
2782                 // Get a handle for the ad view.  This cannot be a class variable because it changes with each ad load.
2783                 View adView = findViewById(R.id.adview);
2784
2785                 // Reload the ad.  `getContext()` can be used instead of `getActivity.getApplicationContext()` once the minimum API >= 23.
2786                 AdHelper.loadAd(adView, getApplicationContext(), this, getString(R.string.ad_unit_id));
2787             }
2788         } else if (currentWebView.canGoBack()) {  // There is at least one item in the current WebView history.
2789             // Get the current web back forward list.
2790             WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList();
2791
2792             // Get the previous entry URL.
2793             String previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.getCurrentIndex() - 1).getUrl();
2794
2795             // Apply the domain settings.
2796             applyDomainSettings(currentWebView, previousUrl, false, false, false);
2797
2798             // Go back.
2799             currentWebView.goBack();
2800         } else if (tabLayout.getTabCount() > 1) {  // There are at least two tabs.
2801             // Close the current tab.
2802             closeCurrentTab();
2803         } else {  // There isn't anything to do in Privacy Browser.
2804             // Close Privacy Browser.  `finishAndRemoveTask()` also removes Privacy Browser from the recent app list.
2805             if (Build.VERSION.SDK_INT >= 21) {
2806                 finishAndRemoveTask();
2807             } else {
2808                 finish();
2809             }
2810
2811             // Manually kill Privacy Browser.  Otherwise, it is glitchy when restarted.
2812             System.exit(0);
2813         }
2814     }
2815
2816     // Process the results of a file browse.
2817     @Override
2818     public void onActivityResult(int requestCode, int resultCode, Intent returnedIntent) {
2819         // Run the default commands.
2820         super.onActivityResult(requestCode, resultCode, returnedIntent);
2821
2822         // Run the commands that correlate to the specified request code.
2823         switch (requestCode) {
2824             case BROWSE_FILE_UPLOAD_REQUEST_CODE:
2825                 // File uploads only work on API >= 21.
2826                 if (Build.VERSION.SDK_INT >= 21) {
2827                     // Pass the file to the WebView.
2828                     fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, returnedIntent));
2829                 }
2830                 break;
2831
2832             case BROWSE_OPEN_REQUEST_CODE:
2833                 // Don't do anything if the user pressed back from the file picker.
2834                 if (resultCode == Activity.RESULT_OK) {
2835                     // Get a handle for the open dialog fragment.
2836                     DialogFragment openDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.open));
2837
2838                     // Only update the file name if the dialog still exists.
2839                     if (openDialogFragment != null) {
2840                         // Get a handle for the open dialog.
2841                         Dialog openDialog = openDialogFragment.getDialog();
2842
2843                         // Remove the incorrect lint warning below that the dialog might be null.
2844                         assert openDialog != null;
2845
2846                         // Get a handle for the file name edit text.
2847                         EditText fileNameEditText = openDialog.findViewById(R.id.file_name_edittext);
2848
2849                         // Get the file name URI from the intent.
2850                         Uri fileNameUri = returnedIntent.getData();
2851
2852                         // Get the file name string from the URI.
2853                         String fileNameString = fileNameUri.toString();
2854
2855                         // Set the file name text.
2856                         fileNameEditText.setText(fileNameString);
2857
2858                         // Move the cursor to the end of the file name edit text.
2859                         fileNameEditText.setSelection(fileNameString.length());
2860                     }
2861                 }
2862                 break;
2863
2864             case BROWSE_SAVE_WEBPAGE_REQUEST_CODE:
2865                 // Don't do anything if the user pressed back from the file picker.
2866                 if (resultCode == Activity.RESULT_OK) {
2867                     // Get a handle for the save dialog fragment.
2868                     DialogFragment saveWebpageDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.save_dialog));
2869
2870                     // Only update the file name if the dialog still exists.
2871                     if (saveWebpageDialogFragment != null) {
2872                         // Get a handle for the save webpage dialog.
2873                         Dialog saveWebpageDialog = saveWebpageDialogFragment.getDialog();
2874
2875                         // Remove the incorrect lint warning below that the dialog might be null.
2876                         assert saveWebpageDialog != null;
2877
2878                         // Get a handle for the file name edit text.
2879                         EditText fileNameEditText = saveWebpageDialog.findViewById(R.id.file_name_edittext);
2880
2881                         // Get the file name URI from the intent.
2882                         Uri fileNameUri = returnedIntent.getData();
2883
2884                         // Get the file name string from the URI.
2885                         String fileNameString = fileNameUri.toString();
2886
2887                         // Set the file name text.
2888                         fileNameEditText.setText(fileNameString);
2889
2890                         // Move the cursor to the end of the file name edit text.
2891                         fileNameEditText.setSelection(fileNameString.length());
2892                     }
2893                 }
2894                 break;
2895         }
2896     }
2897
2898     private void loadUrlFromTextBox() {
2899         // Get the text from urlTextBox and convert it to a string.  trim() removes white spaces from the beginning and end of the string.
2900         String unformattedUrlString = urlEditText.getText().toString().trim();
2901
2902         // Initialize the formatted URL string.
2903         String url = "";
2904
2905         // Check to see if `unformattedUrlString` is a valid URL.  Otherwise, convert it into a search.
2906         if (unformattedUrlString.startsWith("content://")) {  // This is a Content URL.
2907             // Load the entire content URL.
2908             url = unformattedUrlString;
2909         } else if (Patterns.WEB_URL.matcher(unformattedUrlString).matches() || unformattedUrlString.startsWith("http://") || unformattedUrlString.startsWith("https://") ||
2910                 unformattedUrlString.startsWith("file://")) {  // This is a standard URL.
2911             // Add `https://` at the beginning if there is no protocol.  Otherwise the app will segfault.
2912             if (!unformattedUrlString.startsWith("http") && !unformattedUrlString.startsWith("file://") && !unformattedUrlString.startsWith("content://")) {
2913                 unformattedUrlString = "https://" + unformattedUrlString;
2914             }
2915
2916             // Initialize `unformattedUrl`.
2917             URL unformattedUrl = null;
2918
2919             // 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.
2920             try {
2921                 unformattedUrl = new URL(unformattedUrlString);
2922             } catch (MalformedURLException e) {
2923                 e.printStackTrace();
2924             }
2925
2926             // The ternary operator (? :) makes sure that a null pointer exception is not thrown, which would happen if `.get` was called on a `null` value.
2927             String scheme = unformattedUrl != null ? unformattedUrl.getProtocol() : null;
2928             String authority = unformattedUrl != null ? unformattedUrl.getAuthority() : null;
2929             String path = unformattedUrl != null ? unformattedUrl.getPath() : null;
2930             String query = unformattedUrl != null ? unformattedUrl.getQuery() : null;
2931             String fragment = unformattedUrl != null ? unformattedUrl.getRef() : null;
2932
2933             // Build the URI.
2934             Uri.Builder uri = new Uri.Builder();
2935             uri.scheme(scheme).authority(authority).path(path).query(query).fragment(fragment);
2936
2937             // Decode the URI as a UTF-8 string in.
2938             try {
2939                 url = URLDecoder.decode(uri.build().toString(), "UTF-8");
2940             } catch (UnsupportedEncodingException exception) {
2941                 // Do nothing.  The formatted URL string will remain blank.
2942             }
2943         } else if (!unformattedUrlString.isEmpty()){  // This is not a URL, but rather a search string.
2944             // Create an encoded URL String.
2945             String encodedUrlString;
2946
2947             // Sanitize the search input.
2948             try {
2949                 encodedUrlString = URLEncoder.encode(unformattedUrlString, "UTF-8");
2950             } catch (UnsupportedEncodingException exception) {
2951                 encodedUrlString = "";
2952             }
2953
2954             // Add the base search URL.
2955             url = searchURL + encodedUrlString;
2956         }
2957
2958         // 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.
2959         urlEditText.clearFocus();
2960
2961         // Make it so.
2962         loadUrl(currentWebView, url);
2963     }
2964
2965     private void loadUrl(NestedScrollWebView nestedScrollWebView, String url) {
2966         // Sanitize the URL.
2967         url = sanitizeUrl(url);
2968
2969         // Apply the domain settings and load the URL.
2970         applyDomainSettings(nestedScrollWebView, url, true, false, true);
2971     }
2972
2973     public void findPreviousOnPage(View view) {
2974         // Go to the previous highlighted phrase on the page.  `false` goes backwards instead of forwards.
2975         currentWebView.findNext(false);
2976     }
2977
2978     public void findNextOnPage(View view) {
2979         // Go to the next highlighted phrase on the page. `true` goes forwards instead of backwards.
2980         currentWebView.findNext(true);
2981     }
2982
2983     public void closeFindOnPage(View view) {
2984         // Get a handle for the views.
2985         Toolbar toolbar = findViewById(R.id.toolbar);
2986         LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
2987         EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
2988
2989         // Delete the contents of `find_on_page_edittext`.
2990         findOnPageEditText.setText(null);
2991
2992         // Clear the highlighted phrases if the WebView is not null.
2993         if (currentWebView != null) {
2994             currentWebView.clearMatches();
2995         }
2996
2997         // Hide the find on page linear layout.
2998         findOnPageLinearLayout.setVisibility(View.GONE);
2999
3000         // Show the toolbar.
3001         toolbar.setVisibility(View.VISIBLE);
3002
3003         // Get a handle for the input method manager.
3004         InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
3005
3006         // Remove the lint warning below that the input method manager might be null.
3007         assert inputMethodManager != null;
3008
3009         // Hide the keyboard.
3010         inputMethodManager.hideSoftInputFromWindow(toolbar.getWindowToken(), 0);
3011     }
3012
3013     @Override
3014     public void onApplyNewFontSize(DialogFragment dialogFragment) {
3015         // Remove the incorrect lint warning below that the dialog fragment might be null.
3016         assert dialogFragment != null;
3017
3018         // Get the dialog.
3019         Dialog dialog = dialogFragment.getDialog();
3020
3021         // Remove the incorrect lint warning below tha the dialog might be null.
3022         assert dialog != null;
3023
3024         // Get a handle for the font size edit text.
3025         EditText fontSizeEditText = dialog.findViewById(R.id.font_size_edittext);
3026
3027         // Initialize the new font size variable with the current font size.
3028         int newFontSize = currentWebView.getSettings().getTextZoom();
3029
3030         // Get the font size from the edit text.
3031         try {
3032             newFontSize = Integer.parseInt(fontSizeEditText.getText().toString());
3033         } catch (Exception exception) {
3034             // If the edit text does not contain a valid font size do nothing.
3035         }
3036
3037         // Apply the new font size.
3038         currentWebView.getSettings().setTextZoom(newFontSize);
3039     }
3040
3041     @Override
3042     public void onOpen(DialogFragment dialogFragment) {
3043         // Get the dialog.
3044         Dialog dialog = dialogFragment.getDialog();
3045
3046         // Remove the incorrect lint warning below that the dialog might be null.
3047         assert dialog != null;
3048
3049         // Get handles for the views.
3050         EditText fileNameEditText = dialog.findViewById(R.id.file_name_edittext);
3051         CheckBox mhtCheckBox = dialog.findViewById(R.id.mht_checkbox);
3052
3053         // Get the file path string.
3054         String openFilePath = fileNameEditText.getText().toString();
3055
3056         // Apply the domain settings.  This resets the favorite icon and removes any domain settings.
3057         applyDomainSettings(currentWebView, openFilePath, true, false, false);
3058
3059         // Open the file according to the type.
3060         if (mhtCheckBox.isChecked()) {  // Force opening of an MHT file.
3061             try {
3062                 // Get the MHT file input stream.
3063                 InputStream mhtFileInputStream = getContentResolver().openInputStream(Uri.parse(openFilePath));
3064
3065                 // Create a temporary MHT file.
3066                 File temporaryMhtFile = File.createTempFile("temporary_mht_file", ".mht", getCacheDir());
3067
3068                 // Get a file output stream for the temporary MHT file.
3069                 FileOutputStream temporaryMhtFileOutputStream = new FileOutputStream(temporaryMhtFile);
3070
3071                 // Create a transfer byte array.
3072                 byte[] transferByteArray = new byte[1024];
3073
3074                 // Create an integer to track the number of bytes read.
3075                 int bytesRead;
3076
3077                 // Copy the temporary MHT file input stream to the MHT output stream.
3078                 while ((bytesRead = mhtFileInputStream.read(transferByteArray)) > 0) {
3079                     temporaryMhtFileOutputStream.write(transferByteArray, 0, bytesRead);
3080                 }
3081
3082                 // Flush the temporary MHT file output stream.
3083                 temporaryMhtFileOutputStream.flush();
3084
3085                 // Close the streams.
3086                 temporaryMhtFileOutputStream.close();
3087                 mhtFileInputStream.close();
3088
3089                 // Load the temporary MHT file.
3090                 currentWebView.loadUrl(temporaryMhtFile.toString());
3091             } catch (Exception exception) {
3092                 // Display a snackbar.
3093                 Snackbar.make(currentWebView, getString(R.string.error) + "  " + exception.toString(), Snackbar.LENGTH_INDEFINITE).show();
3094             }
3095         } else {  // Let the WebView handle opening of the file.
3096             // Open the file.
3097             currentWebView.loadUrl(openFilePath);
3098         }
3099     }
3100
3101     @Override
3102     public void onSaveWebpage(int saveType, @NonNull String originalUrlString, DialogFragment dialogFragment) {
3103         // Get the dialog.
3104         Dialog dialog = dialogFragment.getDialog();
3105
3106         // Remove the incorrect lint warning below that the dialog might be null.
3107         assert dialog != null;
3108
3109         // Get a handle for the file name edit text.
3110         EditText fileNameEditText = dialog.findViewById(R.id.file_name_edittext);
3111
3112         // Get the file path from the edit text.
3113         String saveWebpageFilePath = fileNameEditText.getText().toString();
3114
3115         //Save the webpage according to the save type.
3116         switch (saveType) {
3117             case SaveWebpageDialog.SAVE_URL:
3118                 // Get a handle for the dialog URL edit text.
3119                 EditText dialogUrlEditText = dialog.findViewById(R.id.url_edittext);
3120
3121                 // Define the save webpage URL.
3122                 String saveWebpageUrl;
3123
3124                 // Store the URL.
3125                 if (originalUrlString.startsWith("data:")) {
3126                     // Save the original URL.
3127                     saveWebpageUrl = originalUrlString;
3128                 } else {
3129                     // Get the URL from the edit text, which may have been modified.
3130                     saveWebpageUrl = dialogUrlEditText.getText().toString();
3131                 }
3132
3133                 // Save the URL.
3134                 new SaveUrl(this, this, saveWebpageFilePath, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptFirstPartyCookies()).execute(saveWebpageUrl);
3135                 break;
3136
3137             case SaveWebpageDialog.SAVE_ARCHIVE:
3138                 try {
3139                     // Create a temporary MHT file.
3140                     File temporaryMhtFile = File.createTempFile("temporary_mht_file", ".mht", getCacheDir());
3141
3142                     // Save the temporary MHT file.
3143                     currentWebView.saveWebArchive(temporaryMhtFile.toString(), false, callbackValue -> {
3144                         if (callbackValue != null) {  // The temporary MHT file was saved successfully.
3145                             try {
3146                                 // Create a temporary MHT file input stream.
3147                                 FileInputStream temporaryMhtFileInputStream = new FileInputStream(temporaryMhtFile);
3148
3149                                 // Get an output stream for the save webpage file path.
3150                                 OutputStream mhtOutputStream = getContentResolver().openOutputStream(Uri.parse(saveWebpageFilePath));
3151
3152                                 // Create a transfer byte array.
3153                                 byte[] transferByteArray = new byte[1024];
3154
3155                                 // Create an integer to track the number of bytes read.
3156                                 int bytesRead;
3157
3158                                 // Copy the temporary MHT file input stream to the MHT output stream.
3159                                 while ((bytesRead = temporaryMhtFileInputStream.read(transferByteArray)) > 0) {
3160                                     mhtOutputStream.write(transferByteArray, 0, bytesRead);
3161                                 }
3162
3163                                 // Close the streams.
3164                                 mhtOutputStream.close();
3165                                 temporaryMhtFileInputStream.close();
3166
3167                                 // Display a snackbar.
3168                                 Snackbar.make(currentWebView, getString(R.string.file_saved) + "  " + currentWebView.getCurrentUrl(), Snackbar.LENGTH_SHORT).show();
3169                             } catch (Exception exception) {
3170                                 // Display a snackbar with the exception.
3171                                 Snackbar.make(currentWebView, getString(R.string.error_saving_file) + "  " + exception.toString(), Snackbar.LENGTH_INDEFINITE).show();
3172                             } finally {
3173                                 // Delete the temporary MHT file.
3174                                 //noinspection ResultOfMethodCallIgnored
3175                                 temporaryMhtFile.delete();
3176                             }
3177                         } else {  // There was an unspecified error while saving the temporary MHT file.
3178                             // Display an error snackbar.
3179                             Snackbar.make(currentWebView, getString(R.string.error_saving_file), Snackbar.LENGTH_INDEFINITE).show();
3180                         }
3181                     });
3182                 } catch (IOException ioException) {
3183                     // Display a snackbar with the IO exception.
3184                     Snackbar.make(currentWebView, getString(R.string.error_saving_file) + "  " + ioException.toString(), Snackbar.LENGTH_INDEFINITE).show();
3185                 }
3186                 break;
3187
3188             case SaveWebpageDialog.SAVE_IMAGE:
3189                 // Save the webpage image.
3190                 new SaveWebpageImage(this, saveWebpageFilePath, currentWebView).execute();
3191                 break;
3192         }
3193     }
3194
3195     private void initializeApp() {
3196         // Get a handle for the input method.
3197         InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
3198
3199         // Remove the lint warning below that the input method manager might be null.
3200         assert inputMethodManager != null;
3201
3202         // Initialize the gray foreground color spans for highlighting the URLs.  The deprecated `getResources()` must be used until API >= 23.
3203         initialGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
3204         finalGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
3205
3206         // Get the current theme status.
3207         int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
3208
3209         // Set the red color span according to the theme.
3210         if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
3211             redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_a700));
3212         } else {
3213             redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_900));
3214         }
3215
3216         // Remove the formatting from the URL edit text when the user is editing the text.
3217         urlEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
3218             if (hasFocus) {  // The user is editing the URL text box.
3219                 // Remove the highlighting.
3220                 urlEditText.getText().removeSpan(redColorSpan);
3221                 urlEditText.getText().removeSpan(initialGrayColorSpan);
3222                 urlEditText.getText().removeSpan(finalGrayColorSpan);
3223             } else {  // The user has stopped editing the URL text box.
3224                 // Move to the beginning of the string.
3225                 urlEditText.setSelection(0);
3226
3227                 // Reapply the highlighting.
3228                 highlightUrlText();
3229             }
3230         });
3231
3232         // Set the go button on the keyboard to load the URL in `urlTextBox`.
3233         urlEditText.setOnKeyListener((View v, int keyCode, KeyEvent event) -> {
3234             // If the event is a key-down event on the `enter` button, load the URL.
3235             if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {
3236                 // Load the URL into the mainWebView and consume the event.
3237                 loadUrlFromTextBox();
3238
3239                 // If the enter key was pressed, consume the event.
3240                 return true;
3241             } else {
3242                 // If any other key was pressed, do not consume the event.
3243                 return false;
3244             }
3245         });
3246
3247         // Create an Orbot status broadcast receiver.
3248         orbotStatusBroadcastReceiver = new BroadcastReceiver() {
3249             @Override
3250             public void onReceive(Context context, Intent intent) {
3251                 // Store the content of the status message in `orbotStatus`.
3252                 orbotStatus = intent.getStringExtra("org.torproject.android.intent.extra.STATUS");
3253
3254                 // If Privacy Browser is waiting on the proxy, load the website now that Orbot is connected.
3255                 if ((orbotStatus != null) && orbotStatus.equals("ON") && waitingForProxy) {
3256                     // Reset the waiting for proxy status.
3257                     waitingForProxy = false;
3258
3259                     // Get a handle for the waiting for proxy dialog.
3260                     DialogFragment waitingForProxyDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.waiting_for_proxy_dialog));
3261
3262                     // Dismiss the waiting for proxy dialog if it is displayed.
3263                     if (waitingForProxyDialogFragment != null) {
3264                         waitingForProxyDialogFragment.dismiss();
3265                     }
3266
3267                     // Reload existing URLs and load any URLs that are waiting for the proxy.
3268                     for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
3269                         // Get the WebView tab fragment.
3270                         WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
3271
3272                         // Get the fragment view.
3273                         View fragmentView = webViewTabFragment.getView();
3274
3275                         // Only process the WebViews if they exist.
3276                         if (fragmentView != null) {
3277                             // Get the nested scroll WebView from the tab fragment.
3278                             NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
3279
3280                             // Get the waiting for proxy URL string.
3281                             String waitingForProxyUrlString = nestedScrollWebView.getWaitingForProxyUrlString();
3282
3283                             // Load the pending URL if it exists.
3284                             if (!waitingForProxyUrlString.isEmpty()) {  // A URL is waiting to be loaded.
3285                                 // Load the URL.
3286                                 loadUrl(nestedScrollWebView, waitingForProxyUrlString);
3287
3288                                 // Reset the waiting for proxy URL string.
3289                                 nestedScrollWebView.resetWaitingForProxyUrlString();
3290                             } else {  // No URL is waiting to be loaded.
3291                                 // Reload the existing URL.
3292                                 nestedScrollWebView.reload();
3293                             }
3294                         }
3295                     }
3296                 }
3297             }
3298         };
3299
3300         // Register the Orbot status broadcast receiver on `this` context.
3301         this.registerReceiver(orbotStatusBroadcastReceiver, new IntentFilter("org.torproject.android.intent.action.STATUS"));
3302
3303         // Get handles for views that need to be modified.
3304         ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
3305         FloatingActionButton launchBookmarksActivityFab = findViewById(R.id.launch_bookmarks_activity_fab);
3306         FloatingActionButton createBookmarkFolderFab = findViewById(R.id.create_bookmark_folder_fab);
3307         FloatingActionButton createBookmarkFab = findViewById(R.id.create_bookmark_fab);
3308         EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
3309
3310         // Update the web view pager every time a tab is modified.
3311         webViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
3312             @Override
3313             public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
3314                 // Do nothing.
3315             }
3316
3317             @Override
3318             public void onPageSelected(int position) {
3319                 // Close the find on page bar if it is open.
3320                 closeFindOnPage(null);
3321
3322                 // Set the current WebView.
3323                 setCurrentWebView(position);
3324
3325                 // Select the corresponding tab if it does not match the currently selected page.  This will happen if the page was scrolled by creating a new tab.
3326                 if (tabLayout.getSelectedTabPosition() != position) {
3327                     // Wait until the new tab has been created.
3328                     tabLayout.post(() -> {
3329                         // Get a handle for the tab.
3330                         TabLayout.Tab tab = tabLayout.getTabAt(position);
3331
3332                         // Assert that the tab is not null.
3333                         assert tab != null;
3334
3335                         // Select the tab.
3336                         tab.select();
3337                     });
3338                 }
3339             }
3340
3341             @Override
3342             public void onPageScrollStateChanged(int state) {
3343                 // Do nothing.
3344             }
3345         });
3346
3347         // Display the View SSL Certificate dialog when the currently selected tab is reselected.
3348         tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
3349             @Override
3350             public void onTabSelected(TabLayout.Tab tab) {
3351                 // Select the same page in the view pager.
3352                 webViewPager.setCurrentItem(tab.getPosition());
3353             }
3354
3355             @Override
3356             public void onTabUnselected(TabLayout.Tab tab) {
3357                 // Do nothing.
3358             }
3359
3360             @Override
3361             public void onTabReselected(TabLayout.Tab tab) {
3362                 // Instantiate the View SSL Certificate dialog.
3363                 DialogFragment viewSslCertificateDialogFragment = ViewSslCertificateDialog.displayDialog(currentWebView.getWebViewFragmentId());
3364
3365                 // Display the View SSL Certificate dialog.
3366                 viewSslCertificateDialogFragment.show(getSupportFragmentManager(), getString(R.string.view_ssl_certificate));
3367             }
3368         });
3369
3370         // Set the launch bookmarks activity FAB to launch the bookmarks activity.
3371         launchBookmarksActivityFab.setOnClickListener(v -> {
3372             // Get a copy of the favorite icon bitmap.
3373             Bitmap favoriteIconBitmap = currentWebView.getFavoriteOrDefaultIcon();
3374
3375             // Create a favorite icon byte array output stream.
3376             ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
3377
3378             // Convert the favorite icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
3379             favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
3380
3381             // Convert the favorite icon byte array stream to a byte array.
3382             byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
3383
3384             // Create an intent to launch the bookmarks activity.
3385             Intent bookmarksIntent = new Intent(getApplicationContext(), BookmarksActivity.class);
3386
3387             // Add the extra information to the intent.
3388             bookmarksIntent.putExtra("current_url", currentWebView.getUrl());
3389             bookmarksIntent.putExtra("current_title", currentWebView.getTitle());
3390             bookmarksIntent.putExtra("current_folder", currentBookmarksFolder);
3391             bookmarksIntent.putExtra("favorite_icon_byte_array", favoriteIconByteArray);
3392
3393             // Make it so.
3394             startActivity(bookmarksIntent);
3395         });
3396
3397         // Set the create new bookmark folder FAB to display an alert dialog.
3398         createBookmarkFolderFab.setOnClickListener(v -> {
3399             // Create a create bookmark folder dialog.
3400             DialogFragment createBookmarkFolderDialog = CreateBookmarkFolderDialog.createBookmarkFolder(currentWebView.getFavoriteOrDefaultIcon());
3401
3402             // Show the create bookmark folder dialog.
3403             createBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.create_folder));
3404         });
3405
3406         // Set the create new bookmark FAB to display an alert dialog.
3407         createBookmarkFab.setOnClickListener(view -> {
3408             // Instantiate the create bookmark dialog.
3409             DialogFragment createBookmarkDialog = CreateBookmarkDialog.createBookmark(currentWebView.getUrl(), currentWebView.getTitle(), currentWebView.getFavoriteOrDefaultIcon());
3410
3411             // Display the create bookmark dialog.
3412             createBookmarkDialog.show(getSupportFragmentManager(), getString(R.string.create_bookmark));
3413         });
3414
3415         // Search for the string on the page whenever a character changes in the `findOnPageEditText`.
3416         findOnPageEditText.addTextChangedListener(new TextWatcher() {
3417             @Override
3418             public void beforeTextChanged(CharSequence s, int start, int count, int after) {
3419                 // Do nothing.
3420             }
3421
3422             @Override
3423             public void onTextChanged(CharSequence s, int start, int before, int count) {
3424                 // Do nothing.
3425             }
3426
3427             @Override
3428             public void afterTextChanged(Editable s) {
3429                 // Search for the text in the WebView if it is not null.  Sometimes on resume after a period of non-use the WebView will be null.
3430                 if (currentWebView != null) {
3431                     currentWebView.findAllAsync(findOnPageEditText.getText().toString());
3432                 }
3433             }
3434         });
3435
3436         // Set the `check mark` button for the `findOnPageEditText` keyboard to close the soft keyboard.
3437         findOnPageEditText.setOnKeyListener((v, keyCode, event) -> {
3438             if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {  // The `enter` key was pressed.
3439                 // Hide the soft keyboard.
3440                 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
3441
3442                 // Consume the event.
3443                 return true;
3444             } else {  // A different key was pressed.
3445                 // Do not consume the event.
3446                 return false;
3447             }
3448         });
3449
3450         // Implement swipe to refresh.
3451         swipeRefreshLayout.setOnRefreshListener(() -> currentWebView.reload());
3452
3453         // Store the default progress view offsets for use later in `initializeWebView()`.
3454         defaultProgressViewStartOffset = swipeRefreshLayout.getProgressViewStartOffset();
3455         defaultProgressViewEndOffset = swipeRefreshLayout.getProgressViewEndOffset();
3456
3457         // Set the refresh color scheme according to the theme.
3458         if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
3459             swipeRefreshLayout.setColorSchemeResources(R.color.blue_700);
3460         } else {
3461             swipeRefreshLayout.setColorSchemeResources(R.color.violet_500);
3462         }
3463
3464         // Initialize a color background typed value.
3465         TypedValue colorBackgroundTypedValue = new TypedValue();
3466
3467         // Get the color background from the theme.
3468         getTheme().resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true);
3469
3470         // Get the color background int from the typed value.
3471         int colorBackgroundInt = colorBackgroundTypedValue.data;
3472
3473         // Set the swipe refresh background color.
3474         swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt);
3475
3476         // The drawer titles identify the drawer layouts in accessibility mode.
3477         drawerLayout.setDrawerTitle(GravityCompat.START, getString(R.string.navigation_drawer));
3478         drawerLayout.setDrawerTitle(GravityCompat.END, getString(R.string.bookmarks));
3479
3480         // Initialize the bookmarks database helper.  The `0` specifies a database version, but that is ignored and set instead using a constant in `BookmarksDatabaseHelper`.
3481         bookmarksDatabaseHelper = new BookmarksDatabaseHelper(this, null, null, 0);
3482
3483         // Initialize `currentBookmarksFolder`.  `""` is the home folder in the database.
3484         currentBookmarksFolder = "";
3485
3486         // Load the home folder, which is `""` in the database.
3487         loadBookmarksFolder();
3488
3489         bookmarksListView.setOnItemClickListener((parent, view, position, id) -> {
3490             // Convert the id from long to int to match the format of the bookmarks database.
3491             int databaseId = (int) id;
3492
3493             // Get the bookmark cursor for this ID.
3494             Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseId);
3495
3496             // Move the bookmark cursor to the first row.
3497             bookmarkCursor.moveToFirst();
3498
3499             // Act upon the bookmark according to the type.
3500             if (bookmarkCursor.getInt(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {  // The selected bookmark is a folder.
3501                 // Store the new folder name in `currentBookmarksFolder`.
3502                 currentBookmarksFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
3503
3504                 // Load the new folder.
3505                 loadBookmarksFolder();
3506             } else {  // The selected bookmark is not a folder.
3507                 // Load the bookmark URL.
3508                 loadUrl(currentWebView, bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_URL)));
3509
3510                 // Close the bookmarks drawer.
3511                 drawerLayout.closeDrawer(GravityCompat.END);
3512             }
3513
3514             // Close the `Cursor`.
3515             bookmarkCursor.close();
3516         });
3517
3518         bookmarksListView.setOnItemLongClickListener((parent, view, position, id) -> {
3519             // Convert the database ID from `long` to `int`.
3520             int databaseId = (int) id;
3521
3522             // Find out if the selected bookmark is a folder.
3523             boolean isFolder = bookmarksDatabaseHelper.isFolder(databaseId);
3524
3525             if (isFolder) {
3526                 // Save the current folder name, which is used in `onSaveEditBookmarkFolder()`.
3527                 oldFolderNameString = bookmarksCursor.getString(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
3528
3529                 // Instantiate the edit folder bookmark dialog.
3530                 DialogFragment editBookmarkFolderDialog = EditBookmarkFolderDialog.folderDatabaseId(databaseId, currentWebView.getFavoriteOrDefaultIcon());
3531
3532                 // Show the edit folder bookmark dialog.
3533                 editBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.edit_folder));
3534             } else {
3535                 // Get the bookmark cursor for this ID.
3536                 Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseId);
3537
3538                 // Move the bookmark cursor to the first row.
3539                 bookmarkCursor.moveToFirst();
3540
3541                 // Load the bookmark in a new tab but do not switch to the tab or close the drawer.
3542                 addNewTab(bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_URL)), false);
3543             }
3544
3545             // Consume the event.
3546             return true;
3547         });
3548
3549         // The drawer listener is used to update the navigation menu.
3550         drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() {
3551             @Override
3552             public void onDrawerSlide(@NonNull View drawerView, float slideOffset) {
3553             }
3554
3555             @Override
3556             public void onDrawerOpened(@NonNull View drawerView) {
3557             }
3558
3559             @Override
3560             public void onDrawerClosed(@NonNull View drawerView) {
3561                 // Reset the drawer icon when the drawer is closed.  Otherwise, it is an arrow if the drawer is open when the app is restarted.
3562                 actionBarDrawerToggle.syncState();
3563             }
3564
3565             @Override
3566             public void onDrawerStateChanged(int newState) {
3567                 if ((newState == DrawerLayout.STATE_SETTLING) || (newState == DrawerLayout.STATE_DRAGGING)) {  // A drawer is opening or closing.
3568                     // Update the navigation menu items if the WebView is not null.
3569                     if (currentWebView != null) {
3570                         navigationBackMenuItem.setEnabled(currentWebView.canGoBack());
3571                         navigationForwardMenuItem.setEnabled(currentWebView.canGoForward());
3572                         navigationHistoryMenuItem.setEnabled((currentWebView.canGoBack() || currentWebView.canGoForward()));
3573                         navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
3574
3575                         // Hide the keyboard (if displayed).
3576                         inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
3577                     }
3578
3579                     // Clear the focus from from the URL text box and the WebView.  This removes any text selection markers and context menus, which otherwise draw above the open drawers.
3580                     urlEditText.clearFocus();
3581                     currentWebView.clearFocus();
3582                 }
3583             }
3584         });
3585
3586         // Replace the header that `WebView` creates for `X-Requested-With` with a null value.  The default value is the application ID (com.stoutner.privacybrowser.standard).
3587         customHeaders.put("X-Requested-With", "");
3588
3589         // Inflate a bare WebView to get the default user agent.  It is not used to render content on the screen.
3590         @SuppressLint("InflateParams") View webViewLayout = getLayoutInflater().inflate(R.layout.bare_webview, null, false);
3591
3592         // Get a handle for the WebView.
3593         WebView bareWebView = webViewLayout.findViewById(R.id.bare_webview);
3594
3595         // Store the default user agent.
3596         webViewDefaultUserAgent = bareWebView.getSettings().getUserAgentString();
3597
3598         // Destroy the bare WebView.
3599         bareWebView.destroy();
3600     }
3601
3602     private void applyAppSettings() {
3603         // Get a handle for the shared preferences.
3604         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3605
3606         // Store the values from the shared preferences in variables.
3607         incognitoModeEnabled = sharedPreferences.getBoolean("incognito_mode", false);
3608         boolean doNotTrackEnabled = sharedPreferences.getBoolean("do_not_track", false);
3609         sanitizeGoogleAnalytics = sharedPreferences.getBoolean("google_analytics", true);
3610         sanitizeFacebookClickIds = sharedPreferences.getBoolean("facebook_click_ids", true);
3611         sanitizeTwitterAmpRedirects = sharedPreferences.getBoolean("twitter_amp_redirects", true);
3612         proxyMode = sharedPreferences.getString("proxy", getString(R.string.proxy_default_value));
3613         fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean("full_screen_browsing_mode", false);
3614         hideAppBar = sharedPreferences.getBoolean("hide_app_bar", true);
3615         scrollAppBar = sharedPreferences.getBoolean("scroll_app_bar", true);
3616
3617         // Apply the saved proxy mode if the app has been restarted.
3618         if (savedProxyMode != null) {
3619             // Apply the saved proxy mode.
3620             proxyMode = savedProxyMode;
3621
3622             // Reset the saved proxy mode.
3623             savedProxyMode = null;
3624         }
3625
3626         // Get the search string.
3627         String searchString = sharedPreferences.getString("search", getString(R.string.search_default_value));
3628
3629         // Set the search string.
3630         if (searchString.equals("Custom URL")) {  // A custom search string is used.
3631             searchURL = sharedPreferences.getString("search_custom_url", getString(R.string.search_custom_url_default_value));
3632         } else {  // A custom search string is not used.
3633             searchURL = searchString;
3634         }
3635
3636         // Apply the proxy.
3637         applyProxy(false);
3638
3639         // Set Do Not Track status.
3640         if (doNotTrackEnabled) {
3641             customHeaders.put("DNT", "1");
3642         } else {
3643             customHeaders.remove("DNT");
3644         }
3645
3646         // Get the current layout parameters.  Using coordinator layout parameters allows the `setBehavior()` command and using app bar layout parameters allows the `setScrollFlags()` command.
3647         CoordinatorLayout.LayoutParams swipeRefreshLayoutParams = (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams();
3648         AppBarLayout.LayoutParams toolbarLayoutParams = (AppBarLayout.LayoutParams) toolbar.getLayoutParams();
3649         AppBarLayout.LayoutParams findOnPageLayoutParams = (AppBarLayout.LayoutParams) findOnPageLinearLayout.getLayoutParams();
3650         AppBarLayout.LayoutParams tabsLayoutParams = (AppBarLayout.LayoutParams) tabsLinearLayout.getLayoutParams();
3651
3652         // Add the scrolling behavior to the layout parameters.
3653         if (scrollAppBar) {
3654             // Enable scrolling of the app bar.
3655             swipeRefreshLayoutParams.setBehavior(new AppBarLayout.ScrollingViewBehavior());
3656             toolbarLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP);
3657             findOnPageLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP);
3658             tabsLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP);
3659         } else {
3660             // Disable scrolling of the app bar.
3661             swipeRefreshLayoutParams.setBehavior(null);
3662             toolbarLayoutParams.setScrollFlags(0);
3663             findOnPageLayoutParams.setScrollFlags(0);
3664             tabsLayoutParams.setScrollFlags(0);
3665
3666             // Expand the app bar if it is currently collapsed.
3667             appBarLayout.setExpanded(true);
3668         }
3669
3670         // Apply the modified layout parameters.
3671         swipeRefreshLayout.setLayoutParams(swipeRefreshLayoutParams);
3672         toolbar.setLayoutParams(toolbarLayoutParams);
3673         findOnPageLinearLayout.setLayoutParams(findOnPageLayoutParams);
3674         tabsLinearLayout.setLayoutParams(tabsLayoutParams);
3675
3676         // Set the app bar scrolling for each WebView.
3677         for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
3678             // Get the WebView tab fragment.
3679             WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
3680
3681             // Get the fragment view.
3682             View fragmentView = webViewTabFragment.getView();
3683
3684             // Only modify the WebViews if they exist.
3685             if (fragmentView != null) {
3686                 // Get the nested scroll WebView from the tab fragment.
3687                 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
3688
3689                 // Set the app bar scrolling.
3690                 nestedScrollWebView.setNestedScrollingEnabled(scrollAppBar);
3691             }
3692         }
3693
3694         // Update the full screen browsing mode settings.
3695         if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) {  // Privacy Browser is currently in full screen browsing mode.
3696             // Update the visibility of the app bar, which might have changed in the settings.
3697             if (hideAppBar) {
3698                 // Hide the tab linear layout.
3699                 tabsLinearLayout.setVisibility(View.GONE);
3700
3701                 // Hide the action bar.
3702                 actionBar.hide();
3703             } else {
3704                 // Show the tab linear layout.
3705                 tabsLinearLayout.setVisibility(View.VISIBLE);
3706
3707                 // Show the action bar.
3708                 actionBar.show();
3709             }
3710
3711             // Hide the banner ad in the free flavor.
3712             if (BuildConfig.FLAVOR.contentEquals("free")) {
3713                 // Get a handle for the ad view.  This cannot be a class variable because it changes with each ad load.
3714                 View adView = findViewById(R.id.adview);
3715
3716                 // Hide the banner ad.
3717                 AdHelper.hideAd(adView);
3718             }
3719
3720             /* Hide the system bars.
3721              * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
3722              * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
3723              * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
3724              * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
3725              */
3726             rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
3727                     View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
3728         } else {  // Privacy Browser is not in full screen browsing mode.
3729             // Reset the full screen tracker, which could be true if Privacy Browser was in full screen mode before entering settings and full screen browsing was disabled.
3730             inFullScreenBrowsingMode = false;
3731
3732             // Show the tab linear layout.
3733             tabsLinearLayout.setVisibility(View.VISIBLE);
3734
3735             // Show the action bar.
3736             actionBar.show();
3737
3738             // Show the banner ad in the free flavor.
3739             if (BuildConfig.FLAVOR.contentEquals("free")) {
3740                 // Get a handle for the ad view.  This cannot be a class variable because it changes with each ad load.
3741                 View adView = findViewById(R.id.adview);
3742
3743                 // Initialize the ads.  If this isn't the first run, `loadAd()` will be automatically called instead.
3744                 // `getContext()` can be used instead of `getActivity.getApplicationContext()` once the minimum API >= 23.
3745                 AdHelper.initializeAds(adView, getApplicationContext(), this, getSupportFragmentManager(), getString(R.string.ad_unit_id));
3746             }
3747
3748             // Remove the `SYSTEM_UI` flags from the root frame layout.
3749             rootFrameLayout.setSystemUiVisibility(0);
3750         }
3751     }
3752
3753     @Override
3754     public void navigateHistory(@NonNull String url, int steps) {
3755         // Apply the domain settings.
3756         applyDomainSettings(currentWebView, url, false, false, false);
3757
3758         // Load the history entry.
3759         currentWebView.goBackOrForward(steps);
3760     }
3761
3762     @Override
3763     public void pinnedErrorGoBack() {
3764         // Get the current web back forward list.
3765         WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList();
3766
3767         // Get the previous entry URL.
3768         String previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.getCurrentIndex() - 1).getUrl();
3769
3770         // Apply the domain settings.
3771         applyDomainSettings(currentWebView, previousUrl, false, false, false);
3772
3773         // Go back.
3774         currentWebView.goBack();
3775     }
3776
3777     // `reloadWebsite` is used if returning from the Domains activity.  Otherwise JavaScript might not function correctly if it is newly enabled.
3778     @SuppressLint("SetJavaScriptEnabled")
3779     private void applyDomainSettings(NestedScrollWebView nestedScrollWebView, String url, boolean resetTab, boolean reloadWebsite, boolean loadUrl) {
3780         // Store the current URL.
3781         nestedScrollWebView.setCurrentUrl(url);
3782
3783         // Parse the URL into a URI.
3784         Uri uri = Uri.parse(url);
3785
3786         // Extract the domain from `uri`.
3787         String newHostName = uri.getHost();
3788
3789         // Strings don't like to be null.
3790         if (newHostName == null) {
3791             newHostName = "";
3792         }
3793
3794         // Apply the domain settings if a new domain is being loaded or if the new domain is blank.  This allows the user to set temporary settings for JavaScript, cookies, DOM storage, etc.
3795         if (!nestedScrollWebView.getCurrentDomainName().equals(newHostName) || newHostName.equals("")) {
3796             // Set the new host name as the current domain name.
3797             nestedScrollWebView.setCurrentDomainName(newHostName);
3798
3799             // Reset the ignoring of pinned domain information.
3800             nestedScrollWebView.setIgnorePinnedDomainInformation(false);
3801
3802             // Clear any pinned SSL certificate or IP addresses.
3803             nestedScrollWebView.clearPinnedSslCertificate();
3804             nestedScrollWebView.clearPinnedIpAddresses();
3805
3806             // Reset the favorite icon if specified.
3807             if (resetTab) {
3808                 // Initialize the favorite icon.
3809                 nestedScrollWebView.initializeFavoriteIcon();
3810
3811                 // Get the current page position.
3812                 int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
3813
3814                 // Get the corresponding tab.
3815                 TabLayout.Tab tab = tabLayout.getTabAt(currentPagePosition);
3816
3817                 // Update the tab if it isn't null, which sometimes happens when restarting from the background.
3818                 if (tab != null) {
3819                     // Get the tab custom view.
3820                     View tabCustomView = tab.getCustomView();
3821
3822                     // Remove the warning below that the tab custom view might be null.
3823                     assert tabCustomView != null;
3824
3825                     // Get the tab views.
3826                     ImageView tabFavoriteIconImageView = tabCustomView.findViewById(R.id.favorite_icon_imageview);
3827                     TextView tabTitleTextView = tabCustomView.findViewById(R.id.title_textview);
3828
3829                     // Set the default favorite icon as the favorite icon for this tab.
3830                     tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(nestedScrollWebView.getFavoriteOrDefaultIcon(), 64, 64, true));
3831
3832                     // Set the loading title text.
3833                     tabTitleTextView.setText(R.string.loading);
3834                 }
3835             }
3836
3837             // Initialize the database handler.  The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
3838             DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
3839
3840             // Get a full cursor from `domainsDatabaseHelper`.
3841             Cursor domainNameCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomain();
3842
3843             // Initialize `domainSettingsSet`.
3844             Set<String> domainSettingsSet = new HashSet<>();
3845
3846             // Get the domain name column index.
3847             int domainNameColumnIndex = domainNameCursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME);
3848
3849             // Populate `domainSettingsSet`.
3850             for (int i = 0; i < domainNameCursor.getCount(); i++) {
3851                 // Move `domainsCursor` to the current row.
3852                 domainNameCursor.moveToPosition(i);
3853
3854                 // Store the domain name in `domainSettingsSet`.
3855                 domainSettingsSet.add(domainNameCursor.getString(domainNameColumnIndex));
3856             }
3857
3858             // Close `domainNameCursor.
3859             domainNameCursor.close();
3860
3861             // Initialize the domain name in database variable.
3862             String domainNameInDatabase = null;
3863
3864             // Check the hostname against the domain settings set.
3865             if (domainSettingsSet.contains(newHostName)) {  // The hostname is contained in the domain settings set.
3866                 // Record the domain name in the database.
3867                 domainNameInDatabase = newHostName;
3868
3869                 // Set the domain settings applied tracker to true.
3870                 nestedScrollWebView.setDomainSettingsApplied(true);
3871             } else {  // The hostname is not contained in the domain settings set.
3872                 // Set the domain settings applied tracker to false.
3873                 nestedScrollWebView.setDomainSettingsApplied(false);
3874             }
3875
3876             // Check all the subdomains of the host name against wildcard domains in the domain cursor.
3877             while (!nestedScrollWebView.getDomainSettingsApplied() && newHostName.contains(".")) {  // Stop checking if domain settings are already applied or there are no more `.` in the host name.
3878                 if (domainSettingsSet.contains("*." + newHostName)) {  // Check the host name prepended by `*.`.
3879                     // Set the domain settings applied tracker to true.
3880                     nestedScrollWebView.setDomainSettingsApplied(true);
3881
3882                     // Store the applied domain names as it appears in the database.
3883                     domainNameInDatabase = "*." + newHostName;
3884                 }
3885
3886                 // Strip out the lowest subdomain of of the host name.
3887                 newHostName = newHostName.substring(newHostName.indexOf(".") + 1);
3888             }
3889
3890
3891             // Get a handle for the shared preferences.
3892             SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3893
3894             // Store the general preference information.
3895             String defaultFontSizeString = sharedPreferences.getString("font_size", getString(R.string.font_size_default_value));
3896             String defaultUserAgentName = sharedPreferences.getString("user_agent", getString(R.string.user_agent_default_value));
3897             boolean defaultSwipeToRefresh = sharedPreferences.getBoolean("swipe_to_refresh", true);
3898             String webViewTheme = sharedPreferences.getString("webview_theme", getString(R.string.webview_theme_default_value));
3899             boolean wideViewport = sharedPreferences.getBoolean("wide_viewport", true);
3900             boolean displayWebpageImages = sharedPreferences.getBoolean("display_webpage_images", true);
3901
3902             // Get the WebView theme entry values string array.
3903             String[] webViewThemeEntryValuesStringArray = getResources().getStringArray(R.array.webview_theme_entry_values);
3904
3905             // Get a handle for the cookie manager.
3906             CookieManager cookieManager = CookieManager.getInstance();
3907
3908             // Initialize the user agent array adapter and string array.
3909             ArrayAdapter<CharSequence> userAgentNamesArray = ArrayAdapter.createFromResource(this, R.array.user_agent_names, R.layout.spinner_item);
3910             String[] userAgentDataArray = getResources().getStringArray(R.array.user_agent_data);
3911
3912             if (nestedScrollWebView.getDomainSettingsApplied()) {  // The url has custom domain settings.
3913                 // Get a cursor for the current host and move it to the first position.
3914                 Cursor currentDomainSettingsCursor = domainsDatabaseHelper.getCursorForDomainName(domainNameInDatabase);
3915                 currentDomainSettingsCursor.moveToFirst();
3916
3917                 // Get the settings from the cursor.
3918                 nestedScrollWebView.setDomainSettingsDatabaseId(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper._ID)));
3919                 nestedScrollWebView.getSettings().setJavaScriptEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)) == 1);
3920                 nestedScrollWebView.setAcceptFirstPartyCookies(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FIRST_PARTY_COOKIES)) == 1);
3921                 boolean domainThirdPartyCookiesEnabled = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_THIRD_PARTY_COOKIES)) == 1);
3922                 nestedScrollWebView.getSettings().setDomStorageEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_DOM_STORAGE)) == 1);
3923                 // Form data can be removed once the minimum API >= 26.
3924                 boolean saveFormData = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FORM_DATA)) == 1);
3925                 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASYLIST,
3926                         currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYLIST)) == 1);
3927                 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASYPRIVACY,
3928                         currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYPRIVACY)) == 1);
3929                 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST,
3930                         currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST)) == 1);
3931                 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST,
3932                         currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)) == 1);
3933                 nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRALIST, currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ULTRALIST)) == 1);
3934                 nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRAPRIVACY,
3935                         currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY)) == 1);
3936                 nestedScrollWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS,
3937                         currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS)) == 1);
3938                 String userAgentName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.USER_AGENT));
3939                 int fontSize = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.FONT_SIZE));
3940                 int swipeToRefreshInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SWIPE_TO_REFRESH));
3941                 int webViewThemeInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.WEBVIEW_THEME));
3942                 int wideViewportInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.WIDE_VIEWPORT));
3943                 int displayWebpageImagesInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.DISPLAY_IMAGES));
3944                 boolean pinnedSslCertificate = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)) == 1);
3945                 String pinnedSslIssuedToCName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME));
3946                 String pinnedSslIssuedToOName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION));
3947                 String pinnedSslIssuedToUName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT));
3948                 String pinnedSslIssuedByCName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME));
3949                 String pinnedSslIssuedByOName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION));
3950                 String pinnedSslIssuedByUName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT));
3951                 boolean pinnedIpAddresses = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_IP_ADDRESSES)) == 1);
3952                 String pinnedHostIpAddresses = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.IP_ADDRESSES));
3953
3954                 // Get the pinned SSL date longs.
3955                 long pinnedSslStartDateLong = currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE));
3956                 long pinnedSslEndDateLong = currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE));
3957
3958                 // Define the pinned SSL date variables.
3959                 Date pinnedSslStartDate;
3960                 Date pinnedSslEndDate;
3961
3962                 // Set the pinned SSL certificate start date to `null` if the saved date long is 0 because creating a new date results in an error if the input is 0.
3963                 if (pinnedSslStartDateLong == 0) {
3964                     pinnedSslStartDate = null;
3965                 } else {
3966                     pinnedSslStartDate = new Date(pinnedSslStartDateLong);
3967                 }
3968
3969                 // Set the pinned SSL certificate end date to `null` if the saved date long is 0 because creating a new date results in an error if the input is 0.
3970                 if (pinnedSslEndDateLong == 0) {
3971                     pinnedSslEndDate = null;
3972                 } else {
3973                     pinnedSslEndDate = new Date(pinnedSslEndDateLong);
3974                 }
3975
3976                 // Close the current host domain settings cursor.
3977                 currentDomainSettingsCursor.close();
3978
3979                 // If there is a pinned SSL certificate, store it in the WebView.
3980                 if (pinnedSslCertificate) {
3981                     nestedScrollWebView.setPinnedSslCertificate(pinnedSslIssuedToCName, pinnedSslIssuedToOName, pinnedSslIssuedToUName, pinnedSslIssuedByCName, pinnedSslIssuedByOName, pinnedSslIssuedByUName,
3982                             pinnedSslStartDate, pinnedSslEndDate);
3983                 }
3984
3985                 // If there is a pinned IP address, store it in the WebView.
3986                 if (pinnedIpAddresses) {
3987                     nestedScrollWebView.setPinnedIpAddresses(pinnedHostIpAddresses);
3988                 }
3989
3990                 // Apply the cookie domain settings.
3991                 cookieManager.setAcceptCookie(nestedScrollWebView.getAcceptFirstPartyCookies());
3992
3993                 // Set third-party cookies status if API >= 21.
3994                 if (Build.VERSION.SDK_INT >= 21) {
3995                     cookieManager.setAcceptThirdPartyCookies(nestedScrollWebView, domainThirdPartyCookiesEnabled);
3996                 }
3997
3998                 // Apply the form data setting if the API < 26.
3999                 if (Build.VERSION.SDK_INT < 26) {
4000                     nestedScrollWebView.getSettings().setSaveFormData(saveFormData);
4001                 }
4002
4003                 // Apply the font size.
4004                 try {  // Try the specified font size to see if it is valid.
4005                     if (fontSize == 0) {  // Apply the default font size.
4006                             // Try to set the font size from the value in the app settings.
4007                             nestedScrollWebView.getSettings().setTextZoom(Integer.parseInt(defaultFontSizeString));
4008                     } else {  // Apply the font size from domain settings.
4009                         nestedScrollWebView.getSettings().setTextZoom(fontSize);
4010                     }
4011                 } catch (Exception exception) {  // The specified font size is invalid
4012                     // Set the font size to be 100%
4013                     nestedScrollWebView.getSettings().setTextZoom(100);
4014                 }
4015
4016                 // Set the user agent.
4017                 if (userAgentName.equals(getString(R.string.system_default_user_agent))) {  // Use the system default user agent.
4018                     // Get the array position of the default user agent name.
4019                     int defaultUserAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
4020
4021                     // Set the user agent according to the system default.
4022                     switch (defaultUserAgentArrayPosition) {
4023                         case UNRECOGNIZED_USER_AGENT:  // The default user agent name is not on the canonical list.
4024                             // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
4025                             nestedScrollWebView.getSettings().setUserAgentString(defaultUserAgentName);
4026                             break;
4027
4028                         case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
4029                             // Set the user agent to `""`, which uses the default value.
4030                             nestedScrollWebView.getSettings().setUserAgentString("");
4031                             break;
4032
4033                         case SETTINGS_CUSTOM_USER_AGENT:
4034                             // Set the default custom user agent.
4035                             nestedScrollWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
4036                             break;
4037
4038                         default:
4039                             // Get the user agent string from the user agent data array
4040                             nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[defaultUserAgentArrayPosition]);
4041                     }
4042                 } else {  // Set the user agent according to the stored name.
4043                     // Get the array position of the user agent name.
4044                     int userAgentArrayPosition = userAgentNamesArray.getPosition(userAgentName);
4045
4046                     switch (userAgentArrayPosition) {
4047                         case UNRECOGNIZED_USER_AGENT:  // The user agent name contains a custom user agent.
4048                             nestedScrollWebView.getSettings().setUserAgentString(userAgentName);
4049                             break;
4050
4051                         case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
4052                             // Set the user agent to `""`, which uses the default value.
4053                             nestedScrollWebView.getSettings().setUserAgentString("");
4054                             break;
4055
4056                         default:
4057                             // Get the user agent string from the user agent data array.
4058                             nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
4059                     }
4060                 }
4061
4062                 // Set swipe to refresh.
4063                 switch (swipeToRefreshInt) {
4064                     case DomainsDatabaseHelper.SYSTEM_DEFAULT:
4065                         // Store the swipe to refresh status in the nested scroll WebView.
4066                         nestedScrollWebView.setSwipeToRefresh(defaultSwipeToRefresh);
4067
4068                         // Update the swipe refresh layout.
4069                         if (defaultSwipeToRefresh) {  // Swipe to refresh is enabled.
4070                             // Only enable the swipe refresh layout if the WebView is scrolled to the top.  It is updated every time the scroll changes.
4071                             swipeRefreshLayout.setEnabled(currentWebView.getScrollY() == 0);
4072                         } else {  // Swipe to refresh is disabled.
4073                             // Disable the swipe refresh layout.
4074                             swipeRefreshLayout.setEnabled(false);
4075                         }
4076                         break;
4077
4078                     case DomainsDatabaseHelper.ENABLED:
4079                         // Store the swipe to refresh status in the nested scroll WebView.
4080                         nestedScrollWebView.setSwipeToRefresh(true);
4081
4082                         // Only enable the swipe refresh layout if the WebView is scrolled to the top.  It is updated every time the scroll changes.
4083                         swipeRefreshLayout.setEnabled(currentWebView.getScrollY() == 0);
4084                         break;
4085
4086                     case DomainsDatabaseHelper.DISABLED:
4087                         // Store the swipe to refresh status in the nested scroll WebView.
4088                         nestedScrollWebView.setSwipeToRefresh(false);
4089
4090                         // Disable swipe to refresh.
4091                         swipeRefreshLayout.setEnabled(false);
4092                 }
4093
4094                 // Check to see if WebView themes are supported.
4095                 if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
4096                     // Set the WebView theme.
4097                     switch (webViewThemeInt) {
4098                         case DomainsDatabaseHelper.SYSTEM_DEFAULT:
4099                             // Set the WebView theme.  A switch statement cannot be used because the WebView theme entry values string array is not a compile time constant.
4100                             if (webViewTheme.equals(webViewThemeEntryValuesStringArray[1])) {  // The light theme is selected.
4101                                 // Turn off the WebView dark mode.
4102                                 WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
4103                             } else if (webViewTheme.equals(webViewThemeEntryValuesStringArray[2])) {  // The dark theme is selected.
4104                                 // Turn on the WebView dark mode.
4105                                 WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
4106                             } else {  // The system default theme is selected.
4107                                 // Get the current system theme status.
4108                                 int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
4109
4110                                 // Set the WebView theme according to the current system theme status.
4111                                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {  // The system is in day mode.
4112                                     // Turn off the WebView dark mode.
4113                                     WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
4114                                 } else {  // The system is in night mode.
4115                                     // Turn on the WebView dark mode.
4116                                     WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
4117                                 }
4118                             }
4119                             break;
4120
4121                         case DomainsDatabaseHelper.LIGHT_THEME:
4122                             // Turn off the WebView dark mode.
4123                             WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
4124                             break;
4125
4126                         case DomainsDatabaseHelper.DARK_THEME:
4127                             // Turn on the WebView dark mode.
4128                             WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
4129                             break;
4130                     }
4131                 }
4132
4133                 // Set the viewport.
4134                 switch (wideViewportInt) {
4135                     case DomainsDatabaseHelper.SYSTEM_DEFAULT:
4136                         nestedScrollWebView.getSettings().setUseWideViewPort(wideViewport);
4137                         break;
4138
4139                     case DomainsDatabaseHelper.ENABLED:
4140                         nestedScrollWebView.getSettings().setUseWideViewPort(true);
4141                         break;
4142
4143                     case DomainsDatabaseHelper.DISABLED:
4144                         nestedScrollWebView.getSettings().setUseWideViewPort(false);
4145                         break;
4146                 }
4147
4148                 // Set the loading of webpage images.
4149                 switch (displayWebpageImagesInt) {
4150                     case DomainsDatabaseHelper.SYSTEM_DEFAULT:
4151                         nestedScrollWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
4152                         break;
4153
4154                     case DomainsDatabaseHelper.ENABLED:
4155                         nestedScrollWebView.getSettings().setLoadsImagesAutomatically(true);
4156                         break;
4157
4158                     case DomainsDatabaseHelper.DISABLED:
4159                         nestedScrollWebView.getSettings().setLoadsImagesAutomatically(false);
4160                         break;
4161                 }
4162
4163                 // Get the current theme status.
4164                 int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
4165
4166                 // Set a background on the URL relative layout to indicate that custom domain settings are being used.
4167                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
4168                     urlRelativeLayout.setBackground(ResourcesCompat.getDrawable(getResources(), R.drawable.url_bar_background_light_green, null));
4169                 } else {
4170                     urlRelativeLayout.setBackground(ResourcesCompat.getDrawable(getResources(), R.drawable.url_bar_background_dark_blue, null));
4171                 }
4172             } else {  // The new URL does not have custom domain settings.  Load the defaults.
4173                 // Store the values from the shared preferences.
4174                 nestedScrollWebView.getSettings().setJavaScriptEnabled(sharedPreferences.getBoolean("javascript", false));
4175                 nestedScrollWebView.setAcceptFirstPartyCookies(sharedPreferences.getBoolean("first_party_cookies", false));
4176                 boolean defaultThirdPartyCookiesEnabled = sharedPreferences.getBoolean("third_party_cookies", false);
4177                 nestedScrollWebView.getSettings().setDomStorageEnabled(sharedPreferences.getBoolean("dom_storage", false));
4178                 boolean saveFormData = sharedPreferences.getBoolean("save_form_data", false);  // Form data can be removed once the minimum API >= 26.
4179                 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASYLIST, sharedPreferences.getBoolean("easylist", true));
4180                 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASYPRIVACY, sharedPreferences.getBoolean("easyprivacy", true));
4181                 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST, sharedPreferences.getBoolean("fanboys_annoyance_list", true));
4182                 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST, sharedPreferences.getBoolean("fanboys_social_blocking_list", true));
4183                 nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRALIST, sharedPreferences.getBoolean("ultralist", true));
4184                 nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRAPRIVACY, sharedPreferences.getBoolean("ultraprivacy", true));
4185                 nestedScrollWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS, sharedPreferences.getBoolean("block_all_third_party_requests", false));
4186
4187                 // Apply the default first-party cookie setting.
4188                 cookieManager.setAcceptCookie(nestedScrollWebView.getAcceptFirstPartyCookies());
4189
4190                 // Apply the default font size setting.
4191                 try {
4192                     // Try to set the font size from the value in the app settings.
4193                     nestedScrollWebView.getSettings().setTextZoom(Integer.parseInt(defaultFontSizeString));
4194                 } catch (Exception exception) {
4195                     // If the app settings value is invalid, set the font size to 100%.
4196                     nestedScrollWebView.getSettings().setTextZoom(100);
4197                 }
4198
4199                 // Apply the form data setting if the API < 26.
4200                 if (Build.VERSION.SDK_INT < 26) {
4201                     nestedScrollWebView.getSettings().setSaveFormData(saveFormData);
4202                 }
4203
4204                 // Store the swipe to refresh status in the nested scroll WebView.
4205                 nestedScrollWebView.setSwipeToRefresh(defaultSwipeToRefresh);
4206
4207                 // Update the swipe refresh layout.
4208                 if (defaultSwipeToRefresh) {  // Swipe to refresh is enabled.
4209                     // Only enable the swipe refresh layout if the WebView is scrolled to the top.  It is updated every time the scroll changes.
4210                     swipeRefreshLayout.setEnabled(currentWebView.getScrollY() == 0);
4211                 } else {  // Swipe to refresh is disabled.
4212                     // Disable the swipe refresh layout.
4213                     swipeRefreshLayout.setEnabled(false);
4214                 }
4215
4216                 // Reset the pinned variables.
4217                 nestedScrollWebView.setDomainSettingsDatabaseId(-1);
4218
4219                 // Set third-party cookies status if API >= 21.
4220                 if (Build.VERSION.SDK_INT >= 21) {
4221                     cookieManager.setAcceptThirdPartyCookies(nestedScrollWebView, defaultThirdPartyCookiesEnabled);
4222                 }
4223
4224                 // Get the array position of the user agent name.
4225                 int userAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
4226
4227                 // Set the user agent.
4228                 switch (userAgentArrayPosition) {
4229                     case UNRECOGNIZED_USER_AGENT:  // The default user agent name is not on the canonical list.
4230                         // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
4231                         nestedScrollWebView.getSettings().setUserAgentString(defaultUserAgentName);
4232                         break;
4233
4234                     case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
4235                         // Set the user agent to `""`, which uses the default value.
4236                         nestedScrollWebView.getSettings().setUserAgentString("");
4237                         break;
4238
4239                     case SETTINGS_CUSTOM_USER_AGENT:
4240                         // Set the default custom user agent.
4241                         nestedScrollWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
4242                         break;
4243
4244                     default:
4245                         // Get the user agent string from the user agent data array
4246                         nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
4247                 }
4248
4249                 // Apply the WebView theme if supported by the installed WebView.
4250                 if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
4251                     // Set the WebView theme.  A switch statement cannot be used because the WebView theme entry values string array is not a compile time constant.
4252                     if (webViewTheme.equals(webViewThemeEntryValuesStringArray[1])) {  // The light theme is selected.
4253                         // Turn off the WebView dark mode.
4254                         WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
4255                     } else if (webViewTheme.equals(webViewThemeEntryValuesStringArray[2])) {  // The dark theme is selected.
4256                         // Turn on the WebView dark mode.
4257                         WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
4258                     } else {  // The system default theme is selected.
4259                         // Get the current system theme status.
4260                         int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
4261
4262                         // Set the WebView theme according to the current system theme status.
4263                         if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {  // The system is in day mode.
4264                             // Turn off the WebView dark mode.
4265                             WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
4266                         } else {  // The system is in night mode.
4267                             // Turn on the WebView dark mode.
4268                             WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
4269                         }
4270                     }
4271                 }
4272
4273                 // Set the viewport.
4274                 nestedScrollWebView.getSettings().setUseWideViewPort(wideViewport);
4275
4276                 // Set the loading of webpage images.
4277                 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
4278
4279                 // Set a transparent background on the URL relative layout.
4280                 urlRelativeLayout.setBackground(ResourcesCompat.getDrawable(getResources(), R.color.transparent, null));
4281             }
4282
4283             // Close the domains database helper.
4284             domainsDatabaseHelper.close();
4285
4286             // Update the privacy icons.
4287             updatePrivacyIcons(true);
4288         }
4289
4290         // Reload the website if returning from the Domains activity.
4291         if (reloadWebsite) {
4292             nestedScrollWebView.reload();
4293         }
4294
4295         // Load the URL if directed.  This makes sure that the domain settings are properly loaded before the URL.  By using `loadUrl()`, instead of `loadUrlFromBase()`, the Referer header will never be sent.
4296         if (loadUrl) {
4297             nestedScrollWebView.loadUrl(url, customHeaders);
4298         }
4299     }
4300
4301     private void applyProxy(boolean reloadWebViews) {
4302         // Set the proxy according to the mode.  `this` refers to the current activity where an alert dialog might be displayed.
4303         ProxyHelper.setProxy(getApplicationContext(), appBarLayout, proxyMode);
4304
4305         // Reset the waiting for proxy tracker.
4306         waitingForProxy = false;
4307
4308         // Get the current theme status.
4309         int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
4310
4311         // Update the user interface and reload the WebViews if requested.
4312         switch (proxyMode) {
4313             case ProxyHelper.NONE:
4314                 // Initialize a color background typed value.
4315                 TypedValue colorBackgroundTypedValue = new TypedValue();
4316
4317                 // Get the color background from the theme.
4318                 getTheme().resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true);
4319
4320                 // Get the color background int from the typed value.
4321                 int colorBackgroundInt = colorBackgroundTypedValue.data;
4322
4323                 // Set the default app bar layout background.
4324                 appBarLayout.setBackgroundColor(colorBackgroundInt);
4325                 break;
4326
4327             case ProxyHelper.TOR:
4328                 // Set the app bar background to indicate proxying through Orbot is enabled.
4329                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
4330                     appBarLayout.setBackgroundResource(R.color.blue_50);
4331                 } else {
4332                     appBarLayout.setBackgroundResource(R.color.dark_blue_30);
4333                 }
4334
4335                 // Check to see if Orbot is installed.
4336                 try {
4337                     // Get the package manager.
4338                     PackageManager packageManager = getPackageManager();
4339
4340                     // Check to see if Orbot is in the list.  This will throw an error and drop to the catch section if it isn't installed.
4341                     packageManager.getPackageInfo("org.torproject.android", 0);
4342
4343                     // Check to see if the proxy is ready.
4344                     if (!orbotStatus.equals("ON")) {  // Orbot is not ready.
4345                         // Set the waiting for proxy status.
4346                         waitingForProxy = true;
4347
4348                         // Show the waiting for proxy dialog if it isn't already displayed.
4349                         if (getSupportFragmentManager().findFragmentByTag(getString(R.string.waiting_for_proxy_dialog)) == null) {
4350                             // Get a handle for the waiting for proxy alert dialog.
4351                             DialogFragment waitingForProxyDialogFragment = new WaitingForProxyDialog();
4352
4353                             // Display the waiting for proxy alert dialog.
4354                             waitingForProxyDialogFragment.show(getSupportFragmentManager(), getString(R.string.waiting_for_proxy_dialog));
4355                         }
4356                     }
4357                 } catch (PackageManager.NameNotFoundException exception) {  // Orbot is not installed.
4358                     // Show the Orbot not installed dialog if it is not already displayed.
4359                     if (getSupportFragmentManager().findFragmentByTag(getString(R.string.proxy_not_installed_dialog)) == null) {
4360                         // Get a handle for the Orbot not installed alert dialog.
4361                         DialogFragment orbotNotInstalledDialogFragment = ProxyNotInstalledDialog.displayDialog(proxyMode);
4362
4363                         // Display the Orbot not installed alert dialog.
4364                         orbotNotInstalledDialogFragment.show(getSupportFragmentManager(), getString(R.string.proxy_not_installed_dialog));
4365                     }
4366                 }
4367                 break;
4368
4369             case ProxyHelper.I2P:
4370                 // Set the app bar background to indicate proxying through Orbot is enabled.
4371                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
4372                     appBarLayout.setBackgroundResource(R.color.blue_50);
4373                 } else {
4374                     appBarLayout.setBackgroundResource(R.color.dark_blue_30);
4375                 }
4376
4377                 // Check to see if I2P is installed.
4378                 try {
4379                     // Get the package manager.
4380                     PackageManager packageManager = getPackageManager();
4381
4382                     // Check to see if I2P is in the list.  This will throw an error and drop to the catch section if it isn't installed.
4383                     packageManager.getPackageInfo("net.i2p.android.router", 0);
4384                 } catch (PackageManager.NameNotFoundException exception) {  // I2P is not installed.
4385                     // Sow the I2P not installed dialog if it is not already displayed.
4386                     if (getSupportFragmentManager().findFragmentByTag(getString(R.string.proxy_not_installed_dialog)) == null) {
4387                         // Get a handle for the waiting for proxy alert dialog.
4388                         DialogFragment i2pNotInstalledDialogFragment = ProxyNotInstalledDialog.displayDialog(proxyMode);
4389
4390                         // Display the I2P not installed alert dialog.
4391                         i2pNotInstalledDialogFragment.show(getSupportFragmentManager(), getString(R.string.proxy_not_installed_dialog));
4392                     }
4393                 }
4394                 break;
4395
4396             case ProxyHelper.CUSTOM:
4397                 // Set the app bar background to indicate proxying through Orbot is enabled.
4398                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
4399                     appBarLayout.setBackgroundResource(R.color.blue_50);
4400                 } else {
4401                     appBarLayout.setBackgroundResource(R.color.dark_blue_30);
4402                 }
4403                 break;
4404         }
4405
4406         // Reload the WebViews if requested and not waiting for the proxy.
4407         if (reloadWebViews && !waitingForProxy) {
4408             // Reload the WebViews.
4409             for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
4410                 // Get the WebView tab fragment.
4411                 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
4412
4413                 // Get the fragment view.
4414                 View fragmentView = webViewTabFragment.getView();
4415
4416                 // Only reload the WebViews if they exist.
4417                 if (fragmentView != null) {
4418                     // Get the nested scroll WebView from the tab fragment.
4419                     NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
4420
4421                     // Reload the WebView.
4422                     nestedScrollWebView.reload();
4423                 }
4424             }
4425         }
4426     }
4427
4428     private void updatePrivacyIcons(boolean runInvalidateOptionsMenu) {
4429         // Only update the privacy icons if the options menu and the current WebView have already been populated.
4430         if ((optionsMenu != null) && (currentWebView != null)) {
4431             // Update the privacy icon.
4432             if (currentWebView.getSettings().getJavaScriptEnabled()) {  // JavaScript is enabled.
4433                 optionsPrivacyMenuItem.setIcon(R.drawable.javascript_enabled);
4434             } else if (currentWebView.getAcceptFirstPartyCookies()) {  // JavaScript is disabled but cookies are enabled.
4435                 optionsPrivacyMenuItem.setIcon(R.drawable.warning);
4436             } else {  // All the dangerous features are disabled.
4437                 optionsPrivacyMenuItem.setIcon(R.drawable.privacy_mode);
4438             }
4439
4440             // Get the current theme status.
4441             int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
4442
4443             // Update the first-party cookies icon.
4444             if (currentWebView.getAcceptFirstPartyCookies()) {  // First-party cookies are enabled.
4445                 optionsFirstPartyCookiesMenuItem.setIcon(R.drawable.cookies_enabled);
4446             } else {  // First-party cookies are disabled.
4447                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
4448                     optionsFirstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_day);
4449                 } else {
4450                     optionsFirstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_night);
4451                 }
4452             }
4453
4454             // Update the refresh icon.
4455             if (optionsRefreshMenuItem.getTitle() == getString(R.string.refresh)) {  // The refresh icon is displayed.
4456                 // Set the icon according to the theme.
4457                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
4458                     optionsRefreshMenuItem.setIcon(R.drawable.refresh_enabled_day);
4459                 } else {
4460                     optionsRefreshMenuItem.setIcon(R.drawable.refresh_enabled_night);
4461                 }
4462             } else {  // The stop icon is displayed.
4463                 // Set the icon according to the theme.
4464                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
4465                     optionsRefreshMenuItem.setIcon(R.drawable.close_blue_day);
4466                 } else {
4467                     optionsRefreshMenuItem.setIcon(R.drawable.close_blue_night);
4468                 }
4469             }
4470
4471             // `invalidateOptionsMenu()` calls `onPrepareOptionsMenu()` and redraws the icons in the app bar.
4472             if (runInvalidateOptionsMenu) {
4473                 invalidateOptionsMenu();
4474             }
4475         }
4476     }
4477
4478     private void highlightUrlText() {
4479         // Only highlight the URL text if the box is not currently selected.
4480         if (!urlEditText.hasFocus()) {
4481             // Get the URL string.
4482             String urlString = urlEditText.getText().toString();
4483
4484             // Highlight the URL according to the protocol.
4485             if (urlString.startsWith("file://") || urlString.startsWith("content://")) {  // This is a file or content URL.
4486                 // De-emphasize everything before the file name.
4487                 urlEditText.getText().setSpan(initialGrayColorSpan, 0, urlString.lastIndexOf("/") + 1,Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4488             } else {  // This is a web URL.
4489                 // Get the index of the `/` immediately after the domain name.
4490                 int endOfDomainName = urlString.indexOf("/", (urlString.indexOf("//") + 2));
4491
4492                 // Create a base URL string.
4493                 String baseUrl;
4494
4495                 // Get the base URL.
4496                 if (endOfDomainName > 0) {  // There is at least one character after the base URL.
4497                     // Get the base URL.
4498                     baseUrl = urlString.substring(0, endOfDomainName);
4499                 } else {  // There are no characters after the base URL.
4500                     // Set the base URL to be the entire URL string.
4501                     baseUrl = urlString;
4502                 }
4503
4504                 // Get the index of the last `.` in the domain.
4505                 int lastDotIndex = baseUrl.lastIndexOf(".");
4506
4507                 // Get the index of the penultimate `.` in the domain.
4508                 int penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1);
4509
4510                 // Markup the beginning of the URL.
4511                 if (urlString.startsWith("http://")) {  // Highlight the protocol of connections that are not encrypted.
4512                     urlEditText.getText().setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4513
4514                     // De-emphasize subdomains.
4515                     if (penultimateDotIndex > 0) {  // There is more than one subdomain in the domain name.
4516                         urlEditText.getText().setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4517                     }
4518                 } else if (urlString.startsWith("https://")) {  // De-emphasize the protocol of connections that are encrypted.
4519                     if (penultimateDotIndex > 0) {  // There is more than one subdomain in the domain name.
4520                         // De-emphasize the protocol and the additional subdomains.
4521                         urlEditText.getText().setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4522                     } else {  // There is only one subdomain in the domain name.
4523                         // De-emphasize only the protocol.
4524                         urlEditText.getText().setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4525                     }
4526                 }
4527
4528                 // De-emphasize the text after the domain name.
4529                 if (endOfDomainName > 0) {
4530                     urlEditText.getText().setSpan(finalGrayColorSpan, endOfDomainName, urlString.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4531                 }
4532             }
4533         }
4534     }
4535
4536     private void loadBookmarksFolder() {
4537         // Update the bookmarks cursor with the contents of the bookmarks database for the current folder.
4538         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
4539
4540         // Populate the bookmarks cursor adapter.  `this` specifies the `Context`.  `false` disables `autoRequery`.
4541         bookmarksCursorAdapter = new CursorAdapter(this, bookmarksCursor, false) {
4542             @Override
4543             public View newView(Context context, Cursor cursor, ViewGroup parent) {
4544                 // Inflate the individual item layout.  `false` does not attach it to the root.
4545                 return getLayoutInflater().inflate(R.layout.bookmarks_drawer_item_linearlayout, parent, false);
4546             }
4547
4548             @Override
4549             public void bindView(View view, Context context, Cursor cursor) {
4550                 // Get handles for the views.
4551                 ImageView bookmarkFavoriteIcon = view.findViewById(R.id.bookmark_favorite_icon);
4552                 TextView bookmarkNameTextView = view.findViewById(R.id.bookmark_name);
4553
4554                 // Get the favorite icon byte array from the cursor.
4555                 byte[] favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON));
4556
4557                 // Convert the byte array to a `Bitmap` beginning at the first byte and ending at the last.
4558                 Bitmap favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.length);
4559
4560                 // Display the bitmap in `bookmarkFavoriteIcon`.
4561                 bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap);
4562
4563                 // Get the bookmark name from the cursor and display it in `bookmarkNameTextView`.
4564                 String bookmarkNameString = cursor.getString(cursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
4565                 bookmarkNameTextView.setText(bookmarkNameString);
4566
4567                 // Make the font bold for folders.
4568                 if (cursor.getInt(cursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {
4569                     bookmarkNameTextView.setTypeface(Typeface.DEFAULT_BOLD);
4570                 } else {  // Reset the font to default for normal bookmarks.
4571                     bookmarkNameTextView.setTypeface(Typeface.DEFAULT);
4572                 }
4573             }
4574         };
4575
4576         // Get a handle for the bookmarks list view.
4577         ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
4578
4579         // Populate the list view with the adapter.
4580         bookmarksListView.setAdapter(bookmarksCursorAdapter);
4581
4582         // Get a handle for the bookmarks title text view.
4583         TextView bookmarksTitleTextView = findViewById(R.id.bookmarks_title_textview);
4584
4585         // Set the bookmarks drawer title.
4586         if (currentBookmarksFolder.isEmpty()) {
4587             bookmarksTitleTextView.setText(R.string.bookmarks);
4588         } else {
4589             bookmarksTitleTextView.setText(currentBookmarksFolder);
4590         }
4591     }
4592
4593     private void openWithApp(String url) {
4594         // Create an open with app intent with `ACTION_VIEW`.
4595         Intent openWithAppIntent = new Intent(Intent.ACTION_VIEW);
4596
4597         // Set the URI but not the MIME type.  This should open all available apps.
4598         openWithAppIntent.setData(Uri.parse(url));
4599
4600         // Flag the intent to open in a new task.
4601         openWithAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4602
4603         // Try the intent.
4604         try {
4605             // Show the chooser.
4606             startActivity(openWithAppIntent);
4607         } catch (ActivityNotFoundException exception) {  // There are no apps available to open the URL.
4608             // Show a snackbar with the error.
4609             Snackbar.make(currentWebView, getString(R.string.error) + "  " + exception, Snackbar.LENGTH_INDEFINITE).show();
4610         }
4611     }
4612
4613     private void openWithBrowser(String url) {
4614         // Create an open with browser intent with `ACTION_VIEW`.
4615         Intent openWithBrowserIntent = new Intent(Intent.ACTION_VIEW);
4616
4617         // Set the URI and the MIME type.  `"text/html"` should load browser options.
4618         openWithBrowserIntent.setDataAndType(Uri.parse(url), "text/html");
4619
4620         // Flag the intent to open in a new task.
4621         openWithBrowserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4622
4623         // Try the intent.
4624         try {
4625             // Show the chooser.
4626             startActivity(openWithBrowserIntent);
4627         } catch (ActivityNotFoundException exception) {  // There are no browsers available to open the URL.
4628             // Show a snackbar with the error.
4629             Snackbar.make(currentWebView, getString(R.string.error) + "  " + exception, Snackbar.LENGTH_INDEFINITE).show();
4630         }
4631     }
4632
4633     private String sanitizeUrl(String url) {
4634         // Sanitize Google Analytics.
4635         if (sanitizeGoogleAnalytics) {
4636             // Remove `?utm_`.
4637             if (url.contains("?utm_")) {
4638                 url = url.substring(0, url.indexOf("?utm_"));
4639             }
4640
4641             // Remove `&utm_`.
4642             if (url.contains("&utm_")) {
4643                 url = url.substring(0, url.indexOf("&utm_"));
4644             }
4645         }
4646
4647         // Sanitize Facebook Click IDs.
4648         if (sanitizeFacebookClickIds) {
4649             // Remove `?fbclid=`.
4650             if (url.contains("?fbclid=")) {
4651                 url = url.substring(0, url.indexOf("?fbclid="));
4652             }
4653
4654             // Remove `&fbclid=`.
4655             if (url.contains("&fbclid=")) {
4656                 url = url.substring(0, url.indexOf("&fbclid="));
4657             }
4658
4659             // Remove `?fbadid=`.
4660             if (url.contains("?fbadid=")) {
4661                 url = url.substring(0, url.indexOf("?fbadid="));
4662             }
4663
4664             // Remove `&fbadid=`.
4665             if (url.contains("&fbadid=")) {
4666                 url = url.substring(0, url.indexOf("&fbadid="));
4667             }
4668         }
4669
4670         // Sanitize Twitter AMP redirects.
4671         if (sanitizeTwitterAmpRedirects) {
4672             // Remove `?amp=1`.
4673             if (url.contains("?amp=1")) {
4674                 url = url.substring(0, url.indexOf("?amp=1"));
4675             }
4676         }
4677
4678         // Return the sanitized URL.
4679         return url;
4680     }
4681
4682     public void finishedPopulatingBlocklists(ArrayList<ArrayList<List<String[]>>> combinedBlocklists) {
4683         // Store the blocklists.
4684         easyList = combinedBlocklists.get(0);
4685         easyPrivacy = combinedBlocklists.get(1);
4686         fanboysAnnoyanceList = combinedBlocklists.get(2);
4687         fanboysSocialList = combinedBlocklists.get(3);
4688         ultraList = combinedBlocklists.get(4);
4689         ultraPrivacy = combinedBlocklists.get(5);
4690
4691         // Check to see if the activity has been restarted with a saved state.
4692         if ((savedStateArrayList == null) || (savedStateArrayList.size() == 0)) {  // The activity has not been restarted or it was restarted on start to force the night theme.
4693             // Add the first tab.
4694             addNewTab("", true);
4695         } else {  // The activity has been restarted.
4696             // Restore each tab.  Once the minimum API >= 24, a `forEach()` command can be used.
4697             for (int i = 0; i < savedStateArrayList.size(); i++) {
4698                 // Add a new tab.
4699                 tabLayout.addTab(tabLayout.newTab());
4700
4701                 // Get the new tab.
4702                 TabLayout.Tab newTab = tabLayout.getTabAt(i);
4703
4704                 // Remove the lint warning below that the current tab might be null.
4705                 assert newTab != null;
4706
4707                 // Set a custom view on the new tab.
4708                 newTab.setCustomView(R.layout.tab_custom_view);
4709
4710                 // Add the new page.
4711                 webViewPagerAdapter.restorePage(savedStateArrayList.get(i), savedNestedScrollWebViewStateArrayList.get(i));
4712             }
4713
4714             // Reset the saved state variables.
4715             savedStateArrayList = null;
4716             savedNestedScrollWebViewStateArrayList = null;
4717
4718             // Restore the selected tab position.
4719             if (savedTabPosition == 0) {  // The first tab is selected.
4720                 // Set the first page as the current WebView.
4721                 setCurrentWebView(0);
4722             } else {  // the first tab is not selected.
4723                 // Move to the selected tab.
4724                 webViewPager.setCurrentItem(savedTabPosition);
4725             }
4726
4727             // Get the intent that started the app.
4728             Intent intent = getIntent();
4729
4730             // Reset the intent.  This prevents a duplicate tab from being created on restart.
4731             setIntent(new Intent());
4732
4733             // Get the information from the intent.
4734             String intentAction = intent.getAction();
4735             Uri intentUriData = intent.getData();
4736             String intentStringExtra = intent.getStringExtra(Intent.EXTRA_TEXT);
4737
4738             // Determine if this is a web search.
4739             boolean isWebSearch = ((intentAction != null) && intentAction.equals(Intent.ACTION_WEB_SEARCH));
4740
4741             // 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.
4742             if (intentUriData != null || intentStringExtra != null || isWebSearch) {
4743                 // Get the shared preferences.
4744                 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4745
4746                 // Create a URL string.
4747                 String url;
4748
4749                 // If the intent action is a web search, perform the search.
4750                 if (isWebSearch) {  // The intent is a web search.
4751                     // Create an encoded URL string.
4752                     String encodedUrlString;
4753
4754                     // Sanitize the search input and convert it to a search.
4755                     try {
4756                         encodedUrlString = URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8");
4757                     } catch (UnsupportedEncodingException exception) {
4758                         encodedUrlString = "";
4759                     }
4760
4761                     // Add the base search URL.
4762                     url = searchURL + encodedUrlString;
4763                 } else if (intentUriData != null) {  // The intent contains a URL formatted as a URI.
4764                     // Set the intent data as the URL.
4765                     url = intentUriData.toString();
4766                 } else {  // The intent contains a string, which might be a URL.
4767                     // Set the intent string as the URL.
4768                     url = intentStringExtra;
4769                 }
4770
4771                 // Add a new tab if specified in the preferences.
4772                 if (sharedPreferences.getBoolean("open_intents_in_new_tab", true)) {  // Load the URL in a new tab.
4773                     // Set the loading new intent flag.
4774                     loadingNewIntent = true;
4775
4776                     // Add a new tab.
4777                     addNewTab(url, true);
4778                 } else {  // Load the URL in the current tab.
4779                     // Make it so.
4780                     loadUrl(currentWebView, url);
4781                 }
4782             }
4783         }
4784     }
4785
4786     public void addTab(View view) {
4787         // Add a new tab with a blank URL.
4788         addNewTab("", true);
4789     }
4790
4791     private void addNewTab(String url, boolean moveToTab) {
4792         // Clear the focus from the URL edit text, so that it will be populated with the information from the new tab.
4793         urlEditText.clearFocus();
4794
4795         // Get the new page number.  The page numbers are 0 indexed, so the new page number will match the current count.
4796         int newTabNumber = tabLayout.getTabCount();
4797
4798         // Add a new tab.
4799         tabLayout.addTab(tabLayout.newTab());
4800
4801         // Get the new tab.
4802         TabLayout.Tab newTab = tabLayout.getTabAt(newTabNumber);
4803
4804         // Remove the lint warning below that the current tab might be null.
4805         assert newTab != null;
4806
4807         // Set a custom view on the new tab.
4808         newTab.setCustomView(R.layout.tab_custom_view);
4809
4810         // Add the new WebView page.
4811         webViewPagerAdapter.addPage(newTabNumber, webViewPager, url, moveToTab);
4812     }
4813
4814     public void closeTab(View view) {
4815         // Run the command according to the number of tabs.
4816         if (tabLayout.getTabCount() > 1) {  // There is more than one tab open.
4817             // Close the current tab.
4818             closeCurrentTab();
4819         } else {  // There is only one tab open.
4820             clearAndExit();
4821         }
4822     }
4823
4824     private void closeCurrentTab() {
4825         // Get the current tab number.
4826         int currentTabNumber = tabLayout.getSelectedTabPosition();
4827
4828         // Delete the current tab.
4829         tabLayout.removeTabAt(currentTabNumber);
4830
4831         // Delete the current page.  If the selected page number did not change during the delete (because the newly selected tab has has same number as the previously deleted tab), it will return true,
4832         // meaning that the current WebView must be reset.  Otherwise it will happen automatically as the selected tab number changes.
4833         if (webViewPagerAdapter.deletePage(currentTabNumber, webViewPager)) {
4834             setCurrentWebView(currentTabNumber);
4835         }
4836     }
4837
4838     private void clearAndExit() {
4839         // Get a handle for the shared preferences.
4840         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4841
4842         // Close the bookmarks cursor and database.
4843         bookmarksCursor.close();
4844         bookmarksDatabaseHelper.close();
4845
4846         // Get the status of the clear everything preference.
4847         boolean clearEverything = sharedPreferences.getBoolean("clear_everything", true);
4848
4849         // Get a handle for the runtime.
4850         Runtime runtime = Runtime.getRuntime();
4851
4852         // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
4853         // which links to `/data/data/com.stoutner.privacybrowser.standard`.
4854         String privateDataDirectoryString = getApplicationInfo().dataDir;
4855
4856         // Clear cookies.
4857         if (clearEverything || sharedPreferences.getBoolean("clear_cookies", true)) {
4858             // The command to remove cookies changed slightly in API 21.
4859             if (Build.VERSION.SDK_INT >= 21) {
4860                 CookieManager.getInstance().removeAllCookies(null);
4861             } else {
4862                 CookieManager.getInstance().removeAllCookie();
4863             }
4864
4865             // Manually delete the cookies database, as `CookieManager` sometimes will not flush its changes to disk before `System.exit(0)` is run.
4866             try {
4867                 // Two commands must be used because `Runtime.exec()` does not like `*`.
4868                 Process deleteCookiesProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies");
4869                 Process deleteCookiesJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies-journal");
4870
4871                 // Wait until the processes have finished.
4872                 deleteCookiesProcess.waitFor();
4873                 deleteCookiesJournalProcess.waitFor();
4874             } catch (Exception exception) {
4875                 // Do nothing if an error is thrown.
4876             }
4877         }
4878
4879         // Clear DOM storage.
4880         if (clearEverything || sharedPreferences.getBoolean("clear_dom_storage", true)) {
4881             // Ask `WebStorage` to clear the DOM storage.
4882             WebStorage webStorage = WebStorage.getInstance();
4883             webStorage.deleteAllData();
4884
4885             // Manually delete the DOM storage files and directories, as `WebStorage` sometimes will not flush its changes to disk before `System.exit(0)` is run.
4886             try {
4887                 // A `String[]` must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
4888                 Process deleteLocalStorageProcess = runtime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
4889
4890                 // Multiple commands must be used because `Runtime.exec()` does not like `*`.
4891                 Process deleteIndexProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
4892                 Process deleteQuotaManagerProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
4893                 Process deleteQuotaManagerJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
4894                 Process deleteDatabaseProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
4895
4896                 // Wait until the processes have finished.
4897                 deleteLocalStorageProcess.waitFor();
4898                 deleteIndexProcess.waitFor();
4899                 deleteQuotaManagerProcess.waitFor();
4900                 deleteQuotaManagerJournalProcess.waitFor();
4901                 deleteDatabaseProcess.waitFor();
4902             } catch (Exception exception) {
4903                 // Do nothing if an error is thrown.
4904             }
4905         }
4906
4907         // Clear form data if the API < 26.
4908         if ((Build.VERSION.SDK_INT < 26) && (clearEverything || sharedPreferences.getBoolean("clear_form_data", true))) {
4909             WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
4910             webViewDatabase.clearFormData();
4911
4912             // Manually delete the form data database, as `WebViewDatabase` sometimes will not flush its changes to disk before `System.exit(0)` is run.
4913             try {
4914                 // A string array must be used because the database contains a space and `Runtime.exec` will not otherwise escape the string correctly.
4915                 Process deleteWebDataProcess = runtime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data"});
4916                 Process deleteWebDataJournalProcess = runtime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data-journal"});
4917
4918                 // Wait until the processes have finished.
4919                 deleteWebDataProcess.waitFor();
4920                 deleteWebDataJournalProcess.waitFor();
4921             } catch (Exception exception) {
4922                 // Do nothing if an error is thrown.
4923             }
4924         }
4925
4926         // Clear the logcat.
4927         if (clearEverything || sharedPreferences.getBoolean(getString(R.string.clear_logcat_key), true)) {
4928             try {
4929                 // Clear the logcat.  `-c` clears the logcat.  `-b all` clears all the buffers (instead of just crash, main, and system).
4930                 Process process = Runtime.getRuntime().exec("logcat -b all -c");
4931
4932                 // Wait for the process to finish.
4933                 process.waitFor();
4934             } catch (IOException|InterruptedException exception) {
4935                 // Do nothing.
4936             }
4937         }
4938
4939         // Clear the cache.
4940         if (clearEverything || sharedPreferences.getBoolean("clear_cache", true)) {
4941             // Clear the cache from each WebView.
4942             for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
4943                 // Get the WebView tab fragment.
4944                 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
4945
4946                 // Get the WebView fragment view.
4947                 View webViewFragmentView = webViewTabFragment.getView();
4948
4949                 // Only clear the cache if the WebView exists.
4950                 if (webViewFragmentView != null) {
4951                     // Get the nested scroll WebView from the tab fragment.
4952                     NestedScrollWebView nestedScrollWebView = webViewFragmentView.findViewById(R.id.nestedscroll_webview);
4953
4954                     // Clear the cache for this WebView.
4955                     nestedScrollWebView.clearCache(true);
4956                 }
4957             }
4958
4959             // Manually delete the cache directories.
4960             try {
4961                 // Delete the main cache directory.
4962                 Process deleteCacheProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/cache");
4963
4964                 // Delete the secondary `Service Worker` cache directory.
4965                 // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
4966                 Process deleteServiceWorkerProcess = runtime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
4967
4968                 // Wait until the processes have finished.
4969                 deleteCacheProcess.waitFor();
4970                 deleteServiceWorkerProcess.waitFor();
4971             } catch (Exception exception) {
4972                 // Do nothing if an error is thrown.
4973             }
4974         }
4975
4976         // Wipe out each WebView.
4977         for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
4978             // Get the WebView tab fragment.
4979             WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
4980
4981             // Get the WebView frame layout.
4982             FrameLayout webViewFrameLayout = (FrameLayout) webViewTabFragment.getView();
4983
4984             // Only wipe out the WebView if it exists.
4985             if (webViewFrameLayout != null) {
4986                 // Get the nested scroll WebView from the tab fragment.
4987                 NestedScrollWebView nestedScrollWebView = webViewFrameLayout.findViewById(R.id.nestedscroll_webview);
4988
4989                 // Clear SSL certificate preferences for this WebView.
4990                 nestedScrollWebView.clearSslPreferences();
4991
4992                 // Clear the back/forward history for this WebView.
4993                 nestedScrollWebView.clearHistory();
4994
4995                 // Remove all the views from the frame layout.
4996                 webViewFrameLayout.removeAllViews();
4997
4998                 // Destroy the internal state of the WebView.
4999                 nestedScrollWebView.destroy();
5000             }
5001         }
5002
5003         // Clear the custom headers.
5004         customHeaders.clear();
5005
5006         // Manually delete the `app_webview` folder, which contains the cookies, DOM storage, form data, and `Service Worker` cache.
5007         // See `https://code.google.com/p/android/issues/detail?id=233826&thanks=233826&ts=1486670530`.
5008         if (clearEverything) {
5009             try {
5010                 // Delete the folder.
5011                 Process deleteAppWebviewProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview");
5012
5013                 // Wait until the process has finished.
5014                 deleteAppWebviewProcess.waitFor();
5015             } catch (Exception exception) {
5016                 // Do nothing if an error is thrown.
5017             }
5018         }
5019
5020         // Close Privacy Browser.  `finishAndRemoveTask` also removes Privacy Browser from the recent app list.
5021         if (Build.VERSION.SDK_INT >= 21) {
5022             finishAndRemoveTask();
5023         } else {
5024             finish();
5025         }
5026
5027         // Remove the terminated program from RAM.  The status code is `0`.
5028         System.exit(0);
5029     }
5030
5031     public void bookmarksBack(View view) {
5032         if (currentBookmarksFolder.isEmpty()) {  // The home folder is displayed.
5033             // close the bookmarks drawer.
5034             drawerLayout.closeDrawer(GravityCompat.END);
5035         } else {  // A subfolder is displayed.
5036             // Place the former parent folder in `currentFolder`.
5037             currentBookmarksFolder = bookmarksDatabaseHelper.getParentFolderName(currentBookmarksFolder);
5038
5039             // Load the new folder.
5040             loadBookmarksFolder();
5041         }
5042     }
5043
5044     private void setCurrentWebView(int pageNumber) {
5045         // Stop the swipe to refresh indicator if it is running
5046         swipeRefreshLayout.setRefreshing(false);
5047
5048         // Get the WebView tab fragment.
5049         WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(pageNumber);
5050
5051         // Get the fragment view.
5052         View webViewFragmentView = webViewTabFragment.getView();
5053
5054         // Set the current WebView if the fragment view is not null.
5055         if (webViewFragmentView != null) {  // The fragment has been populated.
5056             // Store the current WebView.
5057             currentWebView = webViewFragmentView.findViewById(R.id.nestedscroll_webview);
5058
5059             // Update the status of swipe to refresh.
5060             if (currentWebView.getSwipeToRefresh()) {  // Swipe to refresh is enabled.
5061                 // Enable the swipe refresh layout if the WebView is scrolled all the way to the top.  It is updated every time the scroll changes.
5062                 swipeRefreshLayout.setEnabled(currentWebView.getScrollY() == 0);
5063             } else {  // Swipe to refresh is disabled.
5064                 // Disable the swipe refresh layout.
5065                 swipeRefreshLayout.setEnabled(false);
5066             }
5067
5068             // Get a handle for the cookie manager.
5069             CookieManager cookieManager = CookieManager.getInstance();
5070
5071             // Set the first-party cookie status.
5072             cookieManager.setAcceptCookie(currentWebView.getAcceptFirstPartyCookies());
5073
5074             // Update the privacy icons.  `true` redraws the icons in the app bar.
5075             updatePrivacyIcons(true);
5076
5077             // Get a handle for the input method manager.
5078             InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
5079
5080             // Remove the lint warning below that the input method manager might be null.
5081             assert inputMethodManager != null;
5082
5083             // Get the current URL.
5084             String url = currentWebView.getUrl();
5085
5086             // Update the URL edit text if not loading a new intent.  Otherwise, this will be handled by `onPageStarted()` (if called) and `onPageFinished()`.
5087             if (!loadingNewIntent) {  // A new intent is not being loaded.
5088                 if ((url == null) || url.equals("about:blank")) {  // The WebView is blank.
5089                     // Display the hint in the URL edit text.
5090                     urlEditText.setText("");
5091
5092                     // Request focus for the URL text box.
5093                     urlEditText.requestFocus();
5094
5095                     // Display the keyboard.
5096                     inputMethodManager.showSoftInput(urlEditText, 0);
5097                 } else {  // The WebView has a loaded URL.
5098                     // Clear the focus from the URL text box.
5099                     urlEditText.clearFocus();
5100
5101                     // Hide the soft keyboard.
5102                     inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
5103
5104                     // Display the current URL in the URL text box.
5105                     urlEditText.setText(url);
5106
5107                     // Highlight the URL text.
5108                     highlightUrlText();
5109                 }
5110             } else {  // A new intent is being loaded.
5111                 // Reset the loading new intent tracker.
5112                 loadingNewIntent = false;
5113             }
5114
5115             // Set the background to indicate the domain settings status.
5116             if (currentWebView.getDomainSettingsApplied()) {
5117                 // Get the current theme status.
5118                 int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
5119
5120                 // Set a green background on the URL relative layout to indicate that custom domain settings are being used.
5121                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
5122                     urlRelativeLayout.setBackground(ResourcesCompat.getDrawable(getResources(), R.drawable.url_bar_background_light_green, null));
5123                 } else {
5124                     urlRelativeLayout.setBackground(ResourcesCompat.getDrawable(getResources(), R.drawable.url_bar_background_dark_blue, null));
5125                 }
5126             } else {
5127                 urlRelativeLayout.setBackground(ResourcesCompat.getDrawable(getResources(), R.color.transparent, null));
5128             }
5129         } else {  // The fragment has not been populated.  Try again in 100 milliseconds.
5130             // Create a handler to set the current WebView.
5131             Handler setCurrentWebViewHandler = new Handler();
5132
5133             // Create a runnable to set the current WebView.
5134             Runnable setCurrentWebWebRunnable = () -> {
5135                 // Set the current WebView.
5136                 setCurrentWebView(pageNumber);
5137             };
5138
5139             // Try setting the current WebView again after 100 milliseconds.
5140             setCurrentWebViewHandler.postDelayed(setCurrentWebWebRunnable, 100);
5141         }
5142     }
5143
5144     @Override
5145     public void initializeWebView(NestedScrollWebView nestedScrollWebView, int pageNumber, ProgressBar progressBar, String url, Boolean restoringState) {
5146         // Get a handle for the shared preferences.
5147         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
5148
5149         // Get the WebView theme.
5150         String webViewTheme = sharedPreferences.getString("webview_theme", getString(R.string.webview_theme_default_value));
5151
5152         // Get the WebView theme entry values string array.
5153         String[] webViewThemeEntryValuesStringArray = getResources().getStringArray(R.array.webview_theme_entry_values);
5154
5155         // Apply the WebView theme if supported by the installed WebView.
5156         if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
5157             // Set the WebView theme.  A switch statement cannot be used because the WebView theme entry values string array is not a compile time constant.
5158             if (webViewTheme.equals(webViewThemeEntryValuesStringArray[1])) {  // The light theme is selected.
5159                 // Turn off the WebView dark mode.
5160                 WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
5161
5162                 // Make the WebView visible. The WebView was created invisible in `webview_framelayout` to prevent a white background splash in night mode.
5163                 // If the system is currently in night mode, showing the WebView will be handled in `onProgressChanged()`.
5164                 nestedScrollWebView.setVisibility(View.VISIBLE);
5165             } else if (webViewTheme.equals(webViewThemeEntryValuesStringArray[2])) {  // The dark theme is selected.
5166                 // Turn on the WebView dark mode.
5167                 WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
5168             } else {  // The system default theme is selected.
5169                 // Get the current system theme status.
5170                 int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
5171
5172                 // Set the WebView theme according to the current system theme status.
5173                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {  // The system is in day mode.
5174                     // Turn off the WebView dark mode.
5175                     WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
5176
5177                     // Make the WebView visible. The WebView was created invisible in `webview_framelayout` to prevent a white background splash in night mode.
5178                     // If the system is currently in night mode, showing the WebView will be handled in `onProgressChanged()`.
5179                     nestedScrollWebView.setVisibility(View.VISIBLE);
5180                 } else {  // The system is in night mode.
5181                     // Turn on the WebView dark mode.
5182                     WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
5183                 }
5184             }
5185         }
5186
5187         // Get a handle for the activity
5188         Activity activity = this;
5189
5190         // Get a handle for the input method manager.
5191         InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
5192
5193         // Instantiate the blocklist helper.
5194         BlocklistHelper blocklistHelper = new BlocklistHelper();
5195
5196         // Remove the lint warning below that the input method manager might be null.
5197         assert inputMethodManager != null;
5198
5199         // Initialize the favorite icon.
5200         nestedScrollWebView.initializeFavoriteIcon();
5201
5202         // Set the app bar scrolling.
5203         nestedScrollWebView.setNestedScrollingEnabled(sharedPreferences.getBoolean("scroll_app_bar", true));
5204
5205         // Allow pinch to zoom.
5206         nestedScrollWebView.getSettings().setBuiltInZoomControls(true);
5207
5208         // Hide zoom controls.
5209         nestedScrollWebView.getSettings().setDisplayZoomControls(false);
5210
5211         // Don't allow mixed content (HTTP and HTTPS) on the same website.
5212         if (Build.VERSION.SDK_INT >= 21) {
5213             nestedScrollWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW);
5214         }
5215
5216         // Set the WebView to load in overview mode (zoomed out to the maximum width).
5217         nestedScrollWebView.getSettings().setLoadWithOverviewMode(true);
5218
5219         // Explicitly disable geolocation.
5220         nestedScrollWebView.getSettings().setGeolocationEnabled(false);
5221
5222         // Allow loading of file:// URLs.  This is necessary for opening MHT web archives, which are copies into a temporary cache location.
5223         nestedScrollWebView.getSettings().setAllowFileAccess(true);
5224
5225         // Create a double-tap gesture detector to toggle full-screen mode.
5226         GestureDetector doubleTapGestureDetector = new GestureDetector(getApplicationContext(), new GestureDetector.SimpleOnGestureListener() {
5227             // Override `onDoubleTap()`.  All other events are handled using the default settings.
5228             @Override
5229             public boolean onDoubleTap(MotionEvent event) {
5230                 if (fullScreenBrowsingModeEnabled) {  // Only process the double-tap if full screen browsing mode is enabled.
5231                     // Toggle the full screen browsing mode tracker.
5232                     inFullScreenBrowsingMode = !inFullScreenBrowsingMode;
5233
5234                     // Toggle the full screen browsing mode.
5235                     if (inFullScreenBrowsingMode) {  // Switch to full screen mode.
5236                         // Hide the app bar if specified.
5237                         if (hideAppBar) {
5238                             // Close the find on page bar if it is visible.
5239                             closeFindOnPage(null);
5240
5241                             // Hide the tab linear layout.
5242                             tabsLinearLayout.setVisibility(View.GONE);
5243
5244                             // Hide the action bar.
5245                             actionBar.hide();
5246
5247                             // Check to see if the app bar is normally scrolled.
5248                             if (scrollAppBar) {  // The app bar is scrolled when it is displayed.
5249                                 // Get the swipe refresh layout parameters.
5250                                 CoordinatorLayout.LayoutParams swipeRefreshLayoutParams = (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams();
5251
5252                                 // Remove the off-screen scrolling layout.
5253                                 swipeRefreshLayoutParams.setBehavior(null);
5254                             } else {  // The app bar is not scrolled when it is displayed.
5255                                 // Remove the padding from the top of the swipe refresh layout.
5256                                 swipeRefreshLayout.setPadding(0, 0, 0, 0);
5257
5258                                 // The swipe refresh circle must be moved above the now removed status bar location.
5259                                 swipeRefreshLayout.setProgressViewOffset(false, -200, defaultProgressViewEndOffset);
5260                             }
5261                         }
5262
5263                         // Hide the banner ad in the free flavor.
5264                         if (BuildConfig.FLAVOR.contentEquals("free")) {
5265                             // Get a handle for the ad view.  This cannot be a class variable because it changes with each ad load.
5266                             View adView = findViewById(R.id.adview);
5267
5268                             // Hide the banner ad.
5269                             AdHelper.hideAd(adView);
5270                         }
5271
5272                         /* Hide the system bars.
5273                          * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
5274                          * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
5275                          * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
5276                          * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
5277                          */
5278                         rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
5279                                 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
5280                     } else {  // Switch to normal viewing mode.
5281                         // Show the app bar if it was hidden.
5282                         if (hideAppBar) {
5283                             // Show the tab linear layout.
5284                             tabsLinearLayout.setVisibility(View.VISIBLE);
5285
5286                             // Show the action bar.
5287                             actionBar.show();
5288
5289                             // Check to see if the app bar is normally scrolled.
5290                             if (scrollAppBar) {  // The app bar is scrolled when it is displayed.
5291                                 // Get the swipe refresh layout parameters.
5292                                 CoordinatorLayout.LayoutParams swipeRefreshLayoutParams = (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams();
5293
5294                                 // Add the off-screen scrolling layout.
5295                                 swipeRefreshLayoutParams.setBehavior(new AppBarLayout.ScrollingViewBehavior());
5296                             } else {  // The app bar is not scrolled when it is displayed.
5297                                 // The swipe refresh layout must be manually moved below the app bar layout.
5298                                 swipeRefreshLayout.setPadding(0, appBarHeight, 0, 0);
5299
5300                                 // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
5301                                 swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10 + appBarHeight, defaultProgressViewEndOffset + appBarHeight);
5302                             }
5303                         }
5304
5305                         // Show the banner ad in the free flavor.
5306                         if (BuildConfig.FLAVOR.contentEquals("free")) {
5307                             // Get a handle for the ad view.  This cannot be a class variable because it changes with each ad load.
5308                             View adView = findViewById(R.id.adview);
5309
5310                             // Reload the ad.  `getContext()` can be used instead of `getActivity.getApplicationContext()` once the minimum API >= 23.
5311                             AdHelper.loadAd(adView, getApplicationContext(), activity, getString(R.string.ad_unit_id));
5312                         }
5313
5314                         // Remove the `SYSTEM_UI` flags from the root frame layout.
5315                         rootFrameLayout.setSystemUiVisibility(0);
5316                     }
5317
5318                     // Consume the double-tap.
5319                     return true;
5320                 } else { // Do not consume the double-tap because full screen browsing mode is disabled.
5321                     return false;
5322                 }
5323             }
5324         });
5325
5326         // Pass all touch events on the WebView through the double-tap gesture detector.
5327         nestedScrollWebView.setOnTouchListener((View view, MotionEvent event) -> {
5328             // Call `performClick()` on the view, which is required for accessibility.
5329             view.performClick();
5330
5331             // Send the event to the gesture detector.
5332             return doubleTapGestureDetector.onTouchEvent(event);
5333         });
5334
5335         // Register the WebView for a context menu.  This is used to see link targets and download images.
5336         registerForContextMenu(nestedScrollWebView);
5337
5338         // Allow the downloading of files.
5339         nestedScrollWebView.setDownloadListener((String downloadUrl, String userAgent, String contentDisposition, String mimetype, long contentLength) -> {
5340             // Define a formatted file size string.
5341             String formattedFileSizeString;
5342
5343             // Process the content length if it contains data.
5344             if (contentLength > 0) {  // The content length is greater than 0.
5345                 // Format the content length as a string.
5346                 formattedFileSizeString = NumberFormat.getInstance().format(contentLength) + " " + getString(R.string.bytes);
5347             } else {  // The content length is not greater than 0.
5348                 // Set the formatted file size string to be `unknown size`.
5349                 formattedFileSizeString = getString(R.string.unknown_size);
5350             }
5351
5352             // Get the file name from the content disposition.
5353             String fileNameString = PrepareSaveDialog.getFileNameFromHeaders(this, contentDisposition, mimetype, downloadUrl);
5354
5355             // Prevent the dialog from displaying if the app window is not visible.
5356             // The download listener continues to function even when the WebView is paused.  Attempting to display a dialog in that state leads to a crash.
5357             while (!activity.getWindow().isActive()) {
5358                 try {
5359                     // The window is not active.  Wait 1 second.
5360                     wait(1000);
5361                 } catch (InterruptedException e) {
5362                     // Do nothing.
5363                 }
5364             }
5365
5366             // Instantiate the save dialog.
5367             DialogFragment saveDialogFragment = SaveWebpageDialog.saveWebpage(SaveWebpageDialog.SAVE_URL, downloadUrl, formattedFileSizeString, fileNameString, userAgent,
5368                     nestedScrollWebView.getAcceptFirstPartyCookies());
5369
5370             // Show the save dialog.  It must be named `save_dialog` so that the file picker can update the file name.
5371             saveDialogFragment.show(getSupportFragmentManager(), getString(R.string.save_dialog));
5372         });
5373
5374         // Update the find on page count.
5375         nestedScrollWebView.setFindListener(new WebView.FindListener() {
5376             // Get a handle for `findOnPageCountTextView`.
5377             final TextView findOnPageCountTextView = findViewById(R.id.find_on_page_count_textview);
5378
5379             @Override
5380             public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) {
5381                 if ((isDoneCounting) && (numberOfMatches == 0)) {  // There are no matches.
5382                     // Set `findOnPageCountTextView` to `0/0`.
5383                     findOnPageCountTextView.setText(R.string.zero_of_zero);
5384                 } else if (isDoneCounting) {  // There are matches.
5385                     // `activeMatchOrdinal` is zero-based.
5386                     int activeMatch = activeMatchOrdinal + 1;
5387
5388                     // Build the match string.
5389                     String matchString = activeMatch + "/" + numberOfMatches;
5390
5391                     // Set `findOnPageCountTextView`.
5392                     findOnPageCountTextView.setText(matchString);
5393                 }
5394             }
5395         });
5396
5397         // Update the status of swipe to refresh based on the scroll position of the nested scroll WebView.  Also reinforce full screen browsing mode.
5398         // On API < 23, `getViewTreeObserver().addOnScrollChangedListener()` must be used, but it is a little bit buggy and appears to get garbage collected from time to time.
5399         if (Build.VERSION.SDK_INT >= 23) {
5400             nestedScrollWebView.setOnScrollChangeListener((view, i, i1, i2, i3) -> {
5401                 if (nestedScrollWebView.getSwipeToRefresh()) {
5402                     // Only enable swipe to refresh if the WebView is scrolled to the top.
5403                     swipeRefreshLayout.setEnabled(nestedScrollWebView.getScrollY() == 0);
5404                 } else {
5405                     // Disable swipe to refresh.
5406                     swipeRefreshLayout.setEnabled(false);
5407                 }
5408
5409                 // Reinforce the system UI visibility flags if in full screen browsing mode.
5410                 // This hides the status and navigation bars, which are displayed if other elements are shown, like dialog boxes, the options menu, or the keyboard.
5411                 if (inFullScreenBrowsingMode) {
5412                     /* Hide the system bars.
5413                      * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
5414                      * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
5415                      * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
5416                      * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
5417                      */
5418                     rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
5419                             View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
5420                 }
5421             });
5422         } else {
5423             nestedScrollWebView.getViewTreeObserver().addOnScrollChangedListener(() -> {
5424                 if (nestedScrollWebView.getSwipeToRefresh()) {
5425                     // Only enable swipe to refresh if the WebView is scrolled to the top.
5426                     swipeRefreshLayout.setEnabled(nestedScrollWebView.getScrollY() == 0);
5427                 } else {
5428                     // Disable swipe to refresh.
5429                     swipeRefreshLayout.setEnabled(false);
5430                 }
5431
5432
5433                 // Reinforce the system UI visibility flags if in full screen browsing mode.
5434                 // This hides the status and navigation bars, which are displayed if other elements are shown, like dialog boxes, the options menu, or the keyboard.
5435                 if (inFullScreenBrowsingMode) {
5436                     /* Hide the system bars.
5437                      * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
5438                      * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
5439                      * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
5440                      * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
5441                      */
5442                     rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
5443                             View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
5444                 }
5445             });
5446         }
5447
5448         // Set the web chrome client.
5449         nestedScrollWebView.setWebChromeClient(new WebChromeClient() {
5450             // Update the progress bar when a page is loading.
5451             @Override
5452             public void onProgressChanged(WebView view, int progress) {
5453                 // Update the progress bar.
5454                 progressBar.setProgress(progress);
5455
5456                 // Set the visibility of the progress bar.
5457                 if (progress < 100) {
5458                     // Show the progress bar.
5459                     progressBar.setVisibility(View.VISIBLE);
5460                 } else {
5461                     // Hide the progress bar.
5462                     progressBar.setVisibility(View.GONE);
5463
5464                     //Stop the swipe to refresh indicator if it is running
5465                     swipeRefreshLayout.setRefreshing(false);
5466
5467                     // Make the current WebView visible.  If this is a new tab, the current WebView would have been created invisible in `webview_framelayout` to prevent a white background splash in night mode.
5468                     nestedScrollWebView.setVisibility(View.VISIBLE);
5469                 }
5470             }
5471
5472             // Set the favorite icon when it changes.
5473             @Override
5474             public void onReceivedIcon(WebView view, Bitmap icon) {
5475                 // Only update the favorite icon if the website has finished loading.
5476                 if (progressBar.getVisibility() == View.GONE) {
5477                     // Store the new favorite icon.
5478                     nestedScrollWebView.setFavoriteOrDefaultIcon(icon);
5479
5480                     // Get the current page position.
5481                     int currentPosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
5482
5483                     // Get the current tab.
5484                     TabLayout.Tab tab = tabLayout.getTabAt(currentPosition);
5485
5486                     // Check to see if the tab has been populated.
5487                     if (tab != null) {
5488                         // Get the custom view from the tab.
5489                         View tabView = tab.getCustomView();
5490
5491                         // Check to see if the custom tab view has been populated.
5492                         if (tabView != null) {
5493                             // Get the favorite icon image view from the tab.
5494                             ImageView tabFavoriteIconImageView = tabView.findViewById(R.id.favorite_icon_imageview);
5495
5496                             // Display the favorite icon in the tab.
5497                             tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(icon, 64, 64, true));
5498                         }
5499                     }
5500                 }
5501             }
5502
5503             // Save a copy of the title when it changes.
5504             @Override
5505             public void onReceivedTitle(WebView view, String title) {
5506                 // Get the current page position.
5507                 int currentPosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
5508
5509                 // Get the current tab.
5510                 TabLayout.Tab tab = tabLayout.getTabAt(currentPosition);
5511
5512                 // Only populate the title text view if the tab has been fully created.
5513                 if (tab != null) {
5514                     // Get the custom view from the tab.
5515                     View tabView = tab.getCustomView();
5516
5517                     // Only populate the title text view if the tab view has been fully populated.
5518                     if (tabView != null) {
5519                         // Get the title text view from the tab.
5520                         TextView tabTitleTextView = tabView.findViewById(R.id.title_textview);
5521
5522                         // Set the title according to the URL.
5523                         if (title.equals("about:blank")) {
5524                             // Set the title to indicate a new tab.
5525                             tabTitleTextView.setText(R.string.new_tab);
5526                         } else {
5527                             // Set the title as the tab text.
5528                             tabTitleTextView.setText(title);
5529                         }
5530                     }
5531                 }
5532             }
5533
5534             // Enter full screen video.
5535             @Override
5536             public void onShowCustomView(View video, CustomViewCallback callback) {
5537                 // Set the full screen video flag.
5538                 displayingFullScreenVideo = true;
5539
5540                 // Pause the ad if this is the free flavor.
5541                 if (BuildConfig.FLAVOR.contentEquals("free")) {
5542                     // Get a handle for the ad view.  This cannot be a class variable because it changes with each ad load.
5543                     View adView = findViewById(R.id.adview);
5544
5545                     // The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
5546                     AdHelper.pauseAd(adView);
5547                 }
5548
5549                 // Hide the keyboard.
5550                 inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0);
5551
5552                 // Hide the main content relative layout.
5553                 mainContentRelativeLayout.setVisibility(View.GONE);
5554
5555                 /* Hide the system bars.
5556                  * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
5557                  * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
5558                  * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
5559                  * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
5560                  */
5561                 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
5562                         View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
5563
5564                 // Disable the sliding drawers.
5565                 drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
5566
5567                 // Add the video view to the full screen video frame layout.
5568                 fullScreenVideoFrameLayout.addView(video);
5569
5570                 // Show the full screen video frame layout.
5571                 fullScreenVideoFrameLayout.setVisibility(View.VISIBLE);
5572
5573                 // Disable the screen timeout while the video is playing.  YouTube does this automatically, but not all other videos do.
5574                 fullScreenVideoFrameLayout.setKeepScreenOn(true);
5575             }
5576
5577             // Exit full screen video.
5578             @Override
5579             public void onHideCustomView() {
5580                 // Re-enable the screen timeout.
5581                 fullScreenVideoFrameLayout.setKeepScreenOn(false);
5582
5583                 // Unset the full screen video flag.
5584                 displayingFullScreenVideo = false;
5585
5586                 // Remove all the views from the full screen video frame layout.
5587                 fullScreenVideoFrameLayout.removeAllViews();
5588
5589                 // Hide the full screen video frame layout.
5590                 fullScreenVideoFrameLayout.setVisibility(View.GONE);
5591
5592                 // Enable the sliding drawers.
5593                 drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
5594
5595                 // Show the main content relative layout.
5596                 mainContentRelativeLayout.setVisibility(View.VISIBLE);
5597
5598                 // Apply the appropriate full screen mode flags.
5599                 if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) {  // Privacy Browser is currently in full screen browsing mode.
5600                     // Hide the app bar if specified.
5601                     if (hideAppBar) {
5602                         // Hide the tab linear layout.
5603                         tabsLinearLayout.setVisibility(View.GONE);
5604
5605                         // Hide the action bar.
5606                         actionBar.hide();
5607                     }
5608
5609                     // Hide the banner ad in the free flavor.
5610                     if (BuildConfig.FLAVOR.contentEquals("free")) {
5611                         // Get a handle for the ad view.  This cannot be a class variable because it changes with each ad load.
5612                         View adView = findViewById(R.id.adview);
5613
5614                         // Hide the banner ad.
5615                         AdHelper.hideAd(adView);
5616                     }
5617
5618                     /* Hide the system bars.
5619                      * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
5620                      * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
5621                      * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
5622                      * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
5623                      */
5624                     rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
5625                             View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
5626                 } else {  // Switch to normal viewing mode.
5627                     // Remove the `SYSTEM_UI` flags from the root frame layout.
5628                     rootFrameLayout.setSystemUiVisibility(0);
5629                 }
5630
5631                 // Reload the ad for the free flavor if not in full screen mode.
5632                 if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
5633                     // Get a handle for the ad view.  This cannot be a class variable because it changes with each ad load.
5634                     View adView = findViewById(R.id.adview);
5635
5636                     // Reload the ad.  `getContext()` can be used instead of `getActivity.getApplicationContext()` once the minimum API >= 23.
5637                     AdHelper.loadAd(adView, getApplicationContext(), activity, getString(R.string.ad_unit_id));
5638                 }
5639             }
5640
5641             // Upload files.
5642             @Override
5643             public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
5644                 // Show the file chooser if the device is running API >= 21.
5645                 if (Build.VERSION.SDK_INT >= 21) {
5646                     // Store the file path callback.
5647                     fileChooserCallback = filePathCallback;
5648
5649                     // Create an intent to open a chooser based on the file chooser parameters.
5650                     Intent fileChooserIntent = fileChooserParams.createIntent();
5651
5652                     // Get a handle for the package manager.
5653                     PackageManager packageManager = getPackageManager();
5654
5655                     // Check to see if the file chooser intent resolves to an installed package.
5656                     if (fileChooserIntent.resolveActivity(packageManager) != null) {  // The file chooser intent is fine.
5657                         // Start the file chooser intent.
5658                         startActivityForResult(fileChooserIntent, BROWSE_FILE_UPLOAD_REQUEST_CODE);
5659                     } else {  // The file chooser intent will cause a crash.
5660                         // Create a generic intent to open a chooser.
5661                         Intent genericFileChooserIntent = new Intent(Intent.ACTION_GET_CONTENT);
5662
5663                         // Request an openable file.
5664                         genericFileChooserIntent.addCategory(Intent.CATEGORY_OPENABLE);
5665
5666                         // Set the file type to everything.
5667                         genericFileChooserIntent.setType("*/*");
5668
5669                         // Start the generic file chooser intent.
5670                         startActivityForResult(genericFileChooserIntent, BROWSE_FILE_UPLOAD_REQUEST_CODE);
5671                     }
5672                 }
5673                 return true;
5674             }
5675         });
5676
5677         nestedScrollWebView.setWebViewClient(new WebViewClient() {
5678             // `shouldOverrideUrlLoading` makes this WebView the default handler for URLs inside the app, so that links are not kicked out to other apps.
5679             // The deprecated `shouldOverrideUrlLoading` must be used until API >= 24.
5680             @Override
5681             public boolean shouldOverrideUrlLoading(WebView view, String url) {
5682                 // Sanitize the url.
5683                 url = sanitizeUrl(url);
5684
5685                 // Handle the URL according to the type.
5686                 if (url.startsWith("http")) {  // Load the URL in Privacy Browser.
5687                     // Load the URL.  By using `loadUrl()`, instead of `loadUrlFromBase()`, the Referer header will never be sent.
5688                     loadUrl(nestedScrollWebView, url);
5689
5690                     // Returning true indicates that Privacy Browser is manually handling the loading of the URL.
5691                     // Custom headers cannot be added if false is returned and the WebView handles the loading of the URL.
5692                     return true;
5693                 } else if (url.startsWith("mailto:")) {  // Load the email address in an external email program.
5694                     // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
5695                     Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
5696
5697                     // Parse the url and set it as the data for the intent.
5698                     emailIntent.setData(Uri.parse(url));
5699
5700                     // Open the email program in a new task instead of as part of Privacy Browser.
5701                     emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
5702
5703                     try {
5704                         // Make it so.
5705                         startActivity(emailIntent);
5706                     } catch (ActivityNotFoundException exception) {
5707                         // Display a snackbar.
5708                         Snackbar.make(currentWebView, getString(R.string.error) + "  " + exception, Snackbar.LENGTH_INDEFINITE).show();
5709                     }
5710
5711
5712                     // Returning true indicates Privacy Browser is handling the URL by creating an intent.
5713                     return true;
5714                 } else if (url.startsWith("tel:")) {  // Load the phone number in the dialer.
5715                     // Open the dialer and load the phone number, but wait for the user to place the call.
5716                     Intent dialIntent = new Intent(Intent.ACTION_DIAL);
5717
5718                     // Add the phone number to the intent.
5719                     dialIntent.setData(Uri.parse(url));
5720
5721                     // Open the dialer in a new task instead of as part of Privacy Browser.
5722                     dialIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
5723
5724                     try {
5725                         // Make it so.
5726                         startActivity(dialIntent);
5727                     } catch (ActivityNotFoundException exception) {
5728                         // Display a snackbar.
5729                         Snackbar.make(currentWebView, getString(R.string.error) + "  " + exception, Snackbar.LENGTH_INDEFINITE).show();
5730                     }
5731
5732                     // Returning true indicates Privacy Browser is handling the URL by creating an intent.
5733                     return true;
5734                 } else {  // Load a system chooser to select an app that can handle the URL.
5735                     // Open an app that can handle the URL.
5736                     Intent genericIntent = new Intent(Intent.ACTION_VIEW);
5737
5738                     // Add the URL to the intent.
5739                     genericIntent.setData(Uri.parse(url));
5740
5741                     // List all apps that can handle the URL instead of just opening the first one.
5742                     genericIntent.addCategory(Intent.CATEGORY_BROWSABLE);
5743
5744                     // Open the app in a new task instead of as part of Privacy Browser.
5745                     genericIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
5746
5747                     // Start the app or display a snackbar if no app is available to handle the URL.
5748                     try {
5749                         startActivity(genericIntent);
5750                     } catch (ActivityNotFoundException exception) {
5751                         Snackbar.make(nestedScrollWebView, getString(R.string.unrecognized_url) + "  " + url, Snackbar.LENGTH_SHORT).show();
5752                     }
5753
5754                     // Returning true indicates Privacy Browser is handling the URL by creating an intent.
5755                     return true;
5756                 }
5757             }
5758
5759             // Check requests against the block lists.  The deprecated `shouldInterceptRequest()` must be used until minimum API >= 21.
5760             @Override
5761             public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
5762                 // Check to see if the resource request is for the main URL.
5763                 if (url.equals(nestedScrollWebView.getCurrentUrl())) {
5764                     // `return null` loads the resource request, which should never be blocked if it is the main URL.
5765                     return null;
5766                 }
5767
5768                 // Wait until the blocklists have been populated.  When Privacy Browser is being resumed after having the process killed in the background it will try to load the URLs immediately.
5769                 while (ultraPrivacy == null) {
5770                     // The wait must be synchronized, which only lets one thread run on it at a time, or `java.lang.IllegalMonitorStateException` is thrown.
5771                     synchronized (this) {
5772                         try {
5773                             // Check to see if the blocklists have been populated after 100 ms.
5774                             wait(100);
5775                         } catch (InterruptedException exception) {
5776                             // Do nothing.
5777                         }
5778                     }
5779                 }
5780
5781                 // Sanitize the URL.
5782                 url = sanitizeUrl(url);
5783
5784                 // Create an empty web resource response to be used if the resource request is blocked.
5785                 WebResourceResponse emptyWebResourceResponse = new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes()));
5786
5787                 // Reset the whitelist results tracker.
5788                 String[] whitelistResultStringArray = null;
5789
5790                 // Initialize the third party request tracker.
5791                 boolean isThirdPartyRequest = false;
5792
5793                 // Get the current URL.  `.getUrl()` throws an error because operations on the WebView cannot be made from this thread.
5794                 String currentBaseDomain = nestedScrollWebView.getCurrentDomainName();
5795
5796                 // Store a copy of the current domain for use in later requests.
5797                 String currentDomain = currentBaseDomain;
5798
5799                 // Nobody is happy when comparing null strings.
5800                 if ((currentBaseDomain != null) && (url != null)) {
5801                     // Convert the request URL to a URI.
5802                     Uri requestUri = Uri.parse(url);
5803
5804                     // Get the request host name.
5805                     String requestBaseDomain = requestUri.getHost();
5806
5807                     // Only check for third-party requests if the current base domain is not empty and the request domain is not null.
5808                     if (!currentBaseDomain.isEmpty() && (requestBaseDomain != null)) {
5809                         // Determine the current base domain.
5810                         while (currentBaseDomain.indexOf(".", currentBaseDomain.indexOf(".") + 1) > 0) {  // There is at least one subdomain.
5811                             // Remove the first subdomain.
5812                             currentBaseDomain = currentBaseDomain.substring(currentBaseDomain.indexOf(".") + 1);
5813                         }
5814
5815                         // Determine the request base domain.
5816                         while (requestBaseDomain.indexOf(".", requestBaseDomain.indexOf(".") + 1) > 0) {  // There is at least one subdomain.
5817                             // Remove the first subdomain.
5818                             requestBaseDomain = requestBaseDomain.substring(requestBaseDomain.indexOf(".") + 1);
5819                         }
5820
5821                         // Update the third party request tracker.
5822                         isThirdPartyRequest = !currentBaseDomain.equals(requestBaseDomain);
5823                     }
5824                 }
5825
5826                 // Get the current WebView page position.
5827                 int webViewPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
5828
5829                 // Determine if the WebView is currently displayed.
5830                 boolean webViewDisplayed = (webViewPagePosition == tabLayout.getSelectedTabPosition());
5831
5832                 // Block third-party requests if enabled.
5833                 if (isThirdPartyRequest && nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS)) {
5834                     // Add the result to the resource requests.
5835                     nestedScrollWebView.addResourceRequest(new String[]{BlocklistHelper.REQUEST_THIRD_PARTY, url});
5836
5837                     // Increment the blocked requests counters.
5838                     nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5839                     nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS);
5840
5841                     // Update the titles of the blocklist menu items if the WebView is currently displayed.
5842                     if (webViewDisplayed) {
5843                         // Updating the UI must be run from the UI thread.
5844                         activity.runOnUiThread(() -> {
5845                             // Update the menu item titles.
5846                             navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5847
5848                             // Update the options menu if it has been populated.
5849                             if (optionsMenu != null) {
5850                                 optionsBlocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5851                                 optionsBlockAllThirdPartyRequestsMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " +
5852                                         getString(R.string.block_all_third_party_requests));
5853                             }
5854                         });
5855                     }
5856
5857                     // Return an empty web resource response.
5858                     return emptyWebResourceResponse;
5859                 }
5860
5861                 // Check UltraList if it is enabled.
5862                 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.ULTRALIST)) {
5863                     // Check the URL against UltraList.
5864                     String[] ultraListResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, ultraList);
5865
5866                     // Process the UltraList results.
5867                     if (ultraListResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) {  // The resource request matched UltraLists's blacklist.
5868                         // Add the result to the resource requests.
5869                         nestedScrollWebView.addResourceRequest(new String[] {ultraListResults[0], ultraListResults[1], ultraListResults[2], ultraListResults[3], ultraListResults[4], ultraListResults[5]});
5870
5871                         // Increment the blocked requests counters.
5872                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5873                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.ULTRALIST);
5874
5875                         // Update the titles of the blocklist menu items if the WebView is currently displayed.
5876                         if (webViewDisplayed) {
5877                             // Updating the UI must be run from the UI thread.
5878                             activity.runOnUiThread(() -> {
5879                                 // Update the menu item titles.
5880                                 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5881
5882                                 // Update the options menu if it has been populated.
5883                                 if (optionsMenu != null) {
5884                                     optionsBlocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5885                                     optionsUltraListMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.ULTRALIST) + " - " + getString(R.string.ultralist));
5886                                 }
5887                             });
5888                         }
5889
5890                         // The resource request was blocked.  Return an empty web resource response.
5891                         return emptyWebResourceResponse;
5892                     } else if (ultraListResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) {  // The resource request matched UltraList's whitelist.
5893                         // Add a whitelist entry to the resource requests array.
5894                         nestedScrollWebView.addResourceRequest(new String[] {ultraListResults[0], ultraListResults[1], ultraListResults[2], ultraListResults[3], ultraListResults[4], ultraListResults[5]});
5895
5896                         // The resource request has been allowed by UltraPrivacy.  `return null` loads the requested resource.
5897                         return null;
5898                     }
5899                 }
5900
5901                 // Check UltraPrivacy if it is enabled.
5902                 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.ULTRAPRIVACY)) {
5903                     // Check the URL against UltraPrivacy.
5904                     String[] ultraPrivacyResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, ultraPrivacy);
5905
5906                     // Process the UltraPrivacy results.
5907                     if (ultraPrivacyResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) {  // The resource request matched UltraPrivacy's blacklist.
5908                         // Add the result to the resource requests.
5909                         nestedScrollWebView.addResourceRequest(new String[] {ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4],
5910                                 ultraPrivacyResults[5]});
5911
5912                         // Increment the blocked requests counters.
5913                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5914                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.ULTRAPRIVACY);
5915
5916                         // Update the titles of the blocklist menu items if the WebView is currently displayed.
5917                         if (webViewDisplayed) {
5918                             // Updating the UI must be run from the UI thread.
5919                             activity.runOnUiThread(() -> {
5920                                 // Update the menu item titles.
5921                                 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5922
5923                                 // Update the options menu if it has been populated.
5924                                 if (optionsMenu != null) {
5925                                     optionsBlocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5926                                     optionsUltraPrivacyMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.ULTRAPRIVACY) + " - " + getString(R.string.ultraprivacy));
5927                                 }
5928                             });
5929                         }
5930
5931                         // The resource request was blocked.  Return an empty web resource response.
5932                         return emptyWebResourceResponse;
5933                     } else if (ultraPrivacyResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) {  // The resource request matched UltraPrivacy's whitelist.
5934                         // Add a whitelist entry to the resource requests array.
5935                         nestedScrollWebView.addResourceRequest(new String[] {ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4],
5936                                 ultraPrivacyResults[5]});
5937
5938                         // The resource request has been allowed by UltraPrivacy.  `return null` loads the requested resource.
5939                         return null;
5940                     }
5941                 }
5942
5943                 // Check EasyList if it is enabled.
5944                 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.EASYLIST)) {
5945                     // Check the URL against EasyList.
5946                     String[] easyListResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, easyList);
5947
5948                     // Process the EasyList results.
5949                     if (easyListResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) {  // The resource request matched EasyList's blacklist.
5950                         // Add the result to the resource requests.
5951                         nestedScrollWebView.addResourceRequest(new String[] {easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5]});
5952
5953                         // Increment the blocked requests counters.
5954                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5955                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.EASYLIST);
5956
5957                         // Update the titles of the blocklist menu items if the WebView is currently displayed.
5958                         if (webViewDisplayed) {
5959                             // Updating the UI must be run from the UI thread.
5960                             activity.runOnUiThread(() -> {
5961                                 // Update the menu item titles.
5962                                 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5963
5964                                 // Update the options menu if it has been populated.
5965                                 if (optionsMenu != null) {
5966                                     optionsBlocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5967                                     optionsEasyListMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.EASYLIST) + " - " + getString(R.string.easylist));
5968                                 }
5969                             });
5970                         }
5971
5972                         // The resource request was blocked.  Return an empty web resource response.
5973                         return emptyWebResourceResponse;
5974                     } else if (easyListResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) {  // The resource request matched EasyList's whitelist.
5975                         // Update the whitelist result string array tracker.
5976                         whitelistResultStringArray = new String[] {easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5]};
5977                     }
5978                 }
5979
5980                 // Check EasyPrivacy if it is enabled.
5981                 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.EASYPRIVACY)) {
5982                     // Check the URL against EasyPrivacy.
5983                     String[] easyPrivacyResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, easyPrivacy);
5984
5985                     // Process the EasyPrivacy results.
5986                     if (easyPrivacyResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) {  // The resource request matched EasyPrivacy's blacklist.
5987                         // Add the result to the resource requests.
5988                         nestedScrollWebView.addResourceRequest(new String[] {easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[4],
5989                                 easyPrivacyResults[5]});
5990
5991                         // Increment the blocked requests counters.
5992                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5993                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.EASYPRIVACY);
5994
5995                         // Update the titles of the blocklist menu items if the WebView is currently displayed.
5996                         if (webViewDisplayed) {
5997                             // Updating the UI must be run from the UI thread.
5998                             activity.runOnUiThread(() -> {
5999                                 // Update the menu item titles.
6000                                 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
6001
6002                                 // Update the options menu if it has been populated.
6003                                 if (optionsMenu != null) {
6004                                     optionsBlocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
6005                                     optionsEasyPrivacyMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.EASYPRIVACY) + " - " + getString(R.string.easyprivacy));
6006                                 }
6007                             });
6008                         }
6009
6010                         // The resource request was blocked.  Return an empty web resource response.
6011                         return emptyWebResourceResponse;
6012                     } else if (easyPrivacyResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) {  // The resource request matched EasyPrivacy's whitelist.
6013                         // Update the whitelist result string array tracker.
6014                         whitelistResultStringArray = new String[] {easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[4], easyPrivacyResults[5]};
6015                     }
6016                 }
6017
6018                 // Check Fanboy’s Annoyance List if it is enabled.
6019                 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST)) {
6020                     // Check the URL against Fanboy's Annoyance List.
6021                     String[] fanboysAnnoyanceListResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, fanboysAnnoyanceList);
6022
6023                     // Process the Fanboy's Annoyance List results.
6024                     if (fanboysAnnoyanceListResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) {  // The resource request matched Fanboy's Annoyance List's blacklist.
6025                         // Add the result to the resource requests.
6026                         nestedScrollWebView.addResourceRequest(new String[] {fanboysAnnoyanceListResults[0], fanboysAnnoyanceListResults[1], fanboysAnnoyanceListResults[2], fanboysAnnoyanceListResults[3],
6027                                 fanboysAnnoyanceListResults[4], fanboysAnnoyanceListResults[5]});
6028
6029                         // Increment the blocked requests counters.
6030                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
6031                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST);
6032
6033                         // Update the titles of the blocklist menu items if the WebView is currently displayed.
6034                         if (webViewDisplayed) {
6035                             // Updating the UI must be run from the UI thread.
6036                             activity.runOnUiThread(() -> {
6037                                 // Update the menu item titles.
6038                                 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
6039
6040                                 // Update the options menu if it has been populated.
6041                                 if (optionsMenu != null) {
6042                                     optionsBlocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
6043                                     optionsFanboysAnnoyanceListMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST) + " - " +
6044                                             getString(R.string.fanboys_annoyance_list));
6045                                 }
6046                             });
6047                         }
6048
6049                         // The resource request was blocked.  Return an empty web resource response.
6050                         return emptyWebResourceResponse;
6051                     } else if (fanboysAnnoyanceListResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)){  // The resource request matched Fanboy's Annoyance List's whitelist.
6052                         // Update the whitelist result string array tracker.
6053                         whitelistResultStringArray = new String[] {fanboysAnnoyanceListResults[0], fanboysAnnoyanceListResults[1], fanboysAnnoyanceListResults[2], fanboysAnnoyanceListResults[3],
6054                                 fanboysAnnoyanceListResults[4], fanboysAnnoyanceListResults[5]};
6055                     }
6056                 } else if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST)) {  // Only check Fanboy’s Social Blocking List if Fanboy’s Annoyance List is disabled.
6057                     // Check the URL against Fanboy's Annoyance List.
6058                     String[] fanboysSocialListResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, fanboysSocialList);
6059
6060                     // Process the Fanboy's Social Blocking List results.
6061                     if (fanboysSocialListResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) {  // The resource request matched Fanboy's Social Blocking List's blacklist.
6062                         // Add the result to the resource requests.
6063                         nestedScrollWebView.addResourceRequest(new String[] {fanboysSocialListResults[0], fanboysSocialListResults[1], fanboysSocialListResults[2], fanboysSocialListResults[3],
6064                                 fanboysSocialListResults[4], fanboysSocialListResults[5]});
6065
6066                         // Increment the blocked requests counters.
6067                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
6068                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST);
6069
6070                         // Update the titles of the blocklist menu items if the WebView is currently displayed.
6071                         if (webViewDisplayed) {
6072                             // Updating the UI must be run from the UI thread.
6073                             activity.runOnUiThread(() -> {
6074                                 // Update the menu item titles.
6075                                 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
6076
6077                                 // Update the options menu if it has been populated.
6078                                 if (optionsMenu != null) {
6079                                     optionsBlocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
6080                                     optionsFanboysSocialBlockingListMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST) + " - " +
6081                                             getString(R.string.fanboys_social_blocking_list));
6082                                 }
6083                             });
6084                         }
6085
6086                         // The resource request was blocked.  Return an empty web resource response.
6087                         return emptyWebResourceResponse;
6088                     } else if (fanboysSocialListResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) {  // The resource request matched Fanboy's Social Blocking List's whitelist.
6089                         // Update the whitelist result string array tracker.
6090                         whitelistResultStringArray = new String[] {fanboysSocialListResults[0], fanboysSocialListResults[1], fanboysSocialListResults[2], fanboysSocialListResults[3],
6091                                 fanboysSocialListResults[4], fanboysSocialListResults[5]};
6092                     }
6093                 }
6094
6095                 // Add the request to the log because it hasn't been processed by any of the previous checks.
6096                 if (whitelistResultStringArray != null) {  // The request was processed by a whitelist.
6097                     nestedScrollWebView.addResourceRequest(whitelistResultStringArray);
6098                 } else {  // The request didn't match any blocklist entry.  Log it as a default request.
6099                     nestedScrollWebView.addResourceRequest(new String[]{BlocklistHelper.REQUEST_DEFAULT, url});
6100                 }
6101
6102                 // The resource request has not been blocked.  `return null` loads the requested resource.
6103                 return null;
6104             }
6105
6106             // Handle HTTP authentication requests.
6107             @Override
6108             public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {
6109                 // Store the handler.
6110                 nestedScrollWebView.setHttpAuthHandler(handler);
6111
6112                 // Instantiate an HTTP authentication dialog.
6113                 DialogFragment httpAuthenticationDialogFragment = HttpAuthenticationDialog.displayDialog(host, realm, nestedScrollWebView.getWebViewFragmentId());
6114
6115                 // Show the HTTP authentication dialog.
6116                 httpAuthenticationDialogFragment.show(getSupportFragmentManager(), getString(R.string.http_authentication));
6117             }
6118
6119             @Override
6120             public void onPageStarted(WebView view, String url, Bitmap favicon) {
6121                 // Get the preferences.
6122                 boolean scrollAppBar = sharedPreferences.getBoolean("scroll_app_bar", true);
6123
6124                 // Set the top padding of the swipe refresh layout according to the app bar scrolling preference.  This can't be done in `appAppSettings()` because the app bar is not yet populated there.
6125                 if (scrollAppBar || (inFullScreenBrowsingMode && hideAppBar)) {
6126                     // No padding is needed because it will automatically be placed below the app bar layout due to the scrolling layout behavior.
6127                     swipeRefreshLayout.setPadding(0, 0, 0, 0);
6128
6129                     // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
6130                     swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10, defaultProgressViewEndOffset);
6131                 } else {
6132                     // Get the app bar layout height.  This can't be done in `applyAppSettings()` because the app bar is not yet populated there.
6133                     appBarHeight = appBarLayout.getHeight();
6134
6135                     // The swipe refresh layout must be manually moved below the app bar layout.
6136                     swipeRefreshLayout.setPadding(0, appBarHeight, 0, 0);
6137
6138                     // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
6139                     swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10 + appBarHeight, defaultProgressViewEndOffset + appBarHeight);
6140                 }
6141
6142                 // Reset the list of resource requests.
6143                 nestedScrollWebView.clearResourceRequests();
6144
6145                 // Reset the requests counters.
6146                 nestedScrollWebView.resetRequestsCounters();
6147
6148                 // Get the current page position.
6149                 int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
6150
6151                 // Update the URL text bar if the page is currently selected and the URL edit text is not currently being edited.
6152                 if ((tabLayout.getSelectedTabPosition() == currentPagePosition) && !urlEditText.hasFocus()) {
6153                     // Display the formatted URL text.
6154                     urlEditText.setText(url);
6155
6156                     // Apply text highlighting to `urlTextBox`.
6157                     highlightUrlText();
6158
6159                     // Hide the keyboard.
6160                     inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0);
6161                 }
6162
6163                 // Reset the list of host IP addresses.
6164                 nestedScrollWebView.clearCurrentIpAddresses();
6165
6166                 // Get a URI for the current URL.
6167                 Uri currentUri = Uri.parse(url);
6168
6169                 // Get the IP addresses for the host.
6170                 new GetHostIpAddresses(activity, getSupportFragmentManager(), nestedScrollWebView).execute(currentUri.getHost());
6171
6172                 // Replace Refresh with Stop if the options menu has been created.  (The first WebView typically begins loading before the menu items are instantiated.)
6173                 if (optionsMenu != null) {
6174                     // Set the title.
6175                     optionsRefreshMenuItem.setTitle(R.string.stop);
6176
6177                     // Get the app bar and theme preferences.
6178                     boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean(getString(R.string.display_additional_app_bar_icons_key), false);
6179
6180                     // If the icon is displayed in the AppBar, set it according to the theme.
6181                     if (displayAdditionalAppBarIcons) {
6182                         // Get the current theme status.
6183                         int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
6184
6185                         // Set the stop icon according to the theme.
6186                         if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
6187                             optionsRefreshMenuItem.setIcon(R.drawable.close_blue_day);
6188                         } else {
6189                             optionsRefreshMenuItem.setIcon(R.drawable.close_blue_night);
6190                         }
6191                     }
6192                 }
6193             }
6194
6195             @Override
6196             public void onPageFinished(WebView view, String url) {
6197                 // Flush any cookies to persistent storage.  The cookie manager has become very lazy about flushing cookies in recent versions.
6198                 if (nestedScrollWebView.getAcceptFirstPartyCookies() && Build.VERSION.SDK_INT >= 21) {
6199                     CookieManager.getInstance().flush();
6200                 }
6201
6202                 // Update the Refresh menu item if the options menu has been created.
6203                 if (optionsMenu != null) {
6204                     // Reset the Refresh title.
6205                     optionsRefreshMenuItem.setTitle(R.string.refresh);
6206
6207                     // Get the app bar and theme preferences.
6208                     boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean(getString(R.string.display_additional_app_bar_icons_key), false);
6209
6210                     // If the icon is displayed in the app bar, reset it according to the theme.
6211                     if (displayAdditionalAppBarIcons) {
6212                         // Get the current theme status.
6213                         int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
6214
6215                         // Set the icon according to the theme.
6216                         if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
6217                             optionsRefreshMenuItem.setIcon(R.drawable.refresh_enabled_day);
6218                         } else {
6219                             optionsRefreshMenuItem.setIcon(R.drawable.refresh_enabled_night);
6220                         }
6221                     }
6222                 }
6223
6224                 // Clear the cache, history, and logcat if Incognito Mode is enabled.
6225                 if (incognitoModeEnabled) {
6226                     // Clear the cache.  `true` includes disk files.
6227                     nestedScrollWebView.clearCache(true);
6228
6229                     // Clear the back/forward history.
6230                     nestedScrollWebView.clearHistory();
6231
6232                     // Manually delete cache folders.
6233                     try {
6234                         // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
6235                         // which links to `/data/data/com.stoutner.privacybrowser.standard`.
6236                         String privateDataDirectoryString = getApplicationInfo().dataDir;
6237
6238                         // Delete the main cache directory.
6239                         Runtime.getRuntime().exec("rm -rf " + privateDataDirectoryString + "/cache");
6240
6241                         // Delete the secondary `Service Worker` cache directory.
6242                         // A `String[]` must be used because the directory contains a space and `Runtime.exec` will not escape the string correctly otherwise.
6243                         Runtime.getRuntime().exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
6244                     } catch (IOException exception) {
6245                         // Do nothing if an error is thrown.
6246                     }
6247
6248                     // Clear the logcat.
6249                     try {
6250                         // Clear the logcat.  `-c` clears the logcat.  `-b all` clears all the buffers (instead of just crash, main, and system).
6251                         Runtime.getRuntime().exec("logcat -b all -c");
6252                     } catch (IOException exception) {
6253                         // Do nothing.
6254                     }
6255                 }
6256
6257                 // Get the current page position.
6258                 int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
6259
6260                 // Get the current URL from the nested scroll WebView.  This is more accurate than using the URL passed into the method, which is sometimes not the final one.
6261                 String currentUrl = nestedScrollWebView.getUrl();
6262
6263                 // Get the current tab.
6264                 TabLayout.Tab tab = tabLayout.getTabAt(currentPagePosition);
6265
6266                 // Update the URL text bar if the page is currently selected and the user is not currently typing in the URL edit text.
6267                 // Crash records show that, in some crazy way, it is possible for the current URL to be blank at this point.
6268                 // Probably some sort of race condition when Privacy Browser is being resumed.
6269                 if ((tabLayout.getSelectedTabPosition() == currentPagePosition) && !urlEditText.hasFocus() && (currentUrl != null)) {
6270                     // Check to see if the URL is `about:blank`.
6271                     if (currentUrl.equals("about:blank")) {  // The WebView is blank.
6272                         // Display the hint in the URL edit text.
6273                         urlEditText.setText("");
6274
6275                         // Request focus for the URL text box.
6276                         urlEditText.requestFocus();
6277
6278                         // Display the keyboard.
6279                         inputMethodManager.showSoftInput(urlEditText, 0);
6280
6281                         // Apply the domain settings.  This clears any settings from the previous domain.
6282                         applyDomainSettings(nestedScrollWebView, "", true, false, false);
6283
6284                         // Only populate the title text view if the tab has been fully created.
6285                         if (tab != null) {
6286                             // Get the custom view from the tab.
6287                             View tabView = tab.getCustomView();
6288
6289                             // Remove the incorrect warning below that the current tab view might be null.
6290                             assert tabView != null;
6291
6292                             // Get the title text view from the tab.
6293                             TextView tabTitleTextView = tabView.findViewById(R.id.title_textview);
6294
6295                             // Set the title as the tab text.
6296                             tabTitleTextView.setText(R.string.new_tab);
6297                         }
6298                     } else {  // The WebView has loaded a webpage.
6299                         // Update the URL edit text if it is not currently being edited.
6300                         if (!urlEditText.hasFocus()) {
6301                             // Sanitize the current URL.  This removes unwanted URL elements that were added by redirects, so that they won't be included if the URL is shared.
6302                             String sanitizedUrl = sanitizeUrl(currentUrl);
6303
6304                             // Display the final URL.  Getting the URL from the WebView instead of using the one provided by `onPageFinished()` makes websites like YouTube function correctly.
6305                             urlEditText.setText(sanitizedUrl);
6306
6307                             // Apply text highlighting to the URL.
6308                             highlightUrlText();
6309                         }
6310
6311                         // Only populate the title text view if the tab has been fully created.
6312                         if (tab != null) {
6313                             // Get the custom view from the tab.
6314                             View tabView = tab.getCustomView();
6315
6316                             // Remove the incorrect warning below that the current tab view might be null.
6317                             assert tabView != null;
6318
6319                             // Get the title text view from the tab.
6320                             TextView tabTitleTextView = tabView.findViewById(R.id.title_textview);
6321
6322                             // Set the title as the tab text.  Sometimes `onReceivedTitle()` is not called, especially when navigating history.
6323                             tabTitleTextView.setText(nestedScrollWebView.getTitle());
6324                         }
6325                     }
6326                 }
6327             }
6328
6329             // Handle SSL Certificate errors.
6330             @Override
6331             public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
6332                 // Get the current website SSL certificate.
6333                 SslCertificate currentWebsiteSslCertificate = error.getCertificate();
6334
6335                 // Extract the individual pieces of information from the current website SSL certificate.
6336                 String currentWebsiteIssuedToCName = currentWebsiteSslCertificate.getIssuedTo().getCName();
6337                 String currentWebsiteIssuedToOName = currentWebsiteSslCertificate.getIssuedTo().getOName();
6338                 String currentWebsiteIssuedToUName = currentWebsiteSslCertificate.getIssuedTo().getUName();
6339                 String currentWebsiteIssuedByCName = currentWebsiteSslCertificate.getIssuedBy().getCName();
6340                 String currentWebsiteIssuedByOName = currentWebsiteSslCertificate.getIssuedBy().getOName();
6341                 String currentWebsiteIssuedByUName = currentWebsiteSslCertificate.getIssuedBy().getUName();
6342                 Date currentWebsiteSslStartDate = currentWebsiteSslCertificate.getValidNotBeforeDate();
6343                 Date currentWebsiteSslEndDate = currentWebsiteSslCertificate.getValidNotAfterDate();
6344
6345                 // Proceed to the website if the current SSL website certificate matches the pinned domain certificate.
6346                 if (nestedScrollWebView.hasPinnedSslCertificate()) {
6347                     // Get the pinned SSL certificate.
6348                     ArrayList<Object> pinnedSslCertificateArrayList = nestedScrollWebView.getPinnedSslCertificate();
6349
6350                     // Extract the arrays from the array list.
6351                     String[] pinnedSslCertificateStringArray = (String[]) pinnedSslCertificateArrayList.get(0);
6352                     Date[] pinnedSslCertificateDateArray = (Date[]) pinnedSslCertificateArrayList.get(1);
6353
6354                     // Check if the current SSL certificate matches the pinned certificate.
6355                     if (currentWebsiteIssuedToCName.equals(pinnedSslCertificateStringArray[0]) && currentWebsiteIssuedToOName.equals(pinnedSslCertificateStringArray[1]) &&
6356                         currentWebsiteIssuedToUName.equals(pinnedSslCertificateStringArray[2]) && currentWebsiteIssuedByCName.equals(pinnedSslCertificateStringArray[3]) &&
6357                         currentWebsiteIssuedByOName.equals(pinnedSslCertificateStringArray[4]) && currentWebsiteIssuedByUName.equals(pinnedSslCertificateStringArray[5]) &&
6358                         currentWebsiteSslStartDate.equals(pinnedSslCertificateDateArray[0]) && currentWebsiteSslEndDate.equals(pinnedSslCertificateDateArray[1])) {
6359
6360                         // An SSL certificate is pinned and matches the current domain certificate.  Proceed to the website without displaying an error.
6361                         handler.proceed();
6362                     }
6363                 } else {  // Either there isn't a pinned SSL certificate or it doesn't match the current website certificate.
6364                     // Store the SSL error handler.
6365                     nestedScrollWebView.setSslErrorHandler(handler);
6366
6367                     // Prevent the dialog from displaying if the app window is not visible.
6368                     // The SSL error handler continues to function even when the WebView is paused.  Attempting to display a dialog in that state leads to a crash.
6369                     while (!activity.getWindow().isActive()) {
6370                         try {
6371                             // The window is not active.  Wait 1 second.
6372                             wait(1000);
6373                         } catch (InterruptedException e) {
6374                             // Do nothing.
6375                         }
6376                     }
6377
6378                     // Instantiate an SSL certificate error alert dialog.
6379                     DialogFragment sslCertificateErrorDialogFragment = SslCertificateErrorDialog.displayDialog(error, nestedScrollWebView.getWebViewFragmentId());
6380
6381                     // Show the SSL certificate error dialog.
6382                     sslCertificateErrorDialogFragment.show(getSupportFragmentManager(), getString(R.string.ssl_certificate_error));
6383                 }
6384             }
6385         });
6386
6387         // Check to see if the state is being restored.
6388         if (restoringState) {  // The state is being restored.
6389             // Resume the nested scroll WebView JavaScript timers.
6390             nestedScrollWebView.resumeTimers();
6391         } else if (pageNumber == 0) {  // The first page is being loaded.
6392             // Set this nested scroll WebView as the current WebView.
6393             currentWebView = nestedScrollWebView;
6394
6395             // Initialize the URL to load string.
6396             String urlToLoadString;
6397
6398             // Get the intent that started the app.
6399             Intent launchingIntent = getIntent();
6400
6401             // Reset the intent.  This prevents a duplicate tab from being created on restart.
6402             setIntent(new Intent());
6403
6404             // Get the information from the intent.
6405             String launchingIntentAction = launchingIntent.getAction();
6406             Uri launchingIntentUriData = launchingIntent.getData();
6407             String launchingIntentStringExtra = launchingIntent.getStringExtra(Intent.EXTRA_TEXT);
6408
6409             // Parse the launching intent URL.
6410             if ((launchingIntentAction != null) && launchingIntentAction.equals(Intent.ACTION_WEB_SEARCH)) {  // The intent contains a search string.
6411                 // Create an encoded URL string.
6412                 String encodedUrlString;
6413
6414                 // Sanitize the search input and convert it to a search.
6415                 try {
6416                     encodedUrlString = URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8");
6417                 } catch (UnsupportedEncodingException exception) {
6418                     encodedUrlString = "";
6419                 }
6420
6421                 // Store the web search as the URL to load.
6422                 urlToLoadString = searchURL + encodedUrlString;
6423             } else if (launchingIntentUriData != null) {  // The launching intent contains a URL formatted as a URI.
6424                 // Store the URI as a URL.
6425                 urlToLoadString = launchingIntentUriData.toString();
6426             } else if (launchingIntentStringExtra != null) {  // The launching intent contains text that might be a URL.
6427                 // Store the URL.
6428                 urlToLoadString = launchingIntentStringExtra;
6429             } else if (!url.equals("")) {  // The activity has been restarted.
6430                 // Load the saved URL.
6431                 urlToLoadString = url;
6432             } else {  // The is no URL in the intent.
6433                 // Store the homepage to be loaded.
6434                 urlToLoadString = sharedPreferences.getString("homepage", getString(R.string.homepage_default_value));
6435             }
6436
6437             // Load the website if not waiting for the proxy.
6438             if (waitingForProxy) {  // Store the URL to be loaded in the Nested Scroll WebView.
6439                 nestedScrollWebView.setWaitingForProxyUrlString(urlToLoadString);
6440             } else {  // Load the URL.
6441                 loadUrl(nestedScrollWebView, urlToLoadString);
6442             }
6443
6444             // Reset the intent.  This prevents a duplicate tab from being created on a subsequent restart if loading an link from a new intent on restart.
6445             // For example, this prevents a duplicate tab if a link is loaded from the Guide after changing the theme in the guide and then changing the theme again in the main activity.
6446             setIntent(new Intent());
6447         } else {  // This is not the first tab.
6448             // Load the URL.
6449             loadUrl(nestedScrollWebView, url);
6450
6451             // Set the focus and display the keyboard if the URL is blank.
6452             if (url.equals("")) {
6453                 // Request focus for the URL text box.
6454                 urlEditText.requestFocus();
6455
6456                 // Create a display keyboard handler.
6457                 Handler displayKeyboardHandler = new Handler();
6458
6459                 // Create a display keyboard runnable.
6460                 Runnable displayKeyboardRunnable = () -> {
6461                     // Display the keyboard.
6462                     inputMethodManager.showSoftInput(urlEditText, 0);
6463                 };
6464
6465                 // Display the keyboard after 100 milliseconds, which leaves enough time for the tab to transition.
6466                 displayKeyboardHandler.postDelayed(displayKeyboardRunnable, 100);
6467             }
6468         }
6469     }
6470 }