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