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