]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java
Persist the current bookmarks folder on restart. https://redmine.stoutner.com/issues/806
[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         // Load the bookmarks folder.
3383         loadBookmarksFolder();
3384
3385         // Handle clicks on bookmarks.
3386         bookmarksListView.setOnItemClickListener((parent, view, position, id) -> {
3387             // Convert the id from long to int to match the format of the bookmarks database.
3388             int databaseId = (int) id;
3389
3390             // Get the bookmark cursor for this ID.
3391             Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseId);
3392
3393             // Move the bookmark cursor to the first row.
3394             bookmarkCursor.moveToFirst();
3395
3396             // Act upon the bookmark according to the type.
3397             if (bookmarkCursor.getInt(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {  // The selected bookmark is a folder.
3398                 // Store the new folder name in `currentBookmarksFolder`.
3399                 currentBookmarksFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME));
3400
3401                 // Load the new folder.
3402                 loadBookmarksFolder();
3403             } else {  // The selected bookmark is not a folder.
3404                 // Load the bookmark URL.
3405                 loadUrl(currentWebView, bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_URL)));
3406
3407                 // Close the bookmarks drawer.
3408                 drawerLayout.closeDrawer(GravityCompat.END);
3409             }
3410
3411             // Close the `Cursor`.
3412             bookmarkCursor.close();
3413         });
3414
3415         bookmarksListView.setOnItemLongClickListener((parent, view, position, id) -> {
3416             // Convert the database ID from `long` to `int`.
3417             int databaseId = (int) id;
3418
3419             // Find out if the selected bookmark is a folder.
3420             boolean isFolder = bookmarksDatabaseHelper.isFolder(databaseId);
3421
3422             // Check to see if the bookmark is a folder.
3423             if (isFolder) {  // The bookmark is a folder.
3424                 // Save the current folder name, which is used in `onSaveEditBookmarkFolder()`.
3425                 oldFolderNameString = bookmarksCursor.getString(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME));
3426
3427                 // Instantiate the edit folder bookmark dialog.
3428                 DialogFragment editBookmarkFolderDialog = EditBookmarkFolderDialog.folderDatabaseId(databaseId, currentWebView.getFavoriteOrDefaultIcon());
3429
3430                 // Show the edit folder bookmark dialog.
3431                 editBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.edit_folder));
3432             } else {  // The bookmark is not a folder.
3433                 // Get the bookmark cursor for this ID.
3434                 Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseId);
3435
3436                 // Move the bookmark cursor to the first row.
3437                 bookmarkCursor.moveToFirst();
3438
3439                 // Load the bookmark in a new tab but do not switch to the tab or close the drawer.
3440                 addNewTab(bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_URL)), false);
3441
3442                 // Display a snackbar.
3443                 Snackbar.make(drawerLayout, R.string.bookmark_opened_in_background, Snackbar.LENGTH_SHORT).show();
3444             }
3445
3446             // Consume the event.
3447             return true;
3448         });
3449
3450         // The drawer listener is used to update the navigation menu.
3451         drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() {
3452             @Override
3453             public void onDrawerSlide(@NonNull View drawerView, float slideOffset) {
3454             }
3455
3456             @Override
3457             public void onDrawerOpened(@NonNull View drawerView) {
3458             }
3459
3460             @Override
3461             public void onDrawerClosed(@NonNull View drawerView) {
3462                 // Reset the drawer icon when the drawer is closed.  Otherwise, it is an arrow if the drawer is open when the app is restarted.
3463                 actionBarDrawerToggle.syncState();
3464             }
3465
3466             @Override
3467             public void onDrawerStateChanged(int newState) {
3468                 if ((newState == DrawerLayout.STATE_SETTLING) || (newState == DrawerLayout.STATE_DRAGGING)) {  // A drawer is opening or closing.
3469                     // Update the navigation menu items if the WebView is not null.
3470                     if (currentWebView != null) {
3471                         navigationBackMenuItem.setEnabled(currentWebView.canGoBack());
3472                         navigationForwardMenuItem.setEnabled(currentWebView.canGoForward());
3473                         navigationHistoryMenuItem.setEnabled((currentWebView.canGoBack() || currentWebView.canGoForward()));
3474                         navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
3475
3476                         // Hide the keyboard (if displayed).
3477                         inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
3478                     }
3479
3480                     // 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.
3481                     urlEditText.clearFocus();
3482
3483                     // 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.
3484                     if (currentWebView != null) {
3485                         // Clearing the focus from the WebView removes any text selection markers and context menus, which otherwise draw above the open drawers.
3486                         currentWebView.clearFocus();
3487                     }
3488                 }
3489             }
3490         });
3491
3492         // 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).
3493         customHeaders.put("X-Requested-With", "");
3494
3495         // Inflate a bare WebView to get the default user agent.  It is not used to render content on the screen.
3496         @SuppressLint("InflateParams") View webViewLayout = getLayoutInflater().inflate(R.layout.bare_webview, null, false);
3497
3498         // Get a handle for the WebView.
3499         WebView bareWebView = webViewLayout.findViewById(R.id.bare_webview);
3500
3501         // Store the default user agent.
3502         webViewDefaultUserAgent = bareWebView.getSettings().getUserAgentString();
3503
3504         // Destroy the bare WebView.
3505         bareWebView.destroy();
3506     }
3507
3508     private void applyAppSettings() {
3509         // Get a handle for the shared preferences.
3510         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3511
3512         // Store the values from the shared preferences in variables.
3513         incognitoModeEnabled = sharedPreferences.getBoolean("incognito_mode", false);
3514         sanitizeTrackingQueries = sharedPreferences.getBoolean(getString(R.string.tracking_queries_key), true);
3515         sanitizeAmpRedirects = sharedPreferences.getBoolean(getString(R.string.amp_redirects_key), true);
3516         proxyMode = sharedPreferences.getString("proxy", getString(R.string.proxy_default_value));
3517         fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean("full_screen_browsing_mode", false);
3518         downloadWithExternalApp = sharedPreferences.getBoolean(getString(R.string.download_with_external_app_key), false);
3519         hideAppBar = sharedPreferences.getBoolean("hide_app_bar", true);
3520         scrollAppBar = sharedPreferences.getBoolean(getString(R.string.scroll_app_bar_key), true);
3521
3522         // Apply the saved proxy mode if the app has been restarted.
3523         if (savedProxyMode != null) {
3524             // Apply the saved proxy mode.
3525             proxyMode = savedProxyMode;
3526
3527             // Reset the saved proxy mode.
3528             savedProxyMode = null;
3529         }
3530
3531         // Get the search string.
3532         String searchString = sharedPreferences.getString("search", getString(R.string.search_default_value));
3533
3534         // Set the search string.
3535         if (searchString.equals("Custom URL")) {  // A custom search string is used.
3536             searchURL = sharedPreferences.getString("search_custom_url", getString(R.string.search_custom_url_default_value));
3537         } else {  // A custom search string is not used.
3538             searchURL = searchString;
3539         }
3540
3541         // Apply the proxy.
3542         applyProxy(false);
3543
3544         // Adjust the layout and scrolling parameters according to the position of the app bar.
3545         if (bottomAppBar) {  // The app bar is on the bottom.
3546             // Adjust the UI.
3547             if (scrollAppBar || (inFullScreenBrowsingMode && hideAppBar)) {  // The app bar scrolls or full screen browsing mode is engaged with the app bar hidden.
3548                 // Reset the WebView padding to fill the available space.
3549                 swipeRefreshLayout.setPadding(0, 0, 0, 0);
3550             } else {  // The app bar doesn't scroll or full screen browsing mode is not engaged with the app bar hidden.
3551                 // Move the WebView above the app bar layout.
3552                 swipeRefreshLayout.setPadding(0, 0, 0, appBarHeight);
3553
3554                 // Show the app bar if it is scrolled off the screen.
3555                 if (appBarLayout.getTranslationY() != 0) {
3556                     // Animate the bottom app bar onto the screen.
3557                     objectAnimator = ObjectAnimator.ofFloat(appBarLayout, "translationY", 0);
3558
3559                     // Make it so.
3560                     objectAnimator.start();
3561                 }
3562             }
3563         } else {  // The app bar is on the top.
3564             // Get the current layout parameters.  Using coordinator layout parameters allows the `setBehavior()` command and using app bar layout parameters allows the `setScrollFlags()` command.
3565             CoordinatorLayout.LayoutParams swipeRefreshLayoutParams = (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams();
3566             AppBarLayout.LayoutParams toolbarLayoutParams = (AppBarLayout.LayoutParams) toolbar.getLayoutParams();
3567             AppBarLayout.LayoutParams findOnPageLayoutParams = (AppBarLayout.LayoutParams) findOnPageLinearLayout.getLayoutParams();
3568             AppBarLayout.LayoutParams tabsLayoutParams = (AppBarLayout.LayoutParams) tabsLinearLayout.getLayoutParams();
3569
3570             // Add the scrolling behavior to the layout parameters.
3571             if (scrollAppBar) {
3572                 // Enable scrolling of the app bar.
3573                 swipeRefreshLayoutParams.setBehavior(new AppBarLayout.ScrollingViewBehavior());
3574                 toolbarLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP);
3575                 findOnPageLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP);
3576                 tabsLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP);
3577             } else {
3578                 // Disable scrolling of the app bar.
3579                 swipeRefreshLayoutParams.setBehavior(null);
3580                 toolbarLayoutParams.setScrollFlags(0);
3581                 findOnPageLayoutParams.setScrollFlags(0);
3582                 tabsLayoutParams.setScrollFlags(0);
3583
3584                 // Expand the app bar if it is currently collapsed.
3585                 appBarLayout.setExpanded(true);
3586             }
3587
3588             // Set the app bar scrolling for each WebView.
3589             for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
3590                 // Get the WebView tab fragment.
3591                 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
3592
3593                 // Get the fragment view.
3594                 View fragmentView = webViewTabFragment.getView();
3595
3596                 // Only modify the WebViews if they exist.
3597                 if (fragmentView != null) {
3598                     // Get the nested scroll WebView from the tab fragment.
3599                     NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
3600
3601                     // Set the app bar scrolling.
3602                     nestedScrollWebView.setNestedScrollingEnabled(scrollAppBar);
3603                 }
3604             }
3605         }
3606
3607         // Update the full screen browsing mode settings.
3608         if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) {  // Privacy Browser is currently in full screen browsing mode.
3609             // Update the visibility of the app bar, which might have changed in the settings.
3610             if (hideAppBar) {
3611                 // Hide the tab linear layout.
3612                 tabsLinearLayout.setVisibility(View.GONE);
3613
3614                 // Hide the action bar.
3615                 actionBar.hide();
3616             } else {
3617                 // Show the tab linear layout.
3618                 tabsLinearLayout.setVisibility(View.VISIBLE);
3619
3620                 // Show the action bar.
3621                 actionBar.show();
3622             }
3623
3624             /* Hide the system bars.
3625              * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
3626              * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
3627              * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
3628              * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
3629              */
3630             rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
3631                     View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
3632         } else {  // Privacy Browser is not in full screen browsing mode.
3633             // 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.
3634             inFullScreenBrowsingMode = false;
3635
3636             // Show the tab linear layout.
3637             tabsLinearLayout.setVisibility(View.VISIBLE);
3638
3639             // Show the action bar.
3640             actionBar.show();
3641
3642             // Remove the `SYSTEM_UI` flags from the root frame layout.
3643             rootFrameLayout.setSystemUiVisibility(0);
3644         }
3645     }
3646
3647     @Override
3648     public void navigateHistory(@NonNull String url, int steps) {
3649         // Apply the domain settings.
3650         applyDomainSettings(currentWebView, url, false, false, false);
3651
3652         // Load the history entry.
3653         currentWebView.goBackOrForward(steps);
3654     }
3655
3656     @Override
3657     public void pinnedErrorGoBack() {
3658         // Get the current web back forward list.
3659         WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList();
3660
3661         // Get the previous entry URL.
3662         String previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.getCurrentIndex() - 1).getUrl();
3663
3664         // Apply the domain settings.
3665         applyDomainSettings(currentWebView, previousUrl, false, false, false);
3666
3667         // Go back.
3668         currentWebView.goBack();
3669     }
3670
3671     // `reloadWebsite` is used if returning from the Domains activity.  Otherwise JavaScript might not function correctly if it is newly enabled.
3672     @SuppressLint("SetJavaScriptEnabled")
3673     private void applyDomainSettings(NestedScrollWebView nestedScrollWebView, String url, boolean resetTab, boolean reloadWebsite, boolean loadUrl) {
3674         // Store the current URL.
3675         nestedScrollWebView.setCurrentUrl(url);
3676
3677         // Parse the URL into a URI.
3678         Uri uri = Uri.parse(url);
3679
3680         // Extract the domain from `uri`.
3681         String newHostName = uri.getHost();
3682
3683         // Strings don't like to be null.
3684         if (newHostName == null) {
3685             newHostName = "";
3686         }
3687
3688         // 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.
3689         if (!nestedScrollWebView.getCurrentDomainName().equals(newHostName) || newHostName.equals("")) {
3690             // Set the new host name as the current domain name.
3691             nestedScrollWebView.setCurrentDomainName(newHostName);
3692
3693             // Reset the ignoring of pinned domain information.
3694             nestedScrollWebView.setIgnorePinnedDomainInformation(false);
3695
3696             // Clear any pinned SSL certificate or IP addresses.
3697             nestedScrollWebView.clearPinnedSslCertificate();
3698             nestedScrollWebView.setPinnedIpAddresses("");
3699
3700             // Reset the favorite icon if specified.
3701             if (resetTab) {
3702                 // Initialize the favorite icon.
3703                 nestedScrollWebView.initializeFavoriteIcon();
3704
3705                 // Get the current page position.
3706                 int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
3707
3708                 // Get the corresponding tab.
3709                 TabLayout.Tab tab = tabLayout.getTabAt(currentPagePosition);
3710
3711                 // Update the tab if it isn't null, which sometimes happens when restarting from the background.
3712                 if (tab != null) {
3713                     // Get the tab custom view.
3714                     View tabCustomView = tab.getCustomView();
3715
3716                     // Remove the warning below that the tab custom view might be null.
3717                     assert tabCustomView != null;
3718
3719                     // Get the tab views.
3720                     ImageView tabFavoriteIconImageView = tabCustomView.findViewById(R.id.favorite_icon_imageview);
3721                     TextView tabTitleTextView = tabCustomView.findViewById(R.id.title_textview);
3722
3723                     // Set the default favorite icon as the favorite icon for this tab.
3724                     tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(nestedScrollWebView.getFavoriteOrDefaultIcon(), 64, 64, true));
3725
3726                     // Set the loading title text.
3727                     tabTitleTextView.setText(R.string.loading);
3728                 }
3729             }
3730
3731             // Get a full domain name cursor.
3732             Cursor domainNameCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomain();
3733
3734             // Initialize `domainSettingsSet`.
3735             Set<String> domainSettingsSet = new HashSet<>();
3736
3737             // Get the domain name column index.
3738             int domainNameColumnIndex = domainNameCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.DOMAIN_NAME);
3739
3740             // Populate the domain settings set.
3741             for (int i = 0; i < domainNameCursor.getCount(); i++) {
3742                 // Move the domains cursor to the current row.
3743                 domainNameCursor.moveToPosition(i);
3744
3745                 // Store the domain name in the domain settings set.
3746                 domainSettingsSet.add(domainNameCursor.getString(domainNameColumnIndex));
3747             }
3748
3749             // Close the domain name cursor.
3750             domainNameCursor.close();
3751
3752             // Initialize the domain name in database variable.
3753             String domainNameInDatabase = null;
3754
3755             // Check the hostname against the domain settings set.
3756             if (domainSettingsSet.contains(newHostName)) {  // The hostname is contained in the domain settings set.
3757                 // Record the domain name in the database.
3758                 domainNameInDatabase = newHostName;
3759
3760                 // Set the domain settings applied tracker to true.
3761                 nestedScrollWebView.setDomainSettingsApplied(true);
3762             } else {  // The hostname is not contained in the domain settings set.
3763                 // Set the domain settings applied tracker to false.
3764                 nestedScrollWebView.setDomainSettingsApplied(false);
3765             }
3766
3767             // Check all the subdomains of the host name against wildcard domains in the domain cursor.
3768             while (!nestedScrollWebView.getDomainSettingsApplied() && newHostName.contains(".")) {  // Stop checking if domain settings are already applied or there are no more `.` in the hostname.
3769                 if (domainSettingsSet.contains("*." + newHostName)) {  // Check the host name prepended by `*.`.
3770                     // Set the domain settings applied tracker to true.
3771                     nestedScrollWebView.setDomainSettingsApplied(true);
3772
3773                     // Store the applied domain names as it appears in the database.
3774                     domainNameInDatabase = "*." + newHostName;
3775                 }
3776
3777                 // Strip out the lowest subdomain of of the host name.
3778                 newHostName = newHostName.substring(newHostName.indexOf(".") + 1);
3779             }
3780
3781
3782             // Get a handle for the shared preferences.
3783             SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3784
3785             // Store the general preference information.
3786             String defaultFontSizeString = sharedPreferences.getString("font_size", getString(R.string.font_size_default_value));
3787             String defaultUserAgentName = sharedPreferences.getString("user_agent", getString(R.string.user_agent_default_value));
3788             boolean defaultSwipeToRefresh = sharedPreferences.getBoolean("swipe_to_refresh", true);
3789             String webViewTheme = sharedPreferences.getString("webview_theme", getString(R.string.webview_theme_default_value));
3790             boolean wideViewport = sharedPreferences.getBoolean("wide_viewport", true);
3791             boolean displayWebpageImages = sharedPreferences.getBoolean("display_webpage_images", true);
3792
3793             // Get the WebView theme entry values string array.
3794             String[] webViewThemeEntryValuesStringArray = getResources().getStringArray(R.array.webview_theme_entry_values);
3795
3796             // Get a handle for the cookie manager.
3797             CookieManager cookieManager = CookieManager.getInstance();
3798
3799             // Initialize the user agent array adapter and string array.
3800             ArrayAdapter<CharSequence> userAgentNamesArray = ArrayAdapter.createFromResource(this, R.array.user_agent_names, R.layout.spinner_item);
3801             String[] userAgentDataArray = getResources().getStringArray(R.array.user_agent_data);
3802
3803             if (nestedScrollWebView.getDomainSettingsApplied()) {  // The url has custom domain settings.
3804                 // Remove the incorrect lint warning below that the domain name in database might be null.
3805                 assert domainNameInDatabase != null;
3806
3807                 // Get a cursor for the current host.
3808                 Cursor currentDomainSettingsCursor = domainsDatabaseHelper.getCursorForDomainName(domainNameInDatabase);
3809
3810                 // Move to the first position.
3811                 currentDomainSettingsCursor.moveToFirst();
3812
3813                 // Get the settings from the cursor.
3814                 nestedScrollWebView.setDomainSettingsDatabaseId(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ID)));
3815                 nestedScrollWebView.getSettings().setJavaScriptEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)) == 1);
3816                 nestedScrollWebView.setAcceptCookies(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.COOKIES)) == 1);
3817                 nestedScrollWebView.getSettings().setDomStorageEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_DOM_STORAGE)) == 1);
3818                 // Form data can be removed once the minimum API >= 26.
3819                 boolean saveFormData = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_FORM_DATA)) == 1);
3820                 nestedScrollWebView.setEasyListEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_EASYLIST)) == 1);
3821                 nestedScrollWebView.setEasyPrivacyEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_EASYPRIVACY)) == 1);
3822                 nestedScrollWebView.setFanboysAnnoyanceListEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST)) == 1);
3823                 nestedScrollWebView.setFanboysSocialBlockingListEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(
3824                         DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)) == 1);
3825                 nestedScrollWebView.setUltraListEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ULTRALIST)) == 1);
3826                 nestedScrollWebView.setUltraPrivacyEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY)) == 1);
3827                 nestedScrollWebView.setBlockAllThirdPartyRequests(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS)) == 1);
3828                 String userAgentName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.USER_AGENT));
3829                 int fontSize = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.FONT_SIZE));
3830                 int swipeToRefreshInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SWIPE_TO_REFRESH));
3831                 int webViewThemeInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.WEBVIEW_THEME));
3832                 int wideViewportInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.WIDE_VIEWPORT));
3833                 int displayWebpageImagesInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.DISPLAY_IMAGES));
3834                 boolean pinnedSslCertificate = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)) == 1);
3835                 String pinnedSslIssuedToCName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME));
3836                 String pinnedSslIssuedToOName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION));
3837                 String pinnedSslIssuedToUName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT));
3838                 String pinnedSslIssuedByCName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME));
3839                 String pinnedSslIssuedByOName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION));
3840                 String pinnedSslIssuedByUName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT));
3841                 Date pinnedSslStartDate = new Date(currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_START_DATE)));
3842                 Date pinnedSslEndDate = new Date(currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_END_DATE)));
3843                 boolean pinnedIpAddresses = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.PINNED_IP_ADDRESSES)) == 1);
3844                 String pinnedHostIpAddresses = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.IP_ADDRESSES));
3845
3846                 // Close the current host domain settings cursor.
3847                 currentDomainSettingsCursor.close();
3848
3849                 // If there is a pinned SSL certificate, store it in the WebView.
3850                 if (pinnedSslCertificate) {
3851                     nestedScrollWebView.setPinnedSslCertificate(pinnedSslIssuedToCName, pinnedSslIssuedToOName, pinnedSslIssuedToUName, pinnedSslIssuedByCName, pinnedSslIssuedByOName, pinnedSslIssuedByUName,
3852                             pinnedSslStartDate, pinnedSslEndDate);
3853                 }
3854
3855                 // If there is a pinned IP address, store it in the WebView.
3856                 if (pinnedIpAddresses) {
3857                     nestedScrollWebView.setPinnedIpAddresses(pinnedHostIpAddresses);
3858                 }
3859
3860                 // Apply the cookie domain settings.
3861                 cookieManager.setAcceptCookie(nestedScrollWebView.getAcceptCookies());
3862
3863                 // Apply the form data setting if the API < 26.
3864                 if (Build.VERSION.SDK_INT < 26) {
3865                     nestedScrollWebView.getSettings().setSaveFormData(saveFormData);
3866                 }
3867
3868                 // Apply the font size.
3869                 try {  // Try the specified font size to see if it is valid.
3870                     if (fontSize == 0) {  // Apply the default font size.
3871                             // Try to set the font size from the value in the app settings.
3872                             nestedScrollWebView.getSettings().setTextZoom(Integer.parseInt(defaultFontSizeString));
3873                     } else {  // Apply the font size from domain settings.
3874                         nestedScrollWebView.getSettings().setTextZoom(fontSize);
3875                     }
3876                 } catch (Exception exception) {  // The specified font size is invalid
3877                     // Set the font size to be 100%
3878                     nestedScrollWebView.getSettings().setTextZoom(100);
3879                 }
3880
3881                 // Set the user agent.
3882                 if (userAgentName.equals(getString(R.string.system_default_user_agent))) {  // Use the system default user agent.
3883                     // Get the array position of the default user agent name.
3884                     int defaultUserAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
3885
3886                     // Set the user agent according to the system default.
3887                     switch (defaultUserAgentArrayPosition) {
3888                         case UNRECOGNIZED_USER_AGENT:  // The default user agent name is not on the canonical list.
3889                             // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
3890                             nestedScrollWebView.getSettings().setUserAgentString(defaultUserAgentName);
3891                             break;
3892
3893                         case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
3894                             // Set the user agent to `""`, which uses the default value.
3895                             nestedScrollWebView.getSettings().setUserAgentString("");
3896                             break;
3897
3898                         case SETTINGS_CUSTOM_USER_AGENT:
3899                             // Set the default custom user agent.
3900                             nestedScrollWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
3901                             break;
3902
3903                         default:
3904                             // Get the user agent string from the user agent data array
3905                             nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[defaultUserAgentArrayPosition]);
3906                     }
3907                 } else {  // Set the user agent according to the stored name.
3908                     // Get the array position of the user agent name.
3909                     int userAgentArrayPosition = userAgentNamesArray.getPosition(userAgentName);
3910
3911                     switch (userAgentArrayPosition) {
3912                         case UNRECOGNIZED_USER_AGENT:  // The user agent name contains a custom user agent.
3913                             nestedScrollWebView.getSettings().setUserAgentString(userAgentName);
3914                             break;
3915
3916                         case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
3917                             // Set the user agent to `""`, which uses the default value.
3918                             nestedScrollWebView.getSettings().setUserAgentString("");
3919                             break;
3920
3921                         default:
3922                             // Get the user agent string from the user agent data array.
3923                             nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
3924                     }
3925                 }
3926
3927                 // Set swipe to refresh.
3928                 switch (swipeToRefreshInt) {
3929                     case DomainsDatabaseHelper.SYSTEM_DEFAULT:
3930                         // Store the swipe to refresh status in the nested scroll WebView.
3931                         nestedScrollWebView.setSwipeToRefresh(defaultSwipeToRefresh);
3932
3933                         // Update the swipe refresh layout.
3934                         if (defaultSwipeToRefresh) {  // Swipe to refresh is enabled.
3935                             // Only enable the swipe refresh layout if the WebView is scrolled to the top.  It is updated every time the scroll changes.
3936                             swipeRefreshLayout.setEnabled(currentWebView.getScrollY() == 0);
3937                         } else {  // Swipe to refresh is disabled.
3938                             // Disable the swipe refresh layout.
3939                             swipeRefreshLayout.setEnabled(false);
3940                         }
3941                         break;
3942
3943                     case DomainsDatabaseHelper.ENABLED:
3944                         // Store the swipe to refresh status in the nested scroll WebView.
3945                         nestedScrollWebView.setSwipeToRefresh(true);
3946
3947                         // Only enable the swipe refresh layout if the WebView is scrolled to the top.  It is updated every time the scroll changes.
3948                         swipeRefreshLayout.setEnabled(currentWebView.getScrollY() == 0);
3949                         break;
3950
3951                     case DomainsDatabaseHelper.DISABLED:
3952                         // Store the swipe to refresh status in the nested scroll WebView.
3953                         nestedScrollWebView.setSwipeToRefresh(false);
3954
3955                         // Disable swipe to refresh.
3956                         swipeRefreshLayout.setEnabled(false);
3957                 }
3958
3959                 // Check to see if WebView themes are supported.
3960                 if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
3961                     // Set the WebView theme.
3962                     switch (webViewThemeInt) {
3963                         case DomainsDatabaseHelper.SYSTEM_DEFAULT:
3964                             // Set the WebView theme.  A switch statement cannot be used because the WebView theme entry values string array is not a compile time constant.
3965                             if (webViewTheme.equals(webViewThemeEntryValuesStringArray[1])) {  // The light theme is selected.
3966                                 // Turn off the WebView dark mode.
3967                                 WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
3968                             } else if (webViewTheme.equals(webViewThemeEntryValuesStringArray[2])) {  // The dark theme is selected.
3969                                 // Turn on the WebView dark mode.
3970                                 WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
3971                             } else {  // The system default theme is selected.
3972                                 // Get the current system theme status.
3973                                 int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
3974
3975                                 // Set the WebView theme according to the current system theme status.
3976                                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {  // The system is in day mode.
3977                                     // Turn off the WebView dark mode.
3978                                     WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
3979                                 } else {  // The system is in night mode.
3980                                     // Turn on the WebView dark mode.
3981                                     WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
3982                                 }
3983                             }
3984                             break;
3985
3986                         case DomainsDatabaseHelper.LIGHT_THEME:
3987                             // Turn off the WebView dark mode.
3988                             WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
3989                             break;
3990
3991                         case DomainsDatabaseHelper.DARK_THEME:
3992                             // Turn on the WebView dark mode.
3993                             WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
3994                             break;
3995                     }
3996                 }
3997
3998                 // Set the viewport.
3999                 switch (wideViewportInt) {
4000                     case DomainsDatabaseHelper.SYSTEM_DEFAULT:
4001                         nestedScrollWebView.getSettings().setUseWideViewPort(wideViewport);
4002                         break;
4003
4004                     case DomainsDatabaseHelper.ENABLED:
4005                         nestedScrollWebView.getSettings().setUseWideViewPort(true);
4006                         break;
4007
4008                     case DomainsDatabaseHelper.DISABLED:
4009                         nestedScrollWebView.getSettings().setUseWideViewPort(false);
4010                         break;
4011                 }
4012
4013                 // Set the loading of webpage images.
4014                 switch (displayWebpageImagesInt) {
4015                     case DomainsDatabaseHelper.SYSTEM_DEFAULT:
4016                         nestedScrollWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
4017                         break;
4018
4019                     case DomainsDatabaseHelper.ENABLED:
4020                         nestedScrollWebView.getSettings().setLoadsImagesAutomatically(true);
4021                         break;
4022
4023                     case DomainsDatabaseHelper.DISABLED:
4024                         nestedScrollWebView.getSettings().setLoadsImagesAutomatically(false);
4025                         break;
4026                 }
4027
4028                 // Set a background on the URL relative layout to indicate that custom domain settings are being used.
4029                 urlRelativeLayout.setBackground(ResourcesCompat.getDrawable(getResources(), R.drawable.domain_settings_url_background, null));
4030             } else {  // The new URL does not have custom domain settings.  Load the defaults.
4031                 // Store the values from the shared preferences.
4032                 nestedScrollWebView.getSettings().setJavaScriptEnabled(sharedPreferences.getBoolean("javascript", false));
4033                 nestedScrollWebView.setAcceptCookies(sharedPreferences.getBoolean(getString(R.string.cookies_key), false));
4034                 nestedScrollWebView.getSettings().setDomStorageEnabled(sharedPreferences.getBoolean("dom_storage", false));
4035                 boolean saveFormData = sharedPreferences.getBoolean("save_form_data", false);  // Form data can be removed once the minimum API >= 26.
4036                 nestedScrollWebView.setEasyListEnabled(sharedPreferences.getBoolean("easylist", true));
4037                 nestedScrollWebView.setEasyPrivacyEnabled(sharedPreferences.getBoolean("easyprivacy", true));
4038                 nestedScrollWebView.setFanboysAnnoyanceListEnabled(sharedPreferences.getBoolean("fanboys_annoyance_list", true));
4039                 nestedScrollWebView.setFanboysSocialBlockingListEnabled(sharedPreferences.getBoolean("fanboys_social_blocking_list", true));
4040                 nestedScrollWebView.setUltraListEnabled(sharedPreferences.getBoolean("ultralist", true));
4041                 nestedScrollWebView.setUltraPrivacyEnabled(sharedPreferences.getBoolean("ultraprivacy", true));
4042                 nestedScrollWebView.setBlockAllThirdPartyRequests(sharedPreferences.getBoolean("block_all_third_party_requests", false));
4043
4044                 // Apply the default cookie setting.
4045                 cookieManager.setAcceptCookie(nestedScrollWebView.getAcceptCookies());
4046
4047                 // Apply the default font size setting.
4048                 try {
4049                     // Try to set the font size from the value in the app settings.
4050                     nestedScrollWebView.getSettings().setTextZoom(Integer.parseInt(defaultFontSizeString));
4051                 } catch (Exception exception) {
4052                     // If the app settings value is invalid, set the font size to 100%.
4053                     nestedScrollWebView.getSettings().setTextZoom(100);
4054                 }
4055
4056                 // Apply the form data setting if the API < 26.
4057                 if (Build.VERSION.SDK_INT < 26) {
4058                     nestedScrollWebView.getSettings().setSaveFormData(saveFormData);
4059                 }
4060
4061                 // Store the swipe to refresh status in the nested scroll WebView.
4062                 nestedScrollWebView.setSwipeToRefresh(defaultSwipeToRefresh);
4063
4064                 // Update the swipe refresh layout.
4065                 if (defaultSwipeToRefresh) {  // Swipe to refresh is enabled.
4066                     // Only enable the swipe refresh layout if the WebView is scrolled to the top.  It is updated every time the scroll changes.
4067                     swipeRefreshLayout.setEnabled(currentWebView.getScrollY() == 0);
4068                 } else {  // Swipe to refresh is disabled.
4069                     // Disable the swipe refresh layout.
4070                     swipeRefreshLayout.setEnabled(false);
4071                 }
4072
4073                 // Reset the pinned variables.
4074                 nestedScrollWebView.setDomainSettingsDatabaseId(-1);
4075
4076                 // Get the array position of the user agent name.
4077                 int userAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
4078
4079                 // Set the user agent.
4080                 switch (userAgentArrayPosition) {
4081                     case UNRECOGNIZED_USER_AGENT:  // The default user agent name is not on the canonical list.
4082                         // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
4083                         nestedScrollWebView.getSettings().setUserAgentString(defaultUserAgentName);
4084                         break;
4085
4086                     case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
4087                         // Set the user agent to `""`, which uses the default value.
4088                         nestedScrollWebView.getSettings().setUserAgentString("");
4089                         break;
4090
4091                     case SETTINGS_CUSTOM_USER_AGENT:
4092                         // Set the default custom user agent.
4093                         nestedScrollWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
4094                         break;
4095
4096                     default:
4097                         // Get the user agent string from the user agent data array
4098                         nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
4099                 }
4100
4101                 // Apply the WebView theme if supported by the installed WebView.
4102                 if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
4103                     // Set the WebView theme.  A switch statement cannot be used because the WebView theme entry values string array is not a compile time constant.
4104                     if (webViewTheme.equals(webViewThemeEntryValuesStringArray[1])) {  // The light theme is selected.
4105                         // Turn off the WebView dark mode.
4106                         WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
4107                     } else if (webViewTheme.equals(webViewThemeEntryValuesStringArray[2])) {  // The dark theme is selected.
4108                         // Turn on the WebView dark mode.
4109                         WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
4110                     } else {  // The system default theme is selected.
4111                         // Get the current system theme status.
4112                         int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
4113
4114                         // Set the WebView theme according to the current system theme status.
4115                         if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {  // The system is in day mode.
4116                             // Turn off the WebView dark mode.
4117                             WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
4118                         } else {  // The system is in night mode.
4119                             // Turn on the WebView dark mode.
4120                             WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
4121                         }
4122                     }
4123                 }
4124
4125                 // Set the viewport.
4126                 nestedScrollWebView.getSettings().setUseWideViewPort(wideViewport);
4127
4128                 // Set the loading of webpage images.
4129                 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
4130
4131                 // Set a transparent background on the URL relative layout.
4132                 urlRelativeLayout.setBackground(ResourcesCompat.getDrawable(getResources(), R.color.transparent, null));
4133             }
4134
4135             // Close the domains database helper.
4136             domainsDatabaseHelper.close();
4137
4138             // Update the privacy icons.
4139             updatePrivacyIcons(true);
4140         }
4141
4142         // Reload the website if returning from the Domains activity.
4143         if (reloadWebsite) {
4144             nestedScrollWebView.reload();
4145         }
4146
4147         // 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.
4148         if (loadUrl) {
4149             nestedScrollWebView.loadUrl(url, customHeaders);
4150         }
4151     }
4152
4153     private void applyProxy(boolean reloadWebViews) {
4154         // Set the proxy according to the mode.
4155         proxyHelper.setProxy(getApplicationContext(), appBarLayout, proxyMode);
4156
4157         // Reset the waiting for proxy tracker.
4158         waitingForProxy = false;
4159
4160         // Get the current theme status.
4161         int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
4162
4163         // Update the user interface and reload the WebViews if requested.
4164         switch (proxyMode) {
4165             case ProxyHelper.NONE:
4166                 // Initialize a color background typed value.
4167                 TypedValue colorBackgroundTypedValue = new TypedValue();
4168
4169                 // Get the color background from the theme.
4170                 getTheme().resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true);
4171
4172                 // Get the color background int from the typed value.
4173                 int colorBackgroundInt = colorBackgroundTypedValue.data;
4174
4175                 // Set the default app bar layout background.
4176                 appBarLayout.setBackgroundColor(colorBackgroundInt);
4177                 break;
4178
4179             case ProxyHelper.TOR:
4180                 // Set the app bar background to indicate proxying through Orbot is enabled.
4181                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
4182                     appBarLayout.setBackgroundResource(R.color.blue_50);
4183                 } else {
4184                     appBarLayout.setBackgroundResource(R.color.dark_blue_30);
4185                 }
4186
4187                 // Check to see if Orbot is installed.
4188                 try {
4189                     // Get the package manager.
4190                     PackageManager packageManager = getPackageManager();
4191
4192                     // 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.
4193                     packageManager.getPackageInfo("org.torproject.android", 0);
4194
4195                     // Check to see if the proxy is ready.
4196                     if (!orbotStatus.equals(ProxyHelper.ORBOT_STATUS_ON)) {  // Orbot is not ready.
4197                         // Set the waiting for proxy status.
4198                         waitingForProxy = true;
4199
4200                         // Show the waiting for proxy dialog if it isn't already displayed.
4201                         if (getSupportFragmentManager().findFragmentByTag(getString(R.string.waiting_for_proxy_dialog)) == null) {
4202                             // Get a handle for the waiting for proxy alert dialog.
4203                             DialogFragment waitingForProxyDialogFragment = new WaitingForProxyDialog();
4204
4205                             // Try to show the dialog.  Sometimes the window is not yet active if returning from Settings.
4206                             try {
4207                                 // Show the waiting for proxy alert dialog.
4208                                 waitingForProxyDialogFragment.show(getSupportFragmentManager(), getString(R.string.waiting_for_proxy_dialog));
4209                             } catch (Exception waitingForTorException) {
4210                                 // Add the dialog to the pending dialog array list.  It will be displayed in `onStart()`.
4211                                 pendingDialogsArrayList.add(new PendingDialog(waitingForProxyDialogFragment, getString(R.string.waiting_for_proxy_dialog)));
4212                             }
4213                         }
4214                     }
4215                 } catch (PackageManager.NameNotFoundException exception) {  // Orbot is not installed.
4216                     // Show the Orbot not installed dialog if it is not already displayed.
4217                     if (getSupportFragmentManager().findFragmentByTag(getString(R.string.proxy_not_installed_dialog)) == null) {
4218                         // Get a handle for the Orbot not installed alert dialog.
4219                         DialogFragment orbotNotInstalledDialogFragment = ProxyNotInstalledDialog.displayDialog(proxyMode);
4220
4221                         // Try to show the dialog.  Sometimes the window is not yet active if returning from Settings.
4222                         try {
4223                             // Display the Orbot not installed alert dialog.
4224                             orbotNotInstalledDialogFragment.show(getSupportFragmentManager(), getString(R.string.proxy_not_installed_dialog));
4225                         } catch (Exception orbotNotInstalledException) {
4226                             // Add the dialog to the pending dialog array list.  It will be displayed in `onStart()`.
4227                             pendingDialogsArrayList.add(new PendingDialog(orbotNotInstalledDialogFragment, getString(R.string.proxy_not_installed_dialog)));
4228                         }
4229                     }
4230                 }
4231                 break;
4232
4233             case ProxyHelper.I2P:
4234                 // Set the app bar background to indicate proxying through Orbot is enabled.
4235                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
4236                     appBarLayout.setBackgroundResource(R.color.blue_50);
4237                 } else {
4238                     appBarLayout.setBackgroundResource(R.color.dark_blue_30);
4239                 }
4240
4241                 // Check to see if I2P is installed.
4242                 try {
4243                     // Get the package manager.
4244                     PackageManager packageManager = getPackageManager();
4245
4246                     // 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.
4247                     packageManager.getPackageInfo("net.i2p.android.router", 0);
4248                 } catch (PackageManager.NameNotFoundException exception) {  // I2P is not installed.
4249                     // Sow the I2P not installed dialog if it is not already displayed.
4250                     if (getSupportFragmentManager().findFragmentByTag(getString(R.string.proxy_not_installed_dialog)) == null) {
4251                         // Get a handle for the waiting for proxy alert dialog.
4252                         DialogFragment i2pNotInstalledDialogFragment = ProxyNotInstalledDialog.displayDialog(proxyMode);
4253
4254                         // Try to show the dialog.  Sometimes the window is not yet active if returning from Settings.
4255                         try {
4256                             // Display the I2P not installed alert dialog.
4257                             i2pNotInstalledDialogFragment.show(getSupportFragmentManager(), getString(R.string.proxy_not_installed_dialog));
4258                         } catch (Exception i2pNotInstalledException) {
4259                             // Add the dialog to the pending dialog array list.  It will be displayed in `onStart()`.
4260                             pendingDialogsArrayList.add(new PendingDialog(i2pNotInstalledDialogFragment, getString(R.string.proxy_not_installed_dialog)));
4261                         }
4262                     }
4263                 }
4264                 break;
4265
4266             case ProxyHelper.CUSTOM:
4267                 // Set the app bar background to indicate proxying through Orbot is enabled.
4268                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
4269                     appBarLayout.setBackgroundResource(R.color.blue_50);
4270                 } else {
4271                     appBarLayout.setBackgroundResource(R.color.dark_blue_30);
4272                 }
4273                 break;
4274         }
4275
4276         // Reload the WebViews if requested and not waiting for the proxy.
4277         if (reloadWebViews && !waitingForProxy) {
4278             // Reload the WebViews.
4279             for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
4280                 // Get the WebView tab fragment.
4281                 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
4282
4283                 // Get the fragment view.
4284                 View fragmentView = webViewTabFragment.getView();
4285
4286                 // Only reload the WebViews if they exist.
4287                 if (fragmentView != null) {
4288                     // Get the nested scroll WebView from the tab fragment.
4289                     NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
4290
4291                     // Reload the WebView.
4292                     nestedScrollWebView.reload();
4293                 }
4294             }
4295         }
4296     }
4297
4298     private void updatePrivacyIcons(boolean runInvalidateOptionsMenu) {
4299         // Only update the privacy icons if the options menu and the current WebView have already been populated.
4300         if ((optionsMenu != null) && (currentWebView != null)) {
4301             // Update the privacy icon.
4302             if (currentWebView.getSettings().getJavaScriptEnabled()) {  // JavaScript is enabled.
4303                 optionsPrivacyMenuItem.setIcon(R.drawable.javascript_enabled);
4304             } else if (currentWebView.getAcceptCookies()) {  // JavaScript is disabled but cookies are enabled.
4305                 optionsPrivacyMenuItem.setIcon(R.drawable.warning);
4306             } else {  // All the dangerous features are disabled.
4307                 optionsPrivacyMenuItem.setIcon(R.drawable.privacy_mode);
4308             }
4309
4310             // Update the cookies icon.
4311             if (currentWebView.getAcceptCookies()) {
4312                 optionsCookiesMenuItem.setIcon(R.drawable.cookies_enabled);
4313             } else {
4314                 optionsCookiesMenuItem.setIcon(R.drawable.cookies_disabled);
4315             }
4316
4317             // Update the refresh icon.
4318             if (optionsRefreshMenuItem.getTitle() == getString(R.string.refresh)) {  // The refresh icon is displayed.
4319                 // Set the icon.  Once the minimum API is >= 26, the blue and black icons can be combined with a tint list.
4320                 optionsRefreshMenuItem.setIcon(R.drawable.refresh_enabled);
4321             } else {  // The stop icon is displayed.
4322                 // Set the icon.  Once the minimum API is >= 26, the blue and black icons can be combined with a tint list.
4323                 optionsRefreshMenuItem.setIcon(R.drawable.close_blue);
4324             }
4325
4326             // `invalidateOptionsMenu()` calls `onPrepareOptionsMenu()` and redraws the icons in the app bar.
4327             if (runInvalidateOptionsMenu) {
4328                 invalidateOptionsMenu();
4329             }
4330         }
4331     }
4332
4333     private void highlightUrlText() {
4334         // Only highlight the URL text if the box is not currently selected.
4335         if (!urlEditText.hasFocus()) {
4336             // Get the URL string.
4337             String urlString = urlEditText.getText().toString();
4338
4339             // Highlight the URL according to the protocol.
4340             if (urlString.startsWith("file://") || urlString.startsWith("content://")) {  // This is a file or content URL.
4341                 // De-emphasize everything before the file name.
4342                 urlEditText.getText().setSpan(initialGrayColorSpan, 0, urlString.lastIndexOf("/") + 1,Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4343             } else {  // This is a web URL.
4344                 // Get the index of the `/` immediately after the domain name.
4345                 int endOfDomainName = urlString.indexOf("/", (urlString.indexOf("//") + 2));
4346
4347                 // Create a base URL string.
4348                 String baseUrl;
4349
4350                 // Get the base URL.
4351                 if (endOfDomainName > 0) {  // There is at least one character after the base URL.
4352                     // Get the base URL.
4353                     baseUrl = urlString.substring(0, endOfDomainName);
4354                 } else {  // There are no characters after the base URL.
4355                     // Set the base URL to be the entire URL string.
4356                     baseUrl = urlString;
4357                 }
4358
4359                 // Get the index of the last `.` in the domain.
4360                 int lastDotIndex = baseUrl.lastIndexOf(".");
4361
4362                 // Get the index of the penultimate `.` in the domain.
4363                 int penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1);
4364
4365                 // Markup the beginning of the URL.
4366                 if (urlString.startsWith("http://")) {  // Highlight the protocol of connections that are not encrypted.
4367                     urlEditText.getText().setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4368
4369                     // De-emphasize subdomains.
4370                     if (penultimateDotIndex > 0) {  // There is more than one subdomain in the domain name.
4371                         urlEditText.getText().setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4372                     }
4373                 } else if (urlString.startsWith("https://")) {  // De-emphasize the protocol of connections that are encrypted.
4374                     if (penultimateDotIndex > 0) {  // There is more than one subdomain in the domain name.
4375                         // De-emphasize the protocol and the additional subdomains.
4376                         urlEditText.getText().setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4377                     } else {  // There is only one subdomain in the domain name.
4378                         // De-emphasize only the protocol.
4379                         urlEditText.getText().setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4380                     }
4381                 }
4382
4383                 // De-emphasize the text after the domain name.
4384                 if (endOfDomainName > 0) {
4385                     urlEditText.getText().setSpan(finalGrayColorSpan, endOfDomainName, urlString.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4386                 }
4387             }
4388         }
4389     }
4390
4391     private void loadBookmarksFolder() {
4392         // Update the bookmarks cursor with the contents of the bookmarks database for the current folder.
4393         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
4394
4395         // Populate the bookmarks cursor adapter.
4396         bookmarksCursorAdapter = new CursorAdapter(this, bookmarksCursor, false) {
4397             @Override
4398             public View newView(Context context, Cursor cursor, ViewGroup parent) {
4399                 // Inflate the individual item layout.
4400                 return getLayoutInflater().inflate(R.layout.bookmarks_drawer_item_linearlayout, parent, false);
4401             }
4402
4403             @Override
4404             public void bindView(View view, Context context, Cursor cursor) {
4405                 // Get handles for the views.
4406                 ImageView bookmarkFavoriteIcon = view.findViewById(R.id.bookmark_favorite_icon);
4407                 TextView bookmarkNameTextView = view.findViewById(R.id.bookmark_name);
4408
4409                 // Get the favorite icon byte array from the cursor.
4410                 byte[] favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.FAVORITE_ICON));
4411
4412                 // Convert the byte array to a `Bitmap` beginning at the first byte and ending at the last.
4413                 Bitmap favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.length);
4414
4415                 // Display the bitmap in `bookmarkFavoriteIcon`.
4416                 bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap);
4417
4418                 // Get the bookmark name from the cursor and display it in `bookmarkNameTextView`.
4419                 String bookmarkNameString = cursor.getString(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME));
4420                 bookmarkNameTextView.setText(bookmarkNameString);
4421
4422                 // Make the font bold for folders.
4423                 if (cursor.getInt(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {
4424                     bookmarkNameTextView.setTypeface(Typeface.DEFAULT_BOLD);
4425                 } else {  // Reset the font to default for normal bookmarks.
4426                     bookmarkNameTextView.setTypeface(Typeface.DEFAULT);
4427                 }
4428             }
4429         };
4430
4431         // Get a handle for the bookmarks list view.
4432         ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
4433
4434         // Populate the list view with the adapter.
4435         bookmarksListView.setAdapter(bookmarksCursorAdapter);
4436
4437         // Get a handle for the bookmarks title text view.
4438         TextView bookmarksTitleTextView = findViewById(R.id.bookmarks_title_textview);
4439
4440         // Set the bookmarks drawer title.
4441         if (currentBookmarksFolder.isEmpty()) {
4442             bookmarksTitleTextView.setText(R.string.bookmarks);
4443         } else {
4444             bookmarksTitleTextView.setText(currentBookmarksFolder);
4445         }
4446     }
4447
4448     private void openWithApp(String url) {
4449         // Create an open with app intent with `ACTION_VIEW`.
4450         Intent openWithAppIntent = new Intent(Intent.ACTION_VIEW);
4451
4452         // Set the URI but not the MIME type.  This should open all available apps.
4453         openWithAppIntent.setData(Uri.parse(url));
4454
4455         // Flag the intent to open in a new task.
4456         openWithAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4457
4458         // Try the intent.
4459         try {
4460             // Show the chooser.
4461             startActivity(openWithAppIntent);
4462         } catch (ActivityNotFoundException exception) {  // There are no apps available to open the URL.
4463             // Show a snackbar with the error.
4464             Snackbar.make(currentWebView, getString(R.string.error) + "  " + exception, Snackbar.LENGTH_INDEFINITE).show();
4465         }
4466     }
4467
4468     private void openWithBrowser(String url) {
4469         // Create an open with browser intent with `ACTION_VIEW`.
4470         Intent openWithBrowserIntent = new Intent(Intent.ACTION_VIEW);
4471
4472         // Set the URI and the MIME type.  `"text/html"` should load browser options.
4473         openWithBrowserIntent.setDataAndType(Uri.parse(url), "text/html");
4474
4475         // Flag the intent to open in a new task.
4476         openWithBrowserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4477
4478         // Try the intent.
4479         try {
4480             // Show the chooser.
4481             startActivity(openWithBrowserIntent);
4482         } catch (ActivityNotFoundException exception) {  // There are no browsers available to open the URL.
4483             // Show a snackbar with the error.
4484             Snackbar.make(currentWebView, getString(R.string.error) + "  " + exception, Snackbar.LENGTH_INDEFINITE).show();
4485         }
4486     }
4487
4488     private String sanitizeUrl(String url) {
4489         // Sanitize tracking queries.
4490         if (sanitizeTrackingQueries)
4491             url = sanitizeUrlHelper.sanitizeTrackingQueries(url);
4492
4493         // Sanitize AMP redirects.
4494         if (sanitizeAmpRedirects)
4495             url = sanitizeUrlHelper.sanitizeAmpRedirects(url);
4496
4497         // Return the sanitized URL.
4498         return url;
4499     }
4500
4501     public void finishedPopulatingBlocklists(ArrayList<ArrayList<List<String[]>>> combinedBlocklists) {
4502         // Store the blocklists.
4503         easyList = combinedBlocklists.get(0);
4504         easyPrivacy = combinedBlocklists.get(1);
4505         fanboysAnnoyanceList = combinedBlocklists.get(2);
4506         fanboysSocialList = combinedBlocklists.get(3);
4507         ultraList = combinedBlocklists.get(4);
4508         ultraPrivacy = combinedBlocklists.get(5);
4509
4510         // Check to see if the activity has been restarted with a saved state.
4511         if ((savedStateArrayList == null) || (savedStateArrayList.size() == 0)) {  // The activity has not been restarted or it was restarted on start to force the night theme.
4512             // Add the first tab.
4513             addNewTab("", true);
4514         } else {  // The activity has been restarted.
4515             // Restore each tab.  Once the minimum API >= 24, a `forEach()` command can be used.
4516             for (int i = 0; i < savedStateArrayList.size(); i++) {
4517                 // Add a new tab.
4518                 tabLayout.addTab(tabLayout.newTab());
4519
4520                 // Get the new tab.
4521                 TabLayout.Tab newTab = tabLayout.getTabAt(i);
4522
4523                 // Remove the lint warning below that the current tab might be null.
4524                 assert newTab != null;
4525
4526                 // Set a custom view on the new tab.
4527                 newTab.setCustomView(R.layout.tab_custom_view);
4528
4529                 // Add the new page.
4530                 webViewPagerAdapter.restorePage(savedStateArrayList.get(i), savedNestedScrollWebViewStateArrayList.get(i));
4531             }
4532
4533             // Reset the saved state variables.
4534             savedStateArrayList = null;
4535             savedNestedScrollWebViewStateArrayList = null;
4536
4537             // Restore the selected tab position.
4538             if (savedTabPosition == 0) {  // The first tab is selected.
4539                 // Set the first page as the current WebView.
4540                 setCurrentWebView(0);
4541             } else {  // the first tab is not selected.
4542                 // Move to the selected tab.
4543                 webViewPager.setCurrentItem(savedTabPosition);
4544             }
4545
4546             // Get the intent that started the app.
4547             Intent intent = getIntent();
4548
4549             // Reset the intent.  This prevents a duplicate tab from being created on restart.
4550             setIntent(new Intent());
4551
4552             // Get the information from the intent.
4553             String intentAction = intent.getAction();
4554             Uri intentUriData = intent.getData();
4555             String intentStringExtra = intent.getStringExtra(Intent.EXTRA_TEXT);
4556
4557             // Determine if this is a web search.
4558             boolean isWebSearch = ((intentAction != null) && intentAction.equals(Intent.ACTION_WEB_SEARCH));
4559
4560             // 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.
4561             if (intentUriData != null || intentStringExtra != null || isWebSearch) {
4562                 // Get the shared preferences.
4563                 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4564
4565                 // Create a URL string.
4566                 String url;
4567
4568                 // If the intent action is a web search, perform the search.
4569                 if (isWebSearch) {  // The intent is a web search.
4570                     // Create an encoded URL string.
4571                     String encodedUrlString;
4572
4573                     // Sanitize the search input and convert it to a search.
4574                     try {
4575                         encodedUrlString = URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8");
4576                     } catch (UnsupportedEncodingException exception) {
4577                         encodedUrlString = "";
4578                     }
4579
4580                     // Add the base search URL.
4581                     url = searchURL + encodedUrlString;
4582                 } else if (intentUriData != null) {  // The intent contains a URL formatted as a URI.
4583                     // Set the intent data as the URL.
4584                     url = intentUriData.toString();
4585                 } else {  // The intent contains a string, which might be a URL.
4586                     // Set the intent string as the URL.
4587                     url = intentStringExtra;
4588                 }
4589
4590                 // Add a new tab if specified in the preferences.
4591                 if (sharedPreferences.getBoolean("open_intents_in_new_tab", true)) {  // Load the URL in a new tab.
4592                     // Set the loading new intent flag.
4593                     loadingNewIntent = true;
4594
4595                     // Add a new tab.
4596                     addNewTab(url, true);
4597                 } else {  // Load the URL in the current tab.
4598                     // Make it so.
4599                     loadUrl(currentWebView, url);
4600                 }
4601             }
4602         }
4603     }
4604
4605     public void addTab(View view) {
4606         // Add a new tab with a blank URL.
4607         addNewTab("", true);
4608     }
4609
4610     private void addNewTab(String url, boolean moveToTab) {
4611         // Clear the focus from the URL edit text, so that it will be populated with the information from the new tab.
4612         urlEditText.clearFocus();
4613
4614         // Get the new page number.  The page numbers are 0 indexed, so the new page number will match the current count.
4615         int newTabNumber = tabLayout.getTabCount();
4616
4617         // Add a new tab.
4618         tabLayout.addTab(tabLayout.newTab());
4619
4620         // Get the new tab.
4621         TabLayout.Tab newTab = tabLayout.getTabAt(newTabNumber);
4622
4623         // Remove the lint warning below that the current tab might be null.
4624         assert newTab != null;
4625
4626         // Set a custom view on the new tab.
4627         newTab.setCustomView(R.layout.tab_custom_view);
4628
4629         // Add the new WebView page.
4630         webViewPagerAdapter.addPage(newTabNumber, webViewPager, url, moveToTab);
4631
4632         // Show the app bar if it is at the bottom of the screen and the new tab is taking focus.
4633         if (bottomAppBar && moveToTab && (appBarLayout.getTranslationY() != 0)) {
4634             // Animate the bottom app bar onto the screen.
4635             objectAnimator = ObjectAnimator.ofFloat(appBarLayout, "translationY", 0);
4636
4637             // Make it so.
4638             objectAnimator.start();
4639         }
4640     }
4641
4642     public void closeTab(View view) {
4643         // Run the command according to the number of tabs.
4644         if (tabLayout.getTabCount() > 1) {  // There is more than one tab open.
4645             // Close the current tab.
4646             closeCurrentTab();
4647         } else {  // There is only one tab open.
4648             clearAndExit();
4649         }
4650     }
4651
4652     private void closeCurrentTab() {
4653         // Get the current tab number.
4654         int currentTabNumber = tabLayout.getSelectedTabPosition();
4655
4656         // Delete the current tab.
4657         tabLayout.removeTabAt(currentTabNumber);
4658
4659         // 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,
4660         // meaning that the current WebView must be reset.  Otherwise it will happen automatically as the selected tab number changes.
4661         if (webViewPagerAdapter.deletePage(currentTabNumber, webViewPager)) {
4662             setCurrentWebView(currentTabNumber);
4663         }
4664     }
4665
4666     private void exitFullScreenVideo() {
4667         // Re-enable the screen timeout.
4668         fullScreenVideoFrameLayout.setKeepScreenOn(false);
4669
4670         // Unset the full screen video flag.
4671         displayingFullScreenVideo = false;
4672
4673         // Remove all the views from the full screen video frame layout.
4674         fullScreenVideoFrameLayout.removeAllViews();
4675
4676         // Hide the full screen video frame layout.
4677         fullScreenVideoFrameLayout.setVisibility(View.GONE);
4678
4679         // Enable the sliding drawers.
4680         drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
4681
4682         // Show the coordinator layout.
4683         coordinatorLayout.setVisibility(View.VISIBLE);
4684
4685         // Apply the appropriate full screen mode flags.
4686         if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) {  // Privacy Browser is currently in full screen browsing mode.
4687             // Hide the app bar if specified.
4688             if (hideAppBar) {
4689                 // Hide the tab linear layout.
4690                 tabsLinearLayout.setVisibility(View.GONE);
4691
4692                 // Hide the action bar.
4693                 actionBar.hide();
4694             }
4695
4696             /* Hide the system bars.
4697              * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4698              * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4699              * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4700              * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4701              */
4702             rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
4703                     View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
4704         } else {  // Switch to normal viewing mode.
4705             // Remove the `SYSTEM_UI` flags from the root frame layout.
4706             rootFrameLayout.setSystemUiVisibility(0);
4707         }
4708     }
4709
4710     private void clearAndExit() {
4711         // Get a handle for the shared preferences.
4712         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4713
4714         // Close the bookmarks cursor and database.
4715         bookmarksCursor.close();
4716         bookmarksDatabaseHelper.close();
4717
4718         // Get the status of the clear everything preference.
4719         boolean clearEverything = sharedPreferences.getBoolean("clear_everything", true);
4720
4721         // Get a handle for the runtime.
4722         Runtime runtime = Runtime.getRuntime();
4723
4724         // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
4725         // which links to `/data/data/com.stoutner.privacybrowser.standard`.
4726         String privateDataDirectoryString = getApplicationInfo().dataDir;
4727
4728         // Clear cookies.
4729         if (clearEverything || sharedPreferences.getBoolean("clear_cookies", true)) {
4730             // Request the cookies be deleted.
4731             CookieManager.getInstance().removeAllCookies(null);
4732
4733             // Manually delete the cookies database, as `CookieManager` sometimes will not flush its changes to disk before `System.exit(0)` is run.
4734             try {
4735                 // Two commands must be used because `Runtime.exec()` does not like `*`.
4736                 Process deleteCookiesProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies");
4737                 Process deleteCookiesJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies-journal");
4738
4739                 // Wait until the processes have finished.
4740                 deleteCookiesProcess.waitFor();
4741                 deleteCookiesJournalProcess.waitFor();
4742             } catch (Exception exception) {
4743                 // Do nothing if an error is thrown.
4744             }
4745         }
4746
4747         // Clear DOM storage.
4748         if (clearEverything || sharedPreferences.getBoolean("clear_dom_storage", true)) {
4749             // Ask `WebStorage` to clear the DOM storage.
4750             WebStorage webStorage = WebStorage.getInstance();
4751             webStorage.deleteAllData();
4752
4753             // Manually delete the DOM storage files and directories, as `WebStorage` sometimes will not flush its changes to disk before `System.exit(0)` is run.
4754             try {
4755                 // A `String[]` must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
4756                 Process deleteLocalStorageProcess = runtime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
4757
4758                 // Multiple commands must be used because `Runtime.exec()` does not like `*`.
4759                 Process deleteIndexProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
4760                 Process deleteQuotaManagerProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
4761                 Process deleteQuotaManagerJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
4762                 Process deleteDatabaseProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
4763
4764                 // Wait until the processes have finished.
4765                 deleteLocalStorageProcess.waitFor();
4766                 deleteIndexProcess.waitFor();
4767                 deleteQuotaManagerProcess.waitFor();
4768                 deleteQuotaManagerJournalProcess.waitFor();
4769                 deleteDatabaseProcess.waitFor();
4770             } catch (Exception exception) {
4771                 // Do nothing if an error is thrown.
4772             }
4773         }
4774
4775         // Clear form data if the API < 26.
4776         if ((Build.VERSION.SDK_INT < 26) && (clearEverything || sharedPreferences.getBoolean("clear_form_data", true))) {
4777             WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
4778             webViewDatabase.clearFormData();
4779
4780             // Manually delete the form data database, as `WebViewDatabase` sometimes will not flush its changes to disk before `System.exit(0)` is run.
4781             try {
4782                 // A string array must be used because the database contains a space and `Runtime.exec` will not otherwise escape the string correctly.
4783                 Process deleteWebDataProcess = runtime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data"});
4784                 Process deleteWebDataJournalProcess = runtime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data-journal"});
4785
4786                 // Wait until the processes have finished.
4787                 deleteWebDataProcess.waitFor();
4788                 deleteWebDataJournalProcess.waitFor();
4789             } catch (Exception exception) {
4790                 // Do nothing if an error is thrown.
4791             }
4792         }
4793
4794         // Clear the logcat.
4795         if (clearEverything || sharedPreferences.getBoolean(getString(R.string.clear_logcat_key), true)) {
4796             try {
4797                 // Clear the logcat.  `-c` clears the logcat.  `-b all` clears all the buffers (instead of just crash, main, and system).
4798                 Process process = Runtime.getRuntime().exec("logcat -b all -c");
4799
4800                 // Wait for the process to finish.
4801                 process.waitFor();
4802             } catch (IOException|InterruptedException exception) {
4803                 // Do nothing.
4804             }
4805         }
4806
4807         // Clear the cache.
4808         if (clearEverything || sharedPreferences.getBoolean("clear_cache", true)) {
4809             // Clear the cache from each WebView.
4810             for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
4811                 // Get the WebView tab fragment.
4812                 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
4813
4814                 // Get the WebView fragment view.
4815                 View webViewFragmentView = webViewTabFragment.getView();
4816
4817                 // Only clear the cache if the WebView exists.
4818                 if (webViewFragmentView != null) {
4819                     // Get the nested scroll WebView from the tab fragment.
4820                     NestedScrollWebView nestedScrollWebView = webViewFragmentView.findViewById(R.id.nestedscroll_webview);
4821
4822                     // Clear the cache for this WebView.
4823                     nestedScrollWebView.clearCache(true);
4824                 }
4825             }
4826
4827             // Manually delete the cache directories.
4828             try {
4829                 // Delete the main cache directory.
4830                 Process deleteCacheProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/cache");
4831
4832                 // Delete the secondary `Service Worker` cache directory.
4833                 // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
4834                 Process deleteServiceWorkerProcess = runtime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
4835
4836                 // Wait until the processes have finished.
4837                 deleteCacheProcess.waitFor();
4838                 deleteServiceWorkerProcess.waitFor();
4839             } catch (Exception exception) {
4840                 // Do nothing if an error is thrown.
4841             }
4842         }
4843
4844         // Wipe out each WebView.
4845         for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
4846             // Get the WebView tab fragment.
4847             WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
4848
4849             // Get the WebView frame layout.
4850             FrameLayout webViewFrameLayout = (FrameLayout) webViewTabFragment.getView();
4851
4852             // Only wipe out the WebView if it exists.
4853             if (webViewFrameLayout != null) {
4854                 // Get the nested scroll WebView from the tab fragment.
4855                 NestedScrollWebView nestedScrollWebView = webViewFrameLayout.findViewById(R.id.nestedscroll_webview);
4856
4857                 // Clear SSL certificate preferences for this WebView.
4858                 nestedScrollWebView.clearSslPreferences();
4859
4860                 // Clear the back/forward history for this WebView.
4861                 nestedScrollWebView.clearHistory();
4862
4863                 // Remove all the views from the frame layout.
4864                 webViewFrameLayout.removeAllViews();
4865
4866                 // Destroy the internal state of the WebView.
4867                 nestedScrollWebView.destroy();
4868             }
4869         }
4870
4871         // Clear the custom headers.
4872         customHeaders.clear();
4873
4874         // Manually delete the `app_webview` folder, which contains the cookies, DOM storage, form data, and `Service Worker` cache.
4875         // See `https://code.google.com/p/android/issues/detail?id=233826&thanks=233826&ts=1486670530`.
4876         if (clearEverything) {
4877             try {
4878                 // Delete the folder.
4879                 Process deleteAppWebviewProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview");
4880
4881                 // Wait until the process has finished.
4882                 deleteAppWebviewProcess.waitFor();
4883             } catch (Exception exception) {
4884                 // Do nothing if an error is thrown.
4885             }
4886         }
4887
4888         // Close Privacy Browser.  `finishAndRemoveTask` also removes Privacy Browser from the recent app list.
4889         finishAndRemoveTask();
4890
4891         // Remove the terminated program from RAM.  The status code is `0`.
4892         System.exit(0);
4893     }
4894
4895     public void bookmarksBack(View view) {
4896         if (currentBookmarksFolder.isEmpty()) {  // The home folder is displayed.
4897             // close the bookmarks drawer.
4898             drawerLayout.closeDrawer(GravityCompat.END);
4899         } else {  // A subfolder is displayed.
4900             // Place the former parent folder in `currentFolder`.
4901             currentBookmarksFolder = bookmarksDatabaseHelper.getParentFolderName(currentBookmarksFolder);
4902
4903             // Load the new folder.
4904             loadBookmarksFolder();
4905         }
4906     }
4907
4908     private void setCurrentWebView(int pageNumber) {
4909         // Stop the swipe to refresh indicator if it is running
4910         swipeRefreshLayout.setRefreshing(false);
4911
4912         // Get the WebView tab fragment.
4913         WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(pageNumber);
4914
4915         // Get the fragment view.
4916         View webViewFragmentView = webViewTabFragment.getView();
4917
4918         // Set the current WebView if the fragment view is not null.
4919         if (webViewFragmentView != null) {  // The fragment has been populated.
4920             // Store the current WebView.
4921             currentWebView = webViewFragmentView.findViewById(R.id.nestedscroll_webview);
4922
4923             // Update the status of swipe to refresh.
4924             if (currentWebView.getSwipeToRefresh()) {  // Swipe to refresh is enabled.
4925                 // Enable the swipe refresh layout if the WebView is scrolled all the way to the top.  It is updated every time the scroll changes.
4926                 swipeRefreshLayout.setEnabled(currentWebView.getScrollY() == 0);
4927             } else {  // Swipe to refresh is disabled.
4928                 // Disable the swipe refresh layout.
4929                 swipeRefreshLayout.setEnabled(false);
4930             }
4931
4932             // Get a handle for the cookie manager.
4933             CookieManager cookieManager = CookieManager.getInstance();
4934
4935             // Set the cookie status.
4936             cookieManager.setAcceptCookie(currentWebView.getAcceptCookies());
4937
4938             // Update the privacy icons.  `true` redraws the icons in the app bar.
4939             updatePrivacyIcons(true);
4940
4941             // Get a handle for the input method manager.
4942             InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
4943
4944             // Remove the lint warning below that the input method manager might be null.
4945             assert inputMethodManager != null;
4946
4947             // Get the current URL.
4948             String url = currentWebView.getUrl();
4949
4950             // Update the URL edit text if not loading a new intent.  Otherwise, this will be handled by `onPageStarted()` (if called) and `onPageFinished()`.
4951             if (!loadingNewIntent) {  // A new intent is not being loaded.
4952                 if ((url == null) || url.equals("about:blank")) {  // The WebView is blank.
4953                     // Display the hint in the URL edit text.
4954                     urlEditText.setText("");
4955
4956                     // Request focus for the URL text box.
4957                     urlEditText.requestFocus();
4958
4959                     // Display the keyboard.
4960                     inputMethodManager.showSoftInput(urlEditText, 0);
4961                 } else {  // The WebView has a loaded URL.
4962                     // Clear the focus from the URL text box.
4963                     urlEditText.clearFocus();
4964
4965                     // Hide the soft keyboard.
4966                     inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
4967
4968                     // Display the current URL in the URL text box.
4969                     urlEditText.setText(url);
4970
4971                     // Highlight the URL text.
4972                     highlightUrlText();
4973                 }
4974             } else {  // A new intent is being loaded.
4975                 // Reset the loading new intent tracker.
4976                 loadingNewIntent = false;
4977             }
4978
4979             // Set the background to indicate the domain settings status.
4980             if (currentWebView.getDomainSettingsApplied()) {
4981                 // Set a background on the URL relative layout to indicate that custom domain settings are being used.
4982                 urlRelativeLayout.setBackground(ResourcesCompat.getDrawable(getResources(), R.drawable.domain_settings_url_background, null));
4983             } else {
4984                 // Remove any background on the URL relative layout.
4985                 urlRelativeLayout.setBackground(ResourcesCompat.getDrawable(getResources(), R.color.transparent, null));
4986             }
4987         } else {  // The fragment has not been populated.  Try again in 100 milliseconds.
4988             // Create a handler to set the current WebView.
4989             Handler setCurrentWebViewHandler = new Handler();
4990
4991             // Create a runnable to set the current WebView.
4992             Runnable setCurrentWebWebRunnable = () -> {
4993                 // Set the current WebView.
4994                 setCurrentWebView(pageNumber);
4995             };
4996
4997             // Try setting the current WebView again after 100 milliseconds.
4998             setCurrentWebViewHandler.postDelayed(setCurrentWebWebRunnable, 100);
4999         }
5000     }
5001
5002     @SuppressLint("ClickableViewAccessibility")
5003     @Override
5004     public void initializeWebView(NestedScrollWebView nestedScrollWebView, int pageNumber, ProgressBar progressBar, String url, Boolean restoringState) {
5005         // Get a handle for the shared preferences.
5006         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
5007
5008         // Get the WebView theme.
5009         String webViewTheme = sharedPreferences.getString("webview_theme", getString(R.string.webview_theme_default_value));
5010
5011         // Get the WebView theme entry values string array.
5012         String[] webViewThemeEntryValuesStringArray = getResources().getStringArray(R.array.webview_theme_entry_values);
5013
5014         // Apply the WebView theme if supported by the installed WebView.
5015         if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
5016             // Set the WebView theme.  A switch statement cannot be used because the WebView theme entry values string array is not a compile time constant.
5017             if (webViewTheme.equals(webViewThemeEntryValuesStringArray[1])) {  // The light theme is selected.
5018                 // Turn off the WebView dark mode.
5019                 WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
5020
5021                 // Make the WebView visible. The WebView was created invisible in `webview_framelayout` to prevent a white background splash in night mode.
5022                 // If the system is currently in night mode, showing the WebView will be handled in `onProgressChanged()`.
5023                 nestedScrollWebView.setVisibility(View.VISIBLE);
5024             } else if (webViewTheme.equals(webViewThemeEntryValuesStringArray[2])) {  // The dark theme is selected.
5025                 // Turn on the WebView dark mode.
5026                 WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
5027             } else {  // The system default theme is selected.
5028                 // Get the current system theme status.
5029                 int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
5030
5031                 // Set the WebView theme according to the current system theme status.
5032                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {  // The system is in day mode.
5033                     // Turn off the WebView dark mode.
5034                     WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
5035
5036                     // Make the WebView visible. The WebView was created invisible in `webview_framelayout` to prevent a white background splash in night mode.
5037                     // If the system is currently in night mode, showing the WebView will be handled in `onProgressChanged()`.
5038                     nestedScrollWebView.setVisibility(View.VISIBLE);
5039                 } else {  // The system is in night mode.
5040                     // Turn on the WebView dark mode.
5041                     WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
5042                 }
5043             }
5044         }
5045
5046         // Get a handle for the activity
5047         Activity activity = this;
5048
5049         // Get a handle for the input method manager.
5050         InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
5051
5052         // Instantiate the blocklist helper.
5053         BlocklistHelper blocklistHelper = new BlocklistHelper();
5054
5055         // Remove the lint warning below that the input method manager might be null.
5056         assert inputMethodManager != null;
5057
5058         // Set the app bar scrolling.
5059         nestedScrollWebView.setNestedScrollingEnabled(scrollAppBar);
5060
5061         // Allow pinch to zoom.
5062         nestedScrollWebView.getSettings().setBuiltInZoomControls(true);
5063
5064         // Hide zoom controls.
5065         nestedScrollWebView.getSettings().setDisplayZoomControls(false);
5066
5067         // Don't allow mixed content (HTTP and HTTPS) on the same website.
5068         nestedScrollWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW);
5069
5070         // Set the WebView to load in overview mode (zoomed out to the maximum width).
5071         nestedScrollWebView.getSettings().setLoadWithOverviewMode(true);
5072
5073         // Explicitly disable geolocation.
5074         nestedScrollWebView.getSettings().setGeolocationEnabled(false);
5075
5076         // Allow loading of file:// URLs.  This is necessary for opening MHT web archives, which are copies into a temporary cache location.
5077         nestedScrollWebView.getSettings().setAllowFileAccess(true);
5078
5079         // Create a double-tap gesture detector to toggle full-screen mode.
5080         GestureDetector doubleTapGestureDetector = new GestureDetector(getApplicationContext(), new GestureDetector.SimpleOnGestureListener() {
5081             // Override `onDoubleTap()`.  All other events are handled using the default settings.
5082             @Override
5083             public boolean onDoubleTap(MotionEvent event) {
5084                 if (fullScreenBrowsingModeEnabled) {  // Only process the double-tap if full screen browsing mode is enabled.
5085                     // Toggle the full screen browsing mode tracker.
5086                     inFullScreenBrowsingMode = !inFullScreenBrowsingMode;
5087
5088                     // Toggle the full screen browsing mode.
5089                     if (inFullScreenBrowsingMode) {  // Switch to full screen mode.
5090                         // Hide the app bar if specified.
5091                         if (hideAppBar) {  // The app bar is hidden.
5092                             // Close the find on page bar if it is visible.
5093                             closeFindOnPage(null);
5094
5095                             // Hide the tab linear layout.
5096                             tabsLinearLayout.setVisibility(View.GONE);
5097
5098                             // Hide the action bar.
5099                             actionBar.hide();
5100
5101                             // Set layout and scrolling parameters according to the position of the app bar.
5102                             if (bottomAppBar) {  // The app bar is at the bottom.
5103                                 // Reset the WebView padding to fill the available space.
5104                                 swipeRefreshLayout.setPadding(0, 0, 0, 0);
5105                             } else {  // The app bar is at the top.
5106                                 // Check to see if the app bar is normally scrolled.
5107                                 if (scrollAppBar) {  // The app bar is scrolled when it is displayed.
5108                                     // Get the swipe refresh layout parameters.
5109                                     CoordinatorLayout.LayoutParams swipeRefreshLayoutParams = (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams();
5110
5111                                     // Remove the off-screen scrolling layout.
5112                                     swipeRefreshLayoutParams.setBehavior(null);
5113                                 } else {  // The app bar is not scrolled when it is displayed.
5114                                     // Remove the padding from the top of the swipe refresh layout.
5115                                     swipeRefreshLayout.setPadding(0, 0, 0, 0);
5116
5117                                     // The swipe refresh circle must be moved above the now removed status bar location.
5118                                     swipeRefreshLayout.setProgressViewOffset(false, -200, defaultProgressViewEndOffset);
5119                                 }
5120                             }
5121                         } else {  // The app bar is not hidden.
5122                             // Adjust the UI for the bottom app bar.
5123                             if (bottomAppBar) {
5124                                 // Adjust the UI according to the scrolling of the app bar.
5125                                 if (scrollAppBar) {
5126                                     // Reset the WebView padding to fill the available space.
5127                                     swipeRefreshLayout.setPadding(0, 0, 0, 0);
5128                                 } else {
5129                                     // Move the WebView above the app bar layout.
5130                                     swipeRefreshLayout.setPadding(0, 0, 0, appBarHeight);
5131                                 }
5132                             }
5133                         }
5134
5135                         /* Hide the system bars.
5136                          * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
5137                          * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
5138                          * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
5139                          * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
5140                          */
5141                         rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
5142                                 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
5143                     } else {  // Switch to normal viewing mode.
5144                         // Show the app bar if it was hidden.
5145                         if (hideAppBar) {
5146                             // Show the tab linear layout.
5147                             tabsLinearLayout.setVisibility(View.VISIBLE);
5148
5149                             // Show the action bar.
5150                             actionBar.show();
5151                         }
5152
5153                         // Set layout and scrolling parameters according to the position of the app bar.
5154                         if (bottomAppBar) {  // The app bar is at the bottom.
5155                             // Adjust the UI.
5156                             if (scrollAppBar) {
5157                                 // Reset the WebView padding to fill the available space.
5158                                 swipeRefreshLayout.setPadding(0, 0, 0, 0);
5159                             } else {
5160                                 // Move the WebView above the app bar layout.
5161                                 swipeRefreshLayout.setPadding(0, 0, 0, appBarHeight);
5162                             }
5163                         } else {  // The app bar is at the top.
5164                             // Check to see if the app bar is normally scrolled.
5165                             if (scrollAppBar) {  // The app bar is scrolled when it is displayed.
5166                                 // Get the swipe refresh layout parameters.
5167                                 CoordinatorLayout.LayoutParams swipeRefreshLayoutParams = (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams();
5168
5169                                 // Add the off-screen scrolling layout.
5170                                 swipeRefreshLayoutParams.setBehavior(new AppBarLayout.ScrollingViewBehavior());
5171                             } else {  // The app bar is not scrolled when it is displayed.
5172                                 // The swipe refresh layout must be manually moved below the app bar layout.
5173                                 swipeRefreshLayout.setPadding(0, appBarHeight, 0, 0);
5174
5175                                 // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
5176                                 swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10 + appBarHeight, defaultProgressViewEndOffset + appBarHeight);
5177                             }
5178                         }
5179
5180                         // Remove the `SYSTEM_UI` flags from the root frame layout.
5181                         rootFrameLayout.setSystemUiVisibility(0);
5182                     }
5183
5184                     // Consume the double-tap.
5185                     return true;
5186                 } else { // Do not consume the double-tap because full screen browsing mode is disabled.
5187                     return false;
5188                 }
5189             }
5190
5191             @Override
5192             public boolean onFling(MotionEvent motionEvent1, MotionEvent motionEvent2, float velocityX, float velocityY) {
5193                 // Scroll the bottom app bar if enabled.
5194                 if (bottomAppBar && scrollAppBar && !objectAnimator.isRunning()) {
5195                     // Calculate the Y change.
5196                     float motionY = motionEvent2.getY() - motionEvent1.getY();
5197
5198                     // Scroll the app bar if the change is greater than 100 pixels.
5199                     if (motionY > 50) {
5200                         // Animate the bottom app bar onto the screen.
5201                         objectAnimator = ObjectAnimator.ofFloat(appBarLayout, "translationY", 0);
5202                     } else if (motionY < -50) {
5203                         // Animate the bottom app bar off the screen.
5204                         objectAnimator = ObjectAnimator.ofFloat(appBarLayout, "translationY", appBarLayout.getHeight());
5205                     }
5206
5207                     // Make it so.
5208                     objectAnimator.start();
5209                 }
5210
5211                 // Do not consume the event.
5212                 return false;
5213             }
5214         });
5215
5216         // Pass all touch events on the WebView through the double-tap gesture detector.
5217         nestedScrollWebView.setOnTouchListener((View view, MotionEvent event) -> {
5218             // Call `performClick()` on the view, which is required for accessibility.
5219             view.performClick();
5220
5221             // Send the event to the gesture detector.
5222             return doubleTapGestureDetector.onTouchEvent(event);
5223         });
5224
5225         // Register the WebView for a context menu.  This is used to see link targets and download images.
5226         registerForContextMenu(nestedScrollWebView);
5227
5228         // Allow the downloading of files.
5229         nestedScrollWebView.setDownloadListener((String downloadUrl, String userAgent, String contentDisposition, String mimetype, long contentLength) -> {
5230             // Check the download preference.
5231             if (downloadWithExternalApp) {  // Download with an external app.
5232                 downloadUrlWithExternalApp(downloadUrl);
5233             } else {  // Handle the download inside of Privacy Browser.
5234                 // Define a formatted file size string.
5235                 String formattedFileSizeString;
5236
5237                 // Process the content length if it contains data.
5238                 if (contentLength > 0) {  // The content length is greater than 0.
5239                     // Format the content length as a string.
5240                     formattedFileSizeString = NumberFormat.getInstance().format(contentLength) + " " + getString(R.string.bytes);
5241                 } else {  // The content length is not greater than 0.
5242                     // Set the formatted file size string to be `unknown size`.
5243                     formattedFileSizeString = getString(R.string.unknown_size);
5244                 }
5245
5246                 // Get the file name from the content disposition.
5247                 String fileNameString = PrepareSaveDialog.getFileNameFromHeaders(this, contentDisposition, mimetype, downloadUrl);
5248
5249                 // Instantiate the save dialog.
5250                 DialogFragment saveDialogFragment = SaveDialog.saveUrl(downloadUrl, formattedFileSizeString, fileNameString, userAgent,
5251                         nestedScrollWebView.getAcceptCookies());
5252
5253                 // 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.
5254                 try {
5255                     // Show the save dialog.  It must be named `save_dialog` so that the file picker can update the file name.
5256                     saveDialogFragment.show(getSupportFragmentManager(), getString(R.string.save_dialog));
5257                 } catch (Exception exception) {  // The dialog could not be shown.
5258                     // Add the dialog to the pending dialog array list.  It will be displayed in `onStart()`.
5259                     pendingDialogsArrayList.add(new PendingDialog(saveDialogFragment, getString(R.string.save_dialog)));
5260                 }
5261             }
5262         });
5263
5264         // Update the find on page count.
5265         nestedScrollWebView.setFindListener(new WebView.FindListener() {
5266             // Get a handle for `findOnPageCountTextView`.
5267             final TextView findOnPageCountTextView = findViewById(R.id.find_on_page_count_textview);
5268
5269             @Override
5270             public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) {
5271                 if ((isDoneCounting) && (numberOfMatches == 0)) {  // There are no matches.
5272                     // Set `findOnPageCountTextView` to `0/0`.
5273                     findOnPageCountTextView.setText(R.string.zero_of_zero);
5274                 } else if (isDoneCounting) {  // There are matches.
5275                     // `activeMatchOrdinal` is zero-based.
5276                     int activeMatch = activeMatchOrdinal + 1;
5277
5278                     // Build the match string.
5279                     String matchString = activeMatch + "/" + numberOfMatches;
5280
5281                     // Set `findOnPageCountTextView`.
5282                     findOnPageCountTextView.setText(matchString);
5283                 }
5284             }
5285         });
5286
5287         // Process scroll changes.
5288         nestedScrollWebView.setOnScrollChangeListener((view, scrollX, scrollY, oldScrollX, oldScrollY) -> {
5289             // Set the swipe to refresh status.
5290             if (nestedScrollWebView.getSwipeToRefresh()) {
5291                 // Only enable swipe to refresh if the WebView is scrolled to the top.
5292                 swipeRefreshLayout.setEnabled(nestedScrollWebView.getScrollY() == 0);
5293             } else {
5294                 // Disable swipe to refresh.
5295                 swipeRefreshLayout.setEnabled(false);
5296             }
5297
5298             // Reinforce the system UI visibility flags if in full screen browsing mode.
5299             // This hides the status and navigation bars, which are displayed if other elements are shown, like dialog boxes, the options menu, or the keyboard.
5300             if (inFullScreenBrowsingMode) {
5301                 /* Hide the system bars.
5302                  * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
5303                  * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
5304                  * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
5305                  * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
5306                  */
5307                 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
5308                         View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
5309             }
5310         });
5311
5312         // Set the web chrome client.
5313         nestedScrollWebView.setWebChromeClient(new WebChromeClient() {
5314             // Update the progress bar when a page is loading.
5315             @Override
5316             public void onProgressChanged(WebView view, int progress) {
5317                 // Update the progress bar.
5318                 progressBar.setProgress(progress);
5319
5320                 // Set the visibility of the progress bar.
5321                 if (progress < 100) {
5322                     // Show the progress bar.
5323                     progressBar.setVisibility(View.VISIBLE);
5324                 } else {
5325                     // Hide the progress bar.
5326                     progressBar.setVisibility(View.GONE);
5327
5328                     //Stop the swipe to refresh indicator if it is running
5329                     swipeRefreshLayout.setRefreshing(false);
5330
5331                     // 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.
5332                     nestedScrollWebView.setVisibility(View.VISIBLE);
5333                 }
5334             }
5335
5336             // Set the favorite icon when it changes.
5337             @Override
5338             public void onReceivedIcon(WebView view, Bitmap icon) {
5339                 // Only update the favorite icon if the website has finished loading.
5340                 if (progressBar.getVisibility() == View.GONE) {
5341                     // Store the new favorite icon.
5342                     nestedScrollWebView.setFavoriteOrDefaultIcon(icon);
5343
5344                     // Get the current page position.
5345                     int currentPosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
5346
5347                     // Get the current tab.
5348                     TabLayout.Tab tab = tabLayout.getTabAt(currentPosition);
5349
5350                     // Check to see if the tab has been populated.
5351                     if (tab != null) {
5352                         // Get the custom view from the tab.
5353                         View tabView = tab.getCustomView();
5354
5355                         // Check to see if the custom tab view has been populated.
5356                         if (tabView != null) {
5357                             // Get the favorite icon image view from the tab.
5358                             ImageView tabFavoriteIconImageView = tabView.findViewById(R.id.favorite_icon_imageview);
5359
5360                             // Display the favorite icon in the tab.
5361                             tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(icon, 64, 64, true));
5362                         }
5363                     }
5364                 }
5365             }
5366
5367             // Save a copy of the title when it changes.
5368             @Override
5369             public void onReceivedTitle(WebView view, String title) {
5370                 // Get the current page position.
5371                 int currentPosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
5372
5373                 // Get the current tab.
5374                 TabLayout.Tab tab = tabLayout.getTabAt(currentPosition);
5375
5376                 // Only populate the title text view if the tab has been fully created.
5377                 if (tab != null) {
5378                     // Get the custom view from the tab.
5379                     View tabView = tab.getCustomView();
5380
5381                     // Only populate the title text view if the tab view has been fully populated.
5382                     if (tabView != null) {
5383                         // Get the title text view from the tab.
5384                         TextView tabTitleTextView = tabView.findViewById(R.id.title_textview);
5385
5386                         // Set the title according to the URL.
5387                         if (title.equals("about:blank")) {
5388                             // Set the title to indicate a new tab.
5389                             tabTitleTextView.setText(R.string.new_tab);
5390                         } else {
5391                             // Set the title as the tab text.
5392                             tabTitleTextView.setText(title);
5393                         }
5394                     }
5395                 }
5396             }
5397
5398             // Enter full screen video.
5399             @Override
5400             public void onShowCustomView(View video, CustomViewCallback callback) {
5401                 // Set the full screen video flag.
5402                 displayingFullScreenVideo = true;
5403
5404                 // Hide the keyboard.
5405                 inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0);
5406
5407                 // Hide the coordinator layout.
5408                 coordinatorLayout.setVisibility(View.GONE);
5409
5410                 /* Hide the system bars.
5411                  * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
5412                  * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
5413                  * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
5414                  * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
5415                  */
5416                 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
5417                         View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
5418
5419                 // Disable the sliding drawers.
5420                 drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
5421
5422                 // Add the video view to the full screen video frame layout.
5423                 fullScreenVideoFrameLayout.addView(video);
5424
5425                 // Show the full screen video frame layout.
5426                 fullScreenVideoFrameLayout.setVisibility(View.VISIBLE);
5427
5428                 // Disable the screen timeout while the video is playing.  YouTube does this automatically, but not all other videos do.
5429                 fullScreenVideoFrameLayout.setKeepScreenOn(true);
5430             }
5431
5432             // Exit full screen video.
5433             @Override
5434             public void onHideCustomView() {
5435                 // Exit the full screen video.
5436                 exitFullScreenVideo();
5437             }
5438
5439             // Upload files.
5440             @Override
5441             public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
5442                 // Store the file path callback.
5443                 fileChooserCallback = filePathCallback;
5444
5445                 // Create an intent to open a chooser based on the file chooser parameters.
5446                 Intent fileChooserIntent = fileChooserParams.createIntent();
5447
5448                 // Get a handle for the package manager.
5449                 PackageManager packageManager = getPackageManager();
5450
5451                 // Check to see if the file chooser intent resolves to an installed package.
5452                 if (fileChooserIntent.resolveActivity(packageManager) != null) {  // The file chooser intent is fine.
5453                     // Start the file chooser intent.
5454                     startActivityForResult(fileChooserIntent, BROWSE_FILE_UPLOAD_REQUEST_CODE);
5455                 } else {  // The file chooser intent will cause a crash.
5456                     // Create a generic intent to open a chooser.
5457                     Intent genericFileChooserIntent = new Intent(Intent.ACTION_GET_CONTENT);
5458
5459                     // Request an openable file.
5460                     genericFileChooserIntent.addCategory(Intent.CATEGORY_OPENABLE);
5461
5462                     // Set the file type to everything.
5463                     genericFileChooserIntent.setType("*/*");
5464
5465                     // Start the generic file chooser intent.
5466                     startActivityForResult(genericFileChooserIntent, BROWSE_FILE_UPLOAD_REQUEST_CODE);
5467                 }
5468                 return true;
5469             }
5470         });
5471
5472         nestedScrollWebView.setWebViewClient(new WebViewClient() {
5473             // `shouldOverrideUrlLoading` makes this WebView the default handler for URLs inside the app, so that links are not kicked out to other apps.
5474             // The deprecated `shouldOverrideUrlLoading` must be used until API >= 24.
5475             @Override
5476             public boolean shouldOverrideUrlLoading(WebView view, String url) {
5477                 // Sanitize the url.
5478                 url = sanitizeUrl(url);
5479
5480                 // Handle the URL according to the type.
5481                 if (url.startsWith("http")) {  // Load the URL in Privacy Browser.
5482                     // Load the URL.  By using `loadUrl()`, instead of `loadUrlFromBase()`, the Referer header will never be sent.
5483                     loadUrl(nestedScrollWebView, url);
5484
5485                     // Returning true indicates that Privacy Browser is manually handling the loading of the URL.
5486                     // Custom headers cannot be added if false is returned and the WebView handles the loading of the URL.
5487                     return true;
5488                 } else if (url.startsWith("mailto:")) {  // Load the email address in an external email program.
5489                     // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
5490                     Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
5491
5492                     // Parse the url and set it as the data for the intent.
5493                     emailIntent.setData(Uri.parse(url));
5494
5495                     // Open the email program in a new task instead of as part of Privacy Browser.
5496                     emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
5497
5498                     try {
5499                         // Make it so.
5500                         startActivity(emailIntent);
5501                     } catch (ActivityNotFoundException exception) {
5502                         // Display a snackbar.
5503                         Snackbar.make(currentWebView, getString(R.string.error) + "  " + exception, Snackbar.LENGTH_INDEFINITE).show();
5504                     }
5505
5506
5507                     // Returning true indicates Privacy Browser is handling the URL by creating an intent.
5508                     return true;
5509                 } else if (url.startsWith("tel:")) {  // Load the phone number in the dialer.
5510                     // Open the dialer and load the phone number, but wait for the user to place the call.
5511                     Intent dialIntent = new Intent(Intent.ACTION_DIAL);
5512
5513                     // Add the phone number to the intent.
5514                     dialIntent.setData(Uri.parse(url));
5515
5516                     // Open the dialer in a new task instead of as part of Privacy Browser.
5517                     dialIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
5518
5519                     try {
5520                         // Make it so.
5521                         startActivity(dialIntent);
5522                     } catch (ActivityNotFoundException exception) {
5523                         // Display a snackbar.
5524                         Snackbar.make(currentWebView, getString(R.string.error) + "  " + exception, Snackbar.LENGTH_INDEFINITE).show();
5525                     }
5526
5527                     // Returning true indicates Privacy Browser is handling the URL by creating an intent.
5528                     return true;
5529                 } else {  // Load a system chooser to select an app that can handle the URL.
5530                     // Open an app that can handle the URL.
5531                     Intent genericIntent = new Intent(Intent.ACTION_VIEW);
5532
5533                     // Add the URL to the intent.
5534                     genericIntent.setData(Uri.parse(url));
5535
5536                     // List all apps that can handle the URL instead of just opening the first one.
5537                     genericIntent.addCategory(Intent.CATEGORY_BROWSABLE);
5538
5539                     // Open the app in a new task instead of as part of Privacy Browser.
5540                     genericIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
5541
5542                     // Start the app or display a snackbar if no app is available to handle the URL.
5543                     try {
5544                         startActivity(genericIntent);
5545                     } catch (ActivityNotFoundException exception) {
5546                         Snackbar.make(nestedScrollWebView, getString(R.string.unrecognized_url) + "  " + url, Snackbar.LENGTH_SHORT).show();
5547                     }
5548
5549                     // Returning true indicates Privacy Browser is handling the URL by creating an intent.
5550                     return true;
5551                 }
5552             }
5553
5554             // Check requests against the block lists.  The deprecated `shouldInterceptRequest()` must be used until minimum API >= 21.
5555             @Override
5556             public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest webResourceRequest) {
5557                 // Get the URL.
5558                 String url = webResourceRequest.getUrl().toString();
5559
5560                 // Check to see if the resource request is for the main URL.
5561                 if (url.equals(nestedScrollWebView.getCurrentUrl())) {
5562                     // `return null` loads the resource request, which should never be blocked if it is the main URL.
5563                     return null;
5564                 }
5565
5566                 // 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.
5567                 while (ultraPrivacy == null) {
5568                     // The wait must be synchronized, which only lets one thread run on it at a time, or `java.lang.IllegalMonitorStateException` is thrown.
5569                     synchronized (this) {
5570                         try {
5571                             // Check to see if the blocklists have been populated after 100 ms.
5572                             wait(100);
5573                         } catch (InterruptedException exception) {
5574                             // Do nothing.
5575                         }
5576                     }
5577                 }
5578
5579                 // Create an empty web resource response to be used if the resource request is blocked.
5580                 WebResourceResponse emptyWebResourceResponse = new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes()));
5581
5582                 // Reset the whitelist results tracker.
5583                 String[] whitelistResultStringArray = null;
5584
5585                 // Initialize the third party request tracker.
5586                 boolean isThirdPartyRequest = false;
5587
5588                 // Get the current URL.  `.getUrl()` throws an error because operations on the WebView cannot be made from this thread.
5589                 String currentBaseDomain = nestedScrollWebView.getCurrentDomainName();
5590
5591                 // Store a copy of the current domain for use in later requests.
5592                 String currentDomain = currentBaseDomain;
5593
5594                 // Get the request host name.
5595                 String requestBaseDomain = webResourceRequest.getUrl().getHost();
5596
5597                 // Only check for third-party requests if the current base domain is not empty and the request domain is not null.
5598                 if (!currentBaseDomain.isEmpty() && (requestBaseDomain != null)) {
5599                     // Determine the current base domain.
5600                     while (currentBaseDomain.indexOf(".", currentBaseDomain.indexOf(".") + 1) > 0) {  // There is at least one subdomain.
5601                         // Remove the first subdomain.
5602                         currentBaseDomain = currentBaseDomain.substring(currentBaseDomain.indexOf(".") + 1);
5603                     }
5604
5605                     // Determine the request base domain.
5606                     while (requestBaseDomain.indexOf(".", requestBaseDomain.indexOf(".") + 1) > 0) {  // There is at least one subdomain.
5607                         // Remove the first subdomain.
5608                         requestBaseDomain = requestBaseDomain.substring(requestBaseDomain.indexOf(".") + 1);
5609                     }
5610
5611                     // Update the third party request tracker.
5612                     isThirdPartyRequest = !currentBaseDomain.equals(requestBaseDomain);
5613                 }
5614
5615                 // Get the current WebView page position.
5616                 int webViewPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
5617
5618                 // Determine if the WebView is currently displayed.
5619                 boolean webViewDisplayed = (webViewPagePosition == tabLayout.getSelectedTabPosition());
5620
5621                 // Block third-party requests if enabled.
5622                 if (isThirdPartyRequest && nestedScrollWebView.getBlockAllThirdPartyRequests()) {
5623                     // Add the result to the resource requests.
5624                     nestedScrollWebView.addResourceRequest(new String[]{BlocklistHelper.REQUEST_THIRD_PARTY, url});
5625
5626                     // Increment the blocked requests counters.
5627                     nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5628                     nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS);
5629
5630                     // Update the titles of the blocklist menu items if the WebView is currently displayed.
5631                     if (webViewDisplayed) {
5632                         // Updating the UI must be run from the UI thread.
5633                         activity.runOnUiThread(() -> {
5634                             // Update the menu item titles.
5635                             navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5636
5637                             // Update the options menu if it has been populated.
5638                             if (optionsMenu != null) {
5639                                 optionsBlocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5640                                 optionsBlockAllThirdPartyRequestsMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " +
5641                                         getString(R.string.block_all_third_party_requests));
5642                             }
5643                         });
5644                     }
5645
5646                     // Return an empty web resource response.
5647                     return emptyWebResourceResponse;
5648                 }
5649
5650                 // Check UltraList if it is enabled.
5651                 if (nestedScrollWebView.getUltraListEnabled()) {
5652                     // Check the URL against UltraList.
5653                     String[] ultraListResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, ultraList);
5654
5655                     // Process the UltraList results.
5656                     if (ultraListResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) {  // The resource request matched UltraList's blacklist.
5657                         // Add the result to the resource requests.
5658                         nestedScrollWebView.addResourceRequest(new String[] {ultraListResults[0], ultraListResults[1], ultraListResults[2], ultraListResults[3], ultraListResults[4], ultraListResults[5]});
5659
5660                         // Increment the blocked requests counters.
5661                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5662                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.ULTRALIST);
5663
5664                         // Update the titles of the blocklist menu items if the WebView is currently displayed.
5665                         if (webViewDisplayed) {
5666                             // Updating the UI must be run from the UI thread.
5667                             activity.runOnUiThread(() -> {
5668                                 // Update the menu item titles.
5669                                 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5670
5671                                 // Update the options menu if it has been populated.
5672                                 if (optionsMenu != null) {
5673                                     optionsBlocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5674                                     optionsUltraListMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.ULTRALIST) + " - " + getString(R.string.ultralist));
5675                                 }
5676                             });
5677                         }
5678
5679                         // The resource request was blocked.  Return an empty web resource response.
5680                         return emptyWebResourceResponse;
5681                     } else if (ultraListResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) {  // The resource request matched UltraList's whitelist.
5682                         // Add a whitelist entry to the resource requests array.
5683                         nestedScrollWebView.addResourceRequest(new String[] {ultraListResults[0], ultraListResults[1], ultraListResults[2], ultraListResults[3], ultraListResults[4], ultraListResults[5]});
5684
5685                         // The resource request has been allowed by UltraPrivacy.  `return null` loads the requested resource.
5686                         return null;
5687                     }
5688                 }
5689
5690                 // Check UltraPrivacy if it is enabled.
5691                 if (nestedScrollWebView.getUltraPrivacyEnabled()) {
5692                     // Check the URL against UltraPrivacy.
5693                     String[] ultraPrivacyResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, ultraPrivacy);
5694
5695                     // Process the UltraPrivacy results.
5696                     if (ultraPrivacyResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) {  // The resource request matched UltraPrivacy's blacklist.
5697                         // Add the result to the resource requests.
5698                         nestedScrollWebView.addResourceRequest(new String[] {ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4],
5699                                 ultraPrivacyResults[5]});
5700
5701                         // Increment the blocked requests counters.
5702                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5703                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.ULTRAPRIVACY);
5704
5705                         // Update the titles of the blocklist menu items if the WebView is currently displayed.
5706                         if (webViewDisplayed) {
5707                             // Updating the UI must be run from the UI thread.
5708                             activity.runOnUiThread(() -> {
5709                                 // Update the menu item titles.
5710                                 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5711
5712                                 // Update the options menu if it has been populated.
5713                                 if (optionsMenu != null) {
5714                                     optionsBlocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5715                                     optionsUltraPrivacyMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.ULTRAPRIVACY) + " - " + getString(R.string.ultraprivacy));
5716                                 }
5717                             });
5718                         }
5719
5720                         // The resource request was blocked.  Return an empty web resource response.
5721                         return emptyWebResourceResponse;
5722                     } else if (ultraPrivacyResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) {  // The resource request matched UltraPrivacy's whitelist.
5723                         // Add a whitelist entry to the resource requests array.
5724                         nestedScrollWebView.addResourceRequest(new String[] {ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4],
5725                                 ultraPrivacyResults[5]});
5726
5727                         // The resource request has been allowed by UltraPrivacy.  `return null` loads the requested resource.
5728                         return null;
5729                     }
5730                 }
5731
5732                 // Check EasyList if it is enabled.
5733                 if (nestedScrollWebView.getEasyListEnabled()) {
5734                     // Check the URL against EasyList.
5735                     String[] easyListResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, easyList);
5736
5737                     // Process the EasyList results.
5738                     if (easyListResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) {  // The resource request matched EasyList's blacklist.
5739                         // Add the result to the resource requests.
5740                         nestedScrollWebView.addResourceRequest(new String[] {easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5]});
5741
5742                         // Increment the blocked requests counters.
5743                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5744                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.EASYLIST);
5745
5746                         // Update the titles of the blocklist menu items if the WebView is currently displayed.
5747                         if (webViewDisplayed) {
5748                             // Updating the UI must be run from the UI thread.
5749                             activity.runOnUiThread(() -> {
5750                                 // Update the menu item titles.
5751                                 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5752
5753                                 // Update the options menu if it has been populated.
5754                                 if (optionsMenu != null) {
5755                                     optionsBlocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5756                                     optionsEasyListMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.EASYLIST) + " - " + getString(R.string.easylist));
5757                                 }
5758                             });
5759                         }
5760
5761                         // The resource request was blocked.  Return an empty web resource response.
5762                         return emptyWebResourceResponse;
5763                     } else if (easyListResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) {  // The resource request matched EasyList's whitelist.
5764                         // Update the whitelist result string array tracker.
5765                         whitelistResultStringArray = new String[] {easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5]};
5766                     }
5767                 }
5768
5769                 // Check EasyPrivacy if it is enabled.
5770                 if (nestedScrollWebView.getEasyPrivacyEnabled()) {
5771                     // Check the URL against EasyPrivacy.
5772                     String[] easyPrivacyResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, easyPrivacy);
5773
5774                     // Process the EasyPrivacy results.
5775                     if (easyPrivacyResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) {  // The resource request matched EasyPrivacy's blacklist.
5776                         // Add the result to the resource requests.
5777                         nestedScrollWebView.addResourceRequest(new String[] {easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[4],
5778                                 easyPrivacyResults[5]});
5779
5780                         // Increment the blocked requests counters.
5781                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5782                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.EASYPRIVACY);
5783
5784                         // Update the titles of the blocklist menu items if the WebView is currently displayed.
5785                         if (webViewDisplayed) {
5786                             // Updating the UI must be run from the UI thread.
5787                             activity.runOnUiThread(() -> {
5788                                 // Update the menu item titles.
5789                                 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5790
5791                                 // Update the options menu if it has been populated.
5792                                 if (optionsMenu != null) {
5793                                     optionsBlocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5794                                     optionsEasyPrivacyMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.EASYPRIVACY) + " - " + getString(R.string.easyprivacy));
5795                                 }
5796                             });
5797                         }
5798
5799                         // The resource request was blocked.  Return an empty web resource response.
5800                         return emptyWebResourceResponse;
5801                     } else if (easyPrivacyResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) {  // The resource request matched EasyPrivacy's whitelist.
5802                         // Update the whitelist result string array tracker.
5803                         whitelistResultStringArray = new String[] {easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[4], easyPrivacyResults[5]};
5804                     }
5805                 }
5806
5807                 // Check Fanboy’s Annoyance List if it is enabled.
5808                 if (nestedScrollWebView.getFanboysAnnoyanceListEnabled()) {
5809                     // Check the URL against Fanboy's Annoyance List.
5810                     String[] fanboysAnnoyanceListResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, fanboysAnnoyanceList);
5811
5812                     // Process the Fanboy's Annoyance List results.
5813                     if (fanboysAnnoyanceListResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) {  // The resource request matched Fanboy's Annoyance List's blacklist.
5814                         // Add the result to the resource requests.
5815                         nestedScrollWebView.addResourceRequest(new String[] {fanboysAnnoyanceListResults[0], fanboysAnnoyanceListResults[1], fanboysAnnoyanceListResults[2], fanboysAnnoyanceListResults[3],
5816                                 fanboysAnnoyanceListResults[4], fanboysAnnoyanceListResults[5]});
5817
5818                         // Increment the blocked requests counters.
5819                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5820                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST);
5821
5822                         // Update the titles of the blocklist menu items if the WebView is currently displayed.
5823                         if (webViewDisplayed) {
5824                             // Updating the UI must be run from the UI thread.
5825                             activity.runOnUiThread(() -> {
5826                                 // Update the menu item titles.
5827                                 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5828
5829                                 // Update the options menu if it has been populated.
5830                                 if (optionsMenu != null) {
5831                                     optionsBlocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5832                                     optionsFanboysAnnoyanceListMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST) + " - " +
5833                                             getString(R.string.fanboys_annoyance_list));
5834                                 }
5835                             });
5836                         }
5837
5838                         // The resource request was blocked.  Return an empty web resource response.
5839                         return emptyWebResourceResponse;
5840                     } else if (fanboysAnnoyanceListResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)){  // The resource request matched Fanboy's Annoyance List's whitelist.
5841                         // Update the whitelist result string array tracker.
5842                         whitelistResultStringArray = new String[] {fanboysAnnoyanceListResults[0], fanboysAnnoyanceListResults[1], fanboysAnnoyanceListResults[2], fanboysAnnoyanceListResults[3],
5843                                 fanboysAnnoyanceListResults[4], fanboysAnnoyanceListResults[5]};
5844                     }
5845                 } else if (nestedScrollWebView.getFanboysSocialBlockingListEnabled()) {  // Only check Fanboy’s Social Blocking List if Fanboy’s Annoyance List is disabled.
5846                     // Check the URL against Fanboy's Annoyance List.
5847                     String[] fanboysSocialListResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, fanboysSocialList);
5848
5849                     // Process the Fanboy's Social Blocking List results.
5850                     if (fanboysSocialListResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) {  // The resource request matched Fanboy's Social Blocking List's blacklist.
5851                         // Add the result to the resource requests.
5852                         nestedScrollWebView.addResourceRequest(new String[] {fanboysSocialListResults[0], fanboysSocialListResults[1], fanboysSocialListResults[2], fanboysSocialListResults[3],
5853                                 fanboysSocialListResults[4], fanboysSocialListResults[5]});
5854
5855                         // Increment the blocked requests counters.
5856                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5857                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST);
5858
5859                         // Update the titles of the blocklist menu items if the WebView is currently displayed.
5860                         if (webViewDisplayed) {
5861                             // Updating the UI must be run from the UI thread.
5862                             activity.runOnUiThread(() -> {
5863                                 // Update the menu item titles.
5864                                 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5865
5866                                 // Update the options menu if it has been populated.
5867                                 if (optionsMenu != null) {
5868                                     optionsBlocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5869                                     optionsFanboysSocialBlockingListMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST) + " - " +
5870                                             getString(R.string.fanboys_social_blocking_list));
5871                                 }
5872                             });
5873                         }
5874
5875                         // The resource request was blocked.  Return an empty web resource response.
5876                         return emptyWebResourceResponse;
5877                     } else if (fanboysSocialListResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) {  // The resource request matched Fanboy's Social Blocking List's whitelist.
5878                         // Update the whitelist result string array tracker.
5879                         whitelistResultStringArray = new String[] {fanboysSocialListResults[0], fanboysSocialListResults[1], fanboysSocialListResults[2], fanboysSocialListResults[3],
5880                                 fanboysSocialListResults[4], fanboysSocialListResults[5]};
5881                     }
5882                 }
5883
5884                 // Add the request to the log because it hasn't been processed by any of the previous checks.
5885                 if (whitelistResultStringArray != null) {  // The request was processed by a whitelist.
5886                     nestedScrollWebView.addResourceRequest(whitelistResultStringArray);
5887                 } else {  // The request didn't match any blocklist entry.  Log it as a default request.
5888                     nestedScrollWebView.addResourceRequest(new String[]{BlocklistHelper.REQUEST_DEFAULT, url});
5889                 }
5890
5891                 // The resource request has not been blocked.  `return null` loads the requested resource.
5892                 return null;
5893             }
5894
5895             // Handle HTTP authentication requests.
5896             @Override
5897             public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {
5898                 // Store the handler.
5899                 nestedScrollWebView.setHttpAuthHandler(handler);
5900
5901                 // Instantiate an HTTP authentication dialog.
5902                 DialogFragment httpAuthenticationDialogFragment = HttpAuthenticationDialog.displayDialog(host, realm, nestedScrollWebView.getWebViewFragmentId());
5903
5904                 // Show the HTTP authentication dialog.
5905                 httpAuthenticationDialogFragment.show(getSupportFragmentManager(), getString(R.string.http_authentication));
5906             }
5907
5908             @Override
5909             public void onPageStarted(WebView view, String url, Bitmap favicon) {
5910                 // Get the app bar layout height.  This can't be done in `applyAppSettings()` because the app bar is not yet populated there.
5911                 // 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.
5912                 if (appBarLayout.getHeight() > 0) appBarHeight = appBarLayout.getHeight();
5913
5914                 // Set the padding and layout settings according to the position of the app bar.
5915                 if (bottomAppBar) {  // The app bar is on the bottom.
5916                     // Adjust the UI.
5917                     if (scrollAppBar || (inFullScreenBrowsingMode && hideAppBar)) {  // The app bar scrolls or full screen browsing mode is engaged with the app bar hidden.
5918                         // Reset the WebView padding to fill the available space.
5919                         swipeRefreshLayout.setPadding(0, 0, 0, 0);
5920                     } else {  // The app bar doesn't scroll or full screen browsing mode is not engaged with the app bar hidden.
5921                         // Move the WebView above the app bar layout.
5922                         swipeRefreshLayout.setPadding(0, 0, 0, appBarHeight);
5923                     }
5924                 } else {  // The app bar is on the top.
5925                     // 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.
5926                     if (scrollAppBar || (inFullScreenBrowsingMode && hideAppBar)) {
5927                         // No padding is needed because it will automatically be placed below the app bar layout due to the scrolling layout behavior.
5928                         swipeRefreshLayout.setPadding(0, 0, 0, 0);
5929
5930                         // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
5931                         swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10, defaultProgressViewEndOffset);
5932                     } else {
5933                         // The swipe refresh layout must be manually moved below the app bar layout.
5934                         swipeRefreshLayout.setPadding(0, appBarHeight, 0, 0);
5935
5936                         // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
5937                         swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10 + appBarHeight, defaultProgressViewEndOffset + appBarHeight);
5938                     }
5939                 }
5940
5941                 // Reset the list of resource requests.
5942                 nestedScrollWebView.clearResourceRequests();
5943
5944                 // Reset the requests counters.
5945                 nestedScrollWebView.resetRequestsCounters();
5946
5947                 // Get the current page position.
5948                 int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
5949
5950                 // Update the URL text bar if the page is currently selected and the URL edit text is not currently being edited.
5951                 if ((tabLayout.getSelectedTabPosition() == currentPagePosition) && !urlEditText.hasFocus()) {
5952                     // Display the formatted URL text.
5953                     urlEditText.setText(url);
5954
5955                     // Apply text highlighting to the URL text box.
5956                     highlightUrlText();
5957
5958                     // Hide the keyboard.
5959                     inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0);
5960                 }
5961
5962                 // Reset the list of host IP addresses.
5963                 nestedScrollWebView.setCurrentIpAddresses("");
5964
5965                 // Get a URI for the current URL.
5966                 Uri currentUri = Uri.parse(url);
5967
5968                 // Get the IP addresses for the host.
5969                 new GetHostIpAddresses(activity, getSupportFragmentManager(), nestedScrollWebView).execute(currentUri.getHost());
5970
5971                 // Replace Refresh with Stop if the options menu has been created.  (The first WebView typically begins loading before the menu items are instantiated.)
5972                 if (optionsMenu != null) {
5973                     // Set the title.
5974                     optionsRefreshMenuItem.setTitle(R.string.stop);
5975
5976                     // Get the app bar and theme preferences.
5977                     boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean(getString(R.string.display_additional_app_bar_icons_key), false);
5978
5979                     // 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.
5980                     if (displayAdditionalAppBarIcons) {
5981                         optionsRefreshMenuItem.setIcon(R.drawable.close_blue);
5982                     }
5983                 }
5984             }
5985
5986             @Override
5987             public void onPageFinished(WebView view, String url) {
5988                 // Flush any cookies to persistent storage.  The cookie manager has become very lazy about flushing cookies in recent versions.
5989                 if (nestedScrollWebView.getAcceptCookies()) {
5990                     CookieManager.getInstance().flush();
5991                 }
5992
5993                 // Update the Refresh menu item if the options menu has been created.
5994                 if (optionsMenu != null) {
5995                     // Reset the Refresh title.
5996                     optionsRefreshMenuItem.setTitle(R.string.refresh);
5997
5998                     // Get the app bar and theme preferences.
5999                     boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean(getString(R.string.display_additional_app_bar_icons_key), false);
6000
6001                     // If the icon is displayed in the app bar, reset it according to the theme.
6002                     if (displayAdditionalAppBarIcons) {
6003                         // Set the icon.
6004                         optionsRefreshMenuItem.setIcon(R.drawable.refresh_enabled);
6005                     }
6006                 }
6007
6008                 // Clear the cache, history, and logcat if Incognito Mode is enabled.
6009                 if (incognitoModeEnabled) {
6010                     // Clear the cache.  `true` includes disk files.
6011                     nestedScrollWebView.clearCache(true);
6012
6013                     // Clear the back/forward history.
6014                     nestedScrollWebView.clearHistory();
6015
6016                     // Manually delete cache folders.
6017                     try {
6018                         // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
6019                         // which links to `/data/data/com.stoutner.privacybrowser.standard`.
6020                         String privateDataDirectoryString = getApplicationInfo().dataDir;
6021
6022                         // Delete the main cache directory.
6023                         Runtime.getRuntime().exec("rm -rf " + privateDataDirectoryString + "/cache");
6024
6025                         // Delete the secondary `Service Worker` cache directory.
6026                         // A `String[]` must be used because the directory contains a space and `Runtime.exec` will not escape the string correctly otherwise.
6027                         Runtime.getRuntime().exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
6028                     } catch (IOException exception) {
6029                         // Do nothing if an error is thrown.
6030                     }
6031
6032                     // Clear the logcat.
6033                     try {
6034                         // Clear the logcat.  `-c` clears the logcat.  `-b all` clears all the buffers (instead of just crash, main, and system).
6035                         Runtime.getRuntime().exec("logcat -b all -c");
6036                     } catch (IOException exception) {
6037                         // Do nothing.
6038                     }
6039                 }
6040
6041                 // Get the current page position.
6042                 int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
6043
6044                 // 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.
6045                 String currentUrl = nestedScrollWebView.getUrl();
6046
6047                 // Get the current tab.
6048                 TabLayout.Tab tab = tabLayout.getTabAt(currentPagePosition);
6049
6050                 // Update the URL text bar if the page is currently selected and the user is not currently typing in the URL edit text.
6051                 // Crash records show that, in some crazy way, it is possible for the current URL to be blank at this point.
6052                 // Probably some sort of race condition when Privacy Browser is being resumed.
6053                 if ((tabLayout.getSelectedTabPosition() == currentPagePosition) && !urlEditText.hasFocus() && (currentUrl != null)) {
6054                     // Check to see if the URL is `about:blank`.
6055                     if (currentUrl.equals("about:blank")) {  // The WebView is blank.
6056                         // Display the hint in the URL edit text.
6057                         urlEditText.setText("");
6058
6059                         // Request focus for the URL text box.
6060                         urlEditText.requestFocus();
6061
6062                         // Display the keyboard.
6063                         inputMethodManager.showSoftInput(urlEditText, 0);
6064
6065                         // Apply the domain settings.  This clears any settings from the previous domain.
6066                         applyDomainSettings(nestedScrollWebView, "", true, false, false);
6067
6068                         // Only populate the title text view if the tab has been fully created.
6069                         if (tab != null) {
6070                             // Get the custom view from the tab.
6071                             View tabView = tab.getCustomView();
6072
6073                             // Remove the incorrect warning below that the current tab view might be null.
6074                             assert tabView != null;
6075
6076                             // Get the title text view from the tab.
6077                             TextView tabTitleTextView = tabView.findViewById(R.id.title_textview);
6078
6079                             // Set the title as the tab text.
6080                             tabTitleTextView.setText(R.string.new_tab);
6081                         }
6082                     } else {  // The WebView has loaded a webpage.
6083                         // Update the URL edit text if it is not currently being edited.
6084                         if (!urlEditText.hasFocus()) {
6085                             // 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.
6086                             String sanitizedUrl = sanitizeUrl(currentUrl);
6087
6088                             // Display the final URL.  Getting the URL from the WebView instead of using the one provided by `onPageFinished()` makes websites like YouTube function correctly.
6089                             urlEditText.setText(sanitizedUrl);
6090
6091                             // Apply text highlighting to the URL.
6092                             highlightUrlText();
6093                         }
6094
6095                         // Only populate the title text view if the tab has been fully created.
6096                         if (tab != null) {
6097                             // Get the custom view from the tab.
6098                             View tabView = tab.getCustomView();
6099
6100                             // Remove the incorrect warning below that the current tab view might be null.
6101                             assert tabView != null;
6102
6103                             // Get the title text view from the tab.
6104                             TextView tabTitleTextView = tabView.findViewById(R.id.title_textview);
6105
6106                             // Set the title as the tab text.  Sometimes `onReceivedTitle()` is not called, especially when navigating history.
6107                             tabTitleTextView.setText(nestedScrollWebView.getTitle());
6108                         }
6109                     }
6110                 }
6111             }
6112
6113             // Handle SSL Certificate errors.  Suppress the lint warning that ignoring the error might be dangerous.
6114             @SuppressLint("WebViewClientOnReceivedSslError")
6115             @Override
6116             public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
6117                 // Get the current website SSL certificate.
6118                 SslCertificate currentWebsiteSslCertificate = error.getCertificate();
6119
6120                 // Extract the individual pieces of information from the current website SSL certificate.
6121                 String currentWebsiteIssuedToCName = currentWebsiteSslCertificate.getIssuedTo().getCName();
6122                 String currentWebsiteIssuedToOName = currentWebsiteSslCertificate.getIssuedTo().getOName();
6123                 String currentWebsiteIssuedToUName = currentWebsiteSslCertificate.getIssuedTo().getUName();
6124                 String currentWebsiteIssuedByCName = currentWebsiteSslCertificate.getIssuedBy().getCName();
6125                 String currentWebsiteIssuedByOName = currentWebsiteSslCertificate.getIssuedBy().getOName();
6126                 String currentWebsiteIssuedByUName = currentWebsiteSslCertificate.getIssuedBy().getUName();
6127                 Date currentWebsiteSslStartDate = currentWebsiteSslCertificate.getValidNotBeforeDate();
6128                 Date currentWebsiteSslEndDate = currentWebsiteSslCertificate.getValidNotAfterDate();
6129
6130                 // Proceed to the website if the current SSL website certificate matches the pinned domain certificate.
6131                 if (nestedScrollWebView.hasPinnedSslCertificate()) {
6132                     // Get the pinned SSL certificate.
6133                     Pair<String[], Date[]> pinnedSslCertificatePair = nestedScrollWebView.getPinnedSslCertificate();
6134
6135                     // Extract the arrays from the array list.
6136                     String[] pinnedSslCertificateStringArray = pinnedSslCertificatePair.getFirst();
6137                     Date[] pinnedSslCertificateDateArray = pinnedSslCertificatePair.getSecond();
6138
6139                     // Check if the current SSL certificate matches the pinned certificate.
6140                     if (currentWebsiteIssuedToCName.equals(pinnedSslCertificateStringArray[0]) && currentWebsiteIssuedToOName.equals(pinnedSslCertificateStringArray[1]) &&
6141                         currentWebsiteIssuedToUName.equals(pinnedSslCertificateStringArray[2]) && currentWebsiteIssuedByCName.equals(pinnedSslCertificateStringArray[3]) &&
6142                         currentWebsiteIssuedByOName.equals(pinnedSslCertificateStringArray[4]) && currentWebsiteIssuedByUName.equals(pinnedSslCertificateStringArray[5]) &&
6143                         currentWebsiteSslStartDate.equals(pinnedSslCertificateDateArray[0]) && currentWebsiteSslEndDate.equals(pinnedSslCertificateDateArray[1])) {
6144
6145                         // An SSL certificate is pinned and matches the current domain certificate.  Proceed to the website without displaying an error.
6146                         handler.proceed();
6147                     }
6148                 } else {  // Either there isn't a pinned SSL certificate or it doesn't match the current website certificate.
6149                     // Store the SSL error handler.
6150                     nestedScrollWebView.setSslErrorHandler(handler);
6151
6152                     // Instantiate an SSL certificate error alert dialog.
6153                     DialogFragment sslCertificateErrorDialogFragment = SslCertificateErrorDialog.displayDialog(error, nestedScrollWebView.getWebViewFragmentId());
6154
6155                     // 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.
6156                     try {
6157                         // Show the SSL certificate error dialog.
6158                         sslCertificateErrorDialogFragment.show(getSupportFragmentManager(), getString(R.string.ssl_certificate_error));
6159                     } catch (Exception exception) {
6160                         // Add the dialog to the pending dialog array list.  It will be displayed in `onStart()`.
6161                         pendingDialogsArrayList.add(new PendingDialog(sslCertificateErrorDialogFragment, getString(R.string.ssl_certificate_error)));
6162                     }
6163                 }
6164             }
6165         });
6166
6167         // Check to see if the state is being restored.
6168         if (restoringState) {  // The state is being restored.
6169             // Resume the nested scroll WebView JavaScript timers.
6170             nestedScrollWebView.resumeTimers();
6171         } else if (pageNumber == 0) {  // The first page is being loaded.
6172             // Set this nested scroll WebView as the current WebView.
6173             currentWebView = nestedScrollWebView;
6174
6175             // Initialize the URL to load string.
6176             String urlToLoadString;
6177
6178             // Get the intent that started the app.
6179             Intent launchingIntent = getIntent();
6180
6181             // Reset the intent.  This prevents a duplicate tab from being created on restart.
6182             setIntent(new Intent());
6183
6184             // Get the information from the intent.
6185             String launchingIntentAction = launchingIntent.getAction();
6186             Uri launchingIntentUriData = launchingIntent.getData();
6187             String launchingIntentStringExtra = launchingIntent.getStringExtra(Intent.EXTRA_TEXT);
6188
6189             // Parse the launching intent URL.
6190             if ((launchingIntentAction != null) && launchingIntentAction.equals(Intent.ACTION_WEB_SEARCH)) {  // The intent contains a search string.
6191                 // Create an encoded URL string.
6192                 String encodedUrlString;
6193
6194                 // Sanitize the search input and convert it to a search.
6195                 try {
6196                     encodedUrlString = URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8");
6197                 } catch (UnsupportedEncodingException exception) {
6198                     encodedUrlString = "";
6199                 }
6200
6201                 // Store the web search as the URL to load.
6202                 urlToLoadString = searchURL + encodedUrlString;
6203             } else if (launchingIntentUriData != null) {  // The launching intent contains a URL formatted as a URI.
6204                 // Store the URI as a URL.
6205                 urlToLoadString = launchingIntentUriData.toString();
6206             } else if (launchingIntentStringExtra != null) {  // The launching intent contains text that might be a URL.
6207                 // Store the URL.
6208                 urlToLoadString = launchingIntentStringExtra;
6209             } else if (!url.equals("")) {  // The activity has been restarted.
6210                 // Load the saved URL.
6211                 urlToLoadString = url;
6212             } else {  // The is no URL in the intent.
6213                 // Store the homepage to be loaded.
6214                 urlToLoadString = sharedPreferences.getString("homepage", getString(R.string.homepage_default_value));
6215             }
6216
6217             // Load the website if not waiting for the proxy.
6218             if (waitingForProxy) {  // Store the URL to be loaded in the Nested Scroll WebView.
6219                 nestedScrollWebView.setWaitingForProxyUrlString(urlToLoadString);
6220             } else {  // Load the URL.
6221                 loadUrl(nestedScrollWebView, urlToLoadString);
6222             }
6223
6224             // 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.
6225             // 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.
6226             setIntent(new Intent());
6227         } else {  // This is not the first tab.
6228             // Load the URL.
6229             loadUrl(nestedScrollWebView, url);
6230
6231             // Set the focus and display the keyboard if the URL is blank.
6232             if (url.equals("")) {
6233                 // Request focus for the URL text box.
6234                 urlEditText.requestFocus();
6235
6236                 // Create a display keyboard handler.
6237                 Handler displayKeyboardHandler = new Handler();
6238
6239                 // Create a display keyboard runnable.
6240                 Runnable displayKeyboardRunnable = () -> {
6241                     // Display the keyboard.
6242                     inputMethodManager.showSoftInput(urlEditText, 0);
6243                 };
6244
6245                 // Display the keyboard after 100 milliseconds, which leaves enough time for the tab to transition.
6246                 displayKeyboardHandler.postDelayed(displayKeyboardRunnable, 100);
6247             }
6248         }
6249     }
6250 }