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