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