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