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