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