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