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