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