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