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