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