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