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