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