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