1595f4a3d04105a63e237b083ca32ac087361132
[PrivacyBrowser.git] / app / src / main / java / com / stoutner / privacybrowser / activities / MainWebViewActivity.java
1 /*
2  * Copyright © 2015-2020 Soren Stoutner <soren@stoutner.com>.
3  *
4  * Download cookie code contributed 2017 Hendrik Knackstedt.  Copyright assigned to Soren Stoutner <soren@stoutner.com>.
5  *
6  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
7  *
8  * Privacy Browser is free software: you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation, either version 3 of the License, or
11  * (at your option) any later version.
12  *
13  * Privacy Browser is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with Privacy Browser.  If not, see <http://www.gnu.org/licenses/>.
20  */
21
22 package com.stoutner.privacybrowser.activities;
23
24 import android.Manifest;
25 import android.annotation.SuppressLint;
26 import android.app.Activity;
27 import android.app.Dialog;
28 import android.app.DownloadManager;
29 import android.app.SearchManager;
30 import android.content.ActivityNotFoundException;
31 import android.content.BroadcastReceiver;
32 import android.content.ClipData;
33 import android.content.ClipboardManager;
34 import android.content.ContentResolver;
35 import android.content.Context;
36 import android.content.Intent;
37 import android.content.IntentFilter;
38 import android.content.SharedPreferences;
39 import android.content.pm.PackageManager;
40 import android.content.res.Configuration;
41 import android.database.Cursor;
42 import android.graphics.Bitmap;
43 import android.graphics.BitmapFactory;
44 import android.graphics.Typeface;
45 import android.graphics.drawable.BitmapDrawable;
46 import android.graphics.drawable.Drawable;
47 import android.net.Uri;
48 import android.net.http.SslCertificate;
49 import android.net.http.SslError;
50 import android.os.AsyncTask;
51 import android.os.Build;
52 import android.os.Bundle;
53 import android.os.Handler;
54 import android.os.Message;
55 import android.preference.PreferenceManager;
56 import android.print.PrintDocumentAdapter;
57 import android.print.PrintManager;
58 import android.text.Editable;
59 import android.text.Spanned;
60 import android.text.TextWatcher;
61 import android.text.style.ForegroundColorSpan;
62 import android.util.Patterns;
63 import android.util.TypedValue;
64 import android.view.ContextMenu;
65 import android.view.GestureDetector;
66 import android.view.KeyEvent;
67 import android.view.Menu;
68 import android.view.MenuItem;
69 import android.view.MotionEvent;
70 import android.view.View;
71 import android.view.ViewGroup;
72 import android.view.WindowManager;
73 import android.view.inputmethod.InputMethodManager;
74 import android.webkit.CookieManager;
75 import android.webkit.HttpAuthHandler;
76 import android.webkit.SslErrorHandler;
77 import android.webkit.ValueCallback;
78 import android.webkit.WebBackForwardList;
79 import android.webkit.WebChromeClient;
80 import android.webkit.WebResourceResponse;
81 import android.webkit.WebSettings;
82 import android.webkit.WebStorage;
83 import android.webkit.WebView;
84 import android.webkit.WebViewClient;
85 import android.webkit.WebViewDatabase;
86 import android.widget.ArrayAdapter;
87 import android.widget.CursorAdapter;
88 import android.widget.EditText;
89 import android.widget.FrameLayout;
90 import android.widget.ImageView;
91 import android.widget.LinearLayout;
92 import android.widget.ListView;
93 import android.widget.ProgressBar;
94 import android.widget.RadioButton;
95 import android.widget.RelativeLayout;
96 import android.widget.TextView;
97
98 import androidx.annotation.NonNull;
99 import androidx.appcompat.app.ActionBar;
100 import androidx.appcompat.app.ActionBarDrawerToggle;
101 import androidx.appcompat.app.AppCompatActivity;
102 import androidx.appcompat.app.AppCompatDelegate;
103 import androidx.appcompat.widget.Toolbar;
104 import androidx.coordinatorlayout.widget.CoordinatorLayout;
105 import androidx.core.app.ActivityCompat;
106 import androidx.core.content.ContextCompat;
107 import androidx.core.content.FileProvider;
108 import androidx.core.content.res.ResourcesCompat;
109 import androidx.core.view.GravityCompat;
110 import androidx.drawerlayout.widget.DrawerLayout;
111 import androidx.fragment.app.DialogFragment;
112 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
113 import androidx.viewpager.widget.ViewPager;
114 import androidx.webkit.WebSettingsCompat;
115 import androidx.webkit.WebViewFeature;
116
117 import com.google.android.material.appbar.AppBarLayout;
118 import com.google.android.material.floatingactionbutton.FloatingActionButton;
119 import com.google.android.material.navigation.NavigationView;
120 import com.google.android.material.snackbar.Snackbar;
121 import com.google.android.material.tabs.TabLayout;
122
123 import com.stoutner.privacybrowser.BuildConfig;
124 import com.stoutner.privacybrowser.R;
125 import com.stoutner.privacybrowser.adapters.WebViewPagerAdapter;
126 import com.stoutner.privacybrowser.asynctasks.GetHostIpAddresses;
127 import com.stoutner.privacybrowser.asynctasks.PopulateBlocklists;
128 import com.stoutner.privacybrowser.asynctasks.PrepareSaveDialog;
129 import com.stoutner.privacybrowser.asynctasks.SaveUrl;
130 import com.stoutner.privacybrowser.asynctasks.SaveWebpageImage;
131 import com.stoutner.privacybrowser.dialogs.AdConsentDialog;
132 import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog;
133 import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog;
134 import com.stoutner.privacybrowser.dialogs.CreateHomeScreenShortcutDialog;
135 import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDialog;
136 import com.stoutner.privacybrowser.dialogs.FontSizeDialog;
137 import com.stoutner.privacybrowser.dialogs.HttpAuthenticationDialog;
138 import com.stoutner.privacybrowser.dialogs.OpenDialog;
139 import com.stoutner.privacybrowser.dialogs.ProxyNotInstalledDialog;
140 import com.stoutner.privacybrowser.dialogs.PinnedMismatchDialog;
141 import com.stoutner.privacybrowser.dialogs.SaveWebpageDialog;
142 import com.stoutner.privacybrowser.dialogs.SslCertificateErrorDialog;
143 import com.stoutner.privacybrowser.dialogs.StoragePermissionDialog;
144 import com.stoutner.privacybrowser.dialogs.UrlHistoryDialog;
145 import com.stoutner.privacybrowser.dialogs.ViewSslCertificateDialog;
146 import com.stoutner.privacybrowser.dialogs.WaitingForProxyDialog;
147 import com.stoutner.privacybrowser.fragments.WebViewTabFragment;
148 import com.stoutner.privacybrowser.helpers.AdHelper;
149 import com.stoutner.privacybrowser.helpers.BlocklistHelper;
150 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper;
151 import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper;
152 import com.stoutner.privacybrowser.helpers.FileNameHelper;
153 import com.stoutner.privacybrowser.helpers.ProxyHelper;
154 import com.stoutner.privacybrowser.views.NestedScrollWebView;
155
156 import java.io.ByteArrayInputStream;
157 import java.io.ByteArrayOutputStream;
158 import java.io.File;
159 import java.io.IOException;
160 import java.io.UnsupportedEncodingException;
161 import java.net.MalformedURLException;
162 import java.net.URL;
163 import java.net.URLDecoder;
164 import java.net.URLEncoder;
165 import java.text.NumberFormat;
166 import java.util.ArrayList;
167 import java.util.Date;
168 import java.util.HashMap;
169 import java.util.HashSet;
170 import java.util.List;
171 import java.util.Map;
172 import java.util.Objects;
173 import java.util.Set;
174 import java.util.concurrent.ExecutorService;
175 import java.util.concurrent.Executors;
176
177 public class MainWebViewActivity extends AppCompatActivity implements CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener,
178         EditBookmarkFolderDialog.EditBookmarkFolderListener, FontSizeDialog.UpdateFontSizeListener, NavigationView.OnNavigationItemSelectedListener, OpenDialog.OpenListener,
179         PinnedMismatchDialog.PinnedMismatchListener, PopulateBlocklists.PopulateBlocklistsListener, SaveWebpageDialog.SaveWebpageListener, StoragePermissionDialog.StoragePermissionDialogListener,
180         UrlHistoryDialog.NavigateHistoryListener, WebViewTabFragment.NewTabListener {
181
182     // The executor service handles background tasks.  It is accessed from `ViewSourceActivity`.
183     public static ExecutorService executorService = Executors.newFixedThreadPool(4);
184
185     // `orbotStatus` is public static so it can be accessed from `OrbotProxyHelper`.  It is also used in `onCreate()`, `onResume()`, and `applyProxy()`.
186     public static String orbotStatus = "unknown";
187
188     // The WebView pager adapter is accessed from `HttpAuthenticationDialog`, `PinnedMismatchDialog`, and `SslCertificateErrorDialog`.  It is also used in `onCreate()`, `onResume()`, and `addTab()`.
189     public static WebViewPagerAdapter webViewPagerAdapter;
190
191     // `restartFromBookmarksActivity` is public static so it can be accessed from `BookmarksActivity`.  It is also used in `onRestart()`.
192     public static boolean restartFromBookmarksActivity;
193
194     // `currentBookmarksFolder` is public static so it can be accessed from `BookmarksActivity`.  It is also used in `onCreate()`, `onBackPressed()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`,
195     // `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
196     public static String currentBookmarksFolder;
197
198     // The user agent constants are public static so they can be accessed from `SettingsFragment`, `DomainsActivity`, and `DomainSettingsFragment`.
199     public final static int UNRECOGNIZED_USER_AGENT = -1;
200     public final static int SETTINGS_WEBVIEW_DEFAULT_USER_AGENT = 1;
201     public final static int SETTINGS_CUSTOM_USER_AGENT = 12;
202     public final static int DOMAINS_SYSTEM_DEFAULT_USER_AGENT = 0;
203     public final static int DOMAINS_WEBVIEW_DEFAULT_USER_AGENT = 2;
204     public final static int DOMAINS_CUSTOM_USER_AGENT = 13;
205
206     // Start activity for result request codes.  The public static entries are accessed from `OpenDialog()` and `SaveWebpageDialog()`.
207     public final static int BROWSE_OPEN_REQUEST_CODE = 0;
208     public final static int BROWSE_SAVE_WEBPAGE_REQUEST_CODE = 1;
209     private final int BROWSE_FILE_UPLOAD_REQUEST_CODE = 2;
210
211     // The proxy mode is public static so it can be accessed from `ProxyHelper()`.
212     // It is also used in `onRestart()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `applyAppSettings()`, and `applyProxy()`.
213     // It will be updated in `applyAppSettings()`, but it needs to be initialized here or the first run of `onPrepareOptionsMenu()` crashes.
214     public static String proxyMode = ProxyHelper.NONE;
215
216     // Define the saved instance state constants.
217     private final String SAVED_STATE_ARRAY_LIST = "saved_state_array_list";
218     private final String SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST = "saved_nested_scroll_webview_state_array_list";
219     private final String SAVED_TAB_POSITION = "saved_tab_position";
220     private final String PROXY_MODE = "proxy_mode";
221
222     // Define the saved instance state variables.
223     private ArrayList<Bundle> savedStateArrayList;
224     private ArrayList<Bundle> savedNestedScrollWebViewStateArrayList;
225     private int savedTabPosition;
226     private String savedProxyMode;
227
228     // Define the class variables.
229     @SuppressWarnings("rawtypes")
230     AsyncTask populateBlocklists;
231
232     // The current WebView is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`,
233     // `findNextOnPage()`, `closeFindOnPage()`, `loadUrlFromTextBox()`, `onSslMismatchBack()`, `applyProxy()`, and `applyDomainSettings()`.
234     private NestedScrollWebView currentWebView;
235
236     // `customHeader` is used in `onCreate()`, `onOptionsItemSelected()`, `onCreateContextMenu()`, and `loadUrl()`.
237     private final Map<String, String> customHeaders = new HashMap<>();
238
239     // The search URL is set in `applyAppSettings()` and used in `onNewIntent()`, `loadUrlFromTextBox()`, `initializeApp()`, and `initializeWebView()`.
240     private String searchURL;
241
242     // The options menu is set in `onCreateOptionsMenu()` and used in `onOptionsItemSelected()`, `updatePrivacyIcons()`, and `initializeWebView()`.
243     private Menu optionsMenu;
244
245     // The blocklists are populated in `finishedPopulatingBlocklists()` and accessed from `initializeWebView()`.
246     private ArrayList<List<String[]>> easyList;
247     private ArrayList<List<String[]>> easyPrivacy;
248     private ArrayList<List<String[]>> fanboysAnnoyanceList;
249     private ArrayList<List<String[]>> fanboysSocialList;
250     private ArrayList<List<String[]>> ultraList;
251     private ArrayList<List<String[]>> ultraPrivacy;
252
253     // `webViewDefaultUserAgent` is used in `onCreate()` and `onPrepareOptionsMenu()`.
254     private String webViewDefaultUserAgent;
255
256     // The incognito mode is set in `applyAppSettings()` and used in `initializeWebView()`.
257     private boolean incognitoModeEnabled;
258
259     // The full screen browsing mode tracker is set it `applyAppSettings()` and used in `initializeWebView()`.
260     private boolean fullScreenBrowsingModeEnabled;
261
262     // `inFullScreenBrowsingMode` is used in `onCreate()`, `onConfigurationChanged()`, and `applyAppSettings()`.
263     private boolean inFullScreenBrowsingMode;
264
265     // The app bar trackers are set in `applyAppSettings()` and used in `initializeWebView()`.
266     private boolean hideAppBar;
267     private boolean scrollAppBar;
268
269     // The loading new intent tracker is set in `onNewIntent()` and used in `setCurrentWebView()`.
270     private boolean loadingNewIntent;
271
272     // `reapplyDomainSettingsOnRestart` is used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, and `onAddDomain()`, .
273     private boolean reapplyDomainSettingsOnRestart;
274
275     // `reapplyAppSettingsOnRestart` is used in `onNavigationItemSelected()` and `onRestart()`.
276     private boolean reapplyAppSettingsOnRestart;
277
278     // `displayingFullScreenVideo` is used in `onCreate()` and `onResume()`.
279     private boolean displayingFullScreenVideo;
280
281     // `orbotStatusBroadcastReceiver` is used in `onCreate()` and `onDestroy()`.
282     private BroadcastReceiver orbotStatusBroadcastReceiver;
283
284     // The waiting for proxy boolean is used in `onResume()`, `initializeApp()` and `applyProxy()`.
285     private boolean waitingForProxy = false;
286
287     // The action bar drawer toggle is initialized in `onCreate()` and used in `onResume()`.
288     private ActionBarDrawerToggle actionBarDrawerToggle;
289
290     // The color spans are used in `onCreate()` and `highlightUrlText()`.
291     private ForegroundColorSpan redColorSpan;
292     private ForegroundColorSpan initialGrayColorSpan;
293     private ForegroundColorSpan finalGrayColorSpan;
294
295     // `bookmarksDatabaseHelper` is used in `onCreate()`, `onDestroy`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`,
296     // and `loadBookmarksFolder()`.
297     private BookmarksDatabaseHelper bookmarksDatabaseHelper;
298
299     // `bookmarksCursor` is used in `onDestroy()`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
300     private Cursor bookmarksCursor;
301
302     // `bookmarksCursorAdapter` is used in `onCreateBookmark()`, `onCreateBookmarkFolder()` `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
303     private CursorAdapter bookmarksCursorAdapter;
304
305     // `oldFolderNameString` is used in `onCreate()` and `onSaveEditBookmarkFolder()`.
306     private String oldFolderNameString;
307
308     // `fileChooserCallback` is used in `onCreate()` and `onActivityResult()`.
309     private ValueCallback<Uri[]> fileChooserCallback;
310
311     // The default progress view offsets are set in `onCreate()` and used in `initializeWebView()`.
312     private int appBarHeight;
313     private int defaultProgressViewStartOffset;
314     private int defaultProgressViewEndOffset;
315
316     // The URL sanitizers are set in `applyAppSettings()` and used in `sanitizeUrl()`.
317     private boolean sanitizeGoogleAnalytics;
318     private boolean sanitizeFacebookClickIds;
319     private boolean sanitizeTwitterAmpRedirects;
320
321     // The file path strings are used in `onSaveWebpage()` and `onRequestPermissionResult()`
322     private String openFilePath;
323     private String saveWebpageUrl;
324     private String saveWebpageFilePath;
325
326     // Declare the class views.
327     private DrawerLayout drawerLayout;
328     private AppBarLayout appBarLayout;
329     private Toolbar toolbar;
330     private LinearLayout findOnPageLinearLayout;
331     private LinearLayout tabsLinearLayout;
332     private TabLayout tabLayout;
333     private SwipeRefreshLayout swipeRefreshLayout;
334     private ViewPager webViewPager;
335
336     @Override
337     // Remove the warning about needing to override `performClick()` when using an `OnTouchListener` with `WebView`.
338     @SuppressLint("ClickableViewAccessibility")
339     protected void onCreate(Bundle savedInstanceState) {
340         // Run the default commands.
341         super.onCreate(savedInstanceState);
342
343         // Check to see if the activity has been restarted.
344         if (savedInstanceState != null) {
345             // Store the saved instance state variables.
346             savedStateArrayList = savedInstanceState.getParcelableArrayList(SAVED_STATE_ARRAY_LIST);
347             savedNestedScrollWebViewStateArrayList = savedInstanceState.getParcelableArrayList(SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST);
348             savedTabPosition = savedInstanceState.getInt(SAVED_TAB_POSITION);
349             savedProxyMode = savedInstanceState.getString(PROXY_MODE);
350         }
351
352         // Initialize the default preference values the first time the program is run.  `false` keeps this command from resetting any current preferences back to default.
353         PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
354
355         // Get a handle for the shared preferences.
356         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
357
358         // Get the screenshot preference.
359         String appTheme = sharedPreferences.getString("app_theme", getString(R.string.app_theme_default_value));
360         boolean allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false);
361
362         // Get the theme entry values string array.
363         String[] appThemeEntryValuesStringArray = getResources().getStringArray(R.array.app_theme_entry_values);
364
365         // 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.
366         if (appTheme.equals(appThemeEntryValuesStringArray[1])) {  // The light theme is selected.
367             // Apply the light theme.
368             AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
369         } else if (appTheme.equals(appThemeEntryValuesStringArray[2])) {  // The dark theme is selected.
370             // Apply the dark theme.
371             AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
372         } else {  // The system default theme is selected.
373             if (Build.VERSION.SDK_INT >= 28) {  // The system default theme is supported.
374                 // Follow the system default theme.
375                 AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
376             } else {  // The system default theme is not supported.
377                 // Follow the battery saver mode.
378                 AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY);
379             }
380         }
381
382         // Disable screenshots if not allowed.
383         if (!allowScreenshots) {
384             getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
385         }
386
387         // 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.
388         if (Build.VERSION.SDK_INT >= 21) {
389             WebView.enableSlowWholeDocumentDraw();
390         }
391
392         // Set the theme.
393         setTheme(R.style.PrivacyBrowser);
394
395         // Set the content view.
396         setContentView(R.layout.main_framelayout);
397
398         // Get handles for the views.
399         drawerLayout = findViewById(R.id.drawerlayout);
400         appBarLayout = findViewById(R.id.appbar_layout);
401         toolbar = findViewById(R.id.toolbar);
402         findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
403         tabsLinearLayout = findViewById(R.id.tabs_linearlayout);
404         tabLayout = findViewById(R.id.tablayout);
405         swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
406         webViewPager = findViewById(R.id.webviewpager);
407
408         // Get a handle for the app compat delegate.
409         AppCompatDelegate appCompatDelegate = getDelegate();
410
411         // Set the support action bar.
412         appCompatDelegate.setSupportActionBar(toolbar);
413
414         // Get a handle for the action bar.
415         ActionBar actionBar = appCompatDelegate.getSupportActionBar();
416
417         // This is needed to get rid of the Android Studio warning that the action bar might be null.
418         assert actionBar != null;
419
420         // Add the custom layout, which shows the URL text bar.
421         actionBar.setCustomView(R.layout.url_app_bar);
422         actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
423
424         // Create the hamburger icon at the start of the AppBar.
425         actionBarDrawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.open_navigation_drawer, R.string.close_navigation_drawer);
426
427         // Initially disable the sliding drawers.  They will be enabled once the blocklists are loaded.
428         drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
429
430         // Initialize the web view pager adapter.
431         webViewPagerAdapter = new WebViewPagerAdapter(getSupportFragmentManager());
432
433         // Set the pager adapter on the web view pager.
434         webViewPager.setAdapter(webViewPagerAdapter);
435
436         // Store up to 100 tabs in memory.
437         webViewPager.setOffscreenPageLimit(100);
438
439         // Initialize the app.
440         initializeApp();
441
442         // Apply the app settings from the shared preferences.
443         applyAppSettings();
444
445         // Populate the blocklists.
446         populateBlocklists = new PopulateBlocklists(this, this).execute();
447     }
448
449     @Override
450     protected void onNewIntent(Intent intent) {
451         // Run the default commands.
452         super.onNewIntent(intent);
453
454         // Replace the intent that started the app with this one.
455         setIntent(intent);
456
457         // Check to see if the app is being restarted from a saved state.
458         if (savedStateArrayList == null || savedStateArrayList.size() == 0) {  // The activity is not being restarted from a saved state.
459             // Get the information from the intent.
460             String intentAction = intent.getAction();
461             Uri intentUriData = intent.getData();
462             String intentStringExtra = intent.getStringExtra(Intent.EXTRA_TEXT);
463
464             // Determine if this is a web search.
465             boolean isWebSearch = ((intentAction != null) && intentAction.equals(Intent.ACTION_WEB_SEARCH));
466
467             // 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.
468             if (intentUriData != null || intentStringExtra != null || isWebSearch) {
469                 // Get the shared preferences.
470                 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
471
472                 // Create a URL string.
473                 String url;
474
475                 // If the intent action is a web search, perform the search.
476                 if (isWebSearch) {  // The intent is a web search.
477                     // Create an encoded URL string.
478                     String encodedUrlString;
479
480                     // Sanitize the search input and convert it to a search.
481                     try {
482                         encodedUrlString = URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8");
483                     } catch (UnsupportedEncodingException exception) {
484                         encodedUrlString = "";
485                     }
486
487                     // Add the base search URL.
488                     url = searchURL + encodedUrlString;
489                 } else if (intentUriData != null) {  // The intent contains a URL formatted as a URI.
490                     // Set the intent data as the URL.
491                     url = intentUriData.toString();
492                 } else {  // The intent contains a string, which might be a URL.
493                     // Set the intent string as the URL.
494                     url = intentStringExtra;
495                 }
496
497                 // Add a new tab if specified in the preferences.
498                 if (sharedPreferences.getBoolean("open_intents_in_new_tab", true)) {  // Load the URL in a new tab.
499                     // Set the loading new intent flag.
500                     loadingNewIntent = true;
501
502                     // Add a new tab.
503                     addNewTab(url, true);
504                 } else {  // Load the URL in the current tab.
505                     // Make it so.
506                     loadUrl(currentWebView, url);
507                 }
508
509                 // Close the navigation drawer if it is open.
510                 if (drawerLayout.isDrawerVisible(GravityCompat.START)) {
511                     drawerLayout.closeDrawer(GravityCompat.START);
512                 }
513
514                 // Close the bookmarks drawer if it is open.
515                 if (drawerLayout.isDrawerVisible(GravityCompat.END)) {
516                     drawerLayout.closeDrawer(GravityCompat.END);
517                 }
518             }
519         }
520     }
521
522     @Override
523     public void onRestart() {
524         // Run the default commands.
525         super.onRestart();
526
527         // Apply the app settings if returning from the Settings activity.
528         if (reapplyAppSettingsOnRestart) {
529             // Reset the reapply app settings on restart tracker.
530             reapplyAppSettingsOnRestart = false;
531
532             // Apply the app settings.
533             applyAppSettings();
534         }
535
536         // Apply the domain settings if returning from the settings or domains activity.
537         if (reapplyDomainSettingsOnRestart) {
538             // Reset the reapply domain settings on restart tracker.
539             reapplyDomainSettingsOnRestart = false;
540
541             // Reapply the domain settings for each tab.
542             for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
543                 // Get the WebView tab fragment.
544                 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
545
546                 // Get the fragment view.
547                 View fragmentView = webViewTabFragment.getView();
548
549                 // Only reload the WebViews if they exist.
550                 if (fragmentView != null) {
551                     // Get the nested scroll WebView from the tab fragment.
552                     NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
553
554                     // Reset the current domain name so the domain settings will be reapplied.
555                     nestedScrollWebView.resetCurrentDomainName();
556
557                     // Reapply the domain settings if the URL is not null, which can happen if an empty tab is active when returning from settings.
558                     if (nestedScrollWebView.getUrl() != null) {
559                         applyDomainSettings(nestedScrollWebView, nestedScrollWebView.getUrl(), false, true, false);
560                     }
561                 }
562             }
563         }
564
565         // Update the bookmarks drawer if returning from the Bookmarks activity.
566         if (restartFromBookmarksActivity) {
567             // Close the bookmarks drawer.
568             drawerLayout.closeDrawer(GravityCompat.END);
569
570             // Reload the bookmarks drawer.
571             loadBookmarksFolder();
572
573             // Reset `restartFromBookmarksActivity`.
574             restartFromBookmarksActivity = false;
575         }
576
577         // Update the privacy icon.  `true` runs `invalidateOptionsMenu` as the last step.  This can be important if the screen was rotated.
578         updatePrivacyIcons(true);
579     }
580
581     // `onResume()` runs after `onStart()`, which runs after `onCreate()` and `onRestart()`.
582     @Override
583     public void onResume() {
584         // Run the default commands.
585         super.onResume();
586
587         // Resume any WebViews.
588         for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
589             // Get the WebView tab fragment.
590             WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
591
592             // Get the fragment view.
593             View fragmentView = webViewTabFragment.getView();
594
595             // Only resume the WebViews if they exist (they won't when the app is first created).
596             if (fragmentView != null) {
597                 // Get the nested scroll WebView from the tab fragment.
598                 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
599
600                 // Resume the nested scroll WebView.
601                 nestedScrollWebView.onResume();
602             }
603         }
604
605         // Resume the nested scroll WebView JavaScript timers.  This is a global command that resumes JavaScript timers on all WebViews.
606         if (currentWebView != null) {
607             currentWebView.resumeTimers();
608         }
609
610         // Reapply the proxy settings if the system is using a proxy.  This redisplays the appropriate alert dialog.
611         if (!proxyMode.equals(ProxyHelper.NONE)) {
612             applyProxy(false);
613         }
614
615         // Reapply any system UI flags and the ad in the free flavor.
616         if (displayingFullScreenVideo || inFullScreenBrowsingMode) {  // The system is displaying a website or a video in full screen mode.
617             // Get a handle for the root frame layouts.
618             FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
619
620             /* Hide the system bars.
621              * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
622              * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
623              * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
624              * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
625              */
626             rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
627                     View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
628         } else if (BuildConfig.FLAVOR.contentEquals("free")) {  // The system in not in full screen mode.
629             // Resume the ad.
630             AdHelper.resumeAd(findViewById(R.id.adview));
631         }
632     }
633
634     @Override
635     public void onPause() {
636         // Run the default commands.
637         super.onPause();
638
639         for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
640             // Get the WebView tab fragment.
641             WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
642
643             // Get the fragment view.
644             View fragmentView = webViewTabFragment.getView();
645
646             // Only pause the WebViews if they exist (they won't when the app is first created).
647             if (fragmentView != null) {
648                 // Get the nested scroll WebView from the tab fragment.
649                 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
650
651                 // Pause the nested scroll WebView.
652                 nestedScrollWebView.onPause();
653             }
654         }
655
656         // Pause the WebView JavaScript timers.  This is a global command that pauses JavaScript on all WebViews.
657         if (currentWebView != null) {
658             currentWebView.pauseTimers();
659         }
660
661         // Pause the ad or it will continue to consume resources in the background on the free flavor.
662         if (BuildConfig.FLAVOR.contentEquals("free")) {
663             // Pause the ad.
664             AdHelper.pauseAd(findViewById(R.id.adview));
665         }
666     }
667
668     @Override
669     public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
670         // Run the default commands.
671         super.onSaveInstanceState(savedInstanceState);
672
673         // Create the saved state array lists.
674         ArrayList<Bundle> savedStateArrayList = new ArrayList<>();
675         ArrayList<Bundle> savedNestedScrollWebViewStateArrayList = new ArrayList<>();
676
677         // Get the URLs from each tab.
678         for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
679             // Get the WebView tab fragment.
680             WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
681
682             // Get the fragment view.
683             View fragmentView = webViewTabFragment.getView();
684
685             if (fragmentView != null) {
686                 // Get the nested scroll WebView from the tab fragment.
687                 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
688
689                 // Create saved state bundle.
690                 Bundle savedStateBundle = new Bundle();
691
692                 // Get the current states.
693                 nestedScrollWebView.saveState(savedStateBundle);
694                 Bundle savedNestedScrollWebViewStateBundle = nestedScrollWebView.saveNestedScrollWebViewState();
695
696                 // Store the saved states in the array lists.
697                 savedStateArrayList.add(savedStateBundle);
698                 savedNestedScrollWebViewStateArrayList.add(savedNestedScrollWebViewStateBundle);
699             }
700         }
701
702         // Get the current tab position.
703         int currentTabPosition = tabLayout.getSelectedTabPosition();
704
705         // Store the saved states in the bundle.
706         savedInstanceState.putParcelableArrayList(SAVED_STATE_ARRAY_LIST, savedStateArrayList);
707         savedInstanceState.putParcelableArrayList(SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST, savedNestedScrollWebViewStateArrayList);
708         savedInstanceState.putInt(SAVED_TAB_POSITION, currentTabPosition);
709         savedInstanceState.putString(PROXY_MODE, proxyMode);
710     }
711
712     @Override
713     public void onDestroy() {
714         // Unregister the orbot status broadcast receiver if it exists.
715         if (orbotStatusBroadcastReceiver != null) {
716             this.unregisterReceiver(orbotStatusBroadcastReceiver);
717         }
718
719         // Close the bookmarks cursor if it exists.
720         if (bookmarksCursor != null) {
721             bookmarksCursor.close();
722         }
723
724         // Close the bookmarks database if it exists.
725         if (bookmarksDatabaseHelper != null) {
726             bookmarksDatabaseHelper.close();
727         }
728
729         // Stop populating the blocklists if the AsyncTask is running in the background.
730         if (populateBlocklists != null) {
731             populateBlocklists.cancel(true);
732         }
733
734         // Run the default commands.
735         super.onDestroy();
736     }
737
738     @Override
739     public boolean onCreateOptionsMenu(Menu menu) {
740         // Inflate the menu.  This adds items to the action bar if it is present.
741         getMenuInflater().inflate(R.menu.webview_options_menu, menu);
742
743         // Store a handle for the options menu so it can be used by `onOptionsItemSelected()` and `updatePrivacyIcons()`.
744         optionsMenu = menu;
745
746         // Set the initial status of the privacy icons.  `false` does not call `invalidateOptionsMenu` as the last step.
747         updatePrivacyIcons(false);
748
749         // Get handles for the menu items.
750         MenuItem bookmarksMenuItem = menu.findItem(R.id.bookmarks);
751         MenuItem toggleFirstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies);
752         MenuItem toggleThirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies);
753         MenuItem toggleSaveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data);  // Form data can be removed once the minimum API >= 26.
754         MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data);  // Form data can be removed once the minimum API >= 26.
755         MenuItem refreshMenuItem = menu.findItem(R.id.refresh);
756         MenuItem darkWebViewMenuItem = menu.findItem(R.id.dark_webview);
757         MenuItem adConsentMenuItem = menu.findItem(R.id.ad_consent);
758
759         // Only display third-party cookies if API >= 21
760         toggleThirdPartyCookiesMenuItem.setVisible(Build.VERSION.SDK_INT >= 21);
761
762         // Only display the form data menu items if the API < 26.
763         toggleSaveFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
764         clearFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
765
766         // Disable the clear form data menu item if the API >= 26 so that the status of the main Clear Data is calculated correctly.
767         clearFormDataMenuItem.setEnabled(Build.VERSION.SDK_INT < 26);
768
769         // Only display the dark WebView menu item if API >= 21.
770         darkWebViewMenuItem.setVisible(Build.VERSION.SDK_INT >= 21);
771
772         // Only show Ad Consent if this is the free flavor.
773         adConsentMenuItem.setVisible(BuildConfig.FLAVOR.contentEquals("free"));
774
775         // Get the shared preferences.
776         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
777
778         // Get the dark theme and app bar preferences.
779         boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean(getString(R.string.display_additional_app_bar_icons_key), false);
780
781         // 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.
782         if (displayAdditionalAppBarIcons) {
783             refreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
784             bookmarksMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
785             toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
786         } else { //Do not display the additional icons.
787             refreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
788             bookmarksMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
789             toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
790         }
791
792         // Replace Refresh with Stop if a URL is already loading.
793         if (currentWebView != null && currentWebView.getProgress() != 100) {
794             // Set the title.
795             refreshMenuItem.setTitle(R.string.stop);
796
797             // Set the icon if it is displayed in the app bar.
798             if (displayAdditionalAppBarIcons) {
799                 // Get the current theme status.
800                 int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
801
802                 // Set the icon according to the current theme status.
803                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
804                     refreshMenuItem.setIcon(R.drawable.close_blue_day);
805                 } else {
806                     refreshMenuItem.setIcon(R.drawable.close_blue_night);
807                 }
808             }
809         }
810
811         // Done.
812         return true;
813     }
814
815     @Override
816     public boolean onPrepareOptionsMenu(Menu menu) {
817         // Get handles for the menu items.
818         MenuItem addOrEditDomain = menu.findItem(R.id.add_or_edit_domain);
819         MenuItem firstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies);
820         MenuItem thirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies);
821         MenuItem domStorageMenuItem = menu.findItem(R.id.toggle_dom_storage);
822         MenuItem saveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data);  // Form data can be removed once the minimum API >= 26.
823         MenuItem clearDataMenuItem = menu.findItem(R.id.clear_data);
824         MenuItem clearCookiesMenuItem = menu.findItem(R.id.clear_cookies);
825         MenuItem clearDOMStorageMenuItem = menu.findItem(R.id.clear_dom_storage);
826         MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data);  // Form data can be removed once the minimum API >= 26.
827         MenuItem blocklistsMenuItem = menu.findItem(R.id.blocklists);
828         MenuItem easyListMenuItem = menu.findItem(R.id.easylist);
829         MenuItem easyPrivacyMenuItem = menu.findItem(R.id.easyprivacy);
830         MenuItem fanboysAnnoyanceListMenuItem = menu.findItem(R.id.fanboys_annoyance_list);
831         MenuItem fanboysSocialBlockingListMenuItem = menu.findItem(R.id.fanboys_social_blocking_list);
832         MenuItem ultraListMenuItem = menu.findItem(R.id.ultralist);
833         MenuItem ultraPrivacyMenuItem = menu.findItem(R.id.ultraprivacy);
834         MenuItem blockAllThirdPartyRequestsMenuItem = menu.findItem(R.id.block_all_third_party_requests);
835         MenuItem proxyMenuItem = menu.findItem(R.id.proxy);
836         MenuItem userAgentMenuItem = menu.findItem(R.id.user_agent);
837         MenuItem fontSizeMenuItem = menu.findItem(R.id.font_size);
838         MenuItem swipeToRefreshMenuItem = menu.findItem(R.id.swipe_to_refresh);
839         MenuItem wideViewportMenuItem = menu.findItem(R.id.wide_viewport);
840         MenuItem displayImagesMenuItem = menu.findItem(R.id.display_images);
841         MenuItem darkWebViewMenuItem = menu.findItem(R.id.dark_webview);
842
843         // Get a handle for the cookie manager.
844         CookieManager cookieManager = CookieManager.getInstance();
845
846         // Initialize the current user agent string and the font size.
847         String currentUserAgent = getString(R.string.user_agent_privacy_browser);
848         int fontSize = 100;
849
850         // 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.
851         if (currentWebView != null) {
852             // Set the add or edit domain text.
853             if (currentWebView.getDomainSettingsApplied()) {
854                 addOrEditDomain.setTitle(R.string.edit_domain_settings);
855             } else {
856                 addOrEditDomain.setTitle(R.string.add_domain_settings);
857             }
858
859             // Get the current user agent from the WebView.
860             currentUserAgent = currentWebView.getSettings().getUserAgentString();
861
862             // Get the current font size from the
863             fontSize = currentWebView.getSettings().getTextZoom();
864
865             // Set the status of the menu item checkboxes.
866             domStorageMenuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled());
867             saveFormDataMenuItem.setChecked(currentWebView.getSettings().getSaveFormData());  // Form data can be removed once the minimum API >= 26.
868             easyListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASYLIST));
869             easyPrivacyMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASYPRIVACY));
870             fanboysAnnoyanceListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
871             fanboysSocialBlockingListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
872             ultraListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRALIST));
873             ultraPrivacyMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRAPRIVACY));
874             blockAllThirdPartyRequestsMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
875             swipeToRefreshMenuItem.setChecked(currentWebView.getSwipeToRefresh());
876             wideViewportMenuItem.setChecked(currentWebView.getSettings().getUseWideViewPort());
877             displayImagesMenuItem.setChecked(currentWebView.getSettings().getLoadsImagesAutomatically());
878
879             // Initialize the display names for the blocklists with the number of blocked requests.
880             blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
881             easyListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASYLIST) + " - " + getString(R.string.easylist));
882             easyPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASYPRIVACY) + " - " + getString(R.string.easyprivacy));
883             fanboysAnnoyanceListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST) + " - " + getString(R.string.fanboys_annoyance_list));
884             fanboysSocialBlockingListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST) + " - " + getString(R.string.fanboys_social_blocking_list));
885             ultraListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.ULTRALIST) + " - " + getString(R.string.ultralist));
886             ultraPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.ULTRAPRIVACY) + " - " + getString(R.string.ultraprivacy));
887             blockAllThirdPartyRequestsMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " + getString(R.string.block_all_third_party_requests));
888
889             // Only modify third-party cookies if the API >= 21.
890             if (Build.VERSION.SDK_INT >= 21) {
891                 // Set the status of the third-party cookies checkbox.
892                 thirdPartyCookiesMenuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView));
893
894                 // Enable third-party cookies if first-party cookies are enabled.
895                 thirdPartyCookiesMenuItem.setEnabled(cookieManager.acceptCookie());
896             }
897
898             // Enable DOM Storage if JavaScript is enabled.
899             domStorageMenuItem.setEnabled(currentWebView.getSettings().getJavaScriptEnabled());
900
901             // Set the checkbox status for dark WebView if the WebView supports it.
902             if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
903                 darkWebViewMenuItem.setChecked(WebSettingsCompat.getForceDark(currentWebView.getSettings()) == WebSettingsCompat.FORCE_DARK_ON);
904             }
905         }
906
907         // Set the checked status of the first party cookies menu item.
908         firstPartyCookiesMenuItem.setChecked(cookieManager.acceptCookie());
909
910         // Enable Clear Cookies if there are any.
911         clearCookiesMenuItem.setEnabled(cookieManager.hasCookies());
912
913         // 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`.
914         String privateDataDirectoryString = getApplicationInfo().dataDir;
915
916         // Get a count of the number of files in the Local Storage directory.
917         File localStorageDirectory = new File (privateDataDirectoryString + "/app_webview/Local Storage/");
918         int localStorageDirectoryNumberOfFiles = 0;
919         if (localStorageDirectory.exists()) {
920             // `Objects.requireNonNull` removes a lint warning that `localStorageDirectory.list` might produce a null pointed exception if it is dereferenced.
921             localStorageDirectoryNumberOfFiles = Objects.requireNonNull(localStorageDirectory.list()).length;
922         }
923
924         // Get a count of the number of files in the IndexedDB directory.
925         File indexedDBDirectory = new File (privateDataDirectoryString + "/app_webview/IndexedDB");
926         int indexedDBDirectoryNumberOfFiles = 0;
927         if (indexedDBDirectory.exists()) {
928             // `Objects.requireNonNull` removes a lint warning that `indexedDBDirectory.list` might produce a null pointed exception if it is dereferenced.
929             indexedDBDirectoryNumberOfFiles = Objects.requireNonNull(indexedDBDirectory.list()).length;
930         }
931
932         // Enable Clear DOM Storage if there is any.
933         clearDOMStorageMenuItem.setEnabled(localStorageDirectoryNumberOfFiles > 0 || indexedDBDirectoryNumberOfFiles > 0);
934
935         // Enable Clear Form Data is there is any.  This can be removed once the minimum API >= 26.
936         if (Build.VERSION.SDK_INT < 26) {
937             // Get the WebView database.
938             WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
939
940             // Enable the clear form data menu item if there is anything to clear.
941             clearFormDataMenuItem.setEnabled(webViewDatabase.hasFormData());
942         }
943
944         // Enable Clear Data if any of the submenu items are enabled.
945         clearDataMenuItem.setEnabled(clearCookiesMenuItem.isEnabled() || clearDOMStorageMenuItem.isEnabled() || clearFormDataMenuItem.isEnabled());
946
947         // Disable Fanboy's Social Blocking List menu item if Fanboy's Annoyance List is checked.
948         fanboysSocialBlockingListMenuItem.setEnabled(!fanboysAnnoyanceListMenuItem.isChecked());
949
950         // Set the proxy title and check the applied proxy.
951         switch (proxyMode) {
952             case ProxyHelper.NONE:
953                 // Set the proxy title.
954                 proxyMenuItem.setTitle(getString(R.string.proxy) + " - " + getString(R.string.proxy_none));
955
956                 // Check the proxy None radio button.
957                 menu.findItem(R.id.proxy_none).setChecked(true);
958                 break;
959
960             case ProxyHelper.TOR:
961                 // Set the proxy title.
962                 proxyMenuItem.setTitle(getString(R.string.proxy) + " - " + getString(R.string.proxy_tor));
963
964                 // Check the proxy Tor radio button.
965                 menu.findItem(R.id.proxy_tor).setChecked(true);
966                 break;
967
968             case ProxyHelper.I2P:
969                 // Set the proxy title.
970                 proxyMenuItem.setTitle(getString(R.string.proxy) + " - " + getString(R.string.proxy_i2p));
971
972                 // Check the proxy I2P radio button.
973                 menu.findItem(R.id.proxy_i2p).setChecked(true);
974                 break;
975
976             case ProxyHelper.CUSTOM:
977                 // Set the proxy title.
978                 proxyMenuItem.setTitle(getString(R.string.proxy) + " - " + getString(R.string.proxy_custom));
979
980                 // Check the proxy Custom radio button.
981                 menu.findItem(R.id.proxy_custom).setChecked(true);
982                 break;
983         }
984
985         // Select the current user agent menu item.  A switch statement cannot be used because the user agents are not compile time constants.
986         if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[0])) {  // Privacy Browser.
987             // Update the user agent menu item title.
988             userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_privacy_browser));
989
990             // Select the Privacy Browser radio box.
991             menu.findItem(R.id.user_agent_privacy_browser).setChecked(true);
992         } else if (currentUserAgent.equals(webViewDefaultUserAgent)) {  // WebView Default.
993             // Update the user agent menu item title.
994             userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_webview_default));
995
996             // Select the WebView Default radio box.
997             menu.findItem(R.id.user_agent_webview_default).setChecked(true);
998         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[2])) {  // Firefox on Android.
999             // Update the user agent menu item title.
1000             userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_firefox_on_android));
1001
1002             // Select the Firefox on Android radio box.
1003             menu.findItem(R.id.user_agent_firefox_on_android).setChecked(true);
1004         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[3])) {  // Chrome on Android.
1005             // Update the user agent menu item title.
1006             userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_chrome_on_android));
1007
1008             // Select the Chrome on Android radio box.
1009             menu.findItem(R.id.user_agent_chrome_on_android).setChecked(true);
1010         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[4])) {  // Safari on iOS.
1011             // Update the user agent menu item title.
1012             userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_safari_on_ios));
1013
1014             // Select the Safari on iOS radio box.
1015             menu.findItem(R.id.user_agent_safari_on_ios).setChecked(true);
1016         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[5])) {  // Firefox on Linux.
1017             // Update the user agent menu item title.
1018             userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_firefox_on_linux));
1019
1020             // Select the Firefox on Linux radio box.
1021             menu.findItem(R.id.user_agent_firefox_on_linux).setChecked(true);
1022         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[6])) {  // Chromium on Linux.
1023             // Update the user agent menu item title.
1024             userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_chromium_on_linux));
1025
1026             // Select the Chromium on Linux radio box.
1027             menu.findItem(R.id.user_agent_chromium_on_linux).setChecked(true);
1028         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[7])) {  // Firefox on Windows.
1029             // Update the user agent menu item title.
1030             userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_firefox_on_windows));
1031
1032             // Select the Firefox on Windows radio box.
1033             menu.findItem(R.id.user_agent_firefox_on_windows).setChecked(true);
1034         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[8])) {  // Chrome on Windows.
1035             // Update the user agent menu item title.
1036             userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_chrome_on_windows));
1037
1038             // Select the Chrome on Windows radio box.
1039             menu.findItem(R.id.user_agent_chrome_on_windows).setChecked(true);
1040         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[9])) {  // Edge on Windows.
1041             // Update the user agent menu item title.
1042             userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_edge_on_windows));
1043
1044             // Select the Edge on Windows radio box.
1045             menu.findItem(R.id.user_agent_edge_on_windows).setChecked(true);
1046         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[10])) {  // Internet Explorer on Windows.
1047             // Update the user agent menu item title.
1048             userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_internet_explorer_on_windows));
1049
1050             // Select the Internet on Windows radio box.
1051             menu.findItem(R.id.user_agent_internet_explorer_on_windows).setChecked(true);
1052         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[11])) {  // Safari on macOS.
1053             // Update the user agent menu item title.
1054             userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_safari_on_macos));
1055
1056             // Select the Safari on macOS radio box.
1057             menu.findItem(R.id.user_agent_safari_on_macos).setChecked(true);
1058         } else {  // Custom user agent.
1059             // Update the user agent menu item title.
1060             userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_custom));
1061
1062             // Select the Custom radio box.
1063             menu.findItem(R.id.user_agent_custom).setChecked(true);
1064         }
1065
1066         // Set the font size title.
1067         fontSizeMenuItem.setTitle(getString(R.string.font_size) + " - " + fontSize + "%");
1068
1069         // Run all the other default commands.
1070         super.onPrepareOptionsMenu(menu);
1071
1072         // Display the menu.
1073         return true;
1074     }
1075
1076     @Override
1077     public boolean onOptionsItemSelected(MenuItem menuItem) {
1078         // Get a handle for the shared preferences.
1079         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
1080
1081         // Get a handle for the cookie manager.
1082         CookieManager cookieManager = CookieManager.getInstance();
1083
1084         // Get the selected menu item ID.
1085         int menuItemId = menuItem.getItemId();
1086
1087         // Run the commands that correlate to the selected menu item.
1088         if (menuItemId == R.id.toggle_javascript) {  // JavaScript.
1089             // Toggle the JavaScript status.
1090             currentWebView.getSettings().setJavaScriptEnabled(!currentWebView.getSettings().getJavaScriptEnabled());
1091
1092             // Update the privacy icon.
1093             updatePrivacyIcons(true);
1094
1095             // Display a `Snackbar`.
1096             if (currentWebView.getSettings().getJavaScriptEnabled()) {  // JavaScrip is enabled.
1097                 Snackbar.make(webViewPager, R.string.javascript_enabled, Snackbar.LENGTH_SHORT).show();
1098             } else if (cookieManager.acceptCookie()) {  // JavaScript is disabled, but first-party cookies are enabled.
1099                 Snackbar.make(webViewPager, R.string.javascript_disabled, Snackbar.LENGTH_SHORT).show();
1100             } else {  // Privacy mode.
1101                 Snackbar.make(webViewPager, R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
1102             }
1103
1104             // Reload the current WebView.
1105             currentWebView.reload();
1106
1107             // Consume the event.
1108             return true;
1109         } else if (menuItemId == R.id.refresh) {  // Refresh.
1110             // Run the command that correlates to the current status of the menu item.
1111             if (menuItem.getTitle().equals(getString(R.string.refresh))) {  // The refresh button was pushed.
1112                 // Reload the current WebView.
1113                 currentWebView.reload();
1114             } else {  // The stop button was pushed.
1115                 // Stop the loading of the WebView.
1116                 currentWebView.stopLoading();
1117             }
1118
1119             // Consume the event.
1120             return true;
1121         } else if (menuItemId == R.id.bookmarks) {  // Bookmarks.
1122             // Open the bookmarks drawer.
1123             drawerLayout.openDrawer(GravityCompat.END);
1124
1125             // Consume the event.
1126             return true;
1127         } else if (menuItemId == R.id.toggle_first_party_cookies) {  // First-party cookies.
1128             // Switch the first-party cookie status.
1129             cookieManager.setAcceptCookie(!cookieManager.acceptCookie());
1130
1131             // Store the first-party cookie status.
1132             currentWebView.setAcceptFirstPartyCookies(cookieManager.acceptCookie());
1133
1134             // Update the menu checkbox.
1135             menuItem.setChecked(cookieManager.acceptCookie());
1136
1137             // Update the privacy icon.
1138             updatePrivacyIcons(true);
1139
1140             // Display a snackbar.
1141             if (cookieManager.acceptCookie()) {  // First-party cookies are enabled.
1142                 Snackbar.make(webViewPager, R.string.first_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
1143             } else if (currentWebView.getSettings().getJavaScriptEnabled()) {  // JavaScript is still enabled.
1144                 Snackbar.make(webViewPager, R.string.first_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
1145             } else {  // Privacy mode.
1146                 Snackbar.make(webViewPager, R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
1147             }
1148
1149             // Reload the current WebView.
1150             currentWebView.reload();
1151
1152             // Consume the event.
1153             return true;
1154         } else if (menuItemId == R.id.toggle_third_party_cookies) {  // Third-party cookies.
1155             // Only act if the API >= 21.  Otherwise, there are no third-party cookie controls.
1156             if (Build.VERSION.SDK_INT >= 21) {
1157                 // Toggle the status of thirdPartyCookiesEnabled.
1158                 cookieManager.setAcceptThirdPartyCookies(currentWebView, !cookieManager.acceptThirdPartyCookies(currentWebView));
1159
1160                 // Update the menu checkbox.
1161                 menuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView));
1162
1163                 // Display a snackbar.
1164                 if (cookieManager.acceptThirdPartyCookies(currentWebView)) {
1165                     Snackbar.make(webViewPager, R.string.third_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
1166                 } else {
1167                     Snackbar.make(webViewPager, R.string.third_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
1168                 }
1169
1170                 // Reload the current WebView.
1171                 currentWebView.reload();
1172             }
1173
1174             // Consume the event.
1175             return true;
1176         } else if (menuItemId == R.id.toggle_dom_storage) {  // DOM storage.
1177             // Toggle the status of domStorageEnabled.
1178             currentWebView.getSettings().setDomStorageEnabled(!currentWebView.getSettings().getDomStorageEnabled());
1179
1180             // Update the menu checkbox.
1181             menuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled());
1182
1183             // Update the privacy icon.
1184             updatePrivacyIcons(true);
1185
1186             // Display a snackbar.
1187             if (currentWebView.getSettings().getDomStorageEnabled()) {
1188                 Snackbar.make(webViewPager, R.string.dom_storage_enabled, Snackbar.LENGTH_SHORT).show();
1189             } else {
1190                 Snackbar.make(webViewPager, R.string.dom_storage_disabled, Snackbar.LENGTH_SHORT).show();
1191             }
1192
1193             // Reload the current WebView.
1194             currentWebView.reload();
1195
1196             // Consume the event.
1197             return true;
1198         } else if (menuItemId == R.id.toggle_save_form_data) {  // Form data.  This can be removed once the minimum API >= 26.
1199             // Switch the status of saveFormDataEnabled.
1200             currentWebView.getSettings().setSaveFormData(!currentWebView.getSettings().getSaveFormData());
1201
1202             // Update the menu checkbox.
1203             menuItem.setChecked(currentWebView.getSettings().getSaveFormData());
1204
1205             // Display a snackbar.
1206             if (currentWebView.getSettings().getSaveFormData()) {
1207                 Snackbar.make(webViewPager, R.string.form_data_enabled, Snackbar.LENGTH_SHORT).show();
1208             } else {
1209                 Snackbar.make(webViewPager, R.string.form_data_disabled, Snackbar.LENGTH_SHORT).show();
1210             }
1211
1212             // Update the privacy icon.
1213             updatePrivacyIcons(true);
1214
1215             // Reload the current WebView.
1216             currentWebView.reload();
1217
1218             // Consume the event.
1219             return true;
1220         } else if (menuItemId == R.id.clear_cookies) {  // Clear cookies.
1221             // Create a snackbar.
1222             Snackbar.make(webViewPager, R.string.cookies_deleted, Snackbar.LENGTH_LONG)
1223                     .setAction(R.string.undo, v -> {
1224                         // Do nothing because everything will be handled by `onDismissed()` below.
1225                     })
1226                     .addCallback(new Snackbar.Callback() {
1227                         @Override
1228                         public void onDismissed(Snackbar snackbar, int event) {
1229                             if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) {  // The snackbar was dismissed without the undo button being pushed.
1230                                 // Delete the cookies, which command varies by SDK.
1231                                 if (Build.VERSION.SDK_INT < 21) {
1232                                     cookieManager.removeAllCookie();
1233                                 } else {
1234                                     cookieManager.removeAllCookies(null);
1235                                 }
1236                             }
1237                         }
1238                     })
1239                     .show();
1240
1241             // Consume the event.
1242             return true;
1243         } else if (menuItemId == R.id.clear_dom_storage) {  // Clear DOM storage.
1244             // Create a snackbar.
1245             Snackbar.make(webViewPager, R.string.dom_storage_deleted, Snackbar.LENGTH_LONG)
1246                     .setAction(R.string.undo, v -> {
1247                         // Do nothing because everything will be handled by `onDismissed()` below.
1248                     })
1249                     .addCallback(new Snackbar.Callback() {
1250                         @Override
1251                         public void onDismissed(Snackbar snackbar, int event) {
1252                             if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) {  // The snackbar was dismissed without the undo button being pushed.
1253                                 // Delete the DOM Storage.
1254                                 WebStorage webStorage = WebStorage.getInstance();
1255                                 webStorage.deleteAllData();
1256
1257                                 // Initialize a handler to manually delete the DOM storage files and directories.
1258                                 Handler deleteDomStorageHandler = new Handler();
1259
1260                                 // Setup a runnable to manually delete the DOM storage files and directories.
1261                                 Runnable deleteDomStorageRunnable = () -> {
1262                                     try {
1263                                         // Get a handle for the runtime.
1264                                         Runtime runtime = Runtime.getRuntime();
1265
1266                                         // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
1267                                         // which links to `/data/data/com.stoutner.privacybrowser.standard`.
1268                                         String privateDataDirectoryString = getApplicationInfo().dataDir;
1269
1270                                         // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
1271                                         Process deleteLocalStorageProcess = runtime.exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
1272
1273                                         // Multiple commands must be used because `Runtime.exec()` does not like `*`.
1274                                         Process deleteIndexProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
1275                                         Process deleteQuotaManagerProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
1276                                         Process deleteQuotaManagerJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
1277                                         Process deleteDatabasesProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
1278
1279                                         // Wait for the processes to finish.
1280                                         deleteLocalStorageProcess.waitFor();
1281                                         deleteIndexProcess.waitFor();
1282                                         deleteQuotaManagerProcess.waitFor();
1283                                         deleteQuotaManagerJournalProcess.waitFor();
1284                                         deleteDatabasesProcess.waitFor();
1285                                     } catch (Exception exception) {
1286                                         // Do nothing if an error is thrown.
1287                                     }
1288                                 };
1289
1290                                 // Manually delete the DOM storage files after 200 milliseconds.
1291                                 deleteDomStorageHandler.postDelayed(deleteDomStorageRunnable, 200);
1292                             }
1293                         }
1294                     })
1295                     .show();
1296
1297             // Consume the event.
1298             return true;
1299         } else if (menuItemId == R.id.clear_form_data) {  // Clear form data.  This can be remove once the minimum API >= 26.
1300             // Create a snackbar.
1301             Snackbar.make(webViewPager, R.string.form_data_deleted, Snackbar.LENGTH_LONG)
1302                     .setAction(R.string.undo, v -> {
1303                         // Do nothing because everything will be handled by `onDismissed()` below.
1304                     })
1305                     .addCallback(new Snackbar.Callback() {
1306                         @Override
1307                         public void onDismissed(Snackbar snackbar, int event) {
1308                             if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) {  // The snackbar was dismissed without the undo button being pushed.
1309                                 // Get a handle for the webView database.
1310                                 WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(getApplicationContext());
1311
1312                                 // Delete the form data.
1313                                 webViewDatabase.clearFormData();
1314                             }
1315                         }
1316                     })
1317                     .show();
1318
1319             // Consume the event.
1320             return true;
1321         } else if (menuItemId == R.id.easylist) {  // EasyList.
1322             // Toggle the EasyList status.
1323             currentWebView.enableBlocklist(NestedScrollWebView.EASYLIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.EASYLIST));
1324
1325             // Update the menu checkbox.
1326             menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASYLIST));
1327
1328             // Reload the current WebView.
1329             currentWebView.reload();
1330
1331             // Consume the event.
1332             return true;
1333         } else if (menuItemId == R.id.easyprivacy) {  // EasyPrivacy.
1334             // Toggle the EasyPrivacy status.
1335             currentWebView.enableBlocklist(NestedScrollWebView.EASYPRIVACY, !currentWebView.isBlocklistEnabled(NestedScrollWebView.EASYPRIVACY));
1336
1337             // Update the menu checkbox.
1338             menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASYPRIVACY));
1339
1340             // Reload the current WebView.
1341             currentWebView.reload();
1342
1343             // Consume the event.
1344             return true;
1345         } else if (menuItemId == R.id.fanboys_annoyance_list) {  // Fanboy's Annoyance List.
1346             // Toggle Fanboy's Annoyance List status.
1347             currentWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1348
1349             // Update the menu checkbox.
1350             menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1351
1352             // Get a handle for the Fanboy's Social Block List menu item.
1353             MenuItem fanboysSocialBlockingListMenuItem = optionsMenu.findItem(R.id.fanboys_social_blocking_list);
1354
1355             // Update the staus of Fanboy's Social Blocking List.
1356             fanboysSocialBlockingListMenuItem.setEnabled(!currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1357
1358             // Reload the current WebView.
1359             currentWebView.reload();
1360
1361             // Consume the event.
1362             return true;
1363         } else if (menuItemId == R.id.fanboys_social_blocking_list) {  // Fanboy's Social Blocking List.
1364             // Toggle Fanboy's Social Blocking List status.
1365             currentWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
1366
1367             // Update the menu checkbox.
1368             menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
1369
1370             // Reload the current WebView.
1371             currentWebView.reload();
1372
1373             // Consume the event.
1374             return true;
1375         } else if (menuItemId == R.id.ultralist) {  // UltraList.
1376             // Toggle the UltraList status.
1377             currentWebView.enableBlocklist(NestedScrollWebView.ULTRALIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRALIST));
1378
1379             // Update the menu checkbox.
1380             menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRALIST));
1381
1382             // Reload the current WebView.
1383             currentWebView.reload();
1384
1385             // Consume the event.
1386             return true;
1387         } else if (menuItemId == R.id.ultraprivacy) {  // UltraPrivacy.
1388             // Toggle the UltraPrivacy status.
1389             currentWebView.enableBlocklist(NestedScrollWebView.ULTRAPRIVACY, !currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRAPRIVACY));
1390
1391             // Update the menu checkbox.
1392             menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRAPRIVACY));
1393
1394             // Reload the current WebView.
1395             currentWebView.reload();
1396
1397             // Consume the event.
1398             return true;
1399         } else if (menuItemId == R.id.block_all_third_party_requests) {  // Block all third-party requests.
1400             //Toggle the third-party requests blocker status.
1401             currentWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS, !currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
1402
1403             // Update the menu checkbox.
1404             menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
1405
1406             // Reload the current WebView.
1407             currentWebView.reload();
1408
1409             // Consume the event.
1410             return true;
1411         } else if (menuItemId == R.id.proxy_none) {  // Proxy - None.
1412             // Update the proxy mode.
1413             proxyMode = ProxyHelper.NONE;
1414
1415             // Apply the proxy mode.
1416             applyProxy(true);
1417
1418             // Consume the event.
1419             return true;
1420         } else if (menuItemId == R.id.proxy_tor) {  // Proxy - Tor.
1421             // Update the proxy mode.
1422             proxyMode = ProxyHelper.TOR;
1423
1424             // Apply the proxy mode.
1425             applyProxy(true);
1426
1427             // Consume the event.
1428             return true;
1429         } else if (menuItemId == R.id.proxy_i2p) {  // Proxy - I2P.
1430             // Update the proxy mode.
1431             proxyMode = ProxyHelper.I2P;
1432
1433             // Apply the proxy mode.
1434             applyProxy(true);
1435
1436             // Consume the event.
1437             return true;
1438         } else if (menuItemId == R.id.proxy_custom) {  // Proxy - Custom.
1439             // Update the proxy mode.
1440             proxyMode = ProxyHelper.CUSTOM;
1441
1442             // Apply the proxy mode.
1443             applyProxy(true);
1444
1445             // Consume the event.
1446             return true;
1447         } else if (menuItemId == R.id.user_agent_privacy_browser) {  // User Agent - Privacy Browser.
1448             // Update the user agent.
1449             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[0]);
1450
1451             // Reload the current WebView.
1452             currentWebView.reload();
1453
1454             // Consume the event.
1455             return true;
1456         } else if (menuItemId == R.id.user_agent_webview_default) {  // User Agent - WebView Default.
1457             // Update the user agent.
1458             currentWebView.getSettings().setUserAgentString("");
1459
1460             // Reload the current WebView.
1461             currentWebView.reload();
1462
1463             // Consume the event.
1464             return true;
1465         } else if (menuItemId == R.id.user_agent_firefox_on_android) {  // User Agent - Firefox on Android.
1466             // Update the user agent.
1467             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[2]);
1468
1469             // Reload the current WebView.
1470             currentWebView.reload();
1471
1472             // Consume the event.
1473             return true;
1474         } else if (menuItemId == R.id.user_agent_chrome_on_android) {  // User Agent - Chrome on Android.
1475             // Update the user agent.
1476             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[3]);
1477
1478             // Reload the current WebView.
1479             currentWebView.reload();
1480
1481             // Consume the event.
1482             return true;
1483         } else if (menuItemId == R.id.user_agent_safari_on_ios) {  // User Agent - Safari on iOS.
1484             // Update the user agent.
1485             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[4]);
1486
1487             // Reload the current WebView.
1488             currentWebView.reload();
1489
1490             // Consume the event.
1491             return true;
1492         } else if (menuItemId == R.id.user_agent_firefox_on_linux) {  // User Agent - Firefox on Linux.
1493             // Update the user agent.
1494             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[5]);
1495
1496             // Reload the current WebView.
1497             currentWebView.reload();
1498
1499             // Consume the event.
1500             return true;
1501         } else if (menuItemId == R.id.user_agent_chromium_on_linux) {  // User Agent - Chromium on Linux.
1502             // Update the user agent.
1503             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[6]);
1504
1505             // Reload the current WebView.
1506             currentWebView.reload();
1507
1508             // Consume the event.
1509             return true;
1510         } else if (menuItemId == R.id.user_agent_firefox_on_windows) {  // User Agent - Firefox on Windows.
1511             // Update the user agent.
1512             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[7]);
1513
1514             // Reload the current WebView.
1515             currentWebView.reload();
1516
1517             // Consume the event.
1518             return true;
1519         } else if (menuItemId == R.id.user_agent_chrome_on_windows) {  // User Agent - Chrome on Windows.
1520             // Update the user agent.
1521             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[8]);
1522
1523             // Reload the current WebView.
1524             currentWebView.reload();
1525
1526             // Consume the event.
1527             return true;
1528         } else if (menuItemId == R.id.user_agent_edge_on_windows) {  // User Agent - Edge on Windows.
1529             // Update the user agent.
1530             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[9]);
1531
1532             // Reload the current WebView.
1533             currentWebView.reload();
1534
1535             // Consume the event.
1536             return true;
1537         } else if (menuItemId == R.id.user_agent_internet_explorer_on_windows) {  // User Agent - Internet Explorer on Windows.
1538             // Update the user agent.
1539             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[10]);
1540
1541             // Reload the current WebView.
1542             currentWebView.reload();
1543
1544             // Consume the event.
1545             return true;
1546         } else if (menuItemId == R.id.user_agent_safari_on_macos) {  // User Agent - Safari on macOS.
1547             // Update the user agent.
1548             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[11]);
1549
1550             // Reload the current WebView.
1551             currentWebView.reload();
1552
1553             // Consume the event.
1554             return true;
1555         } else if (menuItemId == R.id.user_agent_custom) {  // User Agent - Custom.
1556             // Update the user agent.
1557             currentWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
1558
1559             // Reload the current WebView.
1560             currentWebView.reload();
1561
1562             // Consume the event.
1563             return true;
1564         } else if (menuItemId == R.id.font_size) {  // Font size.
1565             // Instantiate the font size dialog.
1566             DialogFragment fontSizeDialogFragment = FontSizeDialog.displayDialog(currentWebView.getSettings().getTextZoom());
1567
1568             // Show the font size dialog.
1569             fontSizeDialogFragment.show(getSupportFragmentManager(), getString(R.string.font_size));
1570
1571             // Consume the event.
1572             return true;
1573         } else if (menuItemId == R.id.swipe_to_refresh) {  // Swipe to refresh.
1574             // Toggle the stored status of swipe to refresh.
1575             currentWebView.setSwipeToRefresh(!currentWebView.getSwipeToRefresh());
1576
1577             // Get a handle for the swipe refresh layout.
1578             SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
1579
1580             // Update the swipe refresh layout.
1581             if (currentWebView.getSwipeToRefresh()) {  // Swipe to refresh is enabled.
1582                 // Only enable the swipe refresh layout if the WebView is scrolled to the top.  It is updated every time the scroll changes.
1583                 swipeRefreshLayout.setEnabled(currentWebView.getY() == 0);
1584             } else {  // Swipe to refresh is disabled.
1585                 // Disable the swipe refresh layout.
1586                 swipeRefreshLayout.setEnabled(false);
1587             }
1588
1589             // Consume the event.
1590             return true;
1591         } else if (menuItemId == R.id.wide_viewport) {  // Wide viewport.
1592             // Toggle the viewport.
1593             currentWebView.getSettings().setUseWideViewPort(!currentWebView.getSettings().getUseWideViewPort());
1594
1595             // Consume the event.
1596             return true;
1597         } else if (menuItemId == R.id.display_images) {  // Display images.
1598             // Toggle the displaying of images.
1599             if (currentWebView.getSettings().getLoadsImagesAutomatically()) {  // Images are currently loaded automatically.
1600                 // Disable loading of images.
1601                 currentWebView.getSettings().setLoadsImagesAutomatically(false);
1602
1603                 // Reload the website to remove existing images.
1604                 currentWebView.reload();
1605             } else {  // Images are not currently loaded automatically.
1606                 // Enable loading of images.  Missing images will be loaded without the need for a reload.
1607                 currentWebView.getSettings().setLoadsImagesAutomatically(true);
1608             }
1609
1610             // Consume the event.
1611             return true;
1612         } else if (menuItemId == R.id.dark_webview) {  // Dark WebView.
1613             // Check to see if dark WebView is supported by this WebView.
1614             if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
1615                 // Toggle the dark WebView setting.
1616                 if (WebSettingsCompat.getForceDark(currentWebView.getSettings()) == WebSettingsCompat.FORCE_DARK_ON) {  // Dark WebView is currently enabled.
1617                     // Turn off dark WebView.
1618                     WebSettingsCompat.setForceDark(currentWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
1619                 } else {  // Dark WebView is currently disabled.
1620                     // Turn on dark WebView.
1621                     WebSettingsCompat.setForceDark(currentWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
1622                 }
1623             }
1624
1625             // Consume the event.
1626             return true;
1627         } else if (menuItemId == R.id.find_on_page) {  // Find on page.
1628             // Get a handle for the views.
1629             Toolbar toolbar = findViewById(R.id.toolbar);
1630             LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
1631             EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
1632
1633             // Set the minimum height of the find on page linear layout to match the toolbar.
1634             findOnPageLinearLayout.setMinimumHeight(toolbar.getHeight());
1635
1636             // Hide the toolbar.
1637             toolbar.setVisibility(View.GONE);
1638
1639             // Show the find on page linear layout.
1640             findOnPageLinearLayout.setVisibility(View.VISIBLE);
1641
1642             // Display the keyboard.  The app must wait 200 ms before running the command to work around a bug in Android.
1643             // http://stackoverflow.com/questions/5520085/android-show-softkeyboard-with-showsoftinput-is-not-working
1644             findOnPageEditText.postDelayed(() -> {
1645                 // Set the focus on the find on page edit text.
1646                 findOnPageEditText.requestFocus();
1647
1648                 // Get a handle for the input method manager.
1649                 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
1650
1651                 // Remove the lint warning below that the input method manager might be null.
1652                 assert inputMethodManager != null;
1653
1654                 // Display the keyboard.  `0` sets no input flags.
1655                 inputMethodManager.showSoftInput(findOnPageEditText, 0);
1656             }, 200);
1657
1658             // Consume the event.
1659             return true;
1660         } else if (menuItemId == R.id.print) {  // Print.
1661             // Get a print manager instance.
1662             PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);
1663
1664             // Remove the lint error below that print manager might be null.
1665             assert printManager != null;
1666
1667             // Create a print document adapter from the current WebView.
1668             PrintDocumentAdapter printDocumentAdapter = currentWebView.createPrintDocumentAdapter();
1669
1670             // Print the document.
1671             printManager.print(getString(R.string.privacy_browser_web_page), printDocumentAdapter, null);
1672
1673             // Consume the event.
1674             return true;
1675         } else if (menuItemId == R.id.save_url) {  // Save URL.
1676             // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
1677             new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
1678                     currentWebView.getAcceptFirstPartyCookies()).execute(currentWebView.getCurrentUrl());
1679
1680             // Consume the event.
1681             return true;
1682         } else if (menuItemId == R.id.save_archive) {  // Save archive.
1683             // Instantiate the save dialog.
1684             DialogFragment saveArchiveFragment = SaveWebpageDialog.saveWebpage(StoragePermissionDialog.SAVE_ARCHIVE, null, null, getString(R.string.webpage_mht), null,
1685                     false);
1686
1687             // Show the save dialog.  It must be named `save_dialog` so that the file picker can update the file name.
1688             saveArchiveFragment.show(getSupportFragmentManager(), getString(R.string.save_dialog));
1689
1690             // Consume the event.
1691             return true;
1692         } else if (menuItemId == R.id.save_image) {  // Save image.
1693             // Instantiate the save dialog.
1694             DialogFragment saveImageFragment = SaveWebpageDialog.saveWebpage(StoragePermissionDialog.SAVE_IMAGE, null, null, getString(R.string.webpage_png), null,
1695                     false);
1696
1697             // Show the save dialog.  It must be named `save_dialog` so that the file picker can update the file name.
1698             saveImageFragment.show(getSupportFragmentManager(), getString(R.string.save_dialog));
1699
1700             // Consume the event.
1701             return true;
1702         } else if (menuItemId == R.id.add_to_homescreen) {  // Add to homescreen.
1703             // Instantiate the create home screen shortcut dialog.
1704             DialogFragment createHomeScreenShortcutDialogFragment = CreateHomeScreenShortcutDialog.createDialog(currentWebView.getTitle(), currentWebView.getUrl(),
1705                     currentWebView.getFavoriteOrDefaultIcon());
1706
1707             // Show the create home screen shortcut dialog.
1708             createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getString(R.string.create_shortcut));
1709
1710             // Consume the event.
1711             return true;
1712         } else if (menuItemId == R.id.view_source) {  // View source.
1713             // Create an intent to launch the view source activity.
1714             Intent viewSourceIntent = new Intent(this, ViewSourceActivity.class);
1715
1716             // Add the variables to the intent.
1717             viewSourceIntent.putExtra(ViewSourceActivityKt.CURRENT_URL, currentWebView.getUrl());
1718             viewSourceIntent.putExtra(ViewSourceActivityKt.USER_AGENT, currentWebView.getSettings().getUserAgentString());
1719
1720             // Make it so.
1721             startActivity(viewSourceIntent);
1722
1723             // Consume the event.
1724             return true;
1725         } else if (menuItemId == R.id.share_url) {  // Share URL.
1726             // Setup the share string.
1727             String shareString = currentWebView.getTitle() + " – " + currentWebView.getUrl();
1728
1729             // Create the share intent.
1730             Intent shareIntent = new Intent(Intent.ACTION_SEND);
1731
1732             // Add the share string to the intent.
1733             shareIntent.putExtra(Intent.EXTRA_TEXT, shareString);
1734
1735             // Set the MIME type.
1736             shareIntent.setType("text/plain");
1737
1738             // Set the intent to open in a new task.
1739             shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1740
1741             // Make it so.
1742             startActivity(Intent.createChooser(shareIntent, getString(R.string.share_url)));
1743
1744             // Consume the event.
1745             return true;
1746         } else if (menuItemId == R.id.open_with_app) {  // Open with app.
1747             // Open the URL with an outside app.
1748             openWithApp(currentWebView.getUrl());
1749
1750             // Consume the event.
1751             return true;
1752         } else if (menuItemId == R.id.open_with_browser) {  // Open with browser.
1753             // Open the URL with an outside browser.
1754             openWithBrowser(currentWebView.getUrl());
1755
1756             // Consume the event.
1757             return true;
1758         } else if (menuItemId == R.id.add_or_edit_domain) {  // Add or edit domain.
1759             // Check if domain settings currently exist.
1760             if (currentWebView.getDomainSettingsApplied()) {  // Edit the current domain settings.
1761                 // Reapply the domain settings on returning to `MainWebViewActivity`.
1762                 reapplyDomainSettingsOnRestart = true;
1763
1764                 // Create an intent to launch the domains activity.
1765                 Intent domainsIntent = new Intent(this, DomainsActivity.class);
1766
1767                 // Add the extra information to the intent.
1768                 domainsIntent.putExtra("load_domain", currentWebView.getDomainSettingsDatabaseId());
1769                 domainsIntent.putExtra("close_on_back", true);
1770                 domainsIntent.putExtra("current_url", currentWebView.getUrl());
1771
1772                 // Get the current certificate.
1773                 SslCertificate sslCertificate = currentWebView.getCertificate();
1774
1775                 // Check to see if the SSL certificate is populated.
1776                 if (sslCertificate != null) {
1777                     // Extract the certificate to strings.
1778                     String issuedToCName = sslCertificate.getIssuedTo().getCName();
1779                     String issuedToOName = sslCertificate.getIssuedTo().getOName();
1780                     String issuedToUName = sslCertificate.getIssuedTo().getUName();
1781                     String issuedByCName = sslCertificate.getIssuedBy().getCName();
1782                     String issuedByOName = sslCertificate.getIssuedBy().getOName();
1783                     String issuedByUName = sslCertificate.getIssuedBy().getUName();
1784                     long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
1785                     long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
1786
1787                     // Add the certificate to the intent.
1788                     domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
1789                     domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
1790                     domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
1791                     domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
1792                     domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
1793                     domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
1794                     domainsIntent.putExtra("ssl_start_date", startDateLong);
1795                     domainsIntent.putExtra("ssl_end_date", endDateLong);
1796                 }
1797
1798                 // Check to see if the current IP addresses have been received.
1799                 if (currentWebView.hasCurrentIpAddresses()) {
1800                     // Add the current IP addresses to the intent.
1801                     domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
1802                 }
1803
1804                 // Make it so.
1805                 startActivity(domainsIntent);
1806             } else {  // Add a new domain.
1807                 // Apply the new domain settings on returning to `MainWebViewActivity`.
1808                 reapplyDomainSettingsOnRestart = true;
1809
1810                 // Get the current domain
1811                 Uri currentUri = Uri.parse(currentWebView.getUrl());
1812                 String currentDomain = currentUri.getHost();
1813
1814                 // Initialize the database handler.  The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
1815                 DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
1816
1817                 // Create the domain and store the database ID.
1818                 int newDomainDatabaseId = domainsDatabaseHelper.addDomain(currentDomain);
1819
1820                 // Create an intent to launch the domains activity.
1821                 Intent domainsIntent = new Intent(this, DomainsActivity.class);
1822
1823                 // Add the extra information to the intent.
1824                 domainsIntent.putExtra("load_domain", newDomainDatabaseId);
1825                 domainsIntent.putExtra("close_on_back", true);
1826                 domainsIntent.putExtra("current_url", currentWebView.getUrl());
1827
1828                 // Get the current certificate.
1829                 SslCertificate sslCertificate = currentWebView.getCertificate();
1830
1831                 // Check to see if the SSL certificate is populated.
1832                 if (sslCertificate != null) {
1833                     // Extract the certificate to strings.
1834                     String issuedToCName = sslCertificate.getIssuedTo().getCName();
1835                     String issuedToOName = sslCertificate.getIssuedTo().getOName();
1836                     String issuedToUName = sslCertificate.getIssuedTo().getUName();
1837                     String issuedByCName = sslCertificate.getIssuedBy().getCName();
1838                     String issuedByOName = sslCertificate.getIssuedBy().getOName();
1839                     String issuedByUName = sslCertificate.getIssuedBy().getUName();
1840                     long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
1841                     long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
1842
1843                     // Add the certificate to the intent.
1844                     domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
1845                     domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
1846                     domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
1847                     domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
1848                     domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
1849                     domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
1850                     domainsIntent.putExtra("ssl_start_date", startDateLong);
1851                     domainsIntent.putExtra("ssl_end_date", endDateLong);
1852                 }
1853
1854                 // Check to see if the current IP addresses have been received.
1855                 if (currentWebView.hasCurrentIpAddresses()) {
1856                     // Add the current IP addresses to the intent.
1857                     domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
1858                 }
1859
1860                 // Make it so.
1861                 startActivity(domainsIntent);
1862             }
1863
1864             // Consume the event.
1865             return true;
1866         } else if (menuItemId == R.id.ad_consent) {  // Ad consent.
1867             // Instantiate the ad consent dialog.
1868             DialogFragment adConsentDialogFragment = new AdConsentDialog();
1869
1870             // Display the ad consent dialog.
1871             adConsentDialogFragment.show(getSupportFragmentManager(), getString(R.string.ad_consent));
1872
1873             // Consume the event.
1874             return true;
1875         } else {  // There is no match with the options menu.  Pass the event up to the parent method.
1876             // Don't consume the event.
1877             return super.onOptionsItemSelected(menuItem);
1878         }
1879     }
1880
1881     // removeAllCookies is deprecated, but it is required for API < 21.
1882     @Override
1883     public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
1884         // Get a handle for the shared preferences.
1885         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
1886
1887         // Get the menu item ID.
1888         int menuItemId = menuItem.getItemId();
1889
1890         // Run the commands that correspond to the selected menu item.
1891         if (menuItemId == R.id.clear_and_exit) {  // Clear and exit.
1892             // Clear and exit Privacy Browser.
1893             clearAndExit();
1894         } else if (menuItemId == R.id.home) {  // Home.
1895             // Load the homepage.
1896             loadUrl(currentWebView, sharedPreferences.getString("homepage", getString(R.string.homepage_default_value)));
1897         } else if (menuItemId == R.id.back) {  // Back.
1898             // Check if the WebView can go back.
1899             if (currentWebView.canGoBack()) {
1900                 // Get the current web back forward list.
1901                 WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList();
1902
1903                 // Get the previous entry URL.
1904                 String previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.getCurrentIndex() - 1).getUrl();
1905
1906                 // Apply the domain settings.
1907                 applyDomainSettings(currentWebView, previousUrl, false, false, false);
1908
1909                 // Load the previous website in the history.
1910                 currentWebView.goBack();
1911             }
1912         } else if (menuItemId == R.id.forward) {  // Forward.
1913             // Check if the WebView can go forward.
1914             if (currentWebView.canGoForward()) {
1915                 // Get the current web back forward list.
1916                 WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList();
1917
1918                 // Get the next entry URL.
1919                 String nextUrl = webBackForwardList.getItemAtIndex(webBackForwardList.getCurrentIndex() + 1).getUrl();
1920
1921                 // Apply the domain settings.
1922                 applyDomainSettings(currentWebView, nextUrl, false, false, false);
1923
1924                 // Load the next website in the history.
1925                 currentWebView.goForward();
1926             }
1927         } else if (menuItemId == R.id.history) {  // History.
1928             // Instantiate the URL history dialog.
1929             DialogFragment urlHistoryDialogFragment = UrlHistoryDialog.loadBackForwardList(currentWebView.getWebViewFragmentId());
1930
1931             // Show the URL history dialog.
1932             urlHistoryDialogFragment.show(getSupportFragmentManager(), getString(R.string.history));
1933         } else if (menuItemId == R.id.open) {  // Open.
1934             // Instantiate the open file dialog.
1935             DialogFragment openDialogFragment = new OpenDialog();
1936
1937             // Show the open file dialog.
1938             openDialogFragment.show(getSupportFragmentManager(), getString(R.string.open));
1939         } else if (menuItemId == R.id.requests) {  // Requests.
1940             // Populate the resource requests.
1941             RequestsActivity.resourceRequests = currentWebView.getResourceRequests();
1942
1943             // Create an intent to launch the Requests activity.
1944             Intent requestsIntent = new Intent(this, RequestsActivity.class);
1945
1946             // Add the block third-party requests status to the intent.
1947             requestsIntent.putExtra("block_all_third_party_requests", currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
1948
1949             // Make it so.
1950             startActivity(requestsIntent);
1951         } else if (menuItemId == R.id.downloads) {  // Downloads.
1952             // Launch the system Download Manager.
1953             Intent downloadManagerIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
1954
1955             // Launch as a new task so that Download Manager and Privacy Browser show as separate windows in the recent tasks list.
1956             downloadManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1957
1958             // Make it so.
1959             startActivity(downloadManagerIntent);
1960         } else if (menuItemId == R.id.domains) {  // Domains.
1961             // Set the flag to reapply the domain settings on restart when returning from Domain Settings.
1962             reapplyDomainSettingsOnRestart = true;
1963
1964             // Launch the domains activity.
1965             Intent domainsIntent = new Intent(this, DomainsActivity.class);
1966
1967             // Add the extra information to the intent.
1968             domainsIntent.putExtra("current_url", currentWebView.getUrl());
1969
1970             // Get the current certificate.
1971             SslCertificate sslCertificate = currentWebView.getCertificate();
1972
1973             // Check to see if the SSL certificate is populated.
1974             if (sslCertificate != null) {
1975                 // Extract the certificate to strings.
1976                 String issuedToCName = sslCertificate.getIssuedTo().getCName();
1977                 String issuedToOName = sslCertificate.getIssuedTo().getOName();
1978                 String issuedToUName = sslCertificate.getIssuedTo().getUName();
1979                 String issuedByCName = sslCertificate.getIssuedBy().getCName();
1980                 String issuedByOName = sslCertificate.getIssuedBy().getOName();
1981                 String issuedByUName = sslCertificate.getIssuedBy().getUName();
1982                 long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
1983                 long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
1984
1985                 // Add the certificate to the intent.
1986                 domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
1987                 domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
1988                 domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
1989                 domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
1990                 domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
1991                 domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
1992                 domainsIntent.putExtra("ssl_start_date", startDateLong);
1993                 domainsIntent.putExtra("ssl_end_date", endDateLong);
1994             }
1995
1996             // Check to see if the current IP addresses have been received.
1997             if (currentWebView.hasCurrentIpAddresses()) {
1998                 // Add the current IP addresses to the intent.
1999                 domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
2000             }
2001
2002             // Make it so.
2003             startActivity(domainsIntent);
2004         } else if (menuItemId == R.id.settings) {  // Settings.
2005             // Set the flag to reapply app settings on restart when returning from Settings.
2006             reapplyAppSettingsOnRestart = true;
2007
2008             // Set the flag to reapply the domain settings on restart when returning from Settings.
2009             reapplyDomainSettingsOnRestart = true;
2010
2011             // Launch the settings activity.
2012             Intent settingsIntent = new Intent(this, SettingsActivity.class);
2013             startActivity(settingsIntent);
2014         } else if (menuItemId == R.id.import_export) { // Import/Export.
2015             // Create an intent to launch the import/export activity.
2016             Intent importExportIntent = new Intent(this, ImportExportActivity.class);
2017
2018             // Make it so.
2019             startActivity(importExportIntent);
2020         } else if (menuItemId == R.id.logcat) {  // Logcat.
2021             // Create an intent to launch the logcat activity.
2022             Intent logcatIntent = new Intent(this, LogcatActivity.class);
2023
2024             // Make it so.
2025             startActivity(logcatIntent);
2026         } else if (menuItemId == R.id.guide) {  // Guide.
2027             // Create an intent to launch the guide activity.
2028             Intent guideIntent = new Intent(this, GuideActivity.class);
2029
2030             // Make it so.
2031             startActivity(guideIntent);
2032         } else if (menuItemId == R.id.about) {  // About
2033             // Create an intent to launch the about activity.
2034             Intent aboutIntent = new Intent(this, AboutActivity.class);
2035
2036             // Create a string array for the blocklist versions.
2037             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],
2038                     ultraList.get(0).get(0)[0], ultraPrivacy.get(0).get(0)[0]};
2039
2040             // Add the blocklist versions to the intent.
2041             aboutIntent.putExtra("blocklist_versions", blocklistVersions);
2042
2043             // Make it so.
2044             startActivity(aboutIntent);
2045         }
2046
2047         // Close the navigation drawer.
2048         drawerLayout.closeDrawer(GravityCompat.START);
2049         return true;
2050     }
2051
2052     @Override
2053     public void onPostCreate(Bundle savedInstanceState) {
2054         // Run the default commands.
2055         super.onPostCreate(savedInstanceState);
2056
2057         // Sync the state of the DrawerToggle after the default `onRestoreInstanceState()` has finished.  This creates the navigation drawer icon.
2058         actionBarDrawerToggle.syncState();
2059     }
2060
2061     @Override
2062     public void onConfigurationChanged(@NonNull Configuration newConfig) {
2063         // Run the default commands.
2064         super.onConfigurationChanged(newConfig);
2065
2066         // Reload the ad for the free flavor if not in full screen mode.
2067         if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
2068             // Reload the ad.  The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
2069             AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
2070         }
2071
2072         // `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:
2073         // https://code.google.com/p/android/issues/detail?id=20493#c8
2074         // ActivityCompat.invalidateOptionsMenu(this);
2075     }
2076
2077     @Override
2078     public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
2079         // Get the hit test result.
2080         final WebView.HitTestResult hitTestResult = currentWebView.getHitTestResult();
2081
2082         // Define the URL strings.
2083         final String imageUrl;
2084         final String linkUrl;
2085
2086         // Get handles for the system managers.
2087         final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
2088
2089         // Remove the lint errors below that the clipboard manager might be null.
2090         assert clipboardManager != null;
2091
2092         // Process the link according to the type.
2093         switch (hitTestResult.getType()) {
2094             // `SRC_ANCHOR_TYPE` is a link.
2095             case WebView.HitTestResult.SRC_ANCHOR_TYPE:
2096                 // Get the target URL.
2097                 linkUrl = hitTestResult.getExtra();
2098
2099                 // Set the target URL as the title of the `ContextMenu`.
2100                 menu.setHeaderTitle(linkUrl);
2101
2102                 // Add an Open in New Tab entry.
2103                 menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
2104                     // Load the link URL in a new tab and move to it.
2105                     addNewTab(linkUrl, true);
2106
2107                     // Consume the event.
2108                     return true;
2109                 });
2110
2111                 // Add an Open in Background entry.
2112                 menu.add(R.string.open_in_background).setOnMenuItemClickListener((MenuItem item) -> {
2113                     // Load the link URL in a new tab but do not move to it.
2114                     addNewTab(linkUrl, false);
2115
2116                     // Consume the event.
2117                     return true;
2118                 });
2119
2120                 // Add an Open with App entry.
2121                 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2122                     openWithApp(linkUrl);
2123
2124                     // Consume the event.
2125                     return true;
2126                 });
2127
2128                 // Add an Open with Browser entry.
2129                 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2130                     openWithBrowser(linkUrl);
2131
2132                     // Consume the event.
2133                     return true;
2134                 });
2135
2136                 // Add a Copy URL entry.
2137                 menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
2138                     // Save the link URL in a `ClipData`.
2139                     ClipData srcAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl);
2140
2141                     // Set the `ClipData` as the clipboard's primary clip.
2142                     clipboardManager.setPrimaryClip(srcAnchorTypeClipData);
2143
2144                     // Consume the event.
2145                     return true;
2146                 });
2147
2148                 // Add a Save URL entry.
2149                 menu.add(R.string.save_url).setOnMenuItemClickListener((MenuItem item) -> {
2150                     // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
2151                     new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
2152                             currentWebView.getAcceptFirstPartyCookies()).execute(linkUrl);
2153
2154                     // Consume the event.
2155                     return true;
2156                 });
2157
2158                 // Add an empty Cancel entry, which by default closes the context menu.
2159                 menu.add(R.string.cancel);
2160                 break;
2161
2162             // `IMAGE_TYPE` is an image.
2163             case WebView.HitTestResult.IMAGE_TYPE:
2164                 // Get the image URL.
2165                 imageUrl = hitTestResult.getExtra();
2166
2167                 // Remove the incorrect lint warning below that the image URL might be null.
2168                 assert imageUrl != null;
2169
2170                 // Set the context menu title.
2171                 if (imageUrl.startsWith("data:")) {  // The image data is contained in within the URL, making it exceedingly long.
2172                     // Truncate the image URL before making it the title.
2173                     menu.setHeaderTitle(imageUrl.substring(0, 100));
2174                 } else {  // The image URL does not contain the full image data.
2175                     // Set the image URL as the title of the context menu.
2176                     menu.setHeaderTitle(imageUrl);
2177                 }
2178
2179                 // Add an Open in New Tab entry.
2180                 menu.add(R.string.open_image_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
2181                     // Load the image in a new tab.
2182                     addNewTab(imageUrl, true);
2183
2184                     // Consume the event.
2185                     return true;
2186                 });
2187
2188                 // Add an Open with App entry.
2189                 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2190                     // Open the image URL with an external app.
2191                     openWithApp(imageUrl);
2192
2193                     // Consume the event.
2194                     return true;
2195                 });
2196
2197                 // Add an Open with Browser entry.
2198                 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2199                     // Open the image URL with an external browser.
2200                     openWithBrowser(imageUrl);
2201
2202                     // Consume the event.
2203                     return true;
2204                 });
2205
2206                 // Add a View Image entry.
2207                 menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
2208                     // Load the image in the current tab.
2209                     loadUrl(currentWebView, imageUrl);
2210
2211                     // Consume the event.
2212                     return true;
2213                 });
2214
2215                 // Add a Save Image entry.
2216                 menu.add(R.string.save_image).setOnMenuItemClickListener((MenuItem item) -> {
2217                    // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
2218                     new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
2219                             currentWebView.getAcceptFirstPartyCookies()).execute(imageUrl);
2220
2221                     // Consume the event.
2222                     return true;
2223                 });
2224
2225                 // Add a Copy URL entry.
2226                 menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
2227                     // Save the image URL in a clip data.
2228                     ClipData imageTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
2229
2230                     // Set the clip data as the clipboard's primary clip.
2231                     clipboardManager.setPrimaryClip(imageTypeClipData);
2232
2233                     // Consume the event.
2234                     return true;
2235                 });
2236
2237                 // Add an empty Cancel entry, which by default closes the context menu.
2238                 menu.add(R.string.cancel);
2239                 break;
2240
2241             // `SRC_IMAGE_ANCHOR_TYPE` is an image that is also a link.
2242             case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
2243                 // Get the image URL.
2244                 imageUrl = hitTestResult.getExtra();
2245
2246                 // Instantiate a handler.
2247                 Handler handler = new Handler();
2248
2249                 // Get a message from the handler.
2250                 Message message = handler.obtainMessage();
2251
2252                 // Request the image details from the last touched node be returned in the message.
2253                 currentWebView.requestFocusNodeHref(message);
2254
2255                 // Get the link URL from the message data.
2256                 linkUrl = message.getData().getString("url");
2257
2258                 // Set the link URL as the title of the context menu.
2259                 menu.setHeaderTitle(linkUrl);
2260
2261                 // Add an Open in New Tab entry.
2262                 menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
2263                     // Load the link URL in a new tab and move to it.
2264                     addNewTab(linkUrl, true);
2265
2266                     // Consume the event.
2267                     return true;
2268                 });
2269
2270                 // Add an Open in Background entry.
2271                 menu.add(R.string.open_in_background).setOnMenuItemClickListener((MenuItem item) -> {
2272                     // Lod the link URL in a new tab but do not move to it.
2273                     addNewTab(linkUrl, false);
2274
2275                     // Consume the event.
2276                     return true;
2277                 });
2278
2279                 // Add an Open Image in New Tab entry.
2280                 menu.add(R.string.open_image_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
2281                     // Load the image in a new tab and move to it.
2282                     addNewTab(imageUrl, true);
2283
2284                     // Consume the event.
2285                     return true;
2286                 });
2287
2288                 // Add an Open with App entry.
2289                 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2290                     // Open the link URL with an external app.
2291                     openWithApp(linkUrl);
2292
2293                     // Consume the event.
2294                     return true;
2295                 });
2296
2297                 // Add an Open with Browser entry.
2298                 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2299                     // Open the link URL with an external browser.
2300                     openWithBrowser(linkUrl);
2301
2302                     // Consume the event.
2303                     return true;
2304                 });
2305
2306                 // Add a View Image entry.
2307                 menu.add(R.string.view_image).setOnMenuItemClickListener((MenuItem item) -> {
2308                    // View the image in the current tab.
2309                    loadUrl(currentWebView, imageUrl);
2310
2311                    // Consume the event.
2312                    return true;
2313                 });
2314
2315                 // Add a Save Image entry.
2316                 menu.add(R.string.save_image).setOnMenuItemClickListener((MenuItem item) -> {
2317                     // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
2318                     new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
2319                             currentWebView.getAcceptFirstPartyCookies()).execute(imageUrl);
2320
2321                     // Consume the event.
2322                     return true;
2323                 });
2324
2325                 // Add a Copy URL entry.
2326                 menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
2327                     // Save the link URL in a clip data.
2328                     ClipData srcImageAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl);
2329
2330                     // Set the clip data as the clipboard's primary clip.
2331                     clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData);
2332
2333                     // Consume the event.
2334                     return true;
2335                 });
2336
2337                 // Add a Save URL entry.
2338                 menu.add(R.string.save_url).setOnMenuItemClickListener((MenuItem item) -> {
2339                     // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
2340                     new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
2341                             currentWebView.getAcceptFirstPartyCookies()).execute(linkUrl);
2342
2343                     // Consume the event.
2344                     return true;
2345                 });
2346
2347                 // Add an empty Cancel entry, which by default closes the context menu.
2348                 menu.add(R.string.cancel);
2349                 break;
2350
2351             case WebView.HitTestResult.EMAIL_TYPE:
2352                 // Get the target URL.
2353                 linkUrl = hitTestResult.getExtra();
2354
2355                 // Set the target URL as the title of the `ContextMenu`.
2356                 menu.setHeaderTitle(linkUrl);
2357
2358                 // Add a Write Email entry.
2359                 menu.add(R.string.write_email).setOnMenuItemClickListener(item -> {
2360                     // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
2361                     Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
2362
2363                     // Parse the url and set it as the data for the `Intent`.
2364                     emailIntent.setData(Uri.parse("mailto:" + linkUrl));
2365
2366                     // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser.
2367                     emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2368
2369                     try {
2370                         // Make it so.
2371                         startActivity(emailIntent);
2372                     } catch (ActivityNotFoundException exception) {
2373                         // Display a snackbar.
2374                         Snackbar.make(currentWebView, getString(R.string.error) + "  " + exception, Snackbar.LENGTH_INDEFINITE).show();
2375                     }
2376
2377                     // Consume the event.
2378                     return true;
2379                 });
2380
2381                 // Add a Copy Email Address entry.
2382                 menu.add(R.string.copy_email_address).setOnMenuItemClickListener(item -> {
2383                     // Save the email address in a `ClipData`.
2384                     ClipData srcEmailTypeClipData = ClipData.newPlainText(getString(R.string.email_address), linkUrl);
2385
2386                     // Set the `ClipData` as the clipboard's primary clip.
2387                     clipboardManager.setPrimaryClip(srcEmailTypeClipData);
2388
2389                     // Consume the event.
2390                     return true;
2391                 });
2392
2393                 // Add an empty Cancel entry, which by default closes the context menu.
2394                 menu.add(R.string.cancel);
2395                 break;
2396         }
2397     }
2398
2399     @Override
2400     public void onCreateBookmark(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) {
2401         // Get a handle for the bookmarks list view.
2402         ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
2403
2404         // Get the dialog.
2405         Dialog dialog = dialogFragment.getDialog();
2406
2407         // Remove the incorrect lint warning below that the dialog might be null.
2408         assert dialog != null;
2409
2410         // Get the views from the dialog fragment.
2411         EditText createBookmarkNameEditText = dialog.findViewById(R.id.create_bookmark_name_edittext);
2412         EditText createBookmarkUrlEditText = dialog.findViewById(R.id.create_bookmark_url_edittext);
2413
2414         // Extract the strings from the edit texts.
2415         String bookmarkNameString = createBookmarkNameEditText.getText().toString();
2416         String bookmarkUrlString = createBookmarkUrlEditText.getText().toString();
2417
2418         // Create a favorite icon byte array output stream.
2419         ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
2420
2421         // Convert the favorite icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2422         favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
2423
2424         // Convert the favorite icon byte array stream to a byte array.
2425         byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
2426
2427         // Display the new bookmark below the current items in the (0 indexed) list.
2428         int newBookmarkDisplayOrder = bookmarksListView.getCount();
2429
2430         // Create the bookmark.
2431         bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolder, newBookmarkDisplayOrder, favoriteIconByteArray);
2432
2433         // Update the bookmarks cursor with the current contents of this folder.
2434         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2435
2436         // Update the list view.
2437         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2438
2439         // Scroll to the new bookmark.
2440         bookmarksListView.setSelection(newBookmarkDisplayOrder);
2441     }
2442
2443     @Override
2444     public void onCreateBookmarkFolder(DialogFragment dialogFragment, @NonNull Bitmap favoriteIconBitmap) {
2445         // Get a handle for the bookmarks list view.
2446         ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
2447
2448         // Get the dialog.
2449         Dialog dialog = dialogFragment.getDialog();
2450
2451         // Remove the incorrect lint warning below that the dialog might be null.
2452         assert dialog != null;
2453
2454         // Get handles for the views in the dialog fragment.
2455         EditText createFolderNameEditText = dialog.findViewById(R.id.create_folder_name_edittext);
2456         RadioButton defaultFolderIconRadioButton = dialog.findViewById(R.id.create_folder_default_icon_radiobutton);
2457         ImageView folderIconImageView = dialog.findViewById(R.id.create_folder_default_icon);
2458
2459         // Get new folder name string.
2460         String folderNameString = createFolderNameEditText.getText().toString();
2461
2462         // Create a folder icon bitmap.
2463         Bitmap folderIconBitmap;
2464
2465         // Set the folder icon bitmap according to the dialog.
2466         if (defaultFolderIconRadioButton.isChecked()) {  // Use the default folder icon.
2467             // Get the default folder icon drawable.
2468             Drawable folderIconDrawable = folderIconImageView.getDrawable();
2469
2470             // Convert the folder icon drawable to a bitmap drawable.
2471             BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2472
2473             // Convert the folder icon bitmap drawable to a bitmap.
2474             folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2475         } else {  // Use the WebView favorite icon.
2476             // Copy the favorite icon bitmap to the folder icon bitmap.
2477             folderIconBitmap = favoriteIconBitmap;
2478         }
2479
2480         // Create a folder icon byte array output stream.
2481         ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream();
2482
2483         // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2484         folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream);
2485
2486         // Convert the folder icon byte array stream to a byte array.
2487         byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray();
2488
2489         // Move all the bookmarks down one in the display order.
2490         for (int i = 0; i < bookmarksListView.getCount(); i++) {
2491             int databaseId = (int) bookmarksListView.getItemIdAtPosition(i);
2492             bookmarksDatabaseHelper.updateDisplayOrder(databaseId, i + 1);
2493         }
2494
2495         // Create the folder, which will be placed at the top of the `ListView`.
2496         bookmarksDatabaseHelper.createFolder(folderNameString, currentBookmarksFolder, folderIconByteArray);
2497
2498         // Update the bookmarks cursor with the current contents of this folder.
2499         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2500
2501         // Update the `ListView`.
2502         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2503
2504         // Scroll to the new folder.
2505         bookmarksListView.setSelection(0);
2506     }
2507
2508     @Override
2509     public void onSaveBookmarkFolder(DialogFragment dialogFragment, int selectedFolderDatabaseId, Bitmap favoriteIconBitmap) {
2510         // Remove the incorrect lint warning below that the dialog fragment might be null.
2511         assert dialogFragment != null;
2512
2513         // Get the dialog.
2514         Dialog dialog = dialogFragment.getDialog();
2515
2516         // Remove the incorrect lint warning below that the dialog might be null.
2517         assert dialog != null;
2518
2519         // Get handles for the views from the dialog.
2520         EditText editFolderNameEditText = dialog.findViewById(R.id.edit_folder_name_edittext);
2521         RadioButton currentFolderIconRadioButton = dialog.findViewById(R.id.edit_folder_current_icon_radiobutton);
2522         RadioButton defaultFolderIconRadioButton = dialog.findViewById(R.id.edit_folder_default_icon_radiobutton);
2523         ImageView defaultFolderIconImageView = dialog.findViewById(R.id.edit_folder_default_icon_imageview);
2524
2525         // Get the new folder name.
2526         String newFolderNameString = editFolderNameEditText.getText().toString();
2527
2528         // Check if the favorite icon has changed.
2529         if (currentFolderIconRadioButton.isChecked()) {  // Only the name has changed.
2530             // Update the name in the database.
2531             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString);
2532         } else if (!currentFolderIconRadioButton.isChecked() && newFolderNameString.equals(oldFolderNameString)) {  // Only the icon has changed.
2533             // Create the new folder icon Bitmap.
2534             Bitmap folderIconBitmap;
2535
2536             // Populate the new folder icon bitmap.
2537             if (defaultFolderIconRadioButton.isChecked()) {
2538                 // Get the default folder icon drawable.
2539                 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
2540
2541                 // Convert the folder icon drawable to a bitmap drawable.
2542                 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2543
2544                 // Convert the folder icon bitmap drawable to a bitmap.
2545                 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2546             } else {  // Use the `WebView` favorite icon.
2547                 // Copy the favorite icon bitmap to the folder icon bitmap.
2548                 folderIconBitmap = favoriteIconBitmap;
2549             }
2550
2551             // Create a folder icon byte array output stream.
2552             ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
2553
2554             // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2555             folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
2556
2557             // Convert the folder icon byte array stream to a byte array.
2558             byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
2559
2560             // Update the folder icon in the database.
2561             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderIconByteArray);
2562         } else {  // The folder icon and the name have changed.
2563             // Get the new folder icon `Bitmap`.
2564             Bitmap folderIconBitmap;
2565             if (defaultFolderIconRadioButton.isChecked()) {
2566                 // Get the default folder icon drawable.
2567                 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
2568
2569                 // Convert the folder icon drawable to a bitmap drawable.
2570                 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2571
2572                 // Convert the folder icon bitmap drawable to a bitmap.
2573                 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2574             } else {  // Use the `WebView` favorite icon.
2575                 // Copy the favorite icon bitmap to the folder icon bitmap.
2576                 folderIconBitmap = favoriteIconBitmap;
2577             }
2578
2579             // Create a folder icon byte array output stream.
2580             ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
2581
2582             // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2583             folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
2584
2585             // Convert the folder icon byte array stream to a byte array.
2586             byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
2587
2588             // Update the folder name and icon in the database.
2589             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, newFolderIconByteArray);
2590         }
2591
2592         // Update the bookmarks cursor with the current contents of this folder.
2593         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2594
2595         // Update the `ListView`.
2596         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2597     }
2598
2599     // Override `onBackPressed()` to handle the navigation drawer and and the WebViews.
2600     @Override
2601     public void onBackPressed() {
2602         // Check the different options for processing `back`.
2603         if (drawerLayout.isDrawerVisible(GravityCompat.START)) {  // The navigation drawer is open.
2604             // Close the navigation drawer.
2605             drawerLayout.closeDrawer(GravityCompat.START);
2606         } else if (drawerLayout.isDrawerVisible(GravityCompat.END)){  // The bookmarks drawer is open.
2607             // close the bookmarks drawer.
2608             drawerLayout.closeDrawer(GravityCompat.END);
2609         } else if (displayingFullScreenVideo) {  // A full screen video is shown.
2610             // Get a handle for the layouts.
2611             FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
2612             RelativeLayout mainContentRelativeLayout = findViewById(R.id.main_content_relativelayout);
2613             FrameLayout fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout);
2614
2615             // Re-enable the screen timeout.
2616             fullScreenVideoFrameLayout.setKeepScreenOn(false);
2617
2618             // Unset the full screen video flag.
2619             displayingFullScreenVideo = false;
2620
2621             // Remove all the views from the full screen video frame layout.
2622             fullScreenVideoFrameLayout.removeAllViews();
2623
2624             // Hide the full screen video frame layout.
2625             fullScreenVideoFrameLayout.setVisibility(View.GONE);
2626
2627             // Enable the sliding drawers.
2628             drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
2629
2630             // Show the main content relative layout.
2631             mainContentRelativeLayout.setVisibility(View.VISIBLE);
2632
2633             // Apply the appropriate full screen mode flags.
2634             if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) {  // Privacy Browser is currently in full screen browsing mode.
2635                 // Hide the banner ad in the free flavor.
2636                 if (BuildConfig.FLAVOR.contentEquals("free")) {
2637                     AdHelper.hideAd(findViewById(R.id.adview));
2638                 }
2639
2640                 /* Hide the system bars.
2641                  * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
2642                  * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
2643                  * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
2644                  * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
2645                  */
2646                 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
2647                         View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
2648
2649                 // Reload the website if the app bar is hidden.  Otherwise, there is some bug in Android that causes the WebView to be entirely black.
2650                 if (hideAppBar) {
2651                     // Reload the WebView.
2652                     currentWebView.reload();
2653                 }
2654             } else {  // Switch to normal viewing mode.
2655                 // Remove the `SYSTEM_UI` flags from the root frame layout.
2656                 rootFrameLayout.setSystemUiVisibility(0);
2657             }
2658
2659             // Reload the ad for the free flavor if not in full screen mode.
2660             if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
2661                 // Reload the ad.
2662                 AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
2663             }
2664         } else if (currentWebView.canGoBack()) {  // There is at least one item in the current WebView history.
2665             // Get the current web back forward list.
2666             WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList();
2667
2668             // Get the previous entry URL.
2669             String previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.getCurrentIndex() - 1).getUrl();
2670
2671             // Apply the domain settings.
2672             applyDomainSettings(currentWebView, previousUrl, false, false, false);
2673
2674             // Go back.
2675             currentWebView.goBack();
2676         } else if (tabLayout.getTabCount() > 1) {  // There are at least two tabs.
2677             // Close the current tab.
2678             closeCurrentTab();
2679         } else {  // There isn't anything to do in Privacy Browser.
2680             // Close Privacy Browser.  `finishAndRemoveTask()` also removes Privacy Browser from the recent app list.
2681             if (Build.VERSION.SDK_INT >= 21) {
2682                 finishAndRemoveTask();
2683             } else {
2684                 finish();
2685             }
2686
2687             // Manually kill Privacy Browser.  Otherwise, it is glitchy when restarted.
2688             System.exit(0);
2689         }
2690     }
2691
2692     // Process the results of a file browse.
2693     @Override
2694     public void onActivityResult(int requestCode, int resultCode, Intent returnedIntent) {
2695         // Run the default commands.
2696         super.onActivityResult(requestCode, resultCode, returnedIntent);
2697
2698         // Run the commands that correlate to the specified request code.
2699         switch (requestCode) {
2700             case BROWSE_FILE_UPLOAD_REQUEST_CODE:
2701                 // File uploads only work on API >= 21.
2702                 if (Build.VERSION.SDK_INT >= 21) {
2703                     // Pass the file to the WebView.
2704                     fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, returnedIntent));
2705                 }
2706                 break;
2707
2708             case BROWSE_SAVE_WEBPAGE_REQUEST_CODE:
2709                 // Don't do anything if the user pressed back from the file picker.
2710                 if (resultCode == Activity.RESULT_OK) {
2711                     // Get a handle for the save dialog fragment.
2712                     DialogFragment saveWebpageDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.save_dialog));
2713
2714                     // Only update the file name if the dialog still exists.
2715                     if (saveWebpageDialogFragment != null) {
2716                         // Get a handle for the save webpage dialog.
2717                         Dialog saveWebpageDialog = saveWebpageDialogFragment.getDialog();
2718
2719                         // Remove the incorrect lint warning below that the dialog might be null.
2720                         assert saveWebpageDialog != null;
2721
2722                         // Get a handle for the file name edit text.
2723                         EditText fileNameEditText = saveWebpageDialog.findViewById(R.id.file_name_edittext);
2724                         TextView fileExistsWarningTextView = saveWebpageDialog.findViewById(R.id.file_exists_warning_textview);
2725
2726                         // Instantiate the file name helper.
2727                         FileNameHelper fileNameHelper = new FileNameHelper();
2728
2729                         // Get the file path if it isn't null.
2730                         if (returnedIntent.getData() != null) {
2731                             // Convert the file name URI to a file name path.
2732                             String fileNamePath = fileNameHelper.convertUriToFileNamePath(returnedIntent.getData());
2733
2734                             // Set the file name path as the text of the file name edit text.
2735                             fileNameEditText.setText(fileNamePath);
2736
2737                             // Move the cursor to the end of the file name edit text.
2738                             fileNameEditText.setSelection(fileNamePath.length());
2739
2740                             // Hide the file exists warning.
2741                             fileExistsWarningTextView.setVisibility(View.GONE);
2742                         }
2743                     }
2744                 }
2745                 break;
2746
2747             case BROWSE_OPEN_REQUEST_CODE:
2748                 // Don't do anything if the user pressed back from the file picker.
2749                 if (resultCode == Activity.RESULT_OK) {
2750                     // Get a handle for the open dialog fragment.
2751                     DialogFragment openDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.open));
2752
2753                     // Only update the file name if the dialog still exists.
2754                     if (openDialogFragment != null) {
2755                         // Get a handle for the open dialog.
2756                         Dialog openDialog = openDialogFragment.getDialog();
2757
2758                         // Remove the incorrect lint warning below that the dialog might be null.
2759                         assert openDialog != null;
2760
2761                         // Get a handle for the file name edit text.
2762                         EditText fileNameEditText = openDialog.findViewById(R.id.file_name_edittext);
2763
2764                         // Instantiate the file name helper.
2765                         FileNameHelper fileNameHelper = new FileNameHelper();
2766
2767                         // Get the file path if it isn't null.
2768                         if (returnedIntent.getData() != null) {
2769                             // Convert the file name URI to a file name path.
2770                             String fileNamePath = fileNameHelper.convertUriToFileNamePath(returnedIntent.getData());
2771
2772                             // Set the file name path as the text of the file name edit text.
2773                             fileNameEditText.setText(fileNamePath);
2774
2775                             // Move the cursor to the end of the file name edit text.
2776                             fileNameEditText.setSelection(fileNamePath.length());
2777                         }
2778                     }
2779                 }
2780                 break;
2781         }
2782     }
2783
2784     private void loadUrlFromTextBox() {
2785         // Get a handle for the URL edit text.
2786         EditText urlEditText = findViewById(R.id.url_edittext);
2787
2788         // Get the text from urlTextBox and convert it to a string.  trim() removes white spaces from the beginning and end of the string.
2789         String unformattedUrlString = urlEditText.getText().toString().trim();
2790
2791         // Initialize the formatted URL string.
2792         String url = "";
2793
2794         // Check to see if `unformattedUrlString` is a valid URL.  Otherwise, convert it into a search.
2795         if (unformattedUrlString.startsWith("content://")) {  // This is a Content URL.
2796             // Load the entire content URL.
2797             url = unformattedUrlString;
2798         } else if (Patterns.WEB_URL.matcher(unformattedUrlString).matches() || unformattedUrlString.startsWith("http://") || unformattedUrlString.startsWith("https://") ||
2799                 unformattedUrlString.startsWith("file://")) {  // This is a standard URL.
2800             // Add `https://` at the beginning if there is no protocol.  Otherwise the app will segfault.
2801             if (!unformattedUrlString.startsWith("http") && !unformattedUrlString.startsWith("file://") && !unformattedUrlString.startsWith("content://")) {
2802                 unformattedUrlString = "https://" + unformattedUrlString;
2803             }
2804
2805             // Initialize `unformattedUrl`.
2806             URL unformattedUrl = null;
2807
2808             // 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.
2809             try {
2810                 unformattedUrl = new URL(unformattedUrlString);
2811             } catch (MalformedURLException e) {
2812                 e.printStackTrace();
2813             }
2814
2815             // The ternary operator (? :) makes sure that a null pointer exception is not thrown, which would happen if `.get` was called on a `null` value.
2816             String scheme = unformattedUrl != null ? unformattedUrl.getProtocol() : null;
2817             String authority = unformattedUrl != null ? unformattedUrl.getAuthority() : null;
2818             String path = unformattedUrl != null ? unformattedUrl.getPath() : null;
2819             String query = unformattedUrl != null ? unformattedUrl.getQuery() : null;
2820             String fragment = unformattedUrl != null ? unformattedUrl.getRef() : null;
2821
2822             // Build the URI.
2823             Uri.Builder uri = new Uri.Builder();
2824             uri.scheme(scheme).authority(authority).path(path).query(query).fragment(fragment);
2825
2826             // Decode the URI as a UTF-8 string in.
2827             try {
2828                 url = URLDecoder.decode(uri.build().toString(), "UTF-8");
2829             } catch (UnsupportedEncodingException exception) {
2830                 // Do nothing.  The formatted URL string will remain blank.
2831             }
2832         } else if (!unformattedUrlString.isEmpty()){  // This is not a URL, but rather a search string.
2833             // Create an encoded URL String.
2834             String encodedUrlString;
2835
2836             // Sanitize the search input.
2837             try {
2838                 encodedUrlString = URLEncoder.encode(unformattedUrlString, "UTF-8");
2839             } catch (UnsupportedEncodingException exception) {
2840                 encodedUrlString = "";
2841             }
2842
2843             // Add the base search URL.
2844             url = searchURL + encodedUrlString;
2845         }
2846
2847         // 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.
2848         urlEditText.clearFocus();
2849
2850         // Make it so.
2851         loadUrl(currentWebView, url);
2852     }
2853
2854     private void loadUrl(NestedScrollWebView nestedScrollWebView, String url) {
2855         // Sanitize the URL.
2856         url = sanitizeUrl(url);
2857
2858         // Apply the domain settings and load the URL.
2859         applyDomainSettings(nestedScrollWebView, url, true, false, true);
2860     }
2861
2862     public void findPreviousOnPage(View view) {
2863         // Go to the previous highlighted phrase on the page.  `false` goes backwards instead of forwards.
2864         currentWebView.findNext(false);
2865     }
2866
2867     public void findNextOnPage(View view) {
2868         // Go to the next highlighted phrase on the page. `true` goes forwards instead of backwards.
2869         currentWebView.findNext(true);
2870     }
2871
2872     public void closeFindOnPage(View view) {
2873         // Get a handle for the views.
2874         Toolbar toolbar = findViewById(R.id.toolbar);
2875         LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
2876         EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
2877
2878         // Delete the contents of `find_on_page_edittext`.
2879         findOnPageEditText.setText(null);
2880
2881         // Clear the highlighted phrases if the WebView is not null.
2882         if (currentWebView != null) {
2883             currentWebView.clearMatches();
2884         }
2885
2886         // Hide the find on page linear layout.
2887         findOnPageLinearLayout.setVisibility(View.GONE);
2888
2889         // Show the toolbar.
2890         toolbar.setVisibility(View.VISIBLE);
2891
2892         // Get a handle for the input method manager.
2893         InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
2894
2895         // Remove the lint warning below that the input method manager might be null.
2896         assert inputMethodManager != null;
2897
2898         // Hide the keyboard.
2899         inputMethodManager.hideSoftInputFromWindow(toolbar.getWindowToken(), 0);
2900     }
2901
2902     @Override
2903     public void onApplyNewFontSize(DialogFragment dialogFragment) {
2904         // Remove the incorrect lint warning below that the dialog fragment might be null.
2905         assert dialogFragment != null;
2906
2907         // Get the dialog.
2908         Dialog dialog = dialogFragment.getDialog();
2909
2910         // Remove the incorrect lint warning below tha the dialog might be null.
2911         assert dialog != null;
2912
2913         // Get a handle for the font size edit text.
2914         EditText fontSizeEditText = dialog.findViewById(R.id.font_size_edittext);
2915
2916         // Initialize the new font size variable with the current font size.
2917         int newFontSize = currentWebView.getSettings().getTextZoom();
2918
2919         // Get the font size from the edit text.
2920         try {
2921             newFontSize = Integer.parseInt(fontSizeEditText.getText().toString());
2922         } catch (Exception exception) {
2923             // If the edit text does not contain a valid font size do nothing.
2924         }
2925
2926         // Apply the new font size.
2927         currentWebView.getSettings().setTextZoom(newFontSize);
2928     }
2929
2930     @Override
2931     public void onOpen(DialogFragment dialogFragment) {
2932         // Get the dialog.
2933         Dialog dialog = dialogFragment.getDialog();
2934
2935         // Remove the incorrect lint warning below that the dialog might be null.
2936         assert dialog != null;
2937
2938         // Get a handle for the file name edit text.
2939         EditText fileNameEditText = dialog.findViewById(R.id.file_name_edittext);
2940
2941         // Get the file path string.
2942         openFilePath = fileNameEditText.getText().toString();
2943
2944         // Apply the domain settings.  This resets the favorite icon and removes any domain settings.
2945         applyDomainSettings(currentWebView, "file://" + openFilePath, true, false, false);
2946
2947         // Check to see if the storage permission is needed.
2948         if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {  // The storage permission has been granted.
2949             // Open the file.
2950             currentWebView.loadUrl("file://" + openFilePath);
2951         } else {  // The storage permission has not been granted.
2952             // Get the external private directory file.
2953             File externalPrivateDirectoryFile = getExternalFilesDir(null);
2954
2955             // Remove the incorrect lint error below that the file might be null.
2956             assert externalPrivateDirectoryFile != null;
2957
2958             // Get the external private directory string.
2959             String externalPrivateDirectory = externalPrivateDirectoryFile.toString();
2960
2961             // Check to see if the file path is in the external private directory.
2962             if (openFilePath.startsWith(externalPrivateDirectory)) {  // the file path is in the external private directory.
2963                 // Open the file.
2964                 currentWebView.loadUrl("file://" + openFilePath);
2965             } else {  // The file path is in a public directory.
2966                 // Check if the user has previously denied the storage permission.
2967                 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
2968                     // Instantiate the storage permission alert dialog.
2969                     DialogFragment storagePermissionDialogFragment = StoragePermissionDialog.displayDialog(StoragePermissionDialog.OPEN);
2970
2971                     // Show the storage permission alert dialog.  The permission will be requested the the dialog is closed.
2972                     storagePermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.storage_permission));
2973                 } else {  // Show the permission request directly.
2974                     // Request the write external storage permission.  The file will be opened when it finishes.
2975                     ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, StoragePermissionDialog.OPEN);
2976                 }
2977             }
2978         }
2979     }
2980
2981     @Override
2982     public void onSaveWebpage(int saveType, String originalUrlString, DialogFragment dialogFragment) {
2983         // Get the dialog.
2984         Dialog dialog = dialogFragment.getDialog();
2985
2986         // Remove the incorrect lint warning below that the dialog might be null.
2987         assert dialog != null;
2988
2989         // Get a handle for the edit texts.
2990         EditText urlEditText = dialog.findViewById(R.id.url_edittext);
2991         EditText fileNameEditText = dialog.findViewById(R.id.file_name_edittext);
2992
2993         // Store the URL.
2994         if ((originalUrlString != null) && originalUrlString.startsWith("data:")) {
2995             // Save the original URL.
2996             saveWebpageUrl = originalUrlString;
2997         } else {
2998             // Get the URL from the edit text, which may have been modified.
2999             saveWebpageUrl = urlEditText.getText().toString();
3000         }
3001
3002         // Get the file path from the edit text.
3003         saveWebpageFilePath = fileNameEditText.getText().toString();
3004
3005         // Check to see if the storage permission is needed.
3006         if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {  // The storage permission has been granted.
3007             //Save the webpage according to the save type.
3008             switch (saveType) {
3009                 case StoragePermissionDialog.SAVE_URL:
3010                     // Save the URL.
3011                     new SaveUrl(this, this, saveWebpageFilePath, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptFirstPartyCookies()).execute(saveWebpageUrl);
3012                     break;
3013