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