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