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