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