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