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