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