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