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