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