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