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