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