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