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