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