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