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