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