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