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