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