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