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