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