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