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