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