]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java
Teach resource requests to be tab aware.
[PrivacyBrowserAndroid.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.content.res.Resources;
40 import android.database.Cursor;
41 import android.graphics.Bitmap;
42 import android.graphics.BitmapFactory;
43 import android.graphics.Typeface;
44 import android.graphics.drawable.BitmapDrawable;
45 import android.graphics.drawable.Drawable;
46 import android.net.Uri;
47 import android.net.http.SslCertificate;
48 import android.net.http.SslError;
49 import android.os.Build;
50 import android.os.Bundle;
51 import android.os.Environment;
52 import android.os.Handler;
53 import android.preference.PreferenceManager;
54 import android.print.PrintDocumentAdapter;
55 import android.print.PrintManager;
56 import android.text.Editable;
57 import android.text.Spanned;
58 import android.text.TextWatcher;
59 import android.text.style.ForegroundColorSpan;
60 import android.util.Patterns;
61 import android.view.ContextMenu;
62 import android.view.GestureDetector;
63 import android.view.KeyEvent;
64 import android.view.Menu;
65 import android.view.MenuItem;
66 import android.view.MotionEvent;
67 import android.view.View;
68 import android.view.ViewGroup;
69 import android.view.WindowManager;
70 import android.view.inputmethod.InputMethodManager;
71 import android.webkit.CookieManager;
72 import android.webkit.HttpAuthHandler;
73 import android.webkit.SslErrorHandler;
74 import android.webkit.ValueCallback;
75 import android.webkit.WebBackForwardList;
76 import android.webkit.WebChromeClient;
77 import android.webkit.WebResourceResponse;
78 import android.webkit.WebSettings;
79 import android.webkit.WebStorage;
80 import android.webkit.WebView;
81 import android.webkit.WebViewClient;
82 import android.webkit.WebViewDatabase;
83 import android.widget.ArrayAdapter;
84 import android.widget.CursorAdapter;
85 import android.widget.EditText;
86 import android.widget.FrameLayout;
87 import android.widget.ImageView;
88 import android.widget.LinearLayout;
89 import android.widget.ListView;
90 import android.widget.ProgressBar;
91 import android.widget.RadioButton;
92 import android.widget.RelativeLayout;
93 import android.widget.TextView;
94
95 import androidx.annotation.NonNull;
96 import androidx.appcompat.app.ActionBar;
97 import androidx.appcompat.app.ActionBarDrawerToggle;
98 import androidx.appcompat.app.AppCompatActivity;
99 import androidx.appcompat.widget.Toolbar;
100 import androidx.core.app.ActivityCompat;
101 import androidx.core.content.ContextCompat;
102 import androidx.core.view.GravityCompat;
103 import androidx.drawerlayout.widget.DrawerLayout;
104 import androidx.fragment.app.DialogFragment;
105 import androidx.fragment.app.FragmentManager;
106 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
107 import androidx.viewpager.widget.ViewPager;
108
109 import com.google.android.material.floatingactionbutton.FloatingActionButton;
110 import com.google.android.material.navigation.NavigationView;
111 import com.google.android.material.snackbar.Snackbar;
112 import com.google.android.material.tabs.TabLayout;
113
114 import com.stoutner.privacybrowser.BuildConfig;
115 import com.stoutner.privacybrowser.R;
116 import com.stoutner.privacybrowser.adapters.WebViewPagerAdapter;
117 import com.stoutner.privacybrowser.asynctasks.GetHostIpAddresses;
118 import com.stoutner.privacybrowser.dialogs.AdConsentDialog;
119 import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog;
120 import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog;
121 import com.stoutner.privacybrowser.dialogs.CreateHomeScreenShortcutDialog;
122 import com.stoutner.privacybrowser.dialogs.DownloadFileDialog;
123 import com.stoutner.privacybrowser.dialogs.DownloadImageDialog;
124 import com.stoutner.privacybrowser.dialogs.DownloadLocationPermissionDialog;
125 import com.stoutner.privacybrowser.dialogs.EditBookmarkDialog;
126 import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDialog;
127 import com.stoutner.privacybrowser.dialogs.HttpAuthenticationDialog;
128 import com.stoutner.privacybrowser.dialogs.PinnedMismatchDialog;
129 import com.stoutner.privacybrowser.dialogs.SslCertificateErrorDialog;
130 import com.stoutner.privacybrowser.dialogs.UrlHistoryDialog;
131 import com.stoutner.privacybrowser.dialogs.ViewSslCertificateDialog;
132 import com.stoutner.privacybrowser.fragments.WebViewTabFragment;
133 import com.stoutner.privacybrowser.helpers.AdHelper;
134 import com.stoutner.privacybrowser.helpers.BlockListHelper;
135 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper;
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 // AppCompatActivity from android.support.v7.app.AppCompatActivity must be used to have access to the SupportActionBar until the minimum API is >= 21.
158 public class MainWebViewActivity extends AppCompatActivity implements CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener,
159         DownloadFileDialog.DownloadFileListener, DownloadImageDialog.DownloadImageListener, DownloadLocationPermissionDialog.DownloadLocationPermissionDialogListener, EditBookmarkDialog.EditBookmarkListener,
160         EditBookmarkFolderDialog.EditBookmarkFolderListener, HttpAuthenticationDialog.HttpAuthenticationListener, NavigationView.OnNavigationItemSelectedListener, WebViewTabFragment.NewTabListener,
161         PinnedMismatchDialog.PinnedMismatchListener, SslCertificateErrorDialog.SslCertificateErrorListener, UrlHistoryDialog.UrlHistoryListener {
162
163     // `darkTheme` is public static so it can be accessed from everywhere.
164     public static boolean darkTheme;
165
166     // `allowScreenshots` is public static so it can be accessed from everywhere.  It is also used in `onCreate()`.
167     public static boolean allowScreenshots;
168
169     // `favoriteIconBitmap` is public static so it can be accessed from `BookmarksActivity`, `BookmarksDatabaseViewActivity`, `CreateBookmarkFolderDialog`,
170     // `EditBookmarkDialog`, `EditBookmarkFolderDialog`, `EditBookmarkDatabaseViewDialog`, and `ViewSslCertificateDialog`.  It is also used in `onCreate()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`,
171     // `onCreateHomeScreenShortcut()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `applyDomainSettings()`.
172     public static Bitmap favoriteIconBitmap;
173
174     // `favoriteIconDefaultBitmap` public static so it can be accessed from `PinnedMismatchDialog`.  It is also used in `onCreate()` and `applyDomainSettings`.
175     public static Bitmap favoriteIconDefaultBitmap;
176
177     // `formattedUrlString` is public static so it can be accessed from `AddDomainDialog`, `BookmarksActivity`, `DomainSettingsFragment`, and `PinnedMismatchDialog`.
178     // It is also used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onCreateHomeScreenShortcutCreate()`, `loadUrlFromTextBox()`, and `applyProxyThroughOrbot()`.
179     public static String formattedUrlString;
180
181     // `sslCertificate` is public static so it can be accessed from `DomainsActivity`, `DomainsListFragment`, `DomainSettingsFragment`, `PinnedMismatchDialog`, and `ViewSslCertificateDialog`.
182     // It is also used in `onCreate()` and `checkPinnedMismatch()`.
183     public static SslCertificate sslCertificate;
184
185     // `currentHostIpAddresses` is public static so it can be accessed from `DomainSettingsFragment`, `GetHostIpAddresses()`, and `ViewSslCertificateDialog`.
186     // It is also used in `onCreate()` and `GetHostIpAddresses()`.
187     public static String currentHostIpAddresses;
188
189     // The getting IP addresses tracker is used in `onCreate() and `GetHostIpAddresses`.
190     public static boolean gettingIpAddresses;
191
192     // The URL loading tracker is public static so it can be accessed from `GetHostIpAddresses`.
193     // It is also used in `onCreate()`, `onCreateOptionsMenu()`, `loadUrl()`, `applyDomainSettings()`, and `GetHostIpAddresses`.
194     public static boolean urlIsLoading;
195
196     // `orbotStatus` is public static so it can be accessed from `OrbotProxyHelper`.  It is also used in `onCreate()`, `onResume()`, and `applyProxyThroughOrbot()`.
197     public static String orbotStatus;
198
199     // `appliedUserAgentString` is public static so it can be accessed from `ViewSourceActivity`.  It is also used in `applyDomainSettings()`.
200     public static String appliedUserAgentString;
201
202     // `reloadOnRestart` is public static so it can be accessed from `SettingsFragment`.  It is also used in `onRestart()`
203     public static boolean reloadOnRestart;
204
205     // `reloadUrlOnRestart` is public static so it can be accessed from `SettingsFragment` and `BookmarksActivity`.  It is also used in `onRestart()`.
206     public static boolean loadUrlOnRestart;
207
208     // `restartFromBookmarksActivity` is public static so it can be accessed from `BookmarksActivity`.  It is also used in `onRestart()`.
209     public static boolean restartFromBookmarksActivity;
210
211     // The blocklist versions are public static so they can be accessed from `AboutTabFragment`.  They are also used in `onCreate()`.
212     public static String easyListVersion;
213     public static String easyPrivacyVersion;
214     public static String fanboysAnnoyanceVersion;
215     public static String fanboysSocialVersion;
216     public static String ultraPrivacyVersion;
217
218     // `blockAllThirdPartyRequests` is public static so it can be accessed from `RequestsActivity`.
219     // It is also used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyAppSettings()`
220     public static boolean blockAllThirdPartyRequests;
221
222     // `currentBookmarksFolder` is public static so it can be accessed from `BookmarksActivity`.  It is also used in `onCreate()`, `onBackPressed()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`,
223     // `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
224     public static String currentBookmarksFolder;
225
226     // The pinned variables are public static so they can be accessed from `PinnedMismatchDialog`.  They are also used in `onCreate()`, `applyDomainSettings()`, and `checkPinnedMismatch()`.
227     public static String pinnedSslIssuedToCName;
228     public static String pinnedSslIssuedToOName;
229     public static String pinnedSslIssuedToUName;
230     public static String pinnedSslIssuedByCName;
231     public static String pinnedSslIssuedByOName;
232     public static String pinnedSslIssuedByUName;
233     public static Date pinnedSslStartDate;
234     public static Date pinnedSslEndDate;
235     public static String pinnedHostIpAddresses;
236
237     // The user agent constants are public static so they can be accessed from `SettingsFragment`, `DomainsActivity`, and `DomainSettingsFragment`.
238     public final static int UNRECOGNIZED_USER_AGENT = -1;
239     public final static int SETTINGS_WEBVIEW_DEFAULT_USER_AGENT = 1;
240     public final static int SETTINGS_CUSTOM_USER_AGENT = 12;
241     public final static int DOMAINS_SYSTEM_DEFAULT_USER_AGENT = 0;
242     public final static int DOMAINS_WEBVIEW_DEFAULT_USER_AGENT = 2;
243     public final static int DOMAINS_CUSTOM_USER_AGENT = 13;
244
245
246
247     // `pinnedDomainSslCertificate` is used in `onCreate()`, `applyDomainSettings()`, and `checkPinnedMismatch()`.
248     private static boolean pinnedSslCertificate;
249
250     // `pinnedIpAddress` is used in `applyDomainSettings()` and `checkPinnedMismatch()`.
251     private static boolean pinnedIpAddresses;
252
253     // `ignorePinnedDomainInformation` is used in `onSslMismatchProceed()`, `applyDomainSettings()`, and `checkPinnedMismatch()`.
254     private static boolean ignorePinnedDomainInformation;
255
256     // The fragment manager is initialized in `onCreate()` and accessed from the static `checkPinnedMismatch()`.
257     private static FragmentManager fragmentManager;
258
259
260     // A handle for the activity is set in `onCreate()` and accessed in `WebViewPagerAdapter`.
261     private Activity activity;
262
263     // `navigatingHistory` is used in `onCreate()`, `onNavigationItemSelected()`, `onSslMismatchBack()`, and `applyDomainSettings()`.
264     private boolean navigatingHistory;
265
266     // The current WebView is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`,
267     // `findNextOnPage()`, `closeFindOnPage()`, `loadUrlFromTextBox()`, `onSslMismatchBack()`, `applyProxyThroughOrbot()`, and `applyDomainSettings()`.
268     private NestedScrollWebView currentWebView;
269
270     // `fullScreenVideoFrameLayout` is used in `onCreate()` and `onConfigurationChanged()`.
271     private FrameLayout fullScreenVideoFrameLayout;
272
273     // `cookieManager` is used in `onCreate()`, `onOptionsItemSelected()`, and `onNavigationItemSelected()`, `loadUrlFromTextBox()`, `onDownloadImage()`, `onDownloadFile()`, and `onRestart()`.
274     private CookieManager cookieManager;
275
276     // `customHeader` is used in `onCreate()`, `onOptionsItemSelected()`, `onCreateContextMenu()`, and `loadUrl()`.
277     private final Map<String, String> customHeaders = new HashMap<>();
278
279     // `javaScriptEnabled` is also used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `applyDomainSettings()`, and `updatePrivacyIcons()`.
280     private boolean javaScriptEnabled;
281
282     // `firstPartyCookiesEnabled` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onDownloadImage()`, `onDownloadFile()`, and `applyDomainSettings()`.
283     private boolean firstPartyCookiesEnabled;
284
285     // `thirdPartyCookiesEnabled` used in `onCreate()`, `onPrepareOptionsMenu()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyDomainSettings()`.
286     private boolean thirdPartyCookiesEnabled;
287
288     // `domStorageEnabled` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyDomainSettings()`.
289     private boolean domStorageEnabled;
290
291     // `saveFormDataEnabled` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyDomainSettings()`.  It can be removed once the minimum API >= 26.
292     private boolean saveFormDataEnabled;
293
294     // `nightMode` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and  `applyDomainSettings()`.
295     private boolean nightMode;
296
297     // 'homepage' is used in `onCreate()`, `onNavigationItemSelected()`, and `applyProxyThroughOrbot()`.
298     private String homepage;
299
300     // `searchURL` is used in `loadURLFromTextBox()` and `applyProxyThroughOrbot()`.
301     private String searchURL;
302
303     // `mainMenu` is used in `onCreateOptionsMenu()` and `updatePrivacyIcons()`.
304     private Menu mainMenu;
305
306     // `refreshMenuItem` is used in `onCreate()` and `onCreateOptionsMenu()`.
307     private MenuItem refreshMenuItem;
308
309     // The WebView pager adapter is used in `onCreate()`, `onResume()`, and `addTab()`.
310     private WebViewPagerAdapter webViewPagerAdapter;
311
312     // The navigation requests menu item is used in `onCreate()` and accessed from `WebViewPagerAdapter`.
313     private MenuItem navigationRequestsMenuItem;
314
315     // The blocklist helper is used in `onCreate()` and `WebViewPagerAdapter`.
316     BlockListHelper blockListHelper;
317
318     // The blocklists are populated in `onCreate()` and accessed from `WebViewPagerAdapter`.
319     private ArrayList<List<String[]>> easyList;
320     private ArrayList<List<String[]>> easyPrivacy;
321     private ArrayList<List<String[]>> fanboysAnnoyanceList;
322     private ArrayList<List<String[]>> fanboysSocialList;
323     private ArrayList<List<String[]>> ultraPrivacy;
324
325     // The blocklist menu items are used in `onCreate()`, `onCreateOptionsMenu()`, and `onPrepareOptionsMenu()`.
326     private MenuItem blocklistsMenuItem;
327     private MenuItem easyListMenuItem;
328     private MenuItem easyPrivacyMenuItem;
329     private MenuItem fanboysAnnoyanceListMenuItem;
330     private MenuItem fanboysSocialBlockingListMenuItem;
331     private MenuItem ultraPrivacyMenuItem;
332     private MenuItem blockAllThirdPartyRequestsMenuItem;
333
334     // The blocklist variables are used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyAppSettings()`.
335     private boolean easyListEnabled;
336     private boolean easyPrivacyEnabled;
337     private boolean fanboysAnnoyanceListEnabled;
338     private boolean fanboysSocialBlockingListEnabled;
339     private boolean ultraPrivacyEnabled;
340
341     // `webViewDefaultUserAgent` is used in `onCreate()` and `onPrepareOptionsMenu()`.
342     private String webViewDefaultUserAgent;
343
344     // `defaultCustomUserAgentString` is used in `onPrepareOptionsMenu()` and `applyDomainSettings()`.
345     private String defaultCustomUserAgentString;
346
347     // `privacyBrowserRuntime` is used in `onCreate()`, `onOptionsItemSelected()`, and `applyAppSettings()`.
348     private Runtime privacyBrowserRuntime;
349
350     // `proxyThroughOrbot` is used in `onRestart()`, `onOptionsItemSelected()`, `applyAppSettings()`, and `applyProxyThroughOrbot()`.
351     private boolean proxyThroughOrbot;
352
353     // `incognitoModeEnabled` is used in `onCreate()` and `applyAppSettings()`.
354     private boolean incognitoModeEnabled;
355
356     // `fullScreenBrowsingModeEnabled` is used in `onCreate()` and `applyAppSettings()`.
357     private boolean fullScreenBrowsingModeEnabled;
358
359     // `inFullScreenBrowsingMode` is used in `onCreate()`, `onConfigurationChanged()`, and `applyAppSettings()`.
360     private boolean inFullScreenBrowsingMode;
361
362     // Hide app bar is used in `onCreate()` and `applyAppSettings()`.
363     private boolean hideAppBar;
364
365     // `reapplyDomainSettingsOnRestart` is used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, and `onAddDomain()`, .
366     private boolean reapplyDomainSettingsOnRestart;
367
368     // `reapplyAppSettingsOnRestart` is used in `onNavigationItemSelected()` and `onRestart()`.
369     private boolean reapplyAppSettingsOnRestart;
370
371     // `displayingFullScreenVideo` is used in `onCreate()` and `onResume()`.
372     private boolean displayingFullScreenVideo;
373
374     // `downloadWithExternalApp` is used in `onCreate()`, `onCreateContextMenu()`, and `applyDomainSettings()`.
375     private boolean downloadWithExternalApp;
376
377     // `currentDomainName` is used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onAddDomain()`, and `applyDomainSettings()`.
378     private String currentDomainName;
379
380     // `orbotStatusBroadcastReceiver` is used in `onCreate()` and `onDestroy()`.
381     private BroadcastReceiver orbotStatusBroadcastReceiver;
382
383     // `waitingForOrbot` is used in `onCreate()`, `onResume()`, and `applyProxyThroughOrbot()`.
384     private boolean waitingForOrbot;
385
386     // `domainSettingsJavaScriptEnabled` is used in `onOptionsItemSelected()` and `applyDomainSettings()`.
387     private Boolean domainSettingsJavaScriptEnabled;
388
389     // `waitingForOrbotHtmlString` is used in `onCreate()` and `applyProxyThroughOrbot()`.
390     private String waitingForOrbotHtmlString;
391
392     // `privateDataDirectoryString` is used in `onCreate()`, `onOptionsItemSelected()`, and `onNavigationItemSelected()`.
393     private String privateDataDirectoryString;
394
395     // `findOnPageEditText` is used in `onCreate()`, `onOptionsItemSelected()`, and `closeFindOnPage()`.
396     private EditText findOnPageEditText;
397
398     // `displayAdditionalAppBarIcons` is used in `onCreate()` and `onCreateOptionsMenu()`.
399     private boolean displayAdditionalAppBarIcons;
400
401     // The action bar drawer toggle is initialized in `onCreate()` and used in `onResume()`.
402     private ActionBarDrawerToggle actionBarDrawerToggle;
403
404     // The color spans are used in `onCreate()` and `highlightUrlText()`.
405     private ForegroundColorSpan redColorSpan;
406     private ForegroundColorSpan initialGrayColorSpan;
407     private ForegroundColorSpan finalGrayColorSpan;
408
409     // The drawer header padding variables are used in `onCreate()` and `onConfigurationChanged()`.
410     private int drawerHeaderPaddingLeftAndRight;
411     private int drawerHeaderPaddingTop;
412     private int drawerHeaderPaddingBottom;
413
414     // `sslErrorHandler` is used in `onCreate()`, `onSslErrorCancel()`, and `onSslErrorProceed`.
415     private SslErrorHandler sslErrorHandler;
416
417     // `httpAuthHandler` is used in `onCreate()`, `onHttpAuthenticationCancel()`, and `onHttpAuthenticationProceed()`.
418     private static HttpAuthHandler httpAuthHandler;
419
420     // `inputMethodManager` is used in `onOptionsItemSelected()`, `loadUrlFromTextBox()`, and `closeFindOnPage()`.
421     private InputMethodManager inputMethodManager;
422
423     // `bookmarksDatabaseHelper` is used in `onCreate()`, `onDestroy`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`,
424     // and `loadBookmarksFolder()`.
425     private BookmarksDatabaseHelper bookmarksDatabaseHelper;
426
427     // `bookmarksListView` is used in `onCreate()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, and `loadBookmarksFolder()`.
428     private ListView bookmarksListView;
429
430     // `bookmarksTitleTextView` is used in `onCreate()` and `loadBookmarksFolder()`.
431     private TextView bookmarksTitleTextView;
432
433     // `bookmarksCursor` is used in `onDestroy()`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
434     private Cursor bookmarksCursor;
435
436     // `bookmarksCursorAdapter` is used in `onCreateBookmark()`, `onCreateBookmarkFolder()` `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
437     private CursorAdapter bookmarksCursorAdapter;
438
439     // `oldFolderNameString` is used in `onCreate()` and `onSaveEditBookmarkFolder()`.
440     private String oldFolderNameString;
441
442     // `fileChooserCallback` is used in `onCreate()` and `onActivityResult()`.
443     private ValueCallback<Uri[]> fileChooserCallback;
444
445     // The download strings are used in `onCreate()`, `onRequestPermissionResult()` and `initializeWebView()`.
446     private String downloadUrl;
447     private String downloadContentDisposition;
448     private long downloadContentLength;
449
450     // `downloadImageUrl` is used in `onCreateContextMenu()` and `onRequestPermissionResult()`.
451     private String downloadImageUrl;
452
453     // The user agent variables are used in `onCreate()` and `applyDomainSettings()`.
454     private ArrayAdapter<CharSequence> userAgentNamesArray;
455     private String[] userAgentDataArray;
456
457     // The request codes are used in `onCreate()`, `onCreateContextMenu()`, `onCloseDownloadLocationPermissionDialog()`, `onRequestPermissionResult()`, and `initializeWebView()`.
458     private final int DOWNLOAD_FILE_REQUEST_CODE = 1;
459     private final int DOWNLOAD_IMAGE_REQUEST_CODE = 2;
460
461     @Override
462     // Remove Android Studio's warning about the dangers of using SetJavaScriptEnabled.  The whole premise of Privacy Browser is built around an understanding of these dangers.
463     // Also, remove the warning about needing to override `performClick()` when using an `OnTouchListener` with `WebView`.
464     @SuppressLint({"SetJavaScriptEnabled", "ClickableViewAccessibility"})
465     // Remove Android Studio's warning about deprecations.  The deprecated `getColor()` must be used until API >= 23.
466     @SuppressWarnings("deprecation")
467     protected void onCreate(Bundle savedInstanceState) {
468         // Get a handle for the shared preferences.
469         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
470
471         // Get the theme and screenshot preferences.
472         darkTheme = sharedPreferences.getBoolean("dark_theme", false);
473         allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
474
475         // Disable screenshots if not allowed.
476         if (!allowScreenshots) {
477             getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
478         }
479
480         // Set the activity theme.
481         if (darkTheme) {
482             setTheme(R.style.PrivacyBrowserDark);
483         } else {
484             setTheme(R.style.PrivacyBrowserLight);
485         }
486
487         // Run the default commands.
488         super.onCreate(savedInstanceState);
489
490         // Set the content view.
491         setContentView(R.layout.main_framelayout);
492
493         // Get handles for views, resources, and managers.
494         activity = this;
495         Resources resources = getResources();
496         fragmentManager = getSupportFragmentManager();
497         inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
498         Toolbar toolbar = findViewById(R.id.toolbar);
499
500         // Set the action bar.  `SupportActionBar` must be used until the minimum API is >= 21.
501         setSupportActionBar(toolbar);
502         ActionBar actionBar = getSupportActionBar();
503
504         // This is needed to get rid of the Android Studio warning that the action bar might be null.
505         assert actionBar != null;
506
507         // Add the custom layout, which shows the URL text bar.
508         actionBar.setCustomView(R.layout.url_app_bar);
509         actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
510
511         // Initialize the foreground color spans for highlighting the URLs.  We have to use the deprecated `getColor()` until API >= 23.
512         redColorSpan = new ForegroundColorSpan(resources.getColor(R.color.red_a700));
513         initialGrayColorSpan = new ForegroundColorSpan(resources.getColor(R.color.gray_500));
514         finalGrayColorSpan = new ForegroundColorSpan(resources.getColor(R.color.gray_500));
515
516         // Get a handle for `urlTextBox`.
517         EditText urlEditText = findViewById(R.id.url_edittext);
518
519         // Remove the formatting from `urlTextBar` when the user is editing the text.
520         urlEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
521             if (hasFocus) {  // The user is editing the URL text box.
522                 // Remove the highlighting.
523                 urlEditText.getText().removeSpan(redColorSpan);
524                 urlEditText.getText().removeSpan(initialGrayColorSpan);
525                 urlEditText.getText().removeSpan(finalGrayColorSpan);
526             } else {  // The user has stopped editing the URL text box.
527                 // Move to the beginning of the string.
528                 urlEditText.setSelection(0);
529
530                 // Reapply the highlighting.
531                 highlightUrlText();
532             }
533         });
534
535         // Set the go button on the keyboard to load the URL in `urlTextBox`.
536         urlEditText.setOnKeyListener((View v, int keyCode, KeyEvent event) -> {
537             // If the event is a key-down event on the `enter` button, load the URL.
538             if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {
539                 // Load the URL into the mainWebView and consume the event.
540                 loadUrlFromTextBox();
541
542                 // If the enter key was pressed, consume the event.
543                 return true;
544             } else {
545                 // If any other key was pressed, do not consume the event.
546                 return false;
547             }
548         });
549
550         // Set `waitingForOrbotHTMLString`.
551         waitingForOrbotHtmlString = "<html><body><br/><center><h1>" + getString(R.string.waiting_for_orbot) + "</h1></center></body></html>";
552
553         // Initialize `currentDomainName`, `orbotStatus`, and `waitingForOrbot`.
554         currentDomainName = "";
555         orbotStatus = "unknown";
556         waitingForOrbot = false;
557
558         // Create an Orbot status `BroadcastReceiver`.
559         orbotStatusBroadcastReceiver = new BroadcastReceiver() {
560             @Override
561             public void onReceive(Context context, Intent intent) {
562                 // Store the content of the status message in `orbotStatus`.
563                 orbotStatus = intent.getStringExtra("org.torproject.android.intent.extra.STATUS");
564
565                 // If Privacy Browser is waiting on Orbot, load the website now that Orbot is connected.
566                 if (orbotStatus.equals("ON") && waitingForOrbot) {
567                     // Reset `waitingForOrbot`.
568                     waitingForOrbot = false;
569
570                     // Load `formattedUrlString
571                     loadUrl(formattedUrlString);
572                 }
573             }
574         };
575
576         // Register `orbotStatusBroadcastReceiver` on `this` context.
577         this.registerReceiver(orbotStatusBroadcastReceiver, new IntentFilter("org.torproject.android.intent.action.STATUS"));
578
579         // Instantiate the block list helper.
580         blockListHelper = new BlockListHelper();
581
582         // Parse the block lists.
583         easyList = blockListHelper.parseBlockList(getAssets(), "blocklists/easylist.txt");
584         easyPrivacy = blockListHelper.parseBlockList(getAssets(), "blocklists/easyprivacy.txt");
585         fanboysAnnoyanceList = blockListHelper.parseBlockList(getAssets(), "blocklists/fanboy-annoyance.txt");
586         fanboysSocialList = blockListHelper.parseBlockList(getAssets(), "blocklists/fanboy-social.txt");
587         ultraPrivacy = blockListHelper.parseBlockList(getAssets(), "blocklists/ultraprivacy.txt");
588
589         // Store the list versions.
590         easyListVersion = easyList.get(0).get(0)[0];
591         easyPrivacyVersion = easyPrivacy.get(0).get(0)[0];
592         fanboysAnnoyanceVersion = fanboysAnnoyanceList.get(0).get(0)[0];
593         fanboysSocialVersion = fanboysSocialList.get(0).get(0)[0];
594         ultraPrivacyVersion = ultraPrivacy.get(0).get(0)[0];
595
596         // Get handles for views that need to be modified.
597         DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
598         final NavigationView navigationView = findViewById(R.id.navigationview);
599         TabLayout tabLayout = findViewById(R.id.tablayout);
600         SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
601         ViewPager webViewPager = findViewById(R.id.webviewpager);
602         bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
603         bookmarksTitleTextView = findViewById(R.id.bookmarks_title_textview);
604         FloatingActionButton launchBookmarksActivityFab = findViewById(R.id.launch_bookmarks_activity_fab);
605         FloatingActionButton createBookmarkFolderFab = findViewById(R.id.create_bookmark_folder_fab);
606         FloatingActionButton createBookmarkFab = findViewById(R.id.create_bookmark_fab);
607         findOnPageEditText = findViewById(R.id.find_on_page_edittext);
608         fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout);
609
610         // Listen for touches on the navigation menu.
611         navigationView.setNavigationItemSelectedListener(this);
612
613         // Get handles for the navigation menu and the back and forward menu items.  The menu is zero-based.
614         final Menu navigationMenu = navigationView.getMenu();
615         final MenuItem navigationCloseTabMenuItem = navigationMenu.getItem(0);
616         final MenuItem navigationBackMenuItem = navigationMenu.getItem(3);
617         final MenuItem navigationForwardMenuItem = navigationMenu.getItem(4);
618         final MenuItem navigationHistoryMenuItem = navigationMenu.getItem(5);
619         navigationRequestsMenuItem = navigationMenu.getItem(6);
620
621         // Initialize the web view pager adapter.
622         webViewPagerAdapter = new WebViewPagerAdapter(fragmentManager);
623
624         // Set the pager adapter on the web view pager.
625         webViewPager.setAdapter(webViewPagerAdapter);
626
627         // Store up to 100 tabs in memory.
628         webViewPager.setOffscreenPageLimit(100);
629
630         // Update the web view pager every time a tab is modified.
631         webViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
632             @Override
633             public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
634                 // Do nothing.
635             }
636
637             @Override
638             public void onPageSelected(int position) {
639                 // Get the WebView tab fragment.
640                 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(position);
641
642                 // Get the fragment view.
643                 View fragmentView = webViewTabFragment.getView();
644
645                 // Remove the incorrect lint warning below that the fragment view might be null.
646                 assert fragmentView != null;
647
648                 // Store the current WebView.
649                 currentWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
650
651                 // Store the current formatted URL string.
652                 formattedUrlString = currentWebView.getUrl();
653
654                 // Clear the focus from the URL text box.
655                 urlEditText.clearFocus();
656
657                 // Hide the soft keyboard.
658                 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
659
660                 // Display the current URL in the URL text box.
661                 urlEditText.setText(formattedUrlString);
662
663                 // Highlight the URL text.
664                 highlightUrlText();
665
666                 // Set the background to indicate the domain settings status.
667                 if (currentWebView.getDomainSettingsApplied()) {
668                     // Set a green background on `urlTextBox` to indicate that custom domain settings are being used. The deprecated `.getDrawable()` must be used until the minimum API >= 21.
669                     if (darkTheme) {
670                         urlEditText.setBackground(getResources().getDrawable(R.drawable.url_bar_background_dark_blue));
671                     } else {
672                         urlEditText.setBackground(getResources().getDrawable(R.drawable.url_bar_background_light_green));
673                     }
674                 } else {
675                     urlEditText.setBackgroundDrawable(getResources().getDrawable(R.color.transparent));
676                 }
677
678                 // 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.
679                 if (tabLayout.getSelectedTabPosition() != position) {
680                     // Get a handle for the corresponding tab.
681                     TabLayout.Tab correspondingTab = tabLayout.getTabAt(position);
682
683                     // Assert that the corresponding tab is not null.
684                     assert correspondingTab != null;
685
686                     // Select the corresponding tab.
687                     correspondingTab.select();
688                 }
689             }
690
691             @Override
692             public void onPageScrollStateChanged(int state) {
693                 // Do nothing.
694             }
695         });
696
697         // Display the View SSL Certificate dialog when the currently selected tab is reselected.
698         tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
699             @Override
700             public void onTabSelected(TabLayout.Tab tab) {
701                 // Select the same page in the view pager.
702                 webViewPager.setCurrentItem(tab.getPosition());
703             }
704
705             @Override
706             public void onTabUnselected(TabLayout.Tab tab) {
707                 // Do nothing.
708             }
709
710             @Override
711             public void onTabReselected(TabLayout.Tab tab) {
712                 // Instantiate the View SSL Certificate dialog.
713                 DialogFragment viewSslCertificateDialogFragment = new ViewSslCertificateDialog();
714
715                 // Display the View SSL Certificate dialog.
716                 viewSslCertificateDialogFragment.show(getSupportFragmentManager(), getString(R.string.view_ssl_certificate));
717             }
718         });
719
720         // Add the first tab.  (It doesn't matter what view is passed.  That is just required as part of the ImageView `onClick()` syntax).
721         addTab(webViewPager);
722
723         // 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.
724         if (darkTheme) {
725             launchBookmarksActivityFab.setImageDrawable(resources.getDrawable(R.drawable.bookmarks_dark));
726             createBookmarkFolderFab.setImageDrawable(resources.getDrawable(R.drawable.create_folder_dark));
727             createBookmarkFab.setImageDrawable(resources.getDrawable(R.drawable.create_bookmark_dark));
728             bookmarksListView.setBackgroundColor(resources.getColor(R.color.gray_850));
729         } else {
730             launchBookmarksActivityFab.setImageDrawable(resources.getDrawable(R.drawable.bookmarks_light));
731             createBookmarkFolderFab.setImageDrawable(resources.getDrawable(R.drawable.create_folder_light));
732             createBookmarkFab.setImageDrawable(resources.getDrawable(R.drawable.create_bookmark_light));
733             bookmarksListView.setBackgroundColor(resources.getColor(R.color.white));
734         }
735
736         // Set the launch bookmarks activity FAB to launch the bookmarks activity.
737         launchBookmarksActivityFab.setOnClickListener(v -> {
738             // Store the current WebView url and title in the bookmarks activity.
739             BookmarksActivity.currentWebViewUrl = currentWebView.getUrl();
740             BookmarksActivity.currentWebViewTitle = currentWebView.getTitle();
741
742             // Create an intent to launch the bookmarks activity.
743             Intent bookmarksIntent = new Intent(getApplicationContext(), BookmarksActivity.class);
744
745             // Include the current folder with the `Intent`.
746             bookmarksIntent.putExtra("Current Folder", currentBookmarksFolder);
747
748             // Make it so.
749             startActivity(bookmarksIntent);
750         });
751
752         // Set the create new bookmark folder FAB to display an alert dialog.
753         createBookmarkFolderFab.setOnClickListener(v -> {
754             // Show the create bookmark folder dialog and name the instance `@string/create_folder`.
755             DialogFragment createBookmarkFolderDialog = new CreateBookmarkFolderDialog();
756             createBookmarkFolderDialog.show(fragmentManager, resources.getString(R.string.create_folder));
757         });
758
759         // Set the create new bookmark FAB to display an alert dialog.
760         createBookmarkFab.setOnClickListener(view -> {
761             // Instantiate the create bookmark dialog.
762             DialogFragment createBookmarkDialog = CreateBookmarkDialog.createBookmark(currentWebView.getUrl(), currentWebView.getTitle(), favoriteIconBitmap);
763
764             // Display the create bookmark dialog.
765             createBookmarkDialog.show(fragmentManager, resources.getString(R.string.create_bookmark));
766         });
767
768         // Search for the string on the page whenever a character changes in the `findOnPageEditText`.
769         findOnPageEditText.addTextChangedListener(new TextWatcher() {
770             @Override
771             public void beforeTextChanged(CharSequence s, int start, int count, int after) {
772                 // Do nothing.
773             }
774
775             @Override
776             public void onTextChanged(CharSequence s, int start, int before, int count) {
777                 // Do nothing.
778             }
779
780             @Override
781             public void afterTextChanged(Editable s) {
782                 // Search for the text in `mainWebView`.
783                 currentWebView.findAllAsync(findOnPageEditText.getText().toString());
784             }
785         });
786
787         // Set the `check mark` button for the `findOnPageEditText` keyboard to close the soft keyboard.
788         findOnPageEditText.setOnKeyListener((v, keyCode, event) -> {
789             if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {  // The `enter` key was pressed.
790                 // Hide the soft keyboard.
791                 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
792
793                 // Consume the event.
794                 return true;
795             } else {  // A different key was pressed.
796                 // Do not consume the event.
797                 return false;
798             }
799         });
800
801         // Implement swipe to refresh.
802         swipeRefreshLayout.setOnRefreshListener(() -> currentWebView.reload());
803
804         // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
805         swipeRefreshLayout.setProgressViewOffset(false, swipeRefreshLayout.getProgressViewStartOffset() - 10, swipeRefreshLayout.getProgressViewEndOffset());
806
807         // Set the swipe to refresh color according to the theme.
808         if (darkTheme) {
809             swipeRefreshLayout.setColorSchemeResources(R.color.blue_800);
810             swipeRefreshLayout.setProgressBackgroundColorSchemeResource(R.color.gray_850);
811         } else {
812             swipeRefreshLayout.setColorSchemeResources(R.color.blue_500);
813         }
814
815         // `DrawerTitle` identifies the `DrawerLayouts` in accessibility mode.
816         drawerLayout.setDrawerTitle(GravityCompat.START, getString(R.string.navigation_drawer));
817         drawerLayout.setDrawerTitle(GravityCompat.END, getString(R.string.bookmarks));
818
819         // Initialize the bookmarks database helper.  The `0` specifies a database version, but that is ignored and set instead using a constant in `BookmarksDatabaseHelper`.
820         bookmarksDatabaseHelper = new BookmarksDatabaseHelper(this, null, null, 0);
821
822         // Initialize `currentBookmarksFolder`.  `""` is the home folder in the database.
823         currentBookmarksFolder = "";
824
825         // Load the home folder, which is `""` in the database.
826         loadBookmarksFolder();
827
828         bookmarksListView.setOnItemClickListener((parent, view, position, id) -> {
829             // Convert the id from long to int to match the format of the bookmarks database.
830             int databaseID = (int) id;
831
832             // Get the bookmark cursor for this ID and move it to the first row.
833             Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseID);
834             bookmarkCursor.moveToFirst();
835
836             // Act upon the bookmark according to the type.
837             if (bookmarkCursor.getInt(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {  // The selected bookmark is a folder.
838                 // Store the new folder name in `currentBookmarksFolder`.
839                 currentBookmarksFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
840
841                 // Load the new folder.
842                 loadBookmarksFolder();
843             } else {  // The selected bookmark is not a folder.
844                 // Load the bookmark URL.
845                 loadUrl(bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_URL)));
846
847                 // Close the bookmarks drawer.
848                 drawerLayout.closeDrawer(GravityCompat.END);
849             }
850
851             // Close the `Cursor`.
852             bookmarkCursor.close();
853         });
854
855         bookmarksListView.setOnItemLongClickListener((parent, view, position, id) -> {
856             // Convert the database ID from `long` to `int`.
857             int databaseId = (int) id;
858
859             // Find out if the selected bookmark is a folder.
860             boolean isFolder = bookmarksDatabaseHelper.isFolder(databaseId);
861
862             if (isFolder) {
863                 // Save the current folder name, which is used in `onSaveEditBookmarkFolder()`.
864                 oldFolderNameString = bookmarksCursor.getString(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
865
866                 // Show the edit bookmark folder `AlertDialog` and name the instance `@string/edit_folder`.
867                 DialogFragment editBookmarkFolderDialog = EditBookmarkFolderDialog.folderDatabaseId(databaseId);
868                 editBookmarkFolderDialog.show(fragmentManager, resources.getString(R.string.edit_folder));
869             } else {
870                 // Show the edit bookmark `AlertDialog` and name the instance `@string/edit_bookmark`.
871                 DialogFragment editBookmarkDialog = EditBookmarkDialog.bookmarkDatabaseId(databaseId);
872                 editBookmarkDialog.show(fragmentManager, resources.getString(R.string.edit_bookmark));
873             }
874
875             // Consume the event.
876             return true;
877         });
878
879         // Get the status bar pixel size.
880         int statusBarResourceId = resources.getIdentifier("status_bar_height", "dimen", "android");
881         int statusBarPixelSize = resources.getDimensionPixelSize(statusBarResourceId);
882
883         // Get the resource density.
884         float screenDensity = resources.getDisplayMetrics().density;
885
886         // Calculate the drawer header padding.  This is used to move the text in the drawer headers below any cutouts.
887         drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity);
888         drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity);
889         drawerHeaderPaddingBottom = (int) (8 * screenDensity);
890
891         // The drawer listener is used to update the navigation menu.`
892         drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() {
893             @Override
894             public void onDrawerSlide(@NonNull View drawerView, float slideOffset) {
895             }
896
897             @Override
898             public void onDrawerOpened(@NonNull View drawerView) {
899             }
900
901             @Override
902             public void onDrawerClosed(@NonNull View drawerView) {
903             }
904
905             @Override
906             public void onDrawerStateChanged(int newState) {
907                 if ((newState == DrawerLayout.STATE_SETTLING) || (newState == DrawerLayout.STATE_DRAGGING)) {  // A drawer is opening or closing.
908                     // Get handles for the drawer headers.
909                     TextView navigationHeaderTextView = findViewById(R.id.navigationText);
910                     TextView bookmarksHeaderTextView = findViewById(R.id.bookmarks_title_textview);
911
912                     // 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.
913                     if (navigationHeaderTextView != null) {
914                         navigationHeaderTextView.setPadding(drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingTop, drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingBottom);
915                     }
916
917                     // 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.
918                     if (bookmarksHeaderTextView != null) {
919                         bookmarksHeaderTextView.setPadding(drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingTop, drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingBottom);
920                     }
921
922                     // Update the navigation menu items.
923                     navigationCloseTabMenuItem.setEnabled(tabLayout.getTabCount() > 1);
924                     navigationBackMenuItem.setEnabled(currentWebView.canGoBack());
925                     navigationForwardMenuItem.setEnabled(currentWebView.canGoForward());
926                     navigationHistoryMenuItem.setEnabled((currentWebView.canGoBack() || currentWebView.canGoForward()));
927                     navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
928
929                     // Hide the keyboard (if displayed).
930                     inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
931
932                     // 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.
933                     urlEditText.clearFocus();
934                     currentWebView.clearFocus();
935                 }
936             }
937         });
938
939         // Create the hamburger icon at the start of the AppBar.
940         actionBarDrawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.open_navigation_drawer, R.string.close_navigation_drawer);
941
942         // Initialize cookieManager.
943         cookieManager = CookieManager.getInstance();
944
945         // 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).
946         customHeaders.put("X-Requested-With", "");
947
948         // Initialize the default preference values the first time the program is run.  `false` keeps this command from resetting any current preferences back to default.
949         PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
950
951         // Get a handle for the `Runtime`.
952         privacyBrowserRuntime = Runtime.getRuntime();
953
954         // Store the application's private data directory.
955         privateDataDirectoryString = getApplicationInfo().dataDir;
956         // `dataDir` will vary, but will be something like `/data/user/0/com.stoutner.privacybrowser.standard`, which links to `/data/data/com.stoutner.privacybrowser.standard`.
957
958         // Initialize `inFullScreenBrowsingMode`, which is always false at this point because Privacy Browser never starts in full screen browsing mode.
959         inFullScreenBrowsingMode = false;
960
961         // Initialize the privacy settings variables.
962         javaScriptEnabled = false;
963         firstPartyCookiesEnabled = false;
964         thirdPartyCookiesEnabled = false;
965         domStorageEnabled = false;
966         saveFormDataEnabled = false;  // Form data can be removed once the minimum API >= 26.
967         nightMode = false;
968
969         // Inflate a bare WebView to get the default user agent.  It is not used to render content on the screen.
970         @SuppressLint("InflateParams") View webViewLayout = getLayoutInflater().inflate(R.layout.bare_webview, null, false);
971
972         // Get a handle for the WebView.
973         WebView bareWebView = webViewLayout.findViewById(R.id.bare_webview);
974
975         // Store the default user agent.
976         webViewDefaultUserAgent = bareWebView.getSettings().getUserAgentString();
977
978         // Destroy the bare WebView.
979         bareWebView.destroy();
980
981         // Initialize the favorite icon bitmap.  `ContextCompat` must be used until API >= 21.
982         Drawable favoriteIconDrawable = ContextCompat.getDrawable(getApplicationContext(), R.drawable.world);
983         BitmapDrawable favoriteIconBitmapDrawable = (BitmapDrawable) favoriteIconDrawable;
984         assert favoriteIconBitmapDrawable != null;
985         favoriteIconDefaultBitmap = favoriteIconBitmapDrawable.getBitmap();
986
987         // If the favorite icon is null, load the default.
988         if (favoriteIconBitmap == null) {
989             favoriteIconBitmap = favoriteIconDefaultBitmap;
990         }
991
992         // Initialize the user agent array adapter and string array.
993         userAgentNamesArray = ArrayAdapter.createFromResource(this, R.array.user_agent_names, R.layout.spinner_item);
994         userAgentDataArray = resources.getStringArray(R.array.user_agent_data);
995
996         // Get the intent that started the app.
997         Intent launchingIntent = getIntent();
998
999         // Get the information from the intent.
1000         String launchingIntentAction = launchingIntent.getAction();
1001         Uri launchingIntentUriData = launchingIntent.getData();
1002
1003         // If the intent action is a web search, perform the search.
1004         if ((launchingIntentAction != null) && launchingIntentAction.equals(Intent.ACTION_WEB_SEARCH)) {
1005             // Create an encoded URL string.
1006             String encodedUrlString;
1007
1008             // Sanitize the search input and convert it to a search.
1009             try {
1010                 encodedUrlString = URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8");
1011             } catch (UnsupportedEncodingException exception) {
1012                 encodedUrlString = "";
1013             }
1014
1015             // Add the base search URL.
1016             formattedUrlString = searchURL + encodedUrlString;
1017         } else if (launchingIntentUriData != null){  // Check to see if the intent contains a new URL.
1018             // Set the formatted URL string.
1019             formattedUrlString = launchingIntentUriData.toString();
1020         }
1021     }
1022
1023     @Override
1024     protected void onNewIntent(Intent intent) {
1025         // Sets the new intent as the activity intent, so that any future `getIntent()`s pick up this one instead of creating a new activity.
1026         setIntent(intent);
1027
1028         // Get the information from the intent.
1029         String intentAction = intent.getAction();
1030         Uri intentUriData = intent.getData();
1031
1032         // If the intent action is a web search, perform the search.
1033         if ((intentAction != null) && intentAction.equals(Intent.ACTION_WEB_SEARCH)) {
1034             // Create an encoded URL string.
1035             String encodedUrlString;
1036
1037             // Sanitize the search input and convert it to a search.
1038             try {
1039                 encodedUrlString = URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8");
1040             } catch (UnsupportedEncodingException exception) {
1041                 encodedUrlString = "";
1042             }
1043
1044             // Add the base search URL.
1045             formattedUrlString = searchURL + encodedUrlString;
1046         } else if (intentUriData != null){  // Check to see if the intent contains a new URL.
1047             // Set the formatted URL string.
1048             formattedUrlString = intentUriData.toString();
1049         }
1050
1051         // Load the URL.
1052         loadUrl(formattedUrlString);
1053
1054         // Get a handle for the drawer layout.
1055         DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
1056
1057         // Close the navigation drawer if it is open.
1058         if (drawerLayout.isDrawerVisible(GravityCompat.START)) {
1059             drawerLayout.closeDrawer(GravityCompat.START);
1060         }
1061
1062         // Close the bookmarks drawer if it is open.
1063         if (drawerLayout.isDrawerVisible(GravityCompat.END)) {
1064             drawerLayout.closeDrawer(GravityCompat.END);
1065         }
1066
1067         // Clear the keyboard if displayed and remove the focus on the urlTextBar if it has it.
1068         currentWebView.requestFocus();
1069     }
1070
1071     @Override
1072     public void onRestart() {
1073         // Run the default commands.
1074         super.onRestart();
1075
1076         // Make sure Orbot is running if Privacy Browser is proxying through Orbot.
1077         if (proxyThroughOrbot) {
1078             // Request Orbot to start.  If Orbot is already running no hard will be caused by this request.
1079             Intent orbotIntent = new Intent("org.torproject.android.intent.action.START");
1080
1081             // Send the intent to the Orbot package.
1082             orbotIntent.setPackage("org.torproject.android");
1083
1084             // Make it so.
1085             sendBroadcast(orbotIntent);
1086         }
1087
1088         // Apply the app settings if returning from the Settings activity..
1089         if (reapplyAppSettingsOnRestart) {
1090             // Apply the app settings.
1091             applyAppSettings();
1092
1093             // Reload the webpage if displaying of images has been disabled in the Settings activity.
1094             if (reloadOnRestart) {
1095                 // Reload the WebViews.
1096                 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
1097                     // Get the WebView tab fragment.
1098                     WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
1099
1100                     // Get the fragment view.
1101                     View fragmentView = webViewTabFragment.getView();
1102
1103                     // Only reload the WebViews if they exist.
1104                     if (fragmentView != null) {
1105                         // Get the nested scroll WebView from the tab fragment.
1106                         NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
1107
1108                         // TODO this doesn't seem to work if for WebViews that aren't visible.
1109                         // Reload the WebView.
1110                         nestedScrollWebView.reload();
1111                     }
1112                 }
1113
1114                 // Reset `reloadOnRestartBoolean`.
1115                 reloadOnRestart = false;
1116             }
1117
1118             // Reset the return from settings flag.
1119             reapplyAppSettingsOnRestart = false;
1120         }
1121
1122         // Apply the domain settings if returning from the Domains activity.
1123         if (reapplyDomainSettingsOnRestart) {
1124             // Reapply the domain settings.
1125             applyDomainSettings(formattedUrlString, false, true);
1126
1127             // Reset `reapplyDomainSettingsOnRestart`.
1128             reapplyDomainSettingsOnRestart = false;
1129         }
1130
1131         // Load the URL on restart to apply changes to night mode.
1132         if (loadUrlOnRestart) {
1133             // Load the current `formattedUrlString`.
1134             loadUrl(formattedUrlString);
1135
1136             // Reset `loadUrlOnRestart.
1137             loadUrlOnRestart = false;
1138         }
1139
1140         // Update the bookmarks drawer if returning from the Bookmarks activity.
1141         if (restartFromBookmarksActivity) {
1142             // Get a handle for the drawer layout.
1143             DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
1144
1145             // Close the bookmarks drawer.
1146             drawerLayout.closeDrawer(GravityCompat.END);
1147
1148             // Reload the bookmarks drawer.
1149             loadBookmarksFolder();
1150
1151             // Reset `restartFromBookmarksActivity`.
1152             restartFromBookmarksActivity = false;
1153         }
1154
1155         // Update the privacy icon.  `true` runs `invalidateOptionsMenu` as the last step.  This can be important if the screen was rotated.
1156         updatePrivacyIcons(true);
1157     }
1158
1159     // `onResume()` runs after `onStart()`, which runs after `onCreate()` and `onRestart()`.
1160     @Override
1161     public void onResume() {
1162         // Run the default commands.
1163         super.onResume();
1164
1165         for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
1166             // Get the WebView tab fragment.
1167             WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
1168
1169             // Get the fragment view.
1170             View fragmentView = webViewTabFragment.getView();
1171
1172             // Only resume the WebViews if they exist (they won't when the app is first created).
1173             if (fragmentView != null) {
1174                 // Get the nested scroll WebView from the tab fragment.
1175                 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
1176
1177                 // Resume the nested scroll WebView JavaScript timers.
1178                 nestedScrollWebView.resumeTimers();
1179
1180                 // Resume the nested scroll WebView.
1181                 nestedScrollWebView.onResume();
1182             }
1183         }
1184
1185         // Display a message to the user if waiting for Orbot.
1186         if (waitingForOrbot && !orbotStatus.equals("ON")) {
1187             // Disable the wide view port so that the waiting for Orbot text is displayed correctly.
1188             currentWebView.getSettings().setUseWideViewPort(false);
1189
1190             // Load a waiting page.  `null` specifies no encoding, which defaults to ASCII.
1191             currentWebView.loadData(waitingForOrbotHtmlString, "text/html", null);
1192         }
1193
1194         if (displayingFullScreenVideo || inFullScreenBrowsingMode) {
1195             // Get a handle for the root frame layouts.
1196             FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
1197
1198             // Remove the translucent status flag.  This is necessary so the root frame layout can fill the entire screen.
1199             getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
1200
1201             /* Hide the system bars.
1202              * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
1203              * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
1204              * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
1205              * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
1206              */
1207             rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
1208                     View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
1209         } else if (BuildConfig.FLAVOR.contentEquals("free")) {  // Resume the adView for the free flavor.
1210             // Resume the ad.
1211             AdHelper.resumeAd(findViewById(R.id.adview));
1212         }
1213     }
1214
1215     @Override
1216     public void onPause() {
1217         // Run the default commands.
1218         super.onPause();
1219
1220         for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
1221             // Get the WebView tab fragment.
1222             WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
1223
1224             // Get the fragment view.
1225             View fragmentView = webViewTabFragment.getView();
1226
1227             // Only pause the WebViews if they exist (they won't when the app is first created).
1228             if (fragmentView != null) {
1229                 // Get the nested scroll WebView from the tab fragment.
1230                 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
1231
1232                 // Pause the nested scroll WebView.
1233                 nestedScrollWebView.onPause();
1234
1235                 // Pause the nested scroll WebView JavaScript timers.
1236                 nestedScrollWebView.pauseTimers();
1237             }
1238         }
1239
1240         // Pause the ad or it will continue to consume resources in the background on the free flavor.
1241         if (BuildConfig.FLAVOR.contentEquals("free")) {
1242             // Pause the ad.
1243             AdHelper.pauseAd(findViewById(R.id.adview));
1244         }
1245     }
1246
1247     @Override
1248     public void onDestroy() {
1249         // Unregister the Orbot status broadcast receiver.
1250         this.unregisterReceiver(orbotStatusBroadcastReceiver);
1251
1252         // Close the bookmarks cursor and database.
1253         bookmarksCursor.close();
1254         bookmarksDatabaseHelper.close();
1255
1256         // Run the default commands.
1257         super.onDestroy();
1258     }
1259
1260     @Override
1261     public boolean onCreateOptionsMenu(Menu menu) {
1262         // Inflate the menu.  This adds items to the action bar if it is present.
1263         getMenuInflater().inflate(R.menu.webview_options_menu, menu);
1264
1265         // Set mainMenu so it can be used by `onOptionsItemSelected()` and `updatePrivacyIcons`.
1266         mainMenu = menu;
1267
1268         // Set the initial status of the privacy icons.  `false` does not call `invalidateOptionsMenu` as the last step.
1269         updatePrivacyIcons(false);
1270
1271         // Get handles for the menu items.
1272         MenuItem toggleFirstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies);
1273         MenuItem toggleThirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies);
1274         MenuItem toggleDomStorageMenuItem = menu.findItem(R.id.toggle_dom_storage);
1275         MenuItem toggleSaveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data);  // Form data can be removed once the minimum API >= 26.
1276         MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data);  // Form data can be removed once the minimum API >= 26.
1277         refreshMenuItem = menu.findItem(R.id.refresh);
1278         blocklistsMenuItem = menu.findItem(R.id.blocklists);
1279         easyListMenuItem = menu.findItem(R.id.easylist);
1280         easyPrivacyMenuItem = menu.findItem(R.id.easyprivacy);
1281         fanboysAnnoyanceListMenuItem = menu.findItem(R.id.fanboys_annoyance_list);
1282         fanboysSocialBlockingListMenuItem = menu.findItem(R.id.fanboys_social_blocking_list);
1283         ultraPrivacyMenuItem = menu.findItem(R.id.ultraprivacy);
1284         blockAllThirdPartyRequestsMenuItem = menu.findItem(R.id.block_all_third_party_requests);
1285         MenuItem adConsentMenuItem = menu.findItem(R.id.ad_consent);
1286
1287         // Only display third-party cookies if API >= 21
1288         toggleThirdPartyCookiesMenuItem.setVisible(Build.VERSION.SDK_INT >= 21);
1289
1290         // Only display the form data menu items if the API < 26.
1291         toggleSaveFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
1292         clearFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
1293
1294         // Only show Ad Consent if this is the free flavor.
1295         adConsentMenuItem.setVisible(BuildConfig.FLAVOR.contentEquals("free"));
1296
1297         // Get the shared preference values.
1298         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
1299
1300         // Get the status of the additional AppBar icons.
1301         displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false);
1302
1303         // 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.
1304         if (displayAdditionalAppBarIcons) {
1305             toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
1306             toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
1307             refreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
1308         } else { //Do not display the additional icons.
1309             toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
1310             toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
1311             refreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
1312         }
1313
1314         // Replace Refresh with Stop if a URL is already loading.
1315         if (urlIsLoading) {
1316             // Set the title.
1317             refreshMenuItem.setTitle(R.string.stop);
1318
1319             // If the icon is displayed in the AppBar, set it according to the theme.
1320             if (displayAdditionalAppBarIcons) {
1321                 if (darkTheme) {
1322                     refreshMenuItem.setIcon(R.drawable.close_dark);
1323                 } else {
1324                     refreshMenuItem.setIcon(R.drawable.close_light);
1325                 }
1326             }
1327         }
1328
1329         return true;
1330     }
1331
1332     @Override
1333     public boolean onPrepareOptionsMenu(Menu menu) {
1334         // Get a handle for the swipe refresh layout.
1335         SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
1336
1337         // Get handles for the menu items.
1338         MenuItem addOrEditDomain = menu.findItem(R.id.add_or_edit_domain);
1339         MenuItem toggleFirstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies);
1340         MenuItem toggleThirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies);
1341         MenuItem toggleDomStorageMenuItem = menu.findItem(R.id.toggle_dom_storage);
1342         MenuItem toggleSaveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data);  // Form data can be removed once the minimum API >= 26.
1343         MenuItem clearDataMenuItem = menu.findItem(R.id.clear_data);
1344         MenuItem clearCookiesMenuItem = menu.findItem(R.id.clear_cookies);
1345         MenuItem clearDOMStorageMenuItem = menu.findItem(R.id.clear_dom_storage);
1346         MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data);  // Form data can be removed once the minimum API >= 26.
1347         MenuItem fontSizeMenuItem = menu.findItem(R.id.font_size);
1348         MenuItem swipeToRefreshMenuItem = menu.findItem(R.id.swipe_to_refresh);
1349         MenuItem displayImagesMenuItem = menu.findItem(R.id.display_images);
1350         MenuItem nightModeMenuItem = menu.findItem(R.id.night_mode);
1351         MenuItem proxyThroughOrbotMenuItem = menu.findItem(R.id.proxy_through_orbot);
1352
1353         // Initialize the current user agent string and the font size.
1354         String currentUserAgent = getString(R.string.user_agent_privacy_browser);
1355         int fontSize = 100;
1356
1357         // 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.
1358         if (currentWebView != null) {
1359             // Set the add or edit domain text.
1360             if (currentWebView.getDomainSettingsApplied()) {
1361                 addOrEditDomain.setTitle(R.string.edit_domain_settings);
1362             } else {
1363                 addOrEditDomain.setTitle(R.string.add_domain_settings);
1364             }
1365
1366             // Get the current user agent from the WebView.
1367             currentUserAgent = currentWebView.getSettings().getUserAgentString();
1368
1369             // Get the current font size from the
1370             fontSize = currentWebView.getSettings().getTextZoom();
1371
1372             // Set the status of the display images menu item.
1373             displayImagesMenuItem.setChecked(currentWebView.getSettings().getLoadsImagesAutomatically());
1374
1375             // Initialize the display names for the blocklists with the number of blocked requests.
1376             blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
1377             easyListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASY_LIST_BLOCKED_REQUESTS) + " - " + getString(R.string.easylist));
1378             easyPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASY_PRIVACY_BLOCKED_REQUESTS) + " - " + getString(R.string.easyprivacy));
1379             fanboysAnnoyanceListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST_BLOCKED_REQUESTS) + " - " + getString(R.string.fanboys_annoyance_list));
1380             fanboysSocialBlockingListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST_BLOCKED_REQUESTS) + " - " +
1381                     getString(R.string.fanboys_social_blocking_list));
1382             ultraPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.ULTRA_PRIVACY_BLOCKED_REQUESTS) + " - " + getString(R.string.ultraprivacy));
1383             blockAllThirdPartyRequestsMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_BLOCKED_REQUESTS) + " - " + getString(R.string.block_all_third_party_requests));
1384         }
1385
1386         // Set the status of the menu item checkboxes.
1387         toggleFirstPartyCookiesMenuItem.setChecked(firstPartyCookiesEnabled);
1388         toggleThirdPartyCookiesMenuItem.setChecked(thirdPartyCookiesEnabled);
1389         toggleDomStorageMenuItem.setChecked(domStorageEnabled);
1390         toggleSaveFormDataMenuItem.setChecked(saveFormDataEnabled);  // Form data can be removed once the minimum API >= 26.
1391         easyListMenuItem.setChecked(easyListEnabled);
1392         easyPrivacyMenuItem.setChecked(easyPrivacyEnabled);
1393         fanboysAnnoyanceListMenuItem.setChecked(fanboysAnnoyanceListEnabled);
1394         fanboysSocialBlockingListMenuItem.setChecked(fanboysSocialBlockingListEnabled);
1395         ultraPrivacyMenuItem.setChecked(ultraPrivacyEnabled);
1396         blockAllThirdPartyRequestsMenuItem.setChecked(blockAllThirdPartyRequests);
1397         swipeToRefreshMenuItem.setChecked(swipeRefreshLayout.isEnabled());
1398         nightModeMenuItem.setChecked(nightMode);
1399         proxyThroughOrbotMenuItem.setChecked(proxyThroughOrbot);
1400
1401         // Enable third-party cookies if first-party cookies are enabled.
1402         toggleThirdPartyCookiesMenuItem.setEnabled(firstPartyCookiesEnabled);
1403
1404         // Enable DOM Storage if JavaScript is enabled.
1405         toggleDomStorageMenuItem.setEnabled(javaScriptEnabled);
1406
1407         // Enable Clear Cookies if there are any.
1408         clearCookiesMenuItem.setEnabled(cookieManager.hasCookies());
1409
1410         // Get a count of the number of files in the Local Storage directory.
1411         File localStorageDirectory = new File (privateDataDirectoryString + "/app_webview/Local Storage/");
1412         int localStorageDirectoryNumberOfFiles = 0;
1413         if (localStorageDirectory.exists()) {
1414             localStorageDirectoryNumberOfFiles = localStorageDirectory.list().length;
1415         }
1416
1417         // Get a count of the number of files in the IndexedDB directory.
1418         File indexedDBDirectory = new File (privateDataDirectoryString + "/app_webview/IndexedDB");
1419         int indexedDBDirectoryNumberOfFiles = 0;
1420         if (indexedDBDirectory.exists()) {
1421             indexedDBDirectoryNumberOfFiles = indexedDBDirectory.list().length;
1422         }
1423
1424         // Enable Clear DOM Storage if there is any.
1425         clearDOMStorageMenuItem.setEnabled(localStorageDirectoryNumberOfFiles > 0 || indexedDBDirectoryNumberOfFiles > 0);
1426
1427         // Enable Clear Form Data is there is any.  This can be removed once the minimum API >= 26.
1428         if (Build.VERSION.SDK_INT < 26) {
1429             WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(this);
1430             clearFormDataMenuItem.setEnabled(mainWebViewDatabase.hasFormData());
1431         } else {
1432             // Disable clear form data because it is not supported on current version of Android.
1433             clearFormDataMenuItem.setEnabled(false);
1434         }
1435
1436         // Enable Clear Data if any of the submenu items are enabled.
1437         clearDataMenuItem.setEnabled(clearCookiesMenuItem.isEnabled() || clearDOMStorageMenuItem.isEnabled() || clearFormDataMenuItem.isEnabled());
1438
1439         // Disable Fanboy's Social Blocking List if Fanboy's Annoyance List is checked.
1440         fanboysSocialBlockingListMenuItem.setEnabled(!fanboysAnnoyanceListEnabled);
1441
1442         // Select the current user agent menu item.  A switch statement cannot be used because the user agents are not compile time constants.
1443         if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[0])) {  // Privacy Browser.
1444             menu.findItem(R.id.user_agent_privacy_browser).setChecked(true);
1445         } else if (currentUserAgent.equals(webViewDefaultUserAgent)) {  // WebView Default.
1446             menu.findItem(R.id.user_agent_webview_default).setChecked(true);
1447         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[2])) {  // Firefox on Android.
1448             menu.findItem(R.id.user_agent_firefox_on_android).setChecked(true);
1449         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[3])) {  // Chrome on Android.
1450             menu.findItem(R.id.user_agent_chrome_on_android).setChecked(true);
1451         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[4])) {  // Safari on iOS.
1452             menu.findItem(R.id.user_agent_safari_on_ios).setChecked(true);
1453         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[5])) {  // Firefox on Linux.
1454             menu.findItem(R.id.user_agent_firefox_on_linux).setChecked(true);
1455         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[6])) {  // Chromium on Linux.
1456             menu.findItem(R.id.user_agent_chromium_on_linux).setChecked(true);
1457         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[7])) {  // Firefox on Windows.
1458             menu.findItem(R.id.user_agent_firefox_on_windows).setChecked(true);
1459         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[8])) {  // Chrome on Windows.
1460             menu.findItem(R.id.user_agent_chrome_on_windows).setChecked(true);
1461         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[9])) {  // Edge on Windows.
1462             menu.findItem(R.id.user_agent_edge_on_windows).setChecked(true);
1463         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[10])) {  // Internet Explorer on Windows.
1464             menu.findItem(R.id.user_agent_internet_explorer_on_windows).setChecked(true);
1465         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[11])) {  // Safari on macOS.
1466             menu.findItem(R.id.user_agent_safari_on_macos).setChecked(true);
1467         } else {  // Custom user agent.
1468             menu.findItem(R.id.user_agent_custom).setChecked(true);
1469         }
1470
1471         // Instantiate the font size title and the selected font size menu item.
1472         String fontSizeTitle;
1473         MenuItem selectedFontSizeMenuItem;
1474
1475         // Prepare the font size title and current size menu item.
1476         switch (fontSize) {
1477             case 25:
1478                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.twenty_five_percent);
1479                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_twenty_five_percent);
1480                 break;
1481
1482             case 50:
1483                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.fifty_percent);
1484                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_fifty_percent);
1485                 break;
1486
1487             case 75:
1488                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.seventy_five_percent);
1489                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_seventy_five_percent);
1490                 break;
1491
1492             case 100:
1493                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent);
1494                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent);
1495                 break;
1496
1497             case 125:
1498                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_twenty_five_percent);
1499                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_twenty_five_percent);
1500                 break;
1501
1502             case 150:
1503                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_fifty_percent);
1504                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_fifty_percent);
1505                 break;
1506
1507             case 175:
1508                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_seventy_five_percent);
1509                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_seventy_five_percent);
1510                 break;
1511
1512             case 200:
1513                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.two_hundred_percent);
1514                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_two_hundred_percent);
1515                 break;
1516
1517             default:
1518                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent);
1519                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent);
1520                 break;
1521         }
1522
1523         // Set the font size title and select the current size menu item.
1524         fontSizeMenuItem.setTitle(fontSizeTitle);
1525         selectedFontSizeMenuItem.setChecked(true);
1526
1527         // Run all the other default commands.
1528         super.onPrepareOptionsMenu(menu);
1529
1530         // Display the menu.
1531         return true;
1532     }
1533
1534     @Override
1535     // Remove Android Studio's warning about the dangers of using SetJavaScriptEnabled.
1536     @SuppressLint("SetJavaScriptEnabled")
1537     // removeAllCookies is deprecated, but it is required for API < 21.
1538     @SuppressWarnings("deprecation")
1539     public boolean onOptionsItemSelected(MenuItem menuItem) {
1540         // Reenter full screen browsing mode if it was interrupted by the options menu.  <https://redmine.stoutner.com/issues/389>
1541         if (inFullScreenBrowsingMode) {
1542             // Remove the translucent status flag.  This is necessary so the root frame layout can fill the entire screen.
1543             getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
1544
1545             FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
1546
1547             /* Hide the system bars.
1548              * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
1549              * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
1550              * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
1551              * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
1552              */
1553             rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
1554                     View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
1555         }
1556
1557         // Get the selected menu item ID.
1558         int menuItemId = menuItem.getItemId();
1559
1560         // Run the commands that correlate to the selected menu item.
1561         switch (menuItemId) {
1562             case R.id.toggle_javascript:
1563                 // Switch the status of javaScriptEnabled.
1564                 javaScriptEnabled = !javaScriptEnabled;
1565
1566                 // Apply the new JavaScript status.
1567                 currentWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled);
1568
1569                 // Update the privacy icon.  `true` runs `invalidateOptionsMenu` as the last step.
1570                 updatePrivacyIcons(true);
1571
1572                 // Display a `Snackbar`.
1573                 if (javaScriptEnabled) {  // JavaScrip is enabled.
1574                     Snackbar.make(findViewById(R.id.webviewpager), R.string.javascript_enabled, Snackbar.LENGTH_SHORT).show();
1575                 } else if (firstPartyCookiesEnabled) {  // JavaScript is disabled, but first-party cookies are enabled.
1576                     Snackbar.make(findViewById(R.id.webviewpager), R.string.javascript_disabled, Snackbar.LENGTH_SHORT).show();
1577                 } else {  // Privacy mode.
1578                     Snackbar.make(findViewById(R.id.webviewpager), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
1579                 }
1580
1581                 // Reload the current WebView.
1582                 currentWebView.reload();
1583                 return true;
1584
1585             case R.id.add_or_edit_domain:
1586                 if (currentWebView.getDomainSettingsApplied()) {  // Edit the current domain settings.
1587                     // Reapply the domain settings on returning to `MainWebViewActivity`.
1588                     reapplyDomainSettingsOnRestart = true;
1589                     currentDomainName = "";
1590
1591                     // Create an intent to launch the domains activity.
1592                     Intent domainsIntent = new Intent(this, DomainsActivity.class);
1593
1594                     // Put extra information instructing the domains activity to directly load the current domain and close on back instead of returning to the domains list.
1595                     domainsIntent.putExtra("loadDomain", currentWebView.getDomainSettingsDatabaseId());
1596                     domainsIntent.putExtra("closeOnBack", true);
1597
1598                     // Make it so.
1599                     startActivity(domainsIntent);
1600                 } else {  // Add a new domain.
1601                     // Apply the new domain settings on returning to `MainWebViewActivity`.
1602                     reapplyDomainSettingsOnRestart = true;
1603                     currentDomainName = "";
1604
1605                     // Get the current domain
1606                     Uri currentUri = Uri.parse(formattedUrlString);
1607                     String currentDomain = currentUri.getHost();
1608
1609                     // Initialize the database handler.  The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
1610                     DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
1611
1612                     // Create the domain and store the database ID.
1613                     int newDomainDatabaseId = domainsDatabaseHelper.addDomain(currentDomain);
1614
1615                     // Create an intent to launch the domains activity.
1616                     Intent domainsIntent = new Intent(this, DomainsActivity.class);
1617
1618                     // Put extra information instructing the domains activity to directly load the new domain and close on back instead of returning to the domains list.
1619                     domainsIntent.putExtra("loadDomain", newDomainDatabaseId);
1620                     domainsIntent.putExtra("closeOnBack", true);
1621
1622                     // Make it so.
1623                     startActivity(domainsIntent);
1624                 }
1625                 return true;
1626
1627             case R.id.toggle_first_party_cookies:
1628                 // Switch the status of firstPartyCookiesEnabled.
1629                 firstPartyCookiesEnabled = !firstPartyCookiesEnabled;
1630
1631                 // Update the menu checkbox.
1632                 menuItem.setChecked(firstPartyCookiesEnabled);
1633
1634                 // Apply the new cookie status.
1635                 cookieManager.setAcceptCookie(firstPartyCookiesEnabled);
1636
1637                 // Update the privacy icon.  `true` runs `invalidateOptionsMenu` as the last step.
1638                 updatePrivacyIcons(true);
1639
1640                 // Display a `Snackbar`.
1641                 if (firstPartyCookiesEnabled) {  // First-party cookies are enabled.
1642                     Snackbar.make(findViewById(R.id.webviewpager), R.string.first_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
1643                 } else if (javaScriptEnabled) {  // JavaScript is still enabled.
1644                     Snackbar.make(findViewById(R.id.webviewpager), R.string.first_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
1645                 } else {  // Privacy mode.
1646                     Snackbar.make(findViewById(R.id.webviewpager), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
1647                 }
1648
1649                 // Reload the current WebView.
1650                 currentWebView.reload();
1651                 return true;
1652
1653             case R.id.toggle_third_party_cookies:
1654                 if (Build.VERSION.SDK_INT >= 21) {
1655                     // Switch the status of thirdPartyCookiesEnabled.
1656                     thirdPartyCookiesEnabled = !thirdPartyCookiesEnabled;
1657
1658                     // Update the menu checkbox.
1659                     menuItem.setChecked(thirdPartyCookiesEnabled);
1660
1661                     // Apply the new cookie status.
1662                     cookieManager.setAcceptThirdPartyCookies(currentWebView, thirdPartyCookiesEnabled);
1663
1664                     // Display a `Snackbar`.
1665                     if (thirdPartyCookiesEnabled) {
1666                         Snackbar.make(findViewById(R.id.webviewpager), R.string.third_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
1667                     } else {
1668                         Snackbar.make(findViewById(R.id.webviewpager), R.string.third_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
1669                     }
1670
1671                     // Reload the current WebView.
1672                     currentWebView.reload();
1673                 } // Else do nothing because SDK < 21.
1674                 return true;
1675
1676             case R.id.toggle_dom_storage:
1677                 // Switch the status of domStorageEnabled.
1678                 domStorageEnabled = !domStorageEnabled;
1679
1680                 // Update the menu checkbox.
1681                 menuItem.setChecked(domStorageEnabled);
1682
1683                 // Apply the new DOM Storage status.
1684                 currentWebView.getSettings().setDomStorageEnabled(domStorageEnabled);
1685
1686                 // Update the privacy icon.  `true` runs `invalidateOptionsMenu` as the last step.
1687                 updatePrivacyIcons(true);
1688
1689                 // Display a `Snackbar`.
1690                 if (domStorageEnabled) {
1691                     Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_enabled, Snackbar.LENGTH_SHORT).show();
1692                 } else {
1693                     Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_disabled, Snackbar.LENGTH_SHORT).show();
1694                 }
1695
1696                 // Reload the current WebView.
1697                 currentWebView.reload();
1698                 return true;
1699
1700             // Form data can be removed once the minimum API >= 26.
1701             case R.id.toggle_save_form_data:
1702                 // Switch the status of saveFormDataEnabled.
1703                 saveFormDataEnabled = !saveFormDataEnabled;
1704
1705                 // Update the menu checkbox.
1706                 menuItem.setChecked(saveFormDataEnabled);
1707
1708                 // Apply the new form data status.
1709                 currentWebView.getSettings().setSaveFormData(saveFormDataEnabled);
1710
1711                 // Display a `Snackbar`.
1712                 if (saveFormDataEnabled) {
1713                     Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_enabled, Snackbar.LENGTH_SHORT).show();
1714                 } else {
1715                     Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_disabled, Snackbar.LENGTH_SHORT).show();
1716                 }
1717
1718                 // Update the privacy icon.  `true` runs `invalidateOptionsMenu` as the last step.
1719                 updatePrivacyIcons(true);
1720
1721                 // Reload the current WebView.
1722                 currentWebView.reload();
1723                 return true;
1724
1725             case R.id.clear_cookies:
1726                 Snackbar.make(findViewById(R.id.webviewpager), R.string.cookies_deleted, Snackbar.LENGTH_LONG)
1727                         .setAction(R.string.undo, v -> {
1728                             // Do nothing because everything will be handled by `onDismissed()` below.
1729                         })
1730                         .addCallback(new Snackbar.Callback() {
1731                             @SuppressLint("SwitchIntDef")  // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1732                             @Override
1733                             public void onDismissed(Snackbar snackbar, int event) {
1734                                 switch (event) {
1735                                     // The user pushed the undo button.
1736                                     case Snackbar.Callback.DISMISS_EVENT_ACTION:
1737                                         // Do nothing.
1738                                         break;
1739
1740                                     // The snackbar was dismissed without the undo button being pushed.
1741                                     default:
1742                                         // `cookieManager.removeAllCookie()` varies by SDK.
1743                                         if (Build.VERSION.SDK_INT < 21) {
1744                                             cookieManager.removeAllCookie();
1745                                         } else {
1746                                             cookieManager.removeAllCookies(null);
1747                                         }
1748                                 }
1749                             }
1750                         })
1751                         .show();
1752                 return true;
1753
1754             case R.id.clear_dom_storage:
1755                 Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_deleted, Snackbar.LENGTH_LONG)
1756                         .setAction(R.string.undo, v -> {
1757                             // Do nothing because everything will be handled by `onDismissed()` below.
1758                         })
1759                         .addCallback(new Snackbar.Callback() {
1760                             @SuppressLint("SwitchIntDef")  // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1761                             @Override
1762                             public void onDismissed(Snackbar snackbar, int event) {
1763                                 switch (event) {
1764                                     // The user pushed the undo button.
1765                                     case Snackbar.Callback.DISMISS_EVENT_ACTION:
1766                                         // Do nothing.
1767                                         break;
1768
1769                                     // The snackbar was dismissed without the undo button being pushed.
1770                                     default:
1771                                         // Delete the DOM Storage.
1772                                         WebStorage webStorage = WebStorage.getInstance();
1773                                         webStorage.deleteAllData();
1774
1775                                         // Initialize a handler to manually delete the DOM storage files and directories.
1776                                         Handler deleteDomStorageHandler = new Handler();
1777
1778                                         // Setup a runnable to manually delete the DOM storage files and directories.
1779                                         Runnable deleteDomStorageRunnable = () -> {
1780                                             try {
1781                                                 // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
1782                                                 Process deleteLocalStorageProcess = privacyBrowserRuntime.exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
1783
1784                                                 // Multiple commands must be used because `Runtime.exec()` does not like `*`.
1785                                                 Process deleteIndexProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
1786                                                 Process deleteQuotaManagerProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
1787                                                 Process deleteQuotaManagerJournalProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
1788                                                 Process deleteDatabasesProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
1789
1790                                                 // Wait for the processes to finish.
1791                                                 deleteLocalStorageProcess.waitFor();
1792                                                 deleteIndexProcess.waitFor();
1793                                                 deleteQuotaManagerProcess.waitFor();
1794                                                 deleteQuotaManagerJournalProcess.waitFor();
1795                                                 deleteDatabasesProcess.waitFor();
1796                                             } catch (Exception exception) {
1797                                                 // Do nothing if an error is thrown.
1798                                             }
1799                                         };
1800
1801                                         // Manually delete the DOM storage files after 200 milliseconds.
1802                                         deleteDomStorageHandler.postDelayed(deleteDomStorageRunnable, 200);
1803                                 }
1804                             }
1805                         })
1806                         .show();
1807                 return true;
1808
1809             // Form data can be remove once the minimum API >= 26.
1810             case R.id.clear_form_data:
1811                 Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_deleted, Snackbar.LENGTH_LONG)
1812                         .setAction(R.string.undo, v -> {
1813                             // Do nothing because everything will be handled by `onDismissed()` below.
1814                         })
1815                         .addCallback(new Snackbar.Callback() {
1816                             @SuppressLint("SwitchIntDef")  // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1817                             @Override
1818                             public void onDismissed(Snackbar snackbar, int event) {
1819                                 switch (event) {
1820                                     // The user pushed the undo button.
1821                                     case Snackbar.Callback.DISMISS_EVENT_ACTION:
1822                                         // Do nothing.
1823                                         break;
1824
1825                                     // The snackbar was dismissed without the `Undo` button being pushed.
1826                                     default:
1827                                         // Delete the form data.
1828                                         WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(getApplicationContext());
1829                                         mainWebViewDatabase.clearFormData();
1830                                 }
1831                             }
1832                         })
1833                         .show();
1834                 return true;
1835
1836             case R.id.easylist:
1837                 // Toggle the EasyList status.
1838                 easyListEnabled = !easyListEnabled;
1839
1840                 // Update the menu checkbox.
1841                 menuItem.setChecked(easyListEnabled);
1842
1843                 // Reload the current WebView.
1844                 currentWebView.reload();
1845                 return true;
1846
1847             case R.id.easyprivacy:
1848                 // Toggle the EasyPrivacy status.
1849                 easyPrivacyEnabled = !easyPrivacyEnabled;
1850
1851                 // Update the menu checkbox.
1852                 menuItem.setChecked(easyPrivacyEnabled);
1853
1854                 // Reload the current WebView.
1855                 currentWebView.reload();
1856                 return true;
1857
1858             case R.id.fanboys_annoyance_list:
1859                 // Toggle Fanboy's Annoyance List status.
1860                 fanboysAnnoyanceListEnabled = !fanboysAnnoyanceListEnabled;
1861
1862                 // Update the menu checkbox.
1863                 menuItem.setChecked(fanboysAnnoyanceListEnabled);
1864
1865                 // Update the staus of Fanboy's Social Blocking List.
1866                 MenuItem fanboysSocialBlockingListMenuItem = mainMenu.findItem(R.id.fanboys_social_blocking_list);
1867                 fanboysSocialBlockingListMenuItem.setEnabled(!fanboysAnnoyanceListEnabled);
1868
1869                 // Reload the current WebView.
1870                 currentWebView.reload();
1871                 return true;
1872
1873             case R.id.fanboys_social_blocking_list:
1874                 // Toggle Fanboy's Social Blocking List status.
1875                 fanboysSocialBlockingListEnabled = !fanboysSocialBlockingListEnabled;
1876
1877                 // Update the menu checkbox.
1878                 menuItem.setChecked(fanboysSocialBlockingListEnabled);
1879
1880                 // Reload the current WebView.
1881                 currentWebView.reload();
1882                 return true;
1883
1884             case R.id.ultraprivacy:
1885                 // Toggle the UltraPrivacy status.
1886                 ultraPrivacyEnabled = !ultraPrivacyEnabled;
1887
1888                 // Update the menu checkbox.
1889                 menuItem.setChecked(ultraPrivacyEnabled);
1890
1891                 // Reload the current WebView.
1892                 currentWebView.reload();
1893                 return true;
1894
1895             case R.id.block_all_third_party_requests:
1896                 //Toggle the third-party requests blocker status.
1897                 blockAllThirdPartyRequests = !blockAllThirdPartyRequests;
1898
1899                 // Update the menu checkbox.
1900                 menuItem.setChecked(blockAllThirdPartyRequests);
1901
1902                 // Reload the current WebView.
1903                 currentWebView.reload();
1904                 return true;
1905
1906             case R.id.user_agent_privacy_browser:
1907                 // Update the user agent.
1908                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[0]);
1909
1910                 // Reload the current WebView.
1911                 currentWebView.reload();
1912                 return true;
1913
1914             case R.id.user_agent_webview_default:
1915                 // Update the user agent.
1916                 currentWebView.getSettings().setUserAgentString("");
1917
1918                 // Reload the current WebView.
1919                 currentWebView.reload();
1920                 return true;
1921
1922             case R.id.user_agent_firefox_on_android:
1923                 // Update the user agent.
1924                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[2]);
1925
1926                 // Reload the current WebView.
1927                 currentWebView.reload();
1928                 return true;
1929
1930             case R.id.user_agent_chrome_on_android:
1931                 // Update the user agent.
1932                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[3]);
1933
1934                 // Reload the current WebView.
1935                 currentWebView.reload();
1936                 return true;
1937
1938             case R.id.user_agent_safari_on_ios:
1939                 // Update the user agent.
1940                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[4]);
1941
1942                 // Reload the current WebView.
1943                 currentWebView.reload();
1944                 return true;
1945
1946             case R.id.user_agent_firefox_on_linux:
1947                 // Update the user agent.
1948                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[5]);
1949
1950                 // Reload the current WebView.
1951                 currentWebView.reload();
1952                 return true;
1953
1954             case R.id.user_agent_chromium_on_linux:
1955                 // Update the user agent.
1956                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[6]);
1957
1958                 // Reload the current WebView.
1959                 currentWebView.reload();
1960                 return true;
1961
1962             case R.id.user_agent_firefox_on_windows:
1963                 // Update the user agent.
1964                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[7]);
1965
1966                 // Reload the current WebView.
1967                 currentWebView.reload();
1968                 return true;
1969
1970             case R.id.user_agent_chrome_on_windows:
1971                 // Update the user agent.
1972                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[8]);
1973
1974                 // Reload the current WebView.
1975                 currentWebView.reload();
1976                 return true;
1977
1978             case R.id.user_agent_edge_on_windows:
1979                 // Update the user agent.
1980                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[9]);
1981
1982                 // Reload the current WebView.
1983                 currentWebView.reload();
1984                 return true;
1985
1986             case R.id.user_agent_internet_explorer_on_windows:
1987                 // Update the user agent.
1988                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[10]);
1989
1990                 // Reload the current WebView.
1991                 currentWebView.reload();
1992                 return true;
1993
1994             case R.id.user_agent_safari_on_macos:
1995                 // Update the user agent.
1996                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[11]);
1997
1998                 // Reload the current WebView.
1999                 currentWebView.reload();
2000                 return true;
2001
2002             case R.id.user_agent_custom:
2003                 // Update the user agent.
2004                 currentWebView.getSettings().setUserAgentString(defaultCustomUserAgentString);
2005
2006                 // Reload the current WebView.
2007                 currentWebView.reload();
2008                 return true;
2009
2010             case R.id.font_size_twenty_five_percent:
2011                 currentWebView.getSettings().setTextZoom(25);
2012                 return true;
2013
2014             case R.id.font_size_fifty_percent:
2015                 currentWebView.getSettings().setTextZoom(50);
2016                 return true;
2017
2018             case R.id.font_size_seventy_five_percent:
2019                 currentWebView.getSettings().setTextZoom(75);
2020                 return true;
2021
2022             case R.id.font_size_one_hundred_percent:
2023                 currentWebView.getSettings().setTextZoom(100);
2024                 return true;
2025
2026             case R.id.font_size_one_hundred_twenty_five_percent:
2027                 currentWebView.getSettings().setTextZoom(125);
2028                 return true;
2029
2030             case R.id.font_size_one_hundred_fifty_percent:
2031                 currentWebView.getSettings().setTextZoom(150);
2032                 return true;
2033
2034             case R.id.font_size_one_hundred_seventy_five_percent:
2035                 currentWebView.getSettings().setTextZoom(175);
2036                 return true;
2037
2038             case R.id.font_size_two_hundred_percent:
2039                 currentWebView.getSettings().setTextZoom(200);
2040                 return true;
2041
2042             case R.id.swipe_to_refresh:
2043                 // Get a handle for the swipe refresh layout.
2044                 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
2045
2046                 // Toggle swipe to refresh.
2047                 swipeRefreshLayout.setEnabled(!swipeRefreshLayout.isEnabled());
2048                 return true;
2049
2050             case R.id.display_images:
2051                 if (currentWebView.getSettings().getLoadsImagesAutomatically()) {  // Images are currently loaded automatically.
2052                     // Disable loading of images.
2053                     currentWebView.getSettings().setLoadsImagesAutomatically(false);
2054
2055                     // Reload the website to remove existing images.
2056                     currentWebView.reload();
2057                 } else {  // Images are not currently loaded automatically.
2058                     // Enable loading of images.  Missing images will be loaded without the need for a reload.
2059                     currentWebView.getSettings().setLoadsImagesAutomatically(true);
2060                 }
2061                 return true;
2062
2063             case R.id.night_mode:
2064                 // Toggle night mode.
2065                 nightMode = !nightMode;
2066
2067                 // Enable or disable JavaScript according to night mode, the global preference, and any domain settings.
2068                 if (nightMode) {  // Night mode is enabled.  Enable JavaScript.
2069                     // Update the global variable.
2070                     javaScriptEnabled = true;
2071                 } else if (currentWebView.getDomainSettingsApplied()) {  // Night mode is disabled and domain settings are applied.  Set JavaScript according to the domain settings.
2072                     // Get the JavaScript preference that was stored the last time domain settings were loaded.
2073                     javaScriptEnabled = domainSettingsJavaScriptEnabled;
2074                 } else {  // Night mode is disabled and domain settings are not applied.  Set JavaScript according to the global preference.
2075                     // Get a handle for the shared preference.
2076                     SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
2077
2078                     // Get the JavaScript preference.
2079                     javaScriptEnabled = sharedPreferences.getBoolean("javascript", false);
2080                 }
2081
2082                 // Apply the JavaScript setting to the WebView.
2083                 currentWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled);
2084
2085                 // Update the privacy icons.
2086                 updatePrivacyIcons(false);
2087
2088                 // Reload the website.
2089                 currentWebView.reload();
2090                 return true;
2091
2092             case R.id.find_on_page:
2093                 // Get a handle for the views.
2094                 Toolbar toolbar = findViewById(R.id.toolbar);
2095                 LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
2096
2097                 // Hide the toolbar.
2098                 toolbar.setVisibility(View.GONE);
2099
2100                 // Show the find on page linear layout.
2101                 findOnPageLinearLayout.setVisibility(View.VISIBLE);
2102
2103                 // Display the keyboard.  The app must wait 200 ms before running the command to work around a bug in Android.
2104                 // http://stackoverflow.com/questions/5520085/android-show-softkeyboard-with-showsoftinput-is-not-working
2105                 findOnPageEditText.postDelayed(() -> {
2106                     // Set the focus on `findOnPageEditText`.
2107                     findOnPageEditText.requestFocus();
2108
2109                     // Display the keyboard.  `0` sets no input flags.
2110                     inputMethodManager.showSoftInput(findOnPageEditText, 0);
2111                 }, 200);
2112                 return true;
2113
2114             case R.id.view_source:
2115                 // Launch the View Source activity.
2116                 Intent viewSourceIntent = new Intent(this, ViewSourceActivity.class);
2117                 startActivity(viewSourceIntent);
2118                 return true;
2119
2120             case R.id.share_url:
2121                 // Setup the share string.
2122                 String shareString = currentWebView.getTitle() + " – " + formattedUrlString;
2123
2124                 // Create the share intent.
2125                 Intent shareIntent = new Intent(Intent.ACTION_SEND);
2126                 shareIntent.putExtra(Intent.EXTRA_TEXT, shareString);
2127                 shareIntent.setType("text/plain");
2128
2129                 // Make it so.
2130                 startActivity(Intent.createChooser(shareIntent, getString(R.string.share_url)));
2131                 return true;
2132
2133             case R.id.print:
2134                 // Get a `PrintManager` instance.
2135                 PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);
2136
2137                 // Create a print document adapter form the current WebView.
2138                 PrintDocumentAdapter printDocumentAdapter = currentWebView.createPrintDocumentAdapter();
2139
2140                 // Remove the lint error below that `printManager` might be `null`.
2141                 assert printManager != null;
2142
2143                 // Print the document.  The print attributes are `null`.
2144                 printManager.print(getString(R.string.privacy_browser_web_page), printDocumentAdapter, null);
2145                 return true;
2146
2147             case R.id.open_with_app:
2148                 openWithApp(formattedUrlString);
2149                 return true;
2150
2151             case R.id.open_with_browser:
2152                 openWithBrowser(formattedUrlString);
2153                 return true;
2154
2155             case R.id.add_to_homescreen:
2156                 // Instantiate the create home screen shortcut dialog.
2157                 DialogFragment createHomeScreenShortcutDialogFragment = CreateHomeScreenShortcutDialog.createDialog(currentWebView.getTitle(), formattedUrlString, favoriteIconBitmap);
2158
2159                 // Show the create home screen shortcut dialog.
2160                 createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getString(R.string.create_shortcut));
2161                 return true;
2162
2163             case R.id.proxy_through_orbot:
2164                 // Toggle the proxy through Orbot variable.
2165                 proxyThroughOrbot = !proxyThroughOrbot;
2166
2167                 // Apply the proxy through Orbot settings.
2168                 applyProxyThroughOrbot(true);
2169                 return true;
2170
2171             case R.id.refresh:
2172                 if (menuItem.getTitle().equals(getString(R.string.refresh))) {  // The refresh button was pushed.
2173                     // Reload the current WebView.
2174                     currentWebView.reload();
2175                 } else {  // The stop button was pushed.
2176                     // Stop the loading of the WebView.
2177                     currentWebView.stopLoading();
2178                 }
2179                 return true;
2180
2181             case R.id.ad_consent:
2182                 // Display the ad consent dialog.
2183                 DialogFragment adConsentDialogFragment = new AdConsentDialog();
2184                 adConsentDialogFragment.show(getSupportFragmentManager(), getString(R.string.ad_consent));
2185                 return true;
2186
2187             default:
2188                 // Don't consume the event.
2189                 return super.onOptionsItemSelected(menuItem);
2190         }
2191     }
2192
2193     // removeAllCookies is deprecated, but it is required for API < 21.
2194     @SuppressWarnings("deprecation")
2195     @Override
2196     public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
2197         // Get the menu item ID.
2198         int menuItemId = menuItem.getItemId();
2199
2200         // Run the commands that correspond to the selected menu item.
2201         switch (menuItemId) {
2202             case R.id.close_tab:
2203                 // Get a handle for the tab layout.
2204                 TabLayout tabLayout = findViewById(R.id.tablayout);
2205
2206                 // Get the current tab number.
2207                 int currentTabNumber = tabLayout.getSelectedTabPosition();
2208
2209                 // Delete the current tab.
2210                 tabLayout.removeTabAt(currentTabNumber);
2211
2212                 // Delete the current page.
2213                 webViewPagerAdapter.deletePage(currentTabNumber);
2214                 break;
2215
2216             case R.id.clear_and_exit:
2217                 // Close the bookmarks cursor and database.
2218                 bookmarksCursor.close();
2219                 bookmarksDatabaseHelper.close();
2220
2221                 // Get a handle for the shared preferences.
2222                 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
2223
2224                 // Get the status of the clear everything preference.
2225                 boolean clearEverything = sharedPreferences.getBoolean("clear_everything", true);
2226
2227                 // Clear cookies.
2228                 if (clearEverything || sharedPreferences.getBoolean("clear_cookies", true)) {
2229                     // The command to remove cookies changed slightly in API 21.
2230                     if (Build.VERSION.SDK_INT >= 21) {
2231                         cookieManager.removeAllCookies(null);
2232                     } else {
2233                         cookieManager.removeAllCookie();
2234                     }
2235
2236                     // Manually delete the cookies database, as `CookieManager` sometimes will not flush its changes to disk before `System.exit(0)` is run.
2237                     try {
2238                         // Two commands must be used because `Runtime.exec()` does not like `*`.
2239                         Process deleteCookiesProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies");
2240                         Process deleteCookiesJournalProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies-journal");
2241
2242                         // Wait until the processes have finished.
2243                         deleteCookiesProcess.waitFor();
2244                         deleteCookiesJournalProcess.waitFor();
2245                     } catch (Exception exception) {
2246                         // Do nothing if an error is thrown.
2247                     }
2248                 }
2249
2250                 // Clear DOM storage.
2251                 if (clearEverything || sharedPreferences.getBoolean("clear_dom_storage", true)) {
2252                     // Ask `WebStorage` to clear the DOM storage.
2253                     WebStorage webStorage = WebStorage.getInstance();
2254                     webStorage.deleteAllData();
2255
2256                     // Manually delete the DOM storage files and directories, as `WebStorage` sometimes will not flush its changes to disk before `System.exit(0)` is run.
2257                     try {
2258                         // A `String[]` must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
2259                         Process deleteLocalStorageProcess = privacyBrowserRuntime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
2260
2261                         // Multiple commands must be used because `Runtime.exec()` does not like `*`.
2262                         Process deleteIndexProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
2263                         Process deleteQuotaManagerProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
2264                         Process deleteQuotaManagerJournalProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
2265                         Process deleteDatabaseProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
2266
2267                         // Wait until the processes have finished.
2268                         deleteLocalStorageProcess.waitFor();
2269                         deleteIndexProcess.waitFor();
2270                         deleteQuotaManagerProcess.waitFor();
2271                         deleteQuotaManagerJournalProcess.waitFor();
2272                         deleteDatabaseProcess.waitFor();
2273                     } catch (Exception exception) {
2274                         // Do nothing if an error is thrown.
2275                     }
2276                 }
2277
2278                 // Clear form data if the API < 26.
2279                 if ((Build.VERSION.SDK_INT < 26) && (clearEverything || sharedPreferences.getBoolean("clear_form_data", true))) {
2280                     WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
2281                     webViewDatabase.clearFormData();
2282
2283                     // Manually delete the form data database, as `WebViewDatabase` sometimes will not flush its changes to disk before `System.exit(0)` is run.
2284                     try {
2285                         // A string array must be used because the database contains a space and `Runtime.exec` will not otherwise escape the string correctly.
2286                         Process deleteWebDataProcess = privacyBrowserRuntime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data"});
2287                         Process deleteWebDataJournalProcess = privacyBrowserRuntime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data-journal"});
2288
2289                         // Wait until the processes have finished.
2290                         deleteWebDataProcess.waitFor();
2291                         deleteWebDataJournalProcess.waitFor();
2292                     } catch (Exception exception) {
2293                         // Do nothing if an error is thrown.
2294                     }
2295                 }
2296
2297                 // Clear the cache.
2298                 if (clearEverything || sharedPreferences.getBoolean("clear_cache", true)) {
2299                     // Clear the cache.
2300                     // TODO
2301                     currentWebView.clearCache(true);
2302
2303                     // Manually delete the cache directories.
2304                     try {
2305                         // Delete the main cache directory.
2306                         Process deleteCacheProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/cache");
2307
2308                         // Delete the secondary `Service Worker` cache directory.
2309                         // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
2310                         Process deleteServiceWorkerProcess = privacyBrowserRuntime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
2311
2312                         // Wait until the processes have finished.
2313                         deleteCacheProcess.waitFor();
2314                         deleteServiceWorkerProcess.waitFor();
2315                     } catch (Exception exception) {
2316                         // Do nothing if an error is thrown.
2317                     }
2318                 }
2319
2320                 // Clear SSL certificate preferences.
2321                 // TODO
2322                 currentWebView.clearSslPreferences();
2323
2324                 // Clear the back/forward history.
2325                 // TODO
2326                 currentWebView.clearHistory();
2327
2328                 // Clear `formattedUrlString`.
2329                 formattedUrlString = null;
2330
2331                 // Clear `customHeaders`.
2332                 customHeaders.clear();
2333
2334                 // Destroy the internal state of `mainWebView`.
2335                 // TODO
2336                 currentWebView.destroy();
2337
2338                 // Manually delete the `app_webview` folder, which contains the cookies, DOM storage, form data, and `Service Worker` cache.
2339                 // See `https://code.google.com/p/android/issues/detail?id=233826&thanks=233826&ts=1486670530`.
2340                 if (clearEverything) {
2341                     try {
2342                         // Delete the folder.
2343                         Process deleteAppWebviewProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview");
2344
2345                         // Wait until the process has finished.
2346                         deleteAppWebviewProcess.waitFor();
2347                     } catch (Exception exception) {
2348                         // Do nothing if an error is thrown.
2349                     }
2350                 }
2351
2352                 // Close Privacy Browser.  `finishAndRemoveTask` also removes Privacy Browser from the recent app list.
2353                 if (Build.VERSION.SDK_INT >= 21) {
2354                     finishAndRemoveTask();
2355                 } else {
2356                     finish();
2357                 }
2358
2359                 // Remove the terminated program from RAM.  The status code is `0`.
2360                 System.exit(0);
2361                 break;
2362
2363             case R.id.home:
2364                 loadUrl(homepage);
2365                 break;
2366
2367             case R.id.back:
2368                 if (currentWebView.canGoBack()) {
2369                     // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
2370                     formattedUrlString = "";
2371
2372                     // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
2373                     navigatingHistory = true;
2374
2375                     // Load the previous website in the history.
2376                     currentWebView.goBack();
2377                 }
2378                 break;
2379
2380             case R.id.forward:
2381                 if (currentWebView.canGoForward()) {
2382                     // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
2383                     formattedUrlString = "";
2384
2385                     // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
2386                     navigatingHistory = true;
2387
2388                     // Load the next website in the history.
2389                     currentWebView.goForward();
2390                 }
2391                 break;
2392
2393             case R.id.history:
2394                 // Get the `WebBackForwardList`.
2395                 WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList();
2396
2397                 // Show the URL history dialog and name this instance `R.string.history`.
2398                 DialogFragment urlHistoryDialogFragment = UrlHistoryDialog.loadBackForwardList(this, webBackForwardList);
2399                 urlHistoryDialogFragment.show(getSupportFragmentManager(), getString(R.string.history));
2400                 break;
2401
2402             case R.id.requests:
2403                 // Populate the resource requests.
2404                 RequestsActivity.resourceRequests = currentWebView.getResourceRequests();
2405
2406                 // Create an intent to launch the Requests activity.
2407                 Intent requestsIntent = new Intent(this, RequestsActivity.class);
2408
2409                 // Make it so.
2410                 startActivity(requestsIntent);
2411                 break;
2412
2413             case R.id.downloads:
2414                 // Launch the system Download Manager.
2415                 Intent downloadManagerIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
2416
2417                 // Launch as a new task so that Download Manager and Privacy Browser show as separate windows in the recent tasks list.
2418                 downloadManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2419
2420                 startActivity(downloadManagerIntent);
2421                 break;
2422
2423             case R.id.domains:
2424                 // Set the flag to reapply the domain settings on restart when returning from Domain Settings.
2425                 reapplyDomainSettingsOnRestart = true;
2426                 currentDomainName = "";
2427
2428                 // Launch the domains activity.
2429                 Intent domainsIntent = new Intent(this, DomainsActivity.class);
2430                 startActivity(domainsIntent);
2431                 break;
2432
2433             case R.id.settings:
2434                 // Set the flag to reapply app settings on restart when returning from Settings.
2435                 reapplyAppSettingsOnRestart = true;
2436
2437                 // Set the flag to reapply the domain settings on restart when returning from Settings.
2438                 reapplyDomainSettingsOnRestart = true;
2439                 currentDomainName = "";
2440
2441                 // Launch the settings activity.
2442                 Intent settingsIntent = new Intent(this, SettingsActivity.class);
2443                 startActivity(settingsIntent);
2444                 break;
2445
2446             case R.id.import_export:
2447                 // Launch the import/export activity.
2448                 Intent importExportIntent = new Intent (this, ImportExportActivity.class);
2449                 startActivity(importExportIntent);
2450                 break;
2451
2452             case R.id.logcat:
2453                 // Launch the logcat activity.
2454                 Intent logcatIntent = new Intent(this, LogcatActivity.class);
2455                 startActivity(logcatIntent);
2456                 break;
2457
2458             case R.id.guide:
2459                 // Launch `GuideActivity`.
2460                 Intent guideIntent = new Intent(this, GuideActivity.class);
2461                 startActivity(guideIntent);
2462                 break;
2463
2464             case R.id.about:
2465                 // Launch `AboutActivity`.
2466                 Intent aboutIntent = new Intent(this, AboutActivity.class);
2467                 startActivity(aboutIntent);
2468                 break;
2469         }
2470
2471         // Get a handle for the drawer layout.
2472         DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
2473
2474         // Close the navigation drawer.
2475         drawerLayout.closeDrawer(GravityCompat.START);
2476         return true;
2477     }
2478
2479     @Override
2480     public void onPostCreate(Bundle savedInstanceState) {
2481         // Run the default commands.
2482         super.onPostCreate(savedInstanceState);
2483
2484         // Sync the state of the DrawerToggle after the default `onRestoreInstanceState()` has finished.  This creates the navigation drawer icon.
2485         actionBarDrawerToggle.syncState();
2486     }
2487
2488     @Override
2489     public void onConfigurationChanged(Configuration newConfig) {
2490         // Run the default commands.
2491         super.onConfigurationChanged(newConfig);
2492
2493         // Get the status bar pixel size.
2494         int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
2495         int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId);
2496
2497         // Get the resource density.
2498         float screenDensity = getResources().getDisplayMetrics().density;
2499
2500         // Recalculate the drawer header padding.
2501         drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity);
2502         drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity);
2503         drawerHeaderPaddingBottom = (int) (8 * screenDensity);
2504
2505         // Reload the ad for the free flavor if not in full screen mode.
2506         if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
2507             // Reload the ad.  The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
2508             AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
2509         }
2510
2511         // `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:
2512         // https://code.google.com/p/android/issues/detail?id=20493#c8
2513         // ActivityCompat.invalidateOptionsMenu(this);
2514     }
2515
2516     @Override
2517     public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
2518         // Store the `HitTestResult`.
2519         final WebView.HitTestResult hitTestResult = currentWebView.getHitTestResult();
2520
2521         // Create strings.
2522         final String imageUrl;
2523         final String linkUrl;
2524
2525         // Get a handle for the the clipboard and fragment managers.
2526         final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
2527         FragmentManager fragmentManager = getSupportFragmentManager();
2528
2529         // Remove the lint errors below that `clipboardManager` might be `null`.
2530         assert clipboardManager != null;
2531
2532         switch (hitTestResult.getType()) {
2533             // `SRC_ANCHOR_TYPE` is a link.
2534             case WebView.HitTestResult.SRC_ANCHOR_TYPE:
2535                 // Get the target URL.
2536                 linkUrl = hitTestResult.getExtra();
2537
2538                 // Set the target URL as the title of the `ContextMenu`.
2539                 menu.setHeaderTitle(linkUrl);
2540
2541                 // Add a Load URL entry.
2542                 menu.add(R.string.load_url).setOnMenuItemClickListener((MenuItem item) -> {
2543                     loadUrl(linkUrl);
2544                     return false;
2545                 });
2546
2547                 // Add a Copy URL entry.
2548                 menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
2549                     // Save the link URL in a `ClipData`.
2550                     ClipData srcAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl);
2551
2552                     // Set the `ClipData` as the clipboard's primary clip.
2553                     clipboardManager.setPrimaryClip(srcAnchorTypeClipData);
2554                     return false;
2555                 });
2556
2557                 // Add a Download URL entry.
2558                 menu.add(R.string.download_url).setOnMenuItemClickListener((MenuItem item) -> {
2559                     // Check if the download should be processed by an external app.
2560                     if (downloadWithExternalApp) {  // Download with an external app.
2561                         openUrlWithExternalApp(linkUrl);
2562                     } else {  // Download with Android's download manager.
2563                         // Check to see if the storage permission has already been granted.
2564                         if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {  // The storage permission needs to be requested.
2565                             // Store the variables for future use by `onRequestPermissionsResult()`.
2566                             downloadUrl = linkUrl;
2567                             downloadContentDisposition = "none";
2568                             downloadContentLength = -1;
2569
2570                             // Show a dialog if the user has previously denied the permission.
2571                             if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
2572                                 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
2573                                 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE);
2574
2575                                 // Show the download location permission alert dialog.  The permission will be requested when the the dialog is closed.
2576                                 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
2577                             } else {  // Show the permission request directly.
2578                                 // Request the permission.  The download dialog will be launched by `onRequestPermissionResult()`.
2579                                 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
2580                             }
2581                         } else {  // The storage permission has already been granted.
2582                             // Get a handle for the download file alert dialog.
2583                             DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(linkUrl, "none", -1);
2584
2585                             // Show the download file alert dialog.
2586                             downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
2587                         }
2588                     }
2589                     return false;
2590                 });
2591
2592                 // Add an Open with App entry.
2593                 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2594                     openWithApp(linkUrl);
2595                     return false;
2596                 });
2597
2598                 // Add an Open with Browser entry.
2599                 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2600                     openWithBrowser(linkUrl);
2601                     return false;
2602                 });
2603
2604                 // Add a Cancel entry, which by default closes the context menu.
2605                 menu.add(R.string.cancel);
2606                 break;
2607
2608             case WebView.HitTestResult.EMAIL_TYPE:
2609                 // Get the target URL.
2610                 linkUrl = hitTestResult.getExtra();
2611
2612                 // Set the target URL as the title of the `ContextMenu`.
2613                 menu.setHeaderTitle(linkUrl);
2614
2615                 // Add a Write Email entry.
2616                 menu.add(R.string.write_email).setOnMenuItemClickListener(item -> {
2617                     // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
2618                     Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
2619
2620                     // Parse the url and set it as the data for the `Intent`.
2621                     emailIntent.setData(Uri.parse("mailto:" + linkUrl));
2622
2623                     // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser.
2624                     emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2625
2626                     // Make it so.
2627                     startActivity(emailIntent);
2628                     return false;
2629                 });
2630
2631                 // Add a Copy Email Address entry.
2632                 menu.add(R.string.copy_email_address).setOnMenuItemClickListener(item -> {
2633                     // Save the email address in a `ClipData`.
2634                     ClipData srcEmailTypeClipData = ClipData.newPlainText(getString(R.string.email_address), linkUrl);
2635
2636                     // Set the `ClipData` as the clipboard's primary clip.
2637                     clipboardManager.setPrimaryClip(srcEmailTypeClipData);
2638                     return false;
2639                 });
2640
2641                 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
2642                 menu.add(R.string.cancel);
2643                 break;
2644
2645             // `IMAGE_TYPE` is an image.
2646             case WebView.HitTestResult.IMAGE_TYPE:
2647                 // Get the image URL.
2648                 imageUrl = hitTestResult.getExtra();
2649
2650                 // Set the image URL as the title of the `ContextMenu`.
2651                 menu.setHeaderTitle(imageUrl);
2652
2653                 // Add a View Image entry.
2654                 menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
2655                     loadUrl(imageUrl);
2656                     return false;
2657                 });
2658
2659                 // Add a Download Image entry.
2660                 menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
2661                     // Check if the download should be processed by an external app.
2662                     if (downloadWithExternalApp) {  // Download with an external app.
2663                         openUrlWithExternalApp(imageUrl);
2664                     } else {  // Download with Android's download manager.
2665                         // Check to see if the storage permission has already been granted.
2666                         if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {  // The storage permission needs to be requested.
2667                             // Store the image URL for use by `onRequestPermissionResult()`.
2668                             downloadImageUrl = imageUrl;
2669
2670                             // Show a dialog if the user has previously denied the permission.
2671                             if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
2672                                 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
2673                                 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
2674
2675                                 // Show the download location permission alert dialog.  The permission will be requested when the dialog is closed.
2676                                 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
2677                             } else {  // Show the permission request directly.
2678                                 // Request the permission.  The download dialog will be launched by `onRequestPermissionResult().
2679                                 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
2680                             }
2681                         } else {  // The storage permission has already been granted.
2682                             // Get a handle for the download image alert dialog.
2683                             DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
2684
2685                             // Show the download image alert dialog.
2686                             downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
2687                         }
2688                     }
2689                     return false;
2690                 });
2691
2692                 // Add a Copy URL entry.
2693                 menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> {
2694                     // Save the image URL in a `ClipData`.
2695                     ClipData srcImageTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
2696
2697                     // Set the `ClipData` as the clipboard's primary clip.
2698                     clipboardManager.setPrimaryClip(srcImageTypeClipData);
2699                     return false;
2700                 });
2701
2702                 // Add an Open with App entry.
2703                 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2704                     openWithApp(imageUrl);
2705                     return false;
2706                 });
2707
2708                 // Add an Open with Browser entry.
2709                 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2710                     openWithBrowser(imageUrl);
2711                     return false;
2712                 });
2713
2714                 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
2715                 menu.add(R.string.cancel);
2716                 break;
2717
2718
2719             // `SRC_IMAGE_ANCHOR_TYPE` is an image that is also a link.
2720             case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
2721                 // Get the image URL.
2722                 imageUrl = hitTestResult.getExtra();
2723
2724                 // Set the image URL as the title of the `ContextMenu`.
2725                 menu.setHeaderTitle(imageUrl);
2726
2727                 // Add a `View Image` entry.
2728                 menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
2729                     loadUrl(imageUrl);
2730                     return false;
2731                 });
2732
2733                 // Add a `Download Image` entry.
2734                 menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
2735                     // Check if the download should be processed by an external app.
2736                     if (downloadWithExternalApp) {  // Download with an external app.
2737                         openUrlWithExternalApp(imageUrl);
2738                     } else {  // Download with Android's download manager.
2739                         // Check to see if the storage permission has already been granted.
2740                         if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {  // The storage permission needs to be requested.
2741                             // Store the image URL for use by `onRequestPermissionResult()`.
2742                             downloadImageUrl = imageUrl;
2743
2744                             // Show a dialog if the user has previously denied the permission.
2745                             if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
2746                                 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
2747                                 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
2748
2749                                 // Show the download location permission alert dialog.  The permission will be requested when the dialog is closed.
2750                                 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
2751                             } else {  // Show the permission request directly.
2752                                 // Request the permission.  The download dialog will be launched by `onRequestPermissionResult().
2753                                 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
2754                             }
2755                         } else {  // The storage permission has already been granted.
2756                             // Get a handle for the download image alert dialog.
2757                             DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
2758
2759                             // Show the download image alert dialog.
2760                             downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
2761                         }
2762                     }
2763                     return false;
2764                 });
2765
2766                 // Add a `Copy URL` entry.
2767                 menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> {
2768                     // Save the image URL in a `ClipData`.
2769                     ClipData srcImageAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
2770
2771                     // Set the `ClipData` as the clipboard's primary clip.
2772                     clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData);
2773                     return false;
2774                 });
2775
2776                 // Add an Open with App entry.
2777                 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2778                     openWithApp(imageUrl);
2779                     return false;
2780                 });
2781
2782                 // Add an Open with Browser entry.
2783                 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2784                     openWithBrowser(imageUrl);
2785                     return false;
2786                 });
2787
2788                 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
2789                 menu.add(R.string.cancel);
2790                 break;
2791         }
2792     }
2793
2794     @Override
2795     public void onCreateBookmark(DialogFragment dialogFragment) {
2796         // Get the views from the dialog fragment.
2797         EditText createBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_name_edittext);
2798         EditText createBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_url_edittext);
2799
2800         // Extract the strings from the edit texts.
2801         String bookmarkNameString = createBookmarkNameEditText.getText().toString();
2802         String bookmarkUrlString = createBookmarkUrlEditText.getText().toString();
2803
2804         // Get a copy of the favorite icon bitmap.
2805         Bitmap favoriteIcon = favoriteIconBitmap;
2806
2807         // Scale the favorite icon bitmap down if it is larger than 256 x 256.  Filtering uses bilinear interpolation.
2808         if ((favoriteIcon.getHeight() > 256) || (favoriteIcon.getWidth() > 256)) {
2809             favoriteIcon = Bitmap.createScaledBitmap(favoriteIcon, 256, 256, true);
2810         }
2811
2812         // Create a favorite icon byte array output stream.
2813         ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
2814
2815         // Convert the favorite icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2816         favoriteIcon.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
2817
2818         // Convert the favorite icon byte array stream to a byte array.
2819         byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
2820
2821         // Display the new bookmark below the current items in the (0 indexed) list.
2822         int newBookmarkDisplayOrder = bookmarksListView.getCount();
2823
2824         // Create the bookmark.
2825         bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolder, newBookmarkDisplayOrder, favoriteIconByteArray);
2826
2827         // Update the bookmarks cursor with the current contents of this folder.
2828         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2829
2830         // Update the list view.
2831         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2832
2833         // Scroll to the new bookmark.
2834         bookmarksListView.setSelection(newBookmarkDisplayOrder);
2835     }
2836
2837     @Override
2838     public void onCreateBookmarkFolder(DialogFragment dialogFragment) {
2839         // Get handles for the views in the dialog fragment.
2840         EditText createFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.create_folder_name_edittext);
2841         RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon_radiobutton);
2842         ImageView folderIconImageView = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon);
2843
2844         // Get new folder name string.
2845         String folderNameString = createFolderNameEditText.getText().toString();
2846
2847         // Create a folder icon bitmap.
2848         Bitmap folderIconBitmap;
2849
2850         // Set the folder icon bitmap according to the dialog.
2851         if (defaultFolderIconRadioButton.isChecked()) {  // Use the default folder icon.
2852             // Get the default folder icon drawable.
2853             Drawable folderIconDrawable = folderIconImageView.getDrawable();
2854
2855             // Convert the folder icon drawable to a bitmap drawable.
2856             BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2857
2858             // Convert the folder icon bitmap drawable to a bitmap.
2859             folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2860         } else {  // Use the WebView favorite icon.
2861             // Get a copy of the favorite icon bitmap.
2862             folderIconBitmap = favoriteIconBitmap;
2863
2864             // Scale the folder icon bitmap down if it is larger than 256 x 256.  Filtering uses bilinear interpolation.
2865             if ((folderIconBitmap.getHeight() > 256) || (folderIconBitmap.getWidth() > 256)) {
2866                 folderIconBitmap = Bitmap.createScaledBitmap(folderIconBitmap, 256, 256, true);
2867             }
2868         }
2869
2870         // Create a folder icon byte array output stream.
2871         ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream();
2872
2873         // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2874         folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream);
2875
2876         // Convert the folder icon byte array stream to a byte array.
2877         byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray();
2878
2879         // Move all the bookmarks down one in the display order.
2880         for (int i = 0; i < bookmarksListView.getCount(); i++) {
2881             int databaseId = (int) bookmarksListView.getItemIdAtPosition(i);
2882             bookmarksDatabaseHelper.updateDisplayOrder(databaseId, i + 1);
2883         }
2884
2885         // Create the folder, which will be placed at the top of the `ListView`.
2886         bookmarksDatabaseHelper.createFolder(folderNameString, currentBookmarksFolder, folderIconByteArray);
2887
2888         // Update the bookmarks cursor with the current contents of this folder.
2889         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2890
2891         // Update the `ListView`.
2892         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2893
2894         // Scroll to the new folder.
2895         bookmarksListView.setSelection(0);
2896     }
2897
2898     @Override
2899     public void onSaveBookmark(DialogFragment dialogFragment, int selectedBookmarkDatabaseId) {
2900         // Get handles for the views from `dialogFragment`.
2901         EditText editBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_name_edittext);
2902         EditText editBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_url_edittext);
2903         RadioButton currentBookmarkIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_current_icon_radiobutton);
2904
2905         // Store the bookmark strings.
2906         String bookmarkNameString = editBookmarkNameEditText.getText().toString();
2907         String bookmarkUrlString = editBookmarkUrlEditText.getText().toString();
2908
2909         // Update the bookmark.
2910         if (currentBookmarkIconRadioButton.isChecked()) {  // Update the bookmark without changing the favorite icon.
2911             bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString);
2912         } else {  // Update the bookmark using the `WebView` favorite icon.
2913             // Get a copy of the favorite icon bitmap.
2914             Bitmap favoriteIcon = favoriteIconBitmap;
2915
2916             // Scale the favorite icon bitmap down if it is larger than 256 x 256.  Filtering uses bilinear interpolation.
2917             if ((favoriteIcon.getHeight() > 256) || (favoriteIcon.getWidth() > 256)) {
2918                 favoriteIcon = Bitmap.createScaledBitmap(favoriteIcon, 256, 256, true);
2919             }
2920
2921             // Create a favorite icon byte array output stream.
2922             ByteArrayOutputStream newFavoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
2923
2924             // Convert the favorite icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2925             favoriteIcon.compress(Bitmap.CompressFormat.PNG, 0, newFavoriteIconByteArrayOutputStream);
2926
2927             // Convert the favorite icon byte array stream to a byte array.
2928             byte[] newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray();
2929
2930             //  Update the bookmark and the favorite icon.
2931             bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, newFavoriteIconByteArray);
2932         }
2933
2934         // Update the bookmarks cursor with the current contents of this folder.
2935         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2936
2937         // Update the list view.
2938         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2939     }
2940
2941     @Override
2942     public void onSaveBookmarkFolder(DialogFragment dialogFragment, int selectedFolderDatabaseId) {
2943         // Get handles for the views from `dialogFragment`.
2944         EditText editFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_folder_name_edittext);
2945         RadioButton currentFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_current_icon_radiobutton);
2946         RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_radiobutton);
2947         ImageView defaultFolderIconImageView = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_imageview);
2948
2949         // Get the new folder name.
2950         String newFolderNameString = editFolderNameEditText.getText().toString();
2951
2952         // Check if the favorite icon has changed.
2953         if (currentFolderIconRadioButton.isChecked()) {  // Only the name has changed.
2954             // Update the name in the database.
2955             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString);
2956         } else if (!currentFolderIconRadioButton.isChecked() && newFolderNameString.equals(oldFolderNameString)) {  // Only the icon has changed.
2957             // Create the new folder icon Bitmap.
2958             Bitmap folderIconBitmap;
2959
2960             // Populate the new folder icon bitmap.
2961             if (defaultFolderIconRadioButton.isChecked()) {
2962                 // Get the default folder icon drawable.
2963                 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
2964
2965                 // Convert the folder icon drawable to a bitmap drawable.
2966                 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2967
2968                 // Convert the folder icon bitmap drawable to a bitmap.
2969                 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2970             } else {  // Use the `WebView` favorite icon.
2971                 // Get a copy of the favorite icon bitmap.
2972                 folderIconBitmap = MainWebViewActivity.favoriteIconBitmap;
2973
2974                 // Scale the folder icon bitmap down if it is larger than 256 x 256.  Filtering uses bilinear interpolation.
2975                 if ((folderIconBitmap.getHeight() > 256) || (folderIconBitmap.getWidth() > 256)) {
2976                     folderIconBitmap = Bitmap.createScaledBitmap(folderIconBitmap, 256, 256, true);
2977                 }
2978             }
2979
2980             // Create a folder icon byte array output stream.
2981             ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
2982
2983             // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2984             folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
2985
2986             // Convert the folder icon byte array stream to a byte array.
2987             byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
2988
2989             // Update the folder icon in the database.
2990             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderIconByteArray);
2991         } else {  // The folder icon and the name have changed.
2992             // Get the new folder icon `Bitmap`.
2993             Bitmap folderIconBitmap;
2994             if (defaultFolderIconRadioButton.isChecked()) {
2995                 // Get the default folder icon drawable.
2996                 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
2997
2998                 // Convert the folder icon drawable to a bitmap drawable.
2999                 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
3000
3001                 // Convert the folder icon bitmap drawable to a bitmap.
3002                 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
3003             } else {  // Use the `WebView` favorite icon.
3004                 // Get a copy of the favorite icon bitmap.
3005                 folderIconBitmap = MainWebViewActivity.favoriteIconBitmap;
3006
3007                 // Scale the folder icon bitmap down if it is larger than 256 x 256.  Filtering uses bilinear interpolation.
3008                 if ((folderIconBitmap.getHeight() > 256) || (folderIconBitmap.getWidth() > 256)) {
3009                     folderIconBitmap = Bitmap.createScaledBitmap(folderIconBitmap, 256, 256, true);
3010                 }
3011             }
3012
3013             // Create a folder icon byte array output stream.
3014             ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
3015
3016             // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
3017             folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
3018
3019             // Convert the folder icon byte array stream to a byte array.
3020             byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
3021
3022             // Update the folder name and icon in the database.
3023             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, newFolderIconByteArray);
3024         }
3025
3026         // Update the bookmarks cursor with the current contents of this folder.
3027         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
3028
3029         // Update the `ListView`.
3030         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
3031     }
3032
3033     @Override
3034     public void onCloseDownloadLocationPermissionDialog(int downloadType) {
3035         switch (downloadType) {
3036             case DownloadLocationPermissionDialog.DOWNLOAD_FILE:
3037                 // Request the WRITE_EXTERNAL_STORAGE permission with a file request code.
3038                 ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
3039                 break;
3040
3041             case DownloadLocationPermissionDialog.DOWNLOAD_IMAGE:
3042                 // Request the WRITE_EXTERNAL_STORAGE permission with an image request code.
3043                 ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
3044                 break;
3045         }
3046     }
3047
3048     @Override
3049     public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
3050         // Get a handle for the fragment manager.
3051         FragmentManager fragmentManager = getSupportFragmentManager();
3052
3053         switch (requestCode) {
3054             case DOWNLOAD_FILE_REQUEST_CODE:
3055                 // Show the download file alert dialog.  When the dialog closes, the correct command will be used based on the permission status.
3056                 DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(downloadUrl, downloadContentDisposition, downloadContentLength);
3057
3058                 // On API 23, displaying the fragment must be delayed or the app will crash.
3059                 if (Build.VERSION.SDK_INT == 23) {
3060                     new Handler().postDelayed(() -> downloadFileDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
3061                 } else {
3062                     downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
3063                 }
3064
3065                 // Reset the download variables.
3066                 downloadUrl = "";
3067                 downloadContentDisposition = "";
3068                 downloadContentLength = 0;
3069                 break;
3070
3071             case DOWNLOAD_IMAGE_REQUEST_CODE:
3072                 // Show the download image alert dialog.  When the dialog closes, the correct command will be used based on the permission status.
3073                 DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(downloadImageUrl);
3074
3075                 // On API 23, displaying the fragment must be delayed or the app will crash.
3076                 if (Build.VERSION.SDK_INT == 23) {
3077                     new Handler().postDelayed(() -> downloadImageDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
3078                 } else {
3079                     downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
3080                 }
3081
3082                 // Reset the image URL variable.
3083                 downloadImageUrl = "";
3084                 break;
3085         }
3086     }
3087
3088     @Override
3089     public void onDownloadImage(DialogFragment dialogFragment, String imageUrl) {
3090         // Download the image if it has an HTTP or HTTPS URI.
3091         if (imageUrl.startsWith("http")) {
3092             // Get a handle for the system `DOWNLOAD_SERVICE`.
3093             DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
3094
3095             // Parse `imageUrl`.
3096             DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(imageUrl));
3097
3098             // Pass cookies to download manager if cookies are enabled.  This is required to download images from websites that require a login.
3099             // Code contributed 2017 Hendrik Knackstedt.  Copyright assigned to Soren Stoutner <soren@stoutner.com>.
3100             if (firstPartyCookiesEnabled) {
3101                 // Get the cookies for `imageUrl`.
3102                 String cookies = cookieManager.getCookie(imageUrl);
3103
3104                 // Add the cookies to `downloadRequest`.  In the HTTP request header, cookies are named `Cookie`.
3105                 downloadRequest.addRequestHeader("Cookie", cookies);
3106             }
3107
3108             // Get the file name from the dialog fragment.
3109             EditText downloadImageNameEditText = dialogFragment.getDialog().findViewById(R.id.download_image_name);
3110             String imageName = downloadImageNameEditText.getText().toString();
3111
3112             // Specify the download location.
3113             if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {  // External write permission granted.
3114                 // Download to the public download directory.
3115                 downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, imageName);
3116             } else {  // External write permission denied.
3117                 // Download to the app's external download directory.
3118                 downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, imageName);
3119             }
3120
3121             // Allow `MediaScanner` to index the download if it is a media file.
3122             downloadRequest.allowScanningByMediaScanner();
3123
3124             // Add the URL as the description for the download.
3125             downloadRequest.setDescription(imageUrl);
3126
3127             // Show the download notification after the download is completed.
3128             downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
3129
3130             // Remove the lint warning below that `downloadManager` might be `null`.
3131             assert downloadManager != null;
3132
3133             // Initiate the download.
3134             downloadManager.enqueue(downloadRequest);
3135         } else {  // The image is not an HTTP or HTTPS URI.
3136             Snackbar.make(currentWebView, R.string.cannot_download_image, Snackbar.LENGTH_INDEFINITE).show();
3137         }
3138     }
3139
3140     @Override
3141     public void onDownloadFile(DialogFragment dialogFragment, String downloadUrl) {
3142         // Download the file if it has an HTTP or HTTPS URI.
3143         if (downloadUrl.startsWith("http")) {
3144             // Get a handle for the system `DOWNLOAD_SERVICE`.
3145             DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
3146
3147             // Parse `downloadUrl`.
3148             DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(downloadUrl));
3149
3150             // Pass cookies to download manager if cookies are enabled.  This is required to download files from websites that require a login.
3151             // Code contributed 2017 Hendrik Knackstedt.  Copyright assigned to Soren Stoutner <soren@stoutner.com>.
3152             if (firstPartyCookiesEnabled) {
3153                 // Get the cookies for `downloadUrl`.
3154                 String cookies = cookieManager.getCookie(downloadUrl);
3155
3156                 // Add the cookies to `downloadRequest`.  In the HTTP request header, cookies are named `Cookie`.
3157                 downloadRequest.addRequestHeader("Cookie", cookies);
3158             }
3159
3160             // Get the file name from the dialog fragment.
3161             EditText downloadFileNameEditText = dialogFragment.getDialog().findViewById(R.id.download_file_name);
3162             String fileName = downloadFileNameEditText.getText().toString();
3163
3164             // Specify the download location.
3165             if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {  // External write permission granted.
3166                 // Download to the public download directory.
3167                 downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
3168             } else {  // External write permission denied.
3169                 // Download to the app's external download directory.
3170                 downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, fileName);
3171             }
3172
3173             // Allow `MediaScanner` to index the download if it is a media file.
3174             downloadRequest.allowScanningByMediaScanner();
3175
3176             // Add the URL as the description for the download.
3177             downloadRequest.setDescription(downloadUrl);
3178
3179             // Show the download notification after the download is completed.
3180             downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
3181
3182             // Remove the lint warning below that `downloadManager` might be `null`.
3183             assert downloadManager != null;
3184
3185             // Initiate the download.
3186             downloadManager.enqueue(downloadRequest);
3187         } else {  // The download is not an HTTP or HTTPS URI.
3188             Snackbar.make(currentWebView, R.string.cannot_download_file, Snackbar.LENGTH_INDEFINITE).show();
3189         }
3190     }
3191
3192     @Override
3193     public void onHttpAuthenticationCancel() {
3194         // Cancel the `HttpAuthHandler`.
3195         httpAuthHandler.cancel();
3196     }
3197
3198     @Override
3199     public void onHttpAuthenticationProceed(DialogFragment dialogFragment) {
3200         // Get handles for the `EditTexts`.
3201         EditText usernameEditText = dialogFragment.getDialog().findViewById(R.id.http_authentication_username);
3202         EditText passwordEditText = dialogFragment.getDialog().findViewById(R.id.http_authentication_password);
3203
3204         // Proceed with the HTTP authentication.
3205         httpAuthHandler.proceed(usernameEditText.getText().toString(), passwordEditText.getText().toString());
3206     }
3207
3208     @Override
3209     public void onSslErrorCancel() {
3210         sslErrorHandler.cancel();
3211     }
3212
3213     @Override
3214     public void onSslErrorProceed() {
3215         sslErrorHandler.proceed();
3216     }
3217
3218     @Override
3219     public void onPinnedMismatchBack() {
3220         if (currentWebView.canGoBack()) {  // There is a back page in the history.
3221             // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
3222             formattedUrlString = "";
3223
3224             // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
3225             navigatingHistory = true;
3226
3227             // Go back.
3228             currentWebView.goBack();
3229         } else {  // There are no pages to go back to.
3230             // Load a blank page
3231             loadUrl("");
3232         }
3233     }
3234
3235     @Override
3236     public void onPinnedMismatchProceed() {
3237         // Do not check the pinned information for this domain again until the domain changes.
3238         ignorePinnedDomainInformation = true;
3239     }
3240
3241     @Override
3242     public void onUrlHistoryEntrySelected(int moveBackOrForwardSteps) {
3243         // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
3244         formattedUrlString = "";
3245
3246         // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
3247         navigatingHistory = true;
3248
3249         // Load the history entry.
3250         currentWebView.goBackOrForward(moveBackOrForwardSteps);
3251     }
3252
3253     @Override
3254     public void onClearHistory() {
3255         // Clear the history.
3256         currentWebView.clearHistory();
3257     }
3258
3259     // Override `onBackPressed` to handle the navigation drawer and `mainWebView`.
3260     @Override
3261     public void onBackPressed() {
3262         // Get a handle for the drawer layout.
3263         DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
3264
3265         if (drawerLayout.isDrawerVisible(GravityCompat.START)) {  // The navigation drawer is open.
3266             // Close the navigation drawer.
3267             drawerLayout.closeDrawer(GravityCompat.START);
3268         } else if (drawerLayout.isDrawerVisible(GravityCompat.END)){  // The bookmarks drawer is open.
3269             if (currentBookmarksFolder.isEmpty()) {  // The home folder is displayed.
3270                 // close the bookmarks drawer.
3271                 drawerLayout.closeDrawer(GravityCompat.END);
3272             } else {  // A subfolder is displayed.
3273                 // Place the former parent folder in `currentFolder`.
3274                 currentBookmarksFolder = bookmarksDatabaseHelper.getParentFolderName(currentBookmarksFolder);
3275
3276                 // Load the new folder.
3277                 loadBookmarksFolder();
3278             }
3279
3280         } else if (currentWebView.canGoBack()) {  // There is at least one item in the current WebView history.
3281             // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
3282             formattedUrlString = "";
3283
3284             // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
3285             navigatingHistory = true;
3286
3287             // Go back.
3288             currentWebView.goBack();
3289         } else {  // There isn't anything to do in Privacy Browser.
3290             // Pass `onBackPressed()` to the system.
3291             super.onBackPressed();
3292         }
3293     }
3294
3295     // Process the results of an upload file chooser.  Currently there is only one `startActivityForResult` in this activity, so the request code, used to differentiate them, is ignored.
3296     @Override
3297     public void onActivityResult(int requestCode, int resultCode, Intent data) {
3298         // File uploads only work on API >= 21.
3299         if (Build.VERSION.SDK_INT >= 21) {
3300             // Pass the file to the WebView.
3301             fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, data));
3302         }
3303     }
3304
3305     private void loadUrlFromTextBox() {
3306         // Get a handle for the URL edit text.
3307         EditText urlEditText = findViewById(R.id.url_edittext);
3308
3309         // Get the text from urlTextBox and convert it to a string.  trim() removes white spaces from the beginning and end of the string.
3310         String unformattedUrlString = urlEditText.getText().toString().trim();
3311
3312         // Check to see if `unformattedUrlString` is a valid URL.  Otherwise, convert it into a search.
3313         if (unformattedUrlString.startsWith("content://")) {
3314             // Load the entire content URL.
3315             formattedUrlString = unformattedUrlString;
3316         } else if (Patterns.WEB_URL.matcher(unformattedUrlString).matches() || unformattedUrlString.startsWith("http://") || unformattedUrlString.startsWith("https://")
3317                 || unformattedUrlString.startsWith("file://")) {
3318             // Add `https://` at the beginning if there is no protocol.  Otherwise the app will segfault.
3319             if (!unformattedUrlString.startsWith("http") && !unformattedUrlString.startsWith("file://") && !unformattedUrlString.startsWith("content://")) {
3320                 unformattedUrlString = "https://" + unformattedUrlString;
3321             }
3322
3323             // Initialize `unformattedUrl`.
3324             URL unformattedUrl = null;
3325
3326             // Convert `unformattedUrlString` to a `URL`, then to a `URI`, and then back to a `String`, which sanitizes the input and adds in any missing components.
3327             try {
3328                 unformattedUrl = new URL(unformattedUrlString);
3329             } catch (MalformedURLException e) {
3330                 e.printStackTrace();
3331             }
3332
3333             // The ternary operator (? :) makes sure that a null pointer exception is not thrown, which would happen if `.get` was called on a `null` value.
3334             String scheme = unformattedUrl != null ? unformattedUrl.getProtocol() : null;
3335             String authority = unformattedUrl != null ? unformattedUrl.getAuthority() : null;
3336             String path = unformattedUrl != null ? unformattedUrl.getPath() : null;
3337             String query = unformattedUrl != null ? unformattedUrl.getQuery() : null;
3338             String fragment = unformattedUrl != null ? unformattedUrl.getRef() : null;
3339
3340             // Build the URI.
3341             Uri.Builder formattedUri = new Uri.Builder();
3342             formattedUri.scheme(scheme).authority(authority).path(path).query(query).fragment(fragment);
3343
3344             // Decode `formattedUri` as a `String` in `UTF-8`.
3345             try {
3346                 formattedUrlString = URLDecoder.decode(formattedUri.build().toString(), "UTF-8");
3347             } catch (UnsupportedEncodingException exception) {
3348                 // Load a blank string.
3349                 formattedUrlString = "";
3350             }
3351         } else if (unformattedUrlString.isEmpty()){  // Load a blank web site.
3352             // Load a blank string.
3353             formattedUrlString = "";
3354         } else {  // Search for the contents of the URL box.
3355             // Create an encoded URL String.
3356             String encodedUrlString;
3357
3358             // Sanitize the search input.
3359             try {
3360                 encodedUrlString = URLEncoder.encode(unformattedUrlString, "UTF-8");
3361             } catch (UnsupportedEncodingException exception) {
3362                 encodedUrlString = "";
3363             }
3364
3365             // Add the base search URL.
3366             formattedUrlString = searchURL + encodedUrlString;
3367         }
3368
3369         // Clear the focus from the URL edit text.  Otherwise, proximate typing in the box will retain the colorized formatting instead of being reset during refocus.
3370         urlEditText.clearFocus();
3371
3372         // Make it so.
3373         loadUrl(formattedUrlString);
3374     }
3375
3376     private void loadUrl(String url) {// Apply any custom domain settings.
3377         // Set the URL as the formatted URL string so that checking third-party requests works correctly.
3378         formattedUrlString = url;
3379
3380         // Apply the domain settings.
3381         applyDomainSettings(url, true, false);
3382
3383         // If loading a website, set `urlIsLoading` to prevent changes in the user agent on websites with redirects from reloading the current website.
3384         urlIsLoading = !url.equals("");
3385
3386         // Load the URL.
3387         currentWebView.loadUrl(url, customHeaders);
3388     }
3389
3390     public void findPreviousOnPage(View view) {
3391         // Go to the previous highlighted phrase on the page.  `false` goes backwards instead of forwards.
3392         currentWebView.findNext(false);
3393     }
3394
3395     public void findNextOnPage(View view) {
3396         // Go to the next highlighted phrase on the page. `true` goes forwards instead of backwards.
3397         currentWebView.findNext(true);
3398     }
3399
3400     public void closeFindOnPage(View view) {
3401         // Get a handle for the views.
3402         Toolbar toolbar = findViewById(R.id.toolbar);
3403         LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
3404
3405         // Delete the contents of `find_on_page_edittext`.
3406         findOnPageEditText.setText(null);
3407
3408         // Clear the highlighted phrases.
3409         currentWebView.clearMatches();
3410
3411         // Hide the find on page linear layout.
3412         findOnPageLinearLayout.setVisibility(View.GONE);
3413
3414         // Show the toolbar.
3415         toolbar.setVisibility(View.VISIBLE);
3416
3417         // Hide the keyboard.
3418         inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
3419     }
3420
3421     private void applyAppSettings() {
3422         // Get a handle for the shared preferences.
3423         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3424
3425         // Store the values from the shared preferences in variables.
3426         incognitoModeEnabled = sharedPreferences.getBoolean("incognito_mode", false);
3427         boolean doNotTrackEnabled = sharedPreferences.getBoolean("do_not_track", false);
3428         proxyThroughOrbot = sharedPreferences.getBoolean("proxy_through_orbot", false);
3429         fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean("full_screen_browsing_mode", false);
3430         hideAppBar = sharedPreferences.getBoolean("hide_app_bar", true);
3431         downloadWithExternalApp = sharedPreferences.getBoolean("download_with_external_app", false);
3432
3433         // Get handles for the views that need to be modified.  `getSupportActionBar()` must be used until the minimum API >= 21.
3434         FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
3435         ActionBar actionBar = getSupportActionBar();
3436
3437         // Remove the incorrect lint warnings below that the action bar might be null.
3438         assert actionBar != null;
3439
3440         // Apply the proxy through Orbot settings.
3441         applyProxyThroughOrbot(false);
3442
3443         // Set Do Not Track status.
3444         if (doNotTrackEnabled) {
3445             customHeaders.put("DNT", "1");
3446         } else {
3447             customHeaders.remove("DNT");
3448         }
3449
3450         // TODO this also needs to be set when creating a new tab.
3451         // Set the app bar scrolling for each WebView.
3452         for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
3453             // Get the WebView tab fragment.
3454             WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
3455
3456             // Get the fragment view.
3457             View fragmentView = webViewTabFragment.getView();
3458
3459             // Only modify the WebViews if they exist.
3460             if (fragmentView != null) {
3461                 // Get the nested scroll WebView from the tab fragment.
3462                 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
3463
3464                 // Set the app bar scrolling.
3465                 nestedScrollWebView.setNestedScrollingEnabled(sharedPreferences.getBoolean("scroll_app_bar", true));
3466             }
3467         }
3468
3469         // Update the full screen browsing mode settings.
3470         if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) {  // Privacy Browser is currently in full screen browsing mode.
3471             // Update the visibility of the app bar, which might have changed in the settings.
3472             if (hideAppBar) {
3473                 actionBar.hide();
3474             } else {
3475                 actionBar.show();
3476             }
3477
3478             // Hide the banner ad in the free flavor.
3479             if (BuildConfig.FLAVOR.contentEquals("free")) {
3480                 AdHelper.hideAd(findViewById(R.id.adview));
3481             }
3482
3483             // Remove the translucent status flag.  This is necessary so the root frame layout can fill the entire screen.
3484             getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
3485
3486             /* Hide the system bars.
3487              * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
3488              * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
3489              * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
3490              * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
3491              */
3492             rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
3493                     View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
3494         } else {  // Privacy Browser is not in full screen browsing mode.
3495             // Reset the full screen tracker, which could be true if Privacy Browser was in full screen mode before entering settings and full screen browsing was disabled.
3496             inFullScreenBrowsingMode = false;
3497
3498             // Show the app bar.
3499             actionBar.show();
3500
3501             // Show the banner ad in the free flavor.
3502             if (BuildConfig.FLAVOR.contentEquals("free")) {
3503                 // Initialize the ads.  If this isn't the first run, `loadAd()` will be automatically called instead.
3504                 AdHelper.initializeAds(findViewById(R.id.adview), getApplicationContext(), fragmentManager, getString(R.string.google_app_id), getString(R.string.ad_unit_id));
3505             }
3506
3507             // Remove the `SYSTEM_UI` flags from the root frame layout.
3508             rootFrameLayout.setSystemUiVisibility(0);
3509
3510             // Add the translucent status flag.
3511             getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
3512         }
3513     }
3514
3515
3516     // `reloadWebsite` is used if returning from the Domains activity.  Otherwise JavaScript might not function correctly if it is newly enabled.
3517     // The deprecated `.getDrawable()` must be used until the minimum API >= 21.
3518     @SuppressWarnings("deprecation")
3519     private boolean applyDomainSettings(String url, boolean resetFavoriteIcon, boolean reloadWebsite) {
3520         // Get a handle for the URL edit text.
3521         EditText urlEditText = findViewById(R.id.url_edittext);
3522
3523         // Get the current user agent.
3524         String initialUserAgent = currentWebView.getSettings().getUserAgentString();
3525
3526         // Initialize a variable to track if the user agent changes.
3527         boolean userAgentChanged = false;
3528
3529         // Parse the URL into a URI.
3530         Uri uri = Uri.parse(url);
3531
3532         // Extract the domain from `uri`.
3533         String hostName = uri.getHost();
3534
3535         // Initialize `loadingNewDomainName`.
3536         boolean loadingNewDomainName;
3537
3538         // If either `hostName` or `currentDomainName` are `null`, run the options for loading a new domain name.
3539         // The lint suggestion to simplify the `if` statement is incorrect, because `hostName.equals(currentDomainName)` can produce a `null object reference.`
3540         //noinspection SimplifiableIfStatement
3541         if ((hostName == null) || (currentDomainName == null)) {
3542             loadingNewDomainName = true;
3543         } else {  // Determine if `hostName` equals `currentDomainName`.
3544             loadingNewDomainName = !hostName.equals(currentDomainName);
3545         }
3546
3547         // Strings don't like to be null.
3548         if (hostName == null) {
3549             hostName = "";
3550         }
3551
3552         // Only apply the domain settings if a new domain is being loaded.  This allows the user to set temporary settings for JavaScript, cookies, DOM storage, etc.
3553         if (loadingNewDomainName) {
3554             // Set the new `hostname` as the `currentDomainName`.
3555             currentDomainName = hostName;
3556
3557             // Reset the ignoring of pinned domain information.
3558             ignorePinnedDomainInformation = false;
3559
3560             // Reset the favorite icon if specified.
3561             if (resetFavoriteIcon) {
3562                 // Store the favorite icon bitmap.
3563                 favoriteIconBitmap = favoriteIconDefaultBitmap;
3564
3565                 // Get a handle for the tab layout.
3566                 TabLayout tabLayout = findViewById(R.id.tablayout);
3567
3568                 // Get the current tab.
3569                 TabLayout.Tab currentTab = tabLayout.getTabAt(tabLayout.getSelectedTabPosition());
3570
3571                 // Remove the warning below that the current tab might be null.
3572                 assert currentTab != null;
3573
3574                 // Get the current tab custom view.
3575                 View currentTabCustomView = currentTab.getCustomView();
3576
3577                 // Remove the warning below that the current tab custom view might be null.
3578                 assert currentTabCustomView != null;
3579
3580                 // Get the current tab favorite icon image view.
3581                 ImageView currentTabFavoriteIconImageView = currentTabCustomView.findViewById(R.id.favorite_icon_imageview);
3582
3583                 // Set the default favorite icon as the favorite icon for this tab.
3584                 currentTabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(favoriteIconBitmap, 64, 64, true));
3585             }
3586
3587             // Get a handle for the swipe refresh layout.
3588             SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
3589
3590             // Initialize the database handler.  The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
3591             DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
3592
3593             // Get a full cursor from `domainsDatabaseHelper`.
3594             Cursor domainNameCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomain();
3595
3596             // Initialize `domainSettingsSet`.
3597             Set<String> domainSettingsSet = new HashSet<>();
3598
3599             // Get the domain name column index.
3600             int domainNameColumnIndex = domainNameCursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME);
3601
3602             // Populate `domainSettingsSet`.
3603             for (int i = 0; i < domainNameCursor.getCount(); i++) {
3604                 // Move `domainsCursor` to the current row.
3605                 domainNameCursor.moveToPosition(i);
3606
3607                 // Store the domain name in `domainSettingsSet`.
3608                 domainSettingsSet.add(domainNameCursor.getString(domainNameColumnIndex));
3609             }
3610
3611             // Close `domainNameCursor.
3612             domainNameCursor.close();
3613
3614             // Initialize the domain name in database variable.
3615             String domainNameInDatabase = null;
3616
3617             // Check the hostname against the domain settings set.
3618             if (domainSettingsSet.contains(hostName)) {  // The hostname is contained in the domain settings set.
3619                 // Record the domain name in the database.
3620                 domainNameInDatabase = hostName;
3621
3622                 // Set the domain settings applied tracker to true.
3623                 currentWebView.setDomainSettingsApplied(true);
3624             } else {  // The hostname is not contained in the domain settings set.
3625                 // Set the domain settings applied tracker to false.
3626                 currentWebView.setDomainSettingsApplied(false);
3627             }
3628
3629             // Check all the subdomains of the host name against wildcard domains in the domain cursor.
3630             while (!currentWebView.getDomainSettingsApplied() && hostName.contains(".")) {  // Stop checking if domain settings are already applied or there are no more `.` in the host name.
3631                 if (domainSettingsSet.contains("*." + hostName)) {  // Check the host name prepended by `*.`.
3632                     // Set the domain settings applied tracker to true.
3633                     currentWebView.setDomainSettingsApplied(true);
3634
3635                     // Store the applied domain names as it appears in the database.
3636                     domainNameInDatabase = "*." + hostName;
3637                 }
3638
3639                 // Strip out the lowest subdomain of of the host name.
3640                 hostName = hostName.substring(hostName.indexOf(".") + 1);
3641             }
3642
3643
3644             // Get a handle for the shared preference.
3645             SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3646
3647             // Store the general preference information.
3648             String defaultFontSizeString = sharedPreferences.getString("font_size", getString(R.string.font_size_default_value));
3649             String defaultUserAgentName = sharedPreferences.getString("user_agent", getString(R.string.user_agent_default_value));
3650             defaultCustomUserAgentString = sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value));
3651             boolean defaultSwipeToRefresh = sharedPreferences.getBoolean("swipe_to_refresh", true);
3652             nightMode = sharedPreferences.getBoolean("night_mode", false);
3653             boolean displayWebpageImages = sharedPreferences.getBoolean("display_webpage_images", true);
3654
3655             if (currentWebView.getDomainSettingsApplied()) {  // The url has custom domain settings.
3656                 // Get a cursor for the current host and move it to the first position.
3657                 Cursor currentHostDomainSettingsCursor = domainsDatabaseHelper.getCursorForDomainName(domainNameInDatabase);
3658                 currentHostDomainSettingsCursor.moveToFirst();
3659
3660                 // Get the settings from the cursor.
3661                 currentWebView.setDomainSettingsDatabaseId(currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper._ID)));
3662                 javaScriptEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)) == 1);
3663                 firstPartyCookiesEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FIRST_PARTY_COOKIES)) == 1);
3664                 thirdPartyCookiesEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_THIRD_PARTY_COOKIES)) == 1);
3665                 domStorageEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_DOM_STORAGE)) == 1);
3666                 // Form data can be removed once the minimum API >= 26.
3667                 saveFormDataEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FORM_DATA)) == 1);
3668                 easyListEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYLIST)) == 1);
3669                 easyPrivacyEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYPRIVACY)) == 1);
3670                 fanboysAnnoyanceListEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST)) == 1);
3671                 fanboysSocialBlockingListEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)) == 1);
3672                 ultraPrivacyEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY)) == 1);
3673                 blockAllThirdPartyRequests = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS)) == 1);
3674                 String userAgentName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.USER_AGENT));
3675                 int fontSize = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.FONT_SIZE));
3676                 int swipeToRefreshInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SWIPE_TO_REFRESH));
3677                 int nightModeInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.NIGHT_MODE));
3678                 int displayWebpageImagesInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.DISPLAY_IMAGES));
3679                 pinnedSslCertificate = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)) == 1);
3680                 pinnedSslIssuedToCName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME));
3681                 pinnedSslIssuedToOName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION));
3682                 pinnedSslIssuedToUName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT));
3683                 pinnedSslIssuedByCName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME));
3684                 pinnedSslIssuedByOName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION));
3685                 pinnedSslIssuedByUName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT));
3686                 pinnedIpAddresses = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_IP_ADDRESSES)) == 1);
3687                 pinnedHostIpAddresses = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.IP_ADDRESSES));
3688
3689                 // Set `nightMode` according to `nightModeInt`.  If `nightModeInt` is `DomainsDatabaseHelper.NIGHT_MODE_SYSTEM_DEFAULT` the current setting from `sharedPreferences` will be used.
3690                 switch (nightModeInt) {
3691                     case DomainsDatabaseHelper.NIGHT_MODE_ENABLED:
3692                         nightMode = true;
3693                         break;
3694
3695                     case DomainsDatabaseHelper.NIGHT_MODE_DISABLED:
3696                         nightMode = false;
3697                         break;
3698                 }
3699
3700                 // Store the domain JavaScript status.  This is used by the options menu night mode toggle.
3701                 domainSettingsJavaScriptEnabled = javaScriptEnabled;
3702
3703                 // Enable JavaScript if night mode is enabled.
3704                 if (nightMode) {
3705                     javaScriptEnabled = true;
3706                 }
3707
3708                 // Set the pinned SSL certificate start date to `null` if the saved date `long` is 0.
3709                 if (currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)) == 0) {
3710                     pinnedSslStartDate = null;
3711                 } else {
3712                     pinnedSslStartDate = new Date(currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)));
3713                 }
3714
3715                 // Set the pinned SSL certificate end date to `null` if the saved date `long` is 0.
3716                 if (currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)) == 0) {
3717                     pinnedSslEndDate = null;
3718                 } else {
3719                     pinnedSslEndDate = new Date(currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)));
3720                 }
3721
3722                 // Close `currentHostDomainSettingsCursor`.
3723                 currentHostDomainSettingsCursor.close();
3724
3725                 // Apply the domain settings.
3726                 currentWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled);
3727                 cookieManager.setAcceptCookie(firstPartyCookiesEnabled);
3728                 currentWebView.getSettings().setDomStorageEnabled(domStorageEnabled);
3729
3730                 // Apply the form data setting if the API < 26.
3731                 if (Build.VERSION.SDK_INT < 26) {
3732                     currentWebView.getSettings().setSaveFormData(saveFormDataEnabled);
3733                 }
3734
3735                 // Apply the font size.
3736                 if (fontSize == 0) {  // Apply the default font size.
3737                     currentWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
3738                 } else {  // Apply the specified font size.
3739                     currentWebView.getSettings().setTextZoom(fontSize);
3740                 }
3741
3742                 // Set third-party cookies status if API >= 21.
3743                 if (Build.VERSION.SDK_INT >= 21) {
3744                     cookieManager.setAcceptThirdPartyCookies(currentWebView, thirdPartyCookiesEnabled);
3745                 }
3746
3747                 // Only set the user agent if the webpage is not currently loading.  Otherwise, changing the user agent on redirects can cause the original website to reload.
3748                 // <https://redmine.stoutner.com/issues/160>
3749                 if (!urlIsLoading) {
3750                     // Set the user agent.
3751                     if (userAgentName.equals(getString(R.string.system_default_user_agent))) {  // Use the system default user agent.
3752                         // Get the array position of the default user agent name.
3753                         int defaultUserAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
3754
3755                         // Set the user agent according to the system default.
3756                         switch (defaultUserAgentArrayPosition) {
3757                             case UNRECOGNIZED_USER_AGENT:  // The default user agent name is not on the canonical list.
3758                                 // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
3759                                 currentWebView.getSettings().setUserAgentString(defaultUserAgentName);
3760                                 break;
3761
3762                             case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
3763                                 // Set the user agent to `""`, which uses the default value.
3764                                 currentWebView.getSettings().setUserAgentString("");
3765                                 break;
3766
3767                             case SETTINGS_CUSTOM_USER_AGENT:
3768                                 // Set the custom user agent.
3769                                 currentWebView.getSettings().setUserAgentString(defaultCustomUserAgentString);
3770                                 break;
3771
3772                             default:
3773                                 // Get the user agent string from the user agent data array
3774                                 currentWebView.getSettings().setUserAgentString(userAgentDataArray[defaultUserAgentArrayPosition]);
3775                         }
3776                     } else {  // Set the user agent according to the stored name.
3777                         // Get the array position of the user agent name.
3778                         int userAgentArrayPosition = userAgentNamesArray.getPosition(userAgentName);
3779
3780                         switch (userAgentArrayPosition) {
3781                             case UNRECOGNIZED_USER_AGENT:  // The user agent name contains a custom user agent.
3782                                 currentWebView.getSettings().setUserAgentString(userAgentName);
3783                                 break;
3784
3785                             case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
3786                                 // Set the user agent to `""`, which uses the default value.
3787                                 currentWebView.getSettings().setUserAgentString("");
3788                                 break;
3789
3790                             default:
3791                                 // Get the user agent string from the user agent data array.
3792                                 currentWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
3793                         }
3794                     }
3795
3796                     // Store the applied user agent string, which is used in the View Source activity.
3797                     appliedUserAgentString = currentWebView.getSettings().getUserAgentString();
3798
3799                     // Update the user agent change tracker.
3800                     userAgentChanged = !appliedUserAgentString.equals(initialUserAgent);
3801                 }
3802
3803                 // Set swipe to refresh.
3804                 switch (swipeToRefreshInt) {
3805                     case DomainsDatabaseHelper.SWIPE_TO_REFRESH_SYSTEM_DEFAULT:
3806                         // Set swipe to refresh according to the default.
3807                         swipeRefreshLayout.setEnabled(defaultSwipeToRefresh);
3808                         break;
3809
3810                     case DomainsDatabaseHelper.SWIPE_TO_REFRESH_ENABLED:
3811                         // Enable swipe to refresh.
3812                         swipeRefreshLayout.setEnabled(true);
3813                         break;
3814
3815                     case DomainsDatabaseHelper.SWIPE_TO_REFRESH_DISABLED:
3816                         // Disable swipe to refresh.
3817                         swipeRefreshLayout.setEnabled(false);
3818                 }
3819
3820                 // Set the loading of webpage images.
3821                 switch (displayWebpageImagesInt) {
3822                     case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_SYSTEM_DEFAULT:
3823                         currentWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
3824                         break;
3825
3826                     case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_ENABLED:
3827                         currentWebView.getSettings().setLoadsImagesAutomatically(true);
3828                         break;
3829
3830                     case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_DISABLED:
3831                         currentWebView.getSettings().setLoadsImagesAutomatically(false);
3832                         break;
3833                 }
3834
3835                 // Set a green background on URL edit text to indicate that custom domain settings are being used. The deprecated `.getDrawable()` must be used until the minimum API >= 21.
3836                 if (darkTheme) {
3837                     urlEditText.setBackground(getResources().getDrawable(R.drawable.url_bar_background_dark_blue));
3838                 } else {
3839                     urlEditText.setBackground(getResources().getDrawable(R.drawable.url_bar_background_light_green));
3840                 }
3841             } else {  // The new URL does not have custom domain settings.  Load the defaults.
3842                 // Store the values from `sharedPreferences` in variables.
3843                 javaScriptEnabled = sharedPreferences.getBoolean("javascript", false);
3844                 firstPartyCookiesEnabled = sharedPreferences.getBoolean("first_party_cookies", false);
3845                 thirdPartyCookiesEnabled = sharedPreferences.getBoolean("third_party_cookies", false);
3846                 domStorageEnabled = sharedPreferences.getBoolean("dom_storage", false);
3847                 saveFormDataEnabled = sharedPreferences.getBoolean("save_form_data", false);  // Form data can be removed once the minimum API >= 26.
3848                 easyListEnabled = sharedPreferences.getBoolean("easylist", true);
3849                 easyPrivacyEnabled = sharedPreferences.getBoolean("easyprivacy", true);
3850                 fanboysAnnoyanceListEnabled = sharedPreferences.getBoolean("fanboys_annoyance_list", true);
3851                 fanboysSocialBlockingListEnabled = sharedPreferences.getBoolean("fanboys_social_blocking_list", true);
3852                 ultraPrivacyEnabled = sharedPreferences.getBoolean("ultraprivacy", true);
3853                 blockAllThirdPartyRequests = sharedPreferences.getBoolean("block_all_third_party_requests", false);
3854
3855                 // Set `javaScriptEnabled` to be `true` if `night_mode` is `true`.
3856                 if (nightMode) {
3857                     javaScriptEnabled = true;
3858                 }
3859
3860                 // Apply the default settings.
3861                 currentWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled);
3862                 cookieManager.setAcceptCookie(firstPartyCookiesEnabled);
3863                 currentWebView.getSettings().setDomStorageEnabled(domStorageEnabled);
3864                 currentWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
3865                 swipeRefreshLayout.setEnabled(defaultSwipeToRefresh);
3866
3867                 // Apply the form data setting if the API < 26.
3868                 if (Build.VERSION.SDK_INT < 26) {
3869                     currentWebView.getSettings().setSaveFormData(saveFormDataEnabled);
3870                 }
3871
3872                 // Reset the pinned variables.
3873                 currentWebView.setDomainSettingsDatabaseId(-1);
3874                 pinnedSslCertificate = false;
3875                 pinnedSslIssuedToCName = "";
3876                 pinnedSslIssuedToOName = "";
3877                 pinnedSslIssuedToUName = "";
3878                 pinnedSslIssuedByCName = "";
3879                 pinnedSslIssuedByOName = "";
3880                 pinnedSslIssuedByUName = "";
3881                 pinnedSslStartDate = null;
3882                 pinnedSslEndDate = null;
3883                 pinnedIpAddresses = false;
3884                 pinnedHostIpAddresses = "";
3885
3886                 // Set third-party cookies status if API >= 21.
3887                 if (Build.VERSION.SDK_INT >= 21) {
3888                     cookieManager.setAcceptThirdPartyCookies(currentWebView, thirdPartyCookiesEnabled);
3889                 }
3890
3891                 // Only set the user agent if the webpage is not currently loading.  Otherwise, changing the user agent on redirects can cause the original website to reload.
3892                 // <https://redmine.stoutner.com/issues/160>
3893                 if (!urlIsLoading) {
3894                     // Get the array position of the user agent name.
3895                     int userAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
3896
3897                     // Set the user agent.
3898                     switch (userAgentArrayPosition) {
3899                         case UNRECOGNIZED_USER_AGENT:  // The default user agent name is not on the canonical list.
3900                             // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
3901                             currentWebView.getSettings().setUserAgentString(defaultUserAgentName);
3902                             break;
3903
3904                         case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
3905                             // Set the user agent to `""`, which uses the default value.
3906                             currentWebView.getSettings().setUserAgentString("");
3907                             break;
3908
3909                         case SETTINGS_CUSTOM_USER_AGENT:
3910                             // Set the custom user agent.
3911                             currentWebView.getSettings().setUserAgentString(defaultCustomUserAgentString);
3912                             break;
3913
3914                         default:
3915                             // Get the user agent string from the user agent data array
3916                             currentWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
3917                     }
3918
3919                     // Store the applied user agent string, which is used in the View Source activity.
3920                     appliedUserAgentString = currentWebView.getSettings().getUserAgentString();
3921
3922                     // Update the user agent change tracker.
3923                     userAgentChanged = !appliedUserAgentString.equals(initialUserAgent);
3924                 }
3925
3926                 // Set the loading of webpage images.
3927                 currentWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
3928
3929                 // Set a transparent background on URL edit text.  The deprecated `.getDrawable()` must be used until the minimum API >= 21.
3930                 urlEditText.setBackgroundDrawable(getResources().getDrawable(R.color.transparent));
3931             }
3932
3933             // Close the domains database helper.
3934             domainsDatabaseHelper.close();
3935
3936             // Update the privacy icons, but only if `mainMenu` has already been populated.
3937             if (mainMenu != null) {
3938                 updatePrivacyIcons(true);
3939             }
3940         }
3941
3942         // Reload the website if returning from the Domains activity.
3943         if (reloadWebsite) {
3944             currentWebView.reload();
3945         }
3946
3947         // Return the user agent changed status.
3948         return userAgentChanged;
3949     }
3950
3951     private void applyProxyThroughOrbot(boolean reloadWebsite) {
3952         // Get a handle for the shared preferences.
3953         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3954
3955         // Get the search preferences.
3956         String homepageString = sharedPreferences.getString("homepage", getString(R.string.homepage_default_value));
3957         String torHomepageString = sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value));
3958         String torSearchString = sharedPreferences.getString("tor_search", getString(R.string.tor_search_default_value));
3959         String torSearchCustomUrlString = sharedPreferences.getString("tor_search_custom_url", getString(R.string.tor_search_custom_url_default_value));
3960         String searchString = sharedPreferences.getString("search", getString(R.string.search_default_value));
3961         String searchCustomUrlString = sharedPreferences.getString("search_custom_url", getString(R.string.search_custom_url_default_value));
3962
3963         // Get a handle for the action bar.  `getSupportActionBar()` must be used until the minimum API >= 21.
3964         ActionBar actionBar = getSupportActionBar();
3965
3966         // Remove the incorrect lint warning later that the action bar might be null.
3967         assert actionBar != null;
3968
3969         // Set the homepage, search, and proxy options.
3970         if (proxyThroughOrbot) {  // Set the Tor options.
3971             // Set `torHomepageString` as `homepage`.
3972             homepage = torHomepageString;
3973
3974             // If formattedUrlString is null assign the homepage to it.
3975             if (formattedUrlString == null) {
3976                 formattedUrlString = homepage;
3977             }
3978
3979             // Set the search URL.
3980             if (torSearchString.equals("Custom URL")) {  // Get the custom URL string.
3981                 searchURL = torSearchCustomUrlString;
3982             } else {  // Use the string from the pre-built list.
3983                 searchURL = torSearchString;
3984             }
3985
3986             // Set the proxy.  `this` refers to the current activity where an `AlertDialog` might be displayed.
3987             OrbotProxyHelper.setProxy(getApplicationContext(), this, "localhost", "8118");
3988
3989             // Set the `appBar` background to indicate proxying through Orbot is enabled.  `this` refers to the context.
3990             if (darkTheme) {
3991                 actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.dark_blue_30));
3992             } else {
3993                 actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.blue_50));
3994             }
3995
3996             // Check to see if Orbot is ready.
3997             if (!orbotStatus.equals("ON")) {  // Orbot is not ready.
3998                 // Set `waitingForOrbot`.
3999                 waitingForOrbot = true;
4000
4001                 // Disable the wide view port so that the waiting for Orbot text is displayed correctly.
4002                 currentWebView.getSettings().setUseWideViewPort(false);
4003
4004                 // Load a waiting page.  `null` specifies no encoding, which defaults to ASCII.
4005                 currentWebView.loadData(waitingForOrbotHtmlString, "text/html", null);
4006             } else if (reloadWebsite) {  // Orbot is ready and the website should be reloaded.
4007                 // Reload the website.
4008                 currentWebView.reload();
4009             }
4010         } else {  // Set the non-Tor options.
4011             // Set `homepageString` as `homepage`.
4012             homepage = homepageString;
4013
4014             // If formattedUrlString is null assign the homepage to it.
4015             if (formattedUrlString == null) {
4016                 formattedUrlString = homepage;
4017             }
4018
4019             // Set the search URL.
4020             if (searchString.equals("Custom URL")) {  // Get the custom URL string.
4021                 searchURL = searchCustomUrlString;
4022             } else {  // Use the string from the pre-built list.
4023                 searchURL = searchString;
4024             }
4025
4026             // Reset the proxy to default.  The host is `""` and the port is `"0"`.
4027             OrbotProxyHelper.setProxy(getApplicationContext(), this, "", "0");
4028
4029             // Set the default `appBar` background.  `this` refers to the context.
4030             if (darkTheme) {
4031                 actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.gray_900));
4032             } else {
4033                 actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.gray_100));
4034             }
4035
4036             // Reset `waitingForOrbot.
4037             waitingForOrbot = false;
4038
4039             // Reload the WebViews if requested.
4040             if (reloadWebsite) {
4041                 // Reload the WebViews.
4042                 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
4043                     // Get the WebView tab fragment.
4044                     WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
4045
4046                     // Get the fragment view.
4047                     View fragmentView = webViewTabFragment.getView();
4048
4049                     // Only reload the WebViews if they exist.
4050                     if (fragmentView != null) {
4051                         // Get the nested scroll WebView from the tab fragment.
4052                         NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
4053
4054                         // Reload the WebView.
4055                         nestedScrollWebView.reload();
4056                     }
4057                 }
4058             }
4059         }
4060     }
4061
4062     private void updatePrivacyIcons(boolean runInvalidateOptionsMenu) {
4063         // Get handles for the menu items.
4064         MenuItem privacyMenuItem = mainMenu.findItem(R.id.toggle_javascript);
4065         MenuItem firstPartyCookiesMenuItem = mainMenu.findItem(R.id.toggle_first_party_cookies);
4066         MenuItem domStorageMenuItem = mainMenu.findItem(R.id.toggle_dom_storage);
4067         MenuItem refreshMenuItem = mainMenu.findItem(R.id.refresh);
4068
4069         // Update the privacy icon.
4070         if (javaScriptEnabled) {  // JavaScript is enabled.
4071             privacyMenuItem.setIcon(R.drawable.javascript_enabled);
4072         } else if (firstPartyCookiesEnabled) {  // JavaScript is disabled but cookies are enabled.
4073             privacyMenuItem.setIcon(R.drawable.warning);
4074         } else {  // All the dangerous features are disabled.
4075             privacyMenuItem.setIcon(R.drawable.privacy_mode);
4076         }
4077
4078         // Update the first-party cookies icon.
4079         if (firstPartyCookiesEnabled) {  // First-party cookies are enabled.
4080             firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_enabled);
4081         } else {  // First-party cookies are disabled.
4082             if (darkTheme) {
4083                 firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_dark);
4084             } else {
4085                 firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_light);
4086             }
4087         }
4088
4089         // Update the DOM storage icon.
4090         if (javaScriptEnabled && domStorageEnabled) {  // Both JavaScript and DOM storage are enabled.
4091             domStorageMenuItem.setIcon(R.drawable.dom_storage_enabled);
4092         } else if (javaScriptEnabled) {  // JavaScript is enabled but DOM storage is disabled.
4093             if (darkTheme) {
4094                 domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_dark);
4095             } else {
4096                 domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_light);
4097             }
4098         } else {  // JavaScript is disabled, so DOM storage is ghosted.
4099             if (darkTheme) {
4100                 domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_dark);
4101             } else {
4102                 domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_light);
4103             }
4104         }
4105
4106         // Update the refresh icon.
4107         if (darkTheme) {
4108             refreshMenuItem.setIcon(R.drawable.refresh_enabled_dark);
4109         } else {
4110             refreshMenuItem.setIcon(R.drawable.refresh_enabled_light);
4111         }
4112
4113         // `invalidateOptionsMenu` calls `onPrepareOptionsMenu()` and redraws the icons in the `AppBar`.
4114         if (runInvalidateOptionsMenu) {
4115             invalidateOptionsMenu();
4116         }
4117     }
4118
4119     private void openUrlWithExternalApp(String url) {
4120         // Create a download intent.  Not specifying the action type will display the maximum number of options.
4121         Intent downloadIntent = new Intent();
4122
4123         // Set the URI and the MIME type.  Specifying `text/html` displays a good number of options.
4124         downloadIntent.setDataAndType(Uri.parse(url), "text/html");
4125
4126         // Flag the intent to open in a new task.
4127         downloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4128
4129         // Show the chooser.
4130         startActivity(Intent.createChooser(downloadIntent, getString(R.string.open_with)));
4131     }
4132
4133     private void highlightUrlText() {
4134         // Get a handle for the URL edit text.
4135         EditText urlEditText = findViewById(R.id.url_edittext);
4136
4137         // Only highlight the URL text if the box is not currently selected.
4138         if (!urlEditText.hasFocus()) {
4139             // Get the URL string.
4140             String urlString = urlEditText.getText().toString();
4141
4142             // Highlight the URL according to the protocol.
4143             if (urlString.startsWith("file://")) {  // This is a file URL.
4144                 // De-emphasize only the protocol.
4145                 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4146             } else if (urlString.startsWith("content://")) {
4147                 // De-emphasize only the protocol.
4148                 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 10, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4149             } else {  // This is a web URL.
4150                 // Get the index of the `/` immediately after the domain name.
4151                 int endOfDomainName = urlString.indexOf("/", (urlString.indexOf("//") + 2));
4152
4153                 // Create a base URL string.
4154                 String baseUrl;
4155
4156                 // Get the base URL.
4157                 if (endOfDomainName > 0) {  // There is at least one character after the base URL.
4158                     // Get the base URL.
4159                     baseUrl = urlString.substring(0, endOfDomainName);
4160                 } else {  // There are no characters after the base URL.
4161                     // Set the base URL to be the entire URL string.
4162                     baseUrl = urlString;
4163                 }
4164
4165                 // Get the index of the last `.` in the domain.
4166                 int lastDotIndex = baseUrl.lastIndexOf(".");
4167
4168                 // Get the index of the penultimate `.` in the domain.
4169                 int penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1);
4170
4171                 // Markup the beginning of the URL.
4172                 if (urlString.startsWith("http://")) {  // Highlight the protocol of connections that are not encrypted.
4173                     urlEditText.getText().setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4174
4175                     // De-emphasize subdomains.
4176                     if (penultimateDotIndex > 0) {  // There is more than one subdomain in the domain name.
4177                         urlEditText.getText().setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4178                     }
4179                 } else if (urlString.startsWith("https://")) {  // De-emphasize the protocol of connections that are encrypted.
4180                     if (penultimateDotIndex > 0) {  // There is more than one subdomain in the domain name.
4181                         // De-emphasize the protocol and the additional subdomains.
4182                         urlEditText.getText().setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4183                     } else {  // There is only one subdomain in the domain name.
4184                         // De-emphasize only the protocol.
4185                         urlEditText.getText().setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4186                     }
4187                 }
4188
4189                 // De-emphasize the text after the domain name.
4190                 if (endOfDomainName > 0) {
4191                     urlEditText.getText().setSpan(finalGrayColorSpan, endOfDomainName, urlString.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4192                 }
4193             }
4194         }
4195     }
4196
4197     private void loadBookmarksFolder() {
4198         // Update the bookmarks cursor with the contents of the bookmarks database for the current folder.
4199         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
4200
4201         // Populate the bookmarks cursor adapter.  `this` specifies the `Context`.  `false` disables `autoRequery`.
4202         bookmarksCursorAdapter = new CursorAdapter(this, bookmarksCursor, false) {
4203             @Override
4204             public View newView(Context context, Cursor cursor, ViewGroup parent) {
4205                 // Inflate the individual item layout.  `false` does not attach it to the root.
4206                 return getLayoutInflater().inflate(R.layout.bookmarks_drawer_item_linearlayout, parent, false);
4207             }
4208
4209             @Override
4210             public void bindView(View view, Context context, Cursor cursor) {
4211                 // Get handles for the views.
4212                 ImageView bookmarkFavoriteIcon = view.findViewById(R.id.bookmark_favorite_icon);
4213                 TextView bookmarkNameTextView = view.findViewById(R.id.bookmark_name);
4214
4215                 // Get the favorite icon byte array from the cursor.
4216                 byte[] favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON));
4217
4218                 // Convert the byte array to a `Bitmap` beginning at the first byte and ending at the last.
4219                 Bitmap favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.length);
4220
4221                 // Display the bitmap in `bookmarkFavoriteIcon`.
4222                 bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap);
4223
4224                 // Get the bookmark name from the cursor and display it in `bookmarkNameTextView`.
4225                 String bookmarkNameString = cursor.getString(cursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
4226                 bookmarkNameTextView.setText(bookmarkNameString);
4227
4228                 // Make the font bold for folders.
4229                 if (cursor.getInt(cursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {
4230                     bookmarkNameTextView.setTypeface(Typeface.DEFAULT_BOLD);
4231                 } else {  // Reset the font to default for normal bookmarks.
4232                     bookmarkNameTextView.setTypeface(Typeface.DEFAULT);
4233                 }
4234             }
4235         };
4236
4237         // Populate the `ListView` with the adapter.
4238         bookmarksListView.setAdapter(bookmarksCursorAdapter);
4239
4240         // Set the bookmarks drawer title.
4241         if (currentBookmarksFolder.isEmpty()) {
4242             bookmarksTitleTextView.setText(R.string.bookmarks);
4243         } else {
4244             bookmarksTitleTextView.setText(currentBookmarksFolder);
4245         }
4246     }
4247
4248     private void openWithApp(String url) {
4249         // Create the open with intent with `ACTION_VIEW`.
4250         Intent openWithAppIntent = new Intent(Intent.ACTION_VIEW);
4251
4252         // Set the URI but not the MIME type.  This should open all available apps.
4253         openWithAppIntent.setData(Uri.parse(url));
4254
4255         // Flag the intent to open in a new task.
4256         openWithAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4257
4258         // Show the chooser.
4259         startActivity(openWithAppIntent);
4260     }
4261
4262     private void openWithBrowser(String url) {
4263         // Create the open with intent with `ACTION_VIEW`.
4264         Intent openWithBrowserIntent = new Intent(Intent.ACTION_VIEW);
4265
4266         // Set the URI and the MIME type.  `"text/html"` should load browser options.
4267         openWithBrowserIntent.setDataAndType(Uri.parse(url), "text/html");
4268
4269         // Flag the intent to open in a new task.
4270         openWithBrowserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4271
4272         // Show the chooser.
4273         startActivity(openWithBrowserIntent);
4274     }
4275
4276     public static void checkPinnedMismatch(int domainSettingsDatabaseId) {
4277         if ((pinnedSslCertificate || pinnedIpAddresses) && !ignorePinnedDomainInformation) {
4278             // Initialize the current SSL certificate variables.
4279             String currentWebsiteIssuedToCName = "";
4280             String currentWebsiteIssuedToOName = "";
4281             String currentWebsiteIssuedToUName = "";
4282             String currentWebsiteIssuedByCName = "";
4283             String currentWebsiteIssuedByOName = "";
4284             String currentWebsiteIssuedByUName = "";
4285             Date currentWebsiteSslStartDate = null;
4286             Date currentWebsiteSslEndDate = null;
4287
4288
4289             // Extract the individual pieces of information from the current website SSL certificate if it is not null.
4290             if (sslCertificate != null) {
4291                 currentWebsiteIssuedToCName = sslCertificate.getIssuedTo().getCName();
4292                 currentWebsiteIssuedToOName = sslCertificate.getIssuedTo().getOName();
4293                 currentWebsiteIssuedToUName = sslCertificate.getIssuedTo().getUName();
4294                 currentWebsiteIssuedByCName = sslCertificate.getIssuedBy().getCName();
4295                 currentWebsiteIssuedByOName = sslCertificate.getIssuedBy().getOName();
4296                 currentWebsiteIssuedByUName = sslCertificate.getIssuedBy().getUName();
4297                 currentWebsiteSslStartDate = sslCertificate.getValidNotBeforeDate();
4298                 currentWebsiteSslEndDate = sslCertificate.getValidNotAfterDate();
4299             }
4300
4301             // Initialize string variables to store the SSL certificate dates.  Strings are needed to compare the values below, which doesn't work with `Dates` if they are `null`.
4302             String currentWebsiteSslStartDateString = "";
4303             String currentWebsiteSslEndDateString = "";
4304             String pinnedSslStartDateString = "";
4305             String pinnedSslEndDateString = "";
4306
4307             // Convert the `Dates` to `Strings` if they are not `null`.
4308             if (currentWebsiteSslStartDate != null) {
4309                 currentWebsiteSslStartDateString = currentWebsiteSslStartDate.toString();
4310             }
4311
4312             if (currentWebsiteSslEndDate != null) {
4313                 currentWebsiteSslEndDateString = currentWebsiteSslEndDate.toString();
4314             }
4315
4316             if (pinnedSslStartDate != null) {
4317                 pinnedSslStartDateString = pinnedSslStartDate.toString();
4318             }
4319
4320             if (pinnedSslEndDate != null) {
4321                 pinnedSslEndDateString = pinnedSslEndDate.toString();
4322             }
4323
4324             // Check to see if the pinned information matches the current information.
4325             if ((pinnedIpAddresses && !currentHostIpAddresses.equals(pinnedHostIpAddresses)) || (pinnedSslCertificate && (!currentWebsiteIssuedToCName.equals(pinnedSslIssuedToCName) ||
4326                     !currentWebsiteIssuedToOName.equals(pinnedSslIssuedToOName) || !currentWebsiteIssuedToUName.equals(pinnedSslIssuedToUName) ||
4327                     !currentWebsiteIssuedByCName.equals(pinnedSslIssuedByCName) || !currentWebsiteIssuedByOName.equals(pinnedSslIssuedByOName) ||
4328                     !currentWebsiteIssuedByUName.equals(pinnedSslIssuedByUName) || !currentWebsiteSslStartDateString.equals(pinnedSslStartDateString) ||
4329                     !currentWebsiteSslEndDateString.equals(pinnedSslEndDateString)))) {
4330
4331                 // Get a handle for the pinned mismatch alert dialog.
4332                 DialogFragment pinnedMismatchDialogFragment = PinnedMismatchDialog.displayDialog(domainSettingsDatabaseId, pinnedSslCertificate, pinnedIpAddresses);
4333
4334                 // Show the pinned mismatch alert dialog.
4335                 pinnedMismatchDialogFragment.show(fragmentManager, "Pinned Mismatch");
4336             }
4337         }
4338     }
4339
4340     public void addTab(View view) {
4341         // Get a handle for the tab layout.
4342         TabLayout tabLayout = findViewById(R.id.tablayout);
4343
4344         // Get the new page number.  The page numbers are 0 indexed, so the new page number will match the current count.
4345         int newTabNumber = tabLayout.getTabCount();
4346
4347         // Add a new tab.
4348         tabLayout.addTab(tabLayout.newTab());
4349
4350         // Get the new tab.
4351         TabLayout.Tab newTab = tabLayout.getTabAt(newTabNumber);
4352
4353         // Remove the lint warning below that the current tab might be null.
4354         assert newTab != null;
4355
4356         // Set a custom view on the current tab.
4357         newTab.setCustomView(R.layout.custom_tab_view);
4358
4359         // Add the new WebView page.
4360         webViewPagerAdapter.addPage(newTabNumber);
4361
4362         if (newTabNumber > 0) {
4363             // Move to the new tab.
4364             newTab.select();
4365         }
4366     }
4367
4368     @Override
4369     public void initializeWebView(long pageId, int pageNumber, ProgressBar progressBar, NestedScrollWebView nestedScrollWebView) {
4370         // Get handles for the activity views.
4371         FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
4372         DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
4373         RelativeLayout mainContentRelativeLayout = findViewById(R.id.main_content_relativelayout);
4374         ActionBar actionBar = getSupportActionBar();
4375         EditText urlEditText = findViewById(R.id.url_edittext);
4376         TabLayout tabLayout = findViewById(R.id.tablayout);
4377         SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
4378
4379         // Remove the incorrect lint warnings below that the some of the views might be null.
4380         assert actionBar != null;
4381
4382         // Get a handle for the shared preferences.
4383         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4384
4385         // Get the relevant preferences.
4386         boolean downloadWithExternalApp = sharedPreferences.getBoolean("download_with_external_app", false);
4387
4388         // Allow pinch to zoom.
4389         nestedScrollWebView.getSettings().setBuiltInZoomControls(true);
4390
4391         // Hide zoom controls.
4392         nestedScrollWebView.getSettings().setDisplayZoomControls(false);
4393
4394         // Don't allow mixed content (HTTP and HTTPS) on the same website.
4395         if (Build.VERSION.SDK_INT >= 21) {
4396             nestedScrollWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW);
4397         }
4398
4399         // Set the WebView to use a wide viewport.  Otherwise, some web pages will be scrunched and some content will render outside the screen.
4400         nestedScrollWebView.getSettings().setUseWideViewPort(true);
4401
4402         // Set the WebView to load in overview mode (zoomed out to the maximum width).
4403         nestedScrollWebView.getSettings().setLoadWithOverviewMode(true);
4404
4405         // Explicitly disable geolocation.
4406         nestedScrollWebView.getSettings().setGeolocationEnabled(false);
4407
4408         // Create a double-tap gesture detector to toggle full-screen mode.
4409         GestureDetector doubleTapGestureDetector = new GestureDetector(getApplicationContext(), new GestureDetector.SimpleOnGestureListener() {
4410             // Override `onDoubleTap()`.  All other events are handled using the default settings.
4411             @Override
4412             public boolean onDoubleTap(MotionEvent event) {
4413                 if (fullScreenBrowsingModeEnabled) {  // Only process the double-tap if full screen browsing mode is enabled.
4414                     // Toggle the full screen browsing mode tracker.
4415                     inFullScreenBrowsingMode = !inFullScreenBrowsingMode;
4416
4417                     // Toggle the full screen browsing mode.
4418                     if (inFullScreenBrowsingMode) {  // Switch to full screen mode.
4419                         // Hide the app bar if specified.
4420                         if (hideAppBar) {
4421                             actionBar.hide();
4422                         }
4423
4424                         // Hide the banner ad in the free flavor.
4425                         if (BuildConfig.FLAVOR.contentEquals("free")) {
4426                             AdHelper.hideAd(findViewById(R.id.adview));
4427                         }
4428
4429                         // Remove the translucent status flag.  This is necessary so the root frame layout can fill the entire screen.
4430                         getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4431
4432                         /* Hide the system bars.
4433                          * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4434                          * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4435                          * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4436                          * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4437                          */
4438                         rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
4439                                 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
4440                     } else {  // Switch to normal viewing mode.
4441                         // Show the app bar.
4442                         actionBar.show();
4443
4444                         // Show the banner ad in the free flavor.
4445                         if (BuildConfig.FLAVOR.contentEquals("free")) {
4446                             // Reload the ad.
4447                             AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
4448                         }
4449
4450                         // Remove the `SYSTEM_UI` flags from the root frame layout.
4451                         rootFrameLayout.setSystemUiVisibility(0);
4452
4453                         // Add the translucent status flag.
4454                         getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4455                     }
4456
4457                     // Consume the double-tap.
4458                     return true;
4459                 } else { // Do not consume the double-tap because full screen browsing mode is disabled.
4460                     return false;
4461                 }
4462             }
4463         });
4464
4465         // Pass all touch events on the WebView through the double-tap gesture detector.
4466         nestedScrollWebView.setOnTouchListener((View view, MotionEvent event) -> {
4467             // Call `performClick()` on the view, which is required for accessibility.
4468             view.performClick();
4469
4470             // Send the event to the gesture detector.
4471             return doubleTapGestureDetector.onTouchEvent(event);
4472         });
4473
4474         // Register the WebView for a context menu.  This is used to see link targets and download images.
4475         registerForContextMenu(nestedScrollWebView);
4476
4477         // Allow the downloading of files.
4478         nestedScrollWebView.setDownloadListener((String url, String userAgent, String contentDisposition, String mimetype, long contentLength) -> {
4479             // Check if the download should be processed by an external app.
4480             if (downloadWithExternalApp) {  // Download with an external app.
4481                 // Create a download intent.  Not specifying the action type will display the maximum number of options.
4482                 Intent downloadIntent = new Intent();
4483
4484                 // Set the URI and the MIME type.  Specifying `text/html` displays a good number of options.
4485                 downloadIntent.setDataAndType(Uri.parse(url), "text/html");
4486
4487                 // Flag the intent to open in a new task.
4488                 downloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4489
4490                 // Show the chooser.
4491                 startActivity(Intent.createChooser(downloadIntent, getString(R.string.open_with)));
4492             } else {  // Download with Android's download manager.
4493                 // Check to see if the WRITE_EXTERNAL_STORAGE permission has already been granted.
4494                 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {  // The storage permission has not been granted.
4495                     // The WRITE_EXTERNAL_STORAGE permission needs to be requested.
4496
4497                     // Store the variables for future use by `onRequestPermissionsResult()`.
4498                     downloadUrl = url;
4499                     downloadContentDisposition = contentDisposition;
4500                     downloadContentLength = contentLength;
4501
4502                     // Show a dialog if the user has previously denied the permission.
4503                     if (ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
4504                         // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
4505                         DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE);
4506
4507                         // Show the download location permission alert dialog.  The permission will be requested when the the dialog is closed.
4508                         downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
4509                     } else {  // Show the permission request directly.
4510                         // Request the permission.  The download dialog will be launched by `onRequestPermissionResult()`.
4511                         ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
4512                     }
4513                 } else {  // The storage permission has already been granted.
4514                     // Get a handle for the download file alert dialog.
4515                     DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(url, contentDisposition, contentLength);
4516
4517                     // Show the download file alert dialog.
4518                     downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
4519                 }
4520             }
4521         });
4522
4523         // Update the find on page count.
4524         nestedScrollWebView.setFindListener(new WebView.FindListener() {
4525             // Get a handle for `findOnPageCountTextView`.
4526             final TextView findOnPageCountTextView = findViewById(R.id.find_on_page_count_textview);
4527
4528             @Override
4529             public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) {
4530                 if ((isDoneCounting) && (numberOfMatches == 0)) {  // There are no matches.
4531                     // Set `findOnPageCountTextView` to `0/0`.
4532                     findOnPageCountTextView.setText(R.string.zero_of_zero);
4533                 } else if (isDoneCounting) {  // There are matches.
4534                     // `activeMatchOrdinal` is zero-based.
4535                     int activeMatch = activeMatchOrdinal + 1;
4536
4537                     // Build the match string.
4538                     String matchString = activeMatch + "/" + numberOfMatches;
4539
4540                     // Set `findOnPageCountTextView`.
4541                     findOnPageCountTextView.setText(matchString);
4542                 }
4543             }
4544         });
4545
4546         // Set the web chrome client.
4547         nestedScrollWebView.setWebChromeClient(new WebChromeClient() {
4548             // Update the progress bar when a page is loading.
4549             @Override
4550             public void onProgressChanged(WebView view, int progress) {
4551                 // Inject the night mode CSS if night mode is enabled.
4552                 if (nightMode) {
4553                     // `background-color: #212121` sets the background to be dark gray.  `color: #BDBDBD` sets the text color to be light gray.  `box-shadow: none` removes a lower underline on links
4554                     // used by WordPress.  `text-decoration: none` removes all text underlines.  `text-shadow: none` removes text shadows, which usually have a hard coded color.
4555                     // `border: none` removes all borders, which can also be used to underline text.  `a {color: #1565C0}` sets links to be a dark blue.
4556                     // `::selection {background: #0D47A1}' sets the text selection highlight color to be a dark blue. `!important` takes precedent over any existing sub-settings.
4557                     nestedScrollWebView.evaluateJavascript("(function() {var parent = document.getElementsByTagName('head').item(0); var style = document.createElement('style'); style.type = 'text/css'; " +
4558                             "style.innerHTML = '* {background-color: #212121 !important; color: #BDBDBD !important; box-shadow: none !important; text-decoration: none !important;" +
4559                             "text-shadow: none !important; border: none !important;} a {color: #1565C0 !important;} ::selection {background: #0D47A1 !important;}'; parent.appendChild(style)})()", value -> {
4560                         // Initialize a handler to display `mainWebView`.
4561                         Handler displayWebViewHandler = new Handler();
4562
4563                         // Setup a runnable to display `mainWebView` after a delay to allow the CSS to be applied.
4564                         Runnable displayWebViewRunnable = () -> {
4565                             // Only display `mainWebView` if the progress bar is gone.  This prevents the display of the `WebView` while it is still loading.
4566                             if (progressBar.getVisibility() == View.GONE) {
4567                                 nestedScrollWebView.setVisibility(View.VISIBLE);
4568                             }
4569                         };
4570
4571                         // Displaying of `mainWebView` after 500 milliseconds.
4572                         displayWebViewHandler.postDelayed(displayWebViewRunnable, 500);
4573                     });
4574                 }
4575
4576                 // Update the progress bar.
4577                 progressBar.setProgress(progress);
4578
4579                 // Set the visibility of the progress bar.
4580                 if (progress < 100) {
4581                     // Show the progress bar.
4582                     progressBar.setVisibility(View.VISIBLE);
4583                 } else {
4584                     // Hide the progress bar.
4585                     progressBar.setVisibility(View.GONE);
4586
4587                     // Display `mainWebView` if night mode is disabled.
4588                     // Because of a race condition between `applyDomainSettings` and `onPageStarted`, when night mode is set by domain settings the `WebView` may be hidden even if night mode is not
4589                     // currently enabled.
4590                     if (!nightMode) {
4591                         nestedScrollWebView.setVisibility(View.VISIBLE);
4592                     }
4593
4594                     //Stop the swipe to refresh indicator if it is running
4595                     swipeRefreshLayout.setRefreshing(false);
4596                 }
4597             }
4598
4599             // Set the favorite icon when it changes.
4600             @Override
4601             public void onReceivedIcon(WebView view, Bitmap icon) {
4602                 // Only update the favorite icon if the website has finished loading.
4603                 if (progressBar.getVisibility() == View.GONE) {
4604                     // Save a copy of the favorite icon.
4605                     favoriteIconBitmap = icon;
4606
4607                     // Get the current page position.
4608                     int currentPosition = webViewPagerAdapter.getPositionForId(pageId);
4609
4610                     // Get the current tab.
4611                     TabLayout.Tab tab = tabLayout.getTabAt(currentPosition);
4612
4613                     // Remove the lint warning below that the current tab might be null.
4614                     assert tab != null;
4615
4616                     // Get the custom view from the tab.
4617                     View tabView = tab.getCustomView();
4618
4619                     // Remove the incorrect warning below that the current tab view might be null.
4620                     assert tabView != null;
4621
4622                     // Get the favorite icon image view from the tab.
4623                     ImageView tabFavoriteIconImageView = tabView.findViewById(R.id.favorite_icon_imageview);
4624
4625                     // Display the favorite icon in the tab.
4626                     tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(icon, 64, 64, true));
4627                 }
4628             }
4629
4630             // Save a copy of the title when it changes.
4631             @Override
4632             public void onReceivedTitle(WebView view, String title) {
4633                 // Get the current page position.
4634                 int currentPosition = webViewPagerAdapter.getPositionForId(pageId);
4635
4636                 // Get the current tab.
4637                 TabLayout.Tab tab = tabLayout.getTabAt(currentPosition);
4638
4639                 // Remove the lint warning below that the current tab might be null.
4640                 assert tab != null;
4641
4642                 // Get the custom view from the tab.
4643                 View tabView = tab.getCustomView();
4644
4645                 // Remove the incorrect warning below that the current tab view might be null.
4646                 assert tabView != null;
4647
4648                 // Get the title text view from the tab.
4649                 TextView tabTitleTextView = tabView.findViewById(R.id.title_textview);
4650
4651                 // Set the title as the tab text.
4652                 tabTitleTextView.setText(title);
4653             }
4654
4655             // Enter full screen video.
4656             @Override
4657             public void onShowCustomView(View video, CustomViewCallback callback) {
4658                 // Set the full screen video flag.
4659                 displayingFullScreenVideo = true;
4660
4661                 // Pause the ad if this is the free flavor.
4662                 if (BuildConfig.FLAVOR.contentEquals("free")) {
4663                     // The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
4664                     AdHelper.pauseAd(findViewById(R.id.adview));
4665                 }
4666
4667                 // Hide the keyboard.
4668                 inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0);
4669
4670                 // Hide the main content relative layout.
4671                 mainContentRelativeLayout.setVisibility(View.GONE);
4672
4673                 // Remove the translucent status bar overlay on the `Drawer Layout`, which is special and needs its own command.
4674                 drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
4675
4676                 // Remove the translucent status flag.  This is necessary so the root frame layout can fill the entire screen.
4677                 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4678
4679                 /* Hide the system bars.
4680                  * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4681                  * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4682                  * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4683                  * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4684                  */
4685                 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
4686                         View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
4687
4688                 // Disable the sliding drawers.
4689                 drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
4690
4691                 // Add the video view to the full screen video frame layout.
4692                 fullScreenVideoFrameLayout.addView(video);
4693
4694                 // Show the full screen video frame layout.
4695                 fullScreenVideoFrameLayout.setVisibility(View.VISIBLE);
4696             }
4697
4698             // Exit full screen video.
4699             @Override
4700             public void onHideCustomView() {
4701                 // Unset the full screen video flag.
4702                 displayingFullScreenVideo = false;
4703
4704                 // Remove all the views from the full screen video frame layout.
4705                 fullScreenVideoFrameLayout.removeAllViews();
4706
4707                 // Hide the full screen video frame layout.
4708                 fullScreenVideoFrameLayout.setVisibility(View.GONE);
4709
4710                 // Enable the sliding drawers.
4711                 drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
4712
4713                 // Show the main content relative layout.
4714                 mainContentRelativeLayout.setVisibility(View.VISIBLE);
4715
4716                 // Apply the appropriate full screen mode the `SYSTEM_UI` flags.
4717                 if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) {  // Privacy Browser is currently in full screen browsing mode.
4718                     // Hide the app bar if specified.
4719                     if (hideAppBar) {
4720                         actionBar.hide();
4721                     }
4722
4723                     // Hide the banner ad in the free flavor.
4724                     if (BuildConfig.FLAVOR.contentEquals("free")) {
4725                         AdHelper.hideAd(findViewById(R.id.adview));
4726                     }
4727
4728                     // Remove the translucent status flag.  This is necessary so the root frame layout can fill the entire screen.
4729                     getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4730
4731                     /* Hide the system bars.
4732                      * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4733                      * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4734                      * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4735                      * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4736                      */
4737                     rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
4738                             View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
4739                 } else {  // Switch to normal viewing mode.
4740                     // Remove the `SYSTEM_UI` flags from the root frame layout.
4741                     rootFrameLayout.setSystemUiVisibility(0);
4742
4743                     // Add the translucent status flag.
4744                     getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4745                 }
4746
4747                 // Reload the ad for the free flavor if not in full screen mode.
4748                 if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
4749                     // Reload the ad.
4750                     AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
4751                 }
4752             }
4753
4754             // Upload files.
4755             @Override
4756             public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
4757                 // Show the file chooser if the device is running API >= 21.
4758                 if (Build.VERSION.SDK_INT >= 21) {
4759                     // Store the file path callback.
4760                     fileChooserCallback = filePathCallback;
4761
4762                     // Create an intent to open a chooser based ont the file chooser parameters.
4763                     Intent fileChooserIntent = fileChooserParams.createIntent();
4764
4765                     // Open the file chooser.  Currently only one `startActivityForResult` exists in this activity, so the request code, used to differentiate them, is simply `0`.
4766                     startActivityForResult(fileChooserIntent, 0);
4767                 }
4768                 return true;
4769             }
4770         });
4771
4772         nestedScrollWebView.setWebViewClient(new WebViewClient() {
4773             // `shouldOverrideUrlLoading` makes this `WebView` the default handler for URLs inside the app, so that links are not kicked out to other apps.
4774             // The deprecated `shouldOverrideUrlLoading` must be used until API >= 24.
4775             @SuppressWarnings("deprecation")
4776             @Override
4777             public boolean shouldOverrideUrlLoading(WebView view, String url) {
4778                 if (url.startsWith("http")) {  // Load the URL in Privacy Browser.
4779                     // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
4780                     formattedUrlString = "";
4781
4782                     // Apply the domain settings for the new URL.  `applyDomainSettings` doesn't do anything if the domain has not changed.
4783                     boolean userAgentChanged = applyDomainSettings(url, true, false);
4784
4785                     // Check if the user agent has changed.
4786                     if (userAgentChanged) {
4787                         // Manually load the URL.  The changing of the user agent will cause WebView to reload the previous URL.
4788                         nestedScrollWebView.loadUrl(url, customHeaders);
4789
4790                         // Returning true indicates that Privacy Browser is manually handling the loading of the URL.
4791                         return true;
4792                     } else {
4793                         // Returning false causes the current WebView to handle the URL and prevents it from adding redirects to the history list.
4794                         return false;
4795                     }
4796                 } else if (url.startsWith("mailto:")) {  // Load the email address in an external email program.
4797                     // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
4798                     Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
4799
4800                     // Parse the url and set it as the data for the intent.
4801                     emailIntent.setData(Uri.parse(url));
4802
4803                     // Open the email program in a new task instead of as part of Privacy Browser.
4804                     emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4805
4806                     // Make it so.
4807                     startActivity(emailIntent);
4808
4809                     // Returning true indicates Privacy Browser is handling the URL by creating an intent.
4810                     return true;
4811                 } else if (url.startsWith("tel:")) {  // Load the phone number in the dialer.
4812                     // Open the dialer and load the phone number, but wait for the user to place the call.
4813                     Intent dialIntent = new Intent(Intent.ACTION_DIAL);
4814
4815                     // Add the phone number to the intent.
4816                     dialIntent.setData(Uri.parse(url));
4817
4818                     // Open the dialer in a new task instead of as part of Privacy Browser.
4819                     dialIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4820
4821                     // Make it so.
4822                     startActivity(dialIntent);
4823
4824                     // Returning true indicates Privacy Browser is handling the URL by creating an intent.
4825                     return true;
4826                 } else {  // Load a system chooser to select an app that can handle the URL.
4827                     // Open an app that can handle the URL.
4828                     Intent genericIntent = new Intent(Intent.ACTION_VIEW);
4829
4830                     // Add the URL to the intent.
4831                     genericIntent.setData(Uri.parse(url));
4832
4833                     // List all apps that can handle the URL instead of just opening the first one.
4834                     genericIntent.addCategory(Intent.CATEGORY_BROWSABLE);
4835
4836                     // Open the app in a new task instead of as part of Privacy Browser.
4837                     genericIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4838
4839                     // Start the app or display a snackbar if no app is available to handle the URL.
4840                     try {
4841                         startActivity(genericIntent);
4842                     } catch (ActivityNotFoundException exception) {
4843                         Snackbar.make(nestedScrollWebView, getString(R.string.unrecognized_url) + "  " + url, Snackbar.LENGTH_SHORT).show();
4844                     }
4845
4846                     // Returning true indicates Privacy Browser is handling the URL by creating an intent.
4847                     return true;
4848                 }
4849             }
4850
4851             // Check requests against the block lists.  The deprecated `shouldInterceptRequest()` must be used until minimum API >= 21.
4852             @SuppressWarnings("deprecation")
4853             @Override
4854             public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
4855                 // Create an empty web resource response to be used if the resource request is blocked.
4856                 WebResourceResponse emptyWebResourceResponse = new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes()));
4857
4858                 // Reset the whitelist results tracker.
4859                 String[] whitelistResultStringArray = null;
4860
4861                 // Initialize the third party request tracker.
4862                 boolean isThirdPartyRequest = false;
4863
4864                 // Initialize the current domain string.
4865                 String currentDomain = "";
4866
4867                 // Nobody is happy when comparing null strings.
4868                 if (!(formattedUrlString == null) && !(url == null)) {
4869                     // Get the domain strings to URIs.
4870                     Uri currentDomainUri = Uri.parse(formattedUrlString);
4871                     Uri requestDomainUri = Uri.parse(url);
4872
4873                     // Get the domain host names.
4874                     String currentBaseDomain = currentDomainUri.getHost();
4875                     String requestBaseDomain = requestDomainUri.getHost();
4876
4877                     // Update the current domain variable.
4878                     currentDomain = currentBaseDomain;
4879
4880                     // Only compare the current base domain and the request base domain if neither is null.
4881                     if (!(currentBaseDomain == null) && !(requestBaseDomain == null)) {
4882                         // Determine the current base domain.
4883                         while (currentBaseDomain.indexOf(".", currentBaseDomain.indexOf(".") + 1) > 0) {  // There is at least one subdomain.
4884                             // Remove the first subdomain.
4885                             currentBaseDomain = currentBaseDomain.substring(currentBaseDomain.indexOf(".") + 1);
4886                         }
4887
4888                         // Determine the request base domain.
4889                         while (requestBaseDomain.indexOf(".", requestBaseDomain.indexOf(".") + 1) > 0) {  // There is at least one subdomain.
4890                             // Remove the first subdomain.
4891                             requestBaseDomain = requestBaseDomain.substring(requestBaseDomain.indexOf(".") + 1);
4892                         }
4893
4894                         // Update the third party request tracker.
4895                         isThirdPartyRequest = !currentBaseDomain.equals(requestBaseDomain);
4896                     }
4897                 }
4898
4899                 // Get the current WebView page position.
4900                 int webViewPagePosition = webViewPagerAdapter.getPositionForId(pageId);
4901
4902                 // Determine if the WebView is currently displayed.
4903                 boolean webViewDisplayed = (webViewPagePosition == tabLayout.getSelectedTabPosition());
4904
4905                 // Block third-party requests if enabled.
4906                 if (isThirdPartyRequest && blockAllThirdPartyRequests) {
4907                     // Add the result to the resource requests.
4908                     nestedScrollWebView.addResourceRequest(new String[]{BlockListHelper.REQUEST_THIRD_PARTY, url});
4909
4910                     // Increment the blocked requests counters.
4911                     nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
4912                     nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.THIRD_PARTY_BLOCKED_REQUESTS);
4913
4914                     // Update the titles of the blocklist menu items if the WebView is currently displayed.
4915                     if (webViewDisplayed) {
4916                         // Updating the UI must be run from the UI thread.
4917                         activity.runOnUiThread(() -> {
4918                             // Update the menu item titles.
4919                             navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4920                             blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4921                             blockAllThirdPartyRequestsMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_BLOCKED_REQUESTS) + " - " +
4922                                     getString(R.string.block_all_third_party_requests));
4923                         });
4924                     }
4925
4926                     // Return an empty web resource response.
4927                     return emptyWebResourceResponse;
4928                 }
4929
4930                 // Check UltraPrivacy if it is enabled.
4931                 if (ultraPrivacyEnabled) {
4932                     // Check the URL against UltraPrivacy.
4933                     String[] ultraPrivacyResults = blockListHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, ultraPrivacy);
4934
4935                     // Process the UltraPrivacy results.
4936                     if (ultraPrivacyResults[0].equals(BlockListHelper.REQUEST_BLOCKED)) {  // The resource request matched UltraPrivacy's blacklist.
4937                         // Add the result to the resource requests.
4938                         nestedScrollWebView.addResourceRequest(new String[] {ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4],
4939                                 ultraPrivacyResults[5]});
4940
4941                         // Increment the blocked requests counters.
4942                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
4943                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.ULTRA_PRIVACY_BLOCKED_REQUESTS);
4944
4945                         // Update the titles of the blocklist menu items if the WebView is currently displayed.
4946                         if (webViewDisplayed) {
4947                             // Updating the UI must be run from the UI thread.
4948                             activity.runOnUiThread(() -> {
4949                                 // Update the menu item titles.
4950                                 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4951                                 blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4952                                 ultraPrivacyMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.ULTRA_PRIVACY_BLOCKED_REQUESTS) + " - " + getString(R.string.ultraprivacy));
4953                             });
4954                         }
4955
4956                         // The resource request was blocked.  Return an empty web resource response.
4957                         return emptyWebResourceResponse;
4958                     } else if (ultraPrivacyResults[0].equals(BlockListHelper.REQUEST_ALLOWED)) {  // The resource request matched UltraPrivacy's whitelist.
4959                         // Add a whitelist entry to the resource requests array.
4960                         nestedScrollWebView.addResourceRequest(new String[] {ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4],
4961                                 ultraPrivacyResults[5]});
4962
4963                         // The resource request has been allowed by UltraPrivacy.  `return null` loads the requested resource.
4964                         return null;
4965                     }
4966                 }
4967
4968                 // Check EasyList if it is enabled.
4969                 if (easyListEnabled) {
4970                     // Check the URL against EasyList.
4971                     String[] easyListResults = blockListHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, easyList);
4972
4973                     // Process the EasyList results.
4974                     if (easyListResults[0].equals(BlockListHelper.REQUEST_BLOCKED)) {  // The resource request matched EasyList's blacklist.
4975                         // Add the result to the resource requests.
4976                         nestedScrollWebView.addResourceRequest(new String[] {easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5]});
4977
4978                         // Increment the blocked requests counters.
4979                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
4980                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.EASY_LIST_BLOCKED_REQUESTS);
4981
4982                         // Update the titles of the blocklist menu items if the WebView is currently displayed.
4983                         if (webViewDisplayed) {
4984                             // Updating the UI must be run from the UI thread.
4985                             activity.runOnUiThread(() -> {
4986                                 // Update the menu item titles.
4987                                 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4988                                 blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
4989                                 easyListMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.EASY_LIST_BLOCKED_REQUESTS) + " - " + getString(R.string.easylist));
4990                             });
4991                         }
4992
4993                         // The resource request was blocked.  Return an empty web resource response.
4994                         return emptyWebResourceResponse;
4995                     } else if (easyListResults[0].equals(BlockListHelper.REQUEST_ALLOWED)) {  // The resource request matched EasyList's whitelist.
4996                         // Update the whitelist result string array tracker.
4997                         whitelistResultStringArray = new String[] {easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5]};
4998                     }
4999                 }
5000
5001                 // Check EasyPrivacy if it is enabled.
5002                 if (easyPrivacyEnabled) {
5003                     // Check the URL against EasyPrivacy.
5004                     String[] easyPrivacyResults = blockListHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, easyPrivacy);
5005
5006                     // Process the EasyPrivacy results.
5007                     if (easyPrivacyResults[0].equals(BlockListHelper.REQUEST_BLOCKED)) {  // The resource request matched EasyPrivacy's blacklist.
5008                         // Add the result to the resource requests.
5009                         nestedScrollWebView.addResourceRequest(new String[] {easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[5],
5010                                 easyPrivacyResults[5]});
5011
5012                         // Increment the blocked requests counters.
5013                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5014                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.EASY_PRIVACY_BLOCKED_REQUESTS);
5015
5016                         // Update the titles of the blocklist menu items if the WebView is currently displayed.
5017                         if (webViewDisplayed) {
5018                             // Updating the UI must be run from the UI thread.
5019                             activity.runOnUiThread(() -> {
5020                                 // Update the menu item titles.
5021                                 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5022                                 blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5023                                 easyPrivacyMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.EASY_PRIVACY_BLOCKED_REQUESTS) + " - " + getString(R.string.easyprivacy));
5024                             });
5025                         }
5026
5027                         // The resource request was blocked.  Return an empty web resource response.
5028                         return emptyWebResourceResponse;
5029                     } else if (easyPrivacyResults[0].equals(BlockListHelper.REQUEST_ALLOWED)) {  // The resource request matched EasyPrivacy's whitelist.
5030                         // Update the whitelist result string array tracker.
5031                         whitelistResultStringArray = new String[] {easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[4], easyPrivacyResults[5]};
5032                     }
5033                 }
5034
5035                 // Check Fanboy’s Annoyance List if it is enabled.
5036                 if (fanboysAnnoyanceListEnabled) {
5037                     // Check the URL against Fanboy's Annoyance List.
5038                     String[] fanboysAnnoyanceListResults = blockListHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, fanboysAnnoyanceList);
5039
5040                     // Process the Fanboy's Annoyance List results.
5041                     if (fanboysAnnoyanceListResults[0].equals(BlockListHelper.REQUEST_BLOCKED)) {  // The resource request matched Fanboy's Annoyance List's blacklist.
5042                         // Add the result to the resource requests.
5043                         nestedScrollWebView.addResourceRequest(new String[] {fanboysAnnoyanceListResults[0], fanboysAnnoyanceListResults[1], fanboysAnnoyanceListResults[2], fanboysAnnoyanceListResults[3],
5044                                 fanboysAnnoyanceListResults[4], fanboysAnnoyanceListResults[5]});
5045
5046                         // Increment the blocked requests counters.
5047                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5048                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST_BLOCKED_REQUESTS);
5049
5050                         // Update the titles of the blocklist menu items if the WebView is currently displayed.
5051                         if (webViewDisplayed) {
5052                             // Updating the UI must be run from the UI thread.
5053                             activity.runOnUiThread(() -> {
5054                                 // Update the menu item titles.
5055                                 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5056                                 blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5057                                 fanboysAnnoyanceListMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST_BLOCKED_REQUESTS) + " - " +
5058                                         getString(R.string.fanboys_annoyance_list));
5059                             });
5060                         }
5061
5062                         // The resource request was blocked.  Return an empty web resource response.
5063                         return emptyWebResourceResponse;
5064                     } else if (fanboysAnnoyanceListResults[0].equals(BlockListHelper.REQUEST_ALLOWED)){  // The resource request matched Fanboy's Annoyance List's whitelist.
5065                         // Update the whitelist result string array tracker.
5066                         whitelistResultStringArray = new String[] {fanboysAnnoyanceListResults[0], fanboysAnnoyanceListResults[1], fanboysAnnoyanceListResults[2], fanboysAnnoyanceListResults[3],
5067                                 fanboysAnnoyanceListResults[4], fanboysAnnoyanceListResults[5]};
5068                     }
5069                 } else if (fanboysSocialBlockingListEnabled) {  // Only check Fanboy’s Social Blocking List if Fanboy’s Annoyance List is disabled.
5070                     // Check the URL against Fanboy's Annoyance List.
5071                     String[] fanboysSocialListResults = blockListHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, fanboysSocialList);
5072
5073                     // Process the Fanboy's Social Blocking List results.
5074                     if (fanboysSocialListResults[0].equals(BlockListHelper.REQUEST_BLOCKED)) {  // The resource request matched Fanboy's Social Blocking List's blacklist.
5075                         // Add the result to the resource requests.
5076                         nestedScrollWebView.addResourceRequest(new String[] {fanboysSocialListResults[0], fanboysSocialListResults[1], fanboysSocialListResults[2], fanboysSocialListResults[3],
5077                                 fanboysSocialListResults[4], fanboysSocialListResults[5]});
5078
5079                         // Increment the blocked requests counters.
5080                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5081                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST_BLOCKED_REQUESTS);
5082
5083                         // Update the titles of the blocklist menu items if the WebView is currently displayed.
5084                         if (webViewDisplayed) {
5085                             // Updating the UI must be run from the UI thread.
5086                             activity.runOnUiThread(() -> {
5087                                 // Update the menu item titles.
5088                                 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5089                                 blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5090                                 fanboysSocialBlockingListMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST_BLOCKED_REQUESTS) + " - " +
5091                                         getString(R.string.fanboys_social_blocking_list));
5092                             });
5093                         }
5094
5095                         // The resource request was blocked.  Return an empty web resource response.
5096                         return emptyWebResourceResponse;
5097                     } else if (fanboysSocialListResults[0].equals(BlockListHelper.REQUEST_ALLOWED)) {  // The resource request matched Fanboy's Social Blocking List's whitelist.
5098                         // Update the whitelist result string array tracker.
5099                         whitelistResultStringArray = new String[] {fanboysSocialListResults[0], fanboysSocialListResults[1], fanboysSocialListResults[2], fanboysSocialListResults[3],
5100                                 fanboysSocialListResults[4], fanboysSocialListResults[5]};
5101                     }
5102                 }
5103
5104                 // Add the request to the log because it hasn't been processed by any of the previous checks.
5105                 if (whitelistResultStringArray != null) {  // The request was processed by a whitelist.
5106                     nestedScrollWebView.addResourceRequest(whitelistResultStringArray);
5107                 } else {  // The request didn't match any blocklist entry.  Log it as a default request.
5108                     nestedScrollWebView.addResourceRequest(new String[]{BlockListHelper.REQUEST_DEFAULT, url});
5109                 }
5110
5111                 // The resource request has not been blocked.  `return null` loads the requested resource.
5112                 return null;
5113             }
5114
5115             // Handle HTTP authentication requests.
5116             @Override
5117             public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {
5118                 // Store `handler` so it can be accessed from `onHttpAuthenticationCancel()` and `onHttpAuthenticationProceed()`.
5119                 httpAuthHandler = handler;
5120
5121                 // Display the HTTP authentication dialog.
5122                 DialogFragment httpAuthenticationDialogFragment = HttpAuthenticationDialog.displayDialog(host, realm);
5123                 httpAuthenticationDialogFragment.show(fragmentManager, getString(R.string.http_authentication));
5124             }
5125
5126             // Update the URL in urlTextBox when the page starts to load.
5127             @Override
5128             public void onPageStarted(WebView view, String url, Bitmap favicon) {
5129                 // Set `urlIsLoading` to `true`, so that redirects while loading do not trigger changes in the user agent, which forces another reload of the existing page.
5130                 // This is also used to determine when to check for pinned mismatches.
5131                 urlIsLoading = true;
5132
5133                 // Reset the list of host IP addresses.
5134                 currentHostIpAddresses = "";
5135
5136                 // Reset the list of resource requests.
5137                 nestedScrollWebView.clearResourceRequests();
5138
5139                 // Initialize the counters for requests blocked by each blocklist.
5140                 nestedScrollWebView.resetRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5141                 nestedScrollWebView.resetRequestsCount(NestedScrollWebView.EASY_LIST_BLOCKED_REQUESTS);
5142                 nestedScrollWebView.resetRequestsCount(NestedScrollWebView.EASY_PRIVACY_BLOCKED_REQUESTS);
5143                 nestedScrollWebView.resetRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST_BLOCKED_REQUESTS);
5144                 nestedScrollWebView.resetRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST_BLOCKED_REQUESTS);
5145                 nestedScrollWebView.resetRequestsCount(NestedScrollWebView.ULTRA_PRIVACY_BLOCKED_REQUESTS);
5146                 nestedScrollWebView.resetRequestsCount(NestedScrollWebView.THIRD_PARTY_BLOCKED_REQUESTS);
5147
5148                 // If night mode is enabled, hide `mainWebView` until after the night mode CSS is applied.
5149                 if (nightMode) {
5150                     nestedScrollWebView.setVisibility(View.INVISIBLE);
5151                 }
5152
5153                 // Hide the keyboard.
5154                 inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0);
5155
5156                 // Check to see if Privacy Browser is waiting on Orbot.
5157                 if (!waitingForOrbot) {  // Process the URL.
5158                     // The formatted URL string must be updated at the beginning of the load, so that if the user toggles JavaScript during the load the new website is reloaded.
5159                     formattedUrlString = url;
5160
5161                     // Display the formatted URL text.
5162                     urlEditText.setText(formattedUrlString);
5163
5164                     // Apply text highlighting to `urlTextBox`.
5165                     highlightUrlText();
5166
5167                     // Get a URI for the current URL.
5168                     Uri currentUri = Uri.parse(formattedUrlString);
5169
5170                     // Get the IP addresses for the host.
5171                     new GetHostIpAddresses(activity, currentWebView.getDomainSettingsDatabaseId()).execute(currentUri.getHost());
5172
5173                     // Apply any custom domain settings if the URL was loaded by navigating history.
5174                     if (navigatingHistory) {
5175                         // Apply the domain settings.
5176                         boolean userAgentChanged = applyDomainSettings(url, true, false);
5177
5178                         // Reset `navigatingHistory`.
5179                         navigatingHistory = false;
5180
5181                         // Manually load the URL if the user agent has changed, which will have caused the previous URL to be reloaded.
5182                         if (userAgentChanged) {
5183                             loadUrl(formattedUrlString);
5184                         }
5185                     }
5186
5187                     // Replace Refresh with Stop if the menu item has been created.  (The WebView typically begins loading before the menu items are instantiated.)
5188                     if (refreshMenuItem != null) {
5189                         // Set the title.
5190                         refreshMenuItem.setTitle(R.string.stop);
5191
5192                         // If the icon is displayed in the AppBar, set it according to the theme.
5193                         if (displayAdditionalAppBarIcons) {
5194                             if (darkTheme) {
5195                                 refreshMenuItem.setIcon(R.drawable.close_dark);
5196                             } else {
5197                                 refreshMenuItem.setIcon(R.drawable.close_light);
5198                             }
5199                         }
5200                     }
5201                 }
5202             }
5203
5204             // It is necessary to update `formattedUrlString` and `urlTextBox` after the page finishes loading because the final URL can change during load.
5205             @Override
5206             public void onPageFinished(WebView view, String url) {
5207                 // Reset the wide view port if it has been turned off by the waiting for Orbot message.
5208                 if (!waitingForOrbot) {
5209                     // Only use a wide view port if the URL starts with `http`, not for `file://` and `content://`.
5210                     nestedScrollWebView.getSettings().setUseWideViewPort(url.startsWith("http"));
5211                 }
5212
5213                 // Flush any cookies to persistent storage.  `CookieManager` has become very lazy about flushing cookies in recent versions.
5214                 if (firstPartyCookiesEnabled && Build.VERSION.SDK_INT >= 21) {
5215                     cookieManager.flush();
5216                 }
5217
5218                 // Update the Refresh menu item if it has been created.
5219                 if (refreshMenuItem != null) {
5220                     // Reset the Refresh title.
5221                     refreshMenuItem.setTitle(R.string.refresh);
5222
5223                     // If the icon is displayed in the AppBar, reset it according to the theme.
5224                     if (displayAdditionalAppBarIcons) {
5225                         if (darkTheme) {
5226                             refreshMenuItem.setIcon(R.drawable.refresh_enabled_dark);
5227                         } else {
5228                             refreshMenuItem.setIcon(R.drawable.refresh_enabled_light);
5229                         }
5230                     }
5231                 }
5232
5233
5234                 // Clear the cache and history if Incognito Mode is enabled.
5235                 if (incognitoModeEnabled) {
5236                     // Clear the cache.  `true` includes disk files.
5237                     nestedScrollWebView.clearCache(true);
5238
5239                     // Clear the back/forward history.
5240                     nestedScrollWebView.clearHistory();
5241
5242                     // Manually delete cache folders.
5243                     try {
5244                         // Delete the main cache directory.
5245                         privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/cache");
5246
5247                         // Delete the secondary `Service Worker` cache directory.
5248                         // A `String[]` must be used because the directory contains a space and `Runtime.exec` will not escape the string correctly otherwise.
5249                         privacyBrowserRuntime.exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
5250                     } catch (IOException e) {
5251                         // Do nothing if an error is thrown.
5252                     }
5253                 }
5254
5255                 // Update the URL text box and apply domain settings if not waiting on Orbot.
5256                 if (!waitingForOrbot) {
5257                     // Check to see if `WebView` has set `url` to be `about:blank`.
5258                     if (url.equals("about:blank")) {  // `WebView` is blank, so `formattedUrlString` should be `""` and `urlTextBox` should display a hint.
5259                         // Set `formattedUrlString` to `""`.
5260                         formattedUrlString = "";
5261
5262                         urlEditText.setText(formattedUrlString);
5263
5264                         // Request focus for `urlTextBox`.
5265                         urlEditText.requestFocus();
5266
5267                         // Display the keyboard.
5268                         inputMethodManager.showSoftInput(urlEditText, 0);
5269
5270                         // Apply the domain settings.  This clears any settings from the previous domain.
5271                         applyDomainSettings(formattedUrlString, true, false);
5272                     } else {  // `WebView` has loaded a webpage.
5273                         // Set the formatted URL string.  Getting the URL from the WebView instead of using the one provided by `onPageFinished` makes websites like YouTube function correctly.
5274                         formattedUrlString = nestedScrollWebView.getUrl();
5275
5276                         // Only update the URL text box if the user is not typing in it.
5277                         if (!urlEditText.hasFocus()) {
5278                             // Display the formatted URL text.
5279                             urlEditText.setText(formattedUrlString);
5280
5281                             // Apply text highlighting to `urlTextBox`.
5282                             highlightUrlText();
5283                         }
5284                     }
5285
5286                     // Store the SSL certificate so it can be accessed from `ViewSslCertificateDialog` and `PinnedMismatchDialog`.
5287                     sslCertificate = nestedScrollWebView.getCertificate();
5288
5289                     // Check the current website information against any pinned domain information if the current IP addresses have been loaded.
5290                     if (!gettingIpAddresses) {
5291                         checkPinnedMismatch(currentWebView.getDomainSettingsDatabaseId());
5292                     }
5293                 }
5294
5295                 // Reset `urlIsLoading`, which is used to prevent reloads on redirect if the user agent changes.  It is also used to determine when to check for pinned mismatches.
5296                 urlIsLoading = false;
5297             }
5298
5299             // Handle SSL Certificate errors.
5300             @Override
5301             public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
5302                 // Get the current website SSL certificate.
5303                 SslCertificate currentWebsiteSslCertificate = error.getCertificate();
5304
5305                 // Extract the individual pieces of information from the current website SSL certificate.
5306                 String currentWebsiteIssuedToCName = currentWebsiteSslCertificate.getIssuedTo().getCName();
5307                 String currentWebsiteIssuedToOName = currentWebsiteSslCertificate.getIssuedTo().getOName();
5308                 String currentWebsiteIssuedToUName = currentWebsiteSslCertificate.getIssuedTo().getUName();
5309                 String currentWebsiteIssuedByCName = currentWebsiteSslCertificate.getIssuedBy().getCName();
5310                 String currentWebsiteIssuedByOName = currentWebsiteSslCertificate.getIssuedBy().getOName();
5311                 String currentWebsiteIssuedByUName = currentWebsiteSslCertificate.getIssuedBy().getUName();
5312                 Date currentWebsiteSslStartDate = currentWebsiteSslCertificate.getValidNotBeforeDate();
5313                 Date currentWebsiteSslEndDate = currentWebsiteSslCertificate.getValidNotAfterDate();
5314
5315                 // Proceed to the website if the current SSL website certificate matches the pinned domain certificate.
5316                 if (pinnedSslCertificate &&
5317                         currentWebsiteIssuedToCName.equals(pinnedSslIssuedToCName) && currentWebsiteIssuedToOName.equals(pinnedSslIssuedToOName) &&
5318                         currentWebsiteIssuedToUName.equals(pinnedSslIssuedToUName) && currentWebsiteIssuedByCName.equals(pinnedSslIssuedByCName) &&
5319                         currentWebsiteIssuedByOName.equals(pinnedSslIssuedByOName) && currentWebsiteIssuedByUName.equals(pinnedSslIssuedByUName) &&
5320                         currentWebsiteSslStartDate.equals(pinnedSslStartDate) && currentWebsiteSslEndDate.equals(pinnedSslEndDate)) {
5321
5322                     // An SSL certificate is pinned and matches the current domain certificate.  Proceed to the website without displaying an error.
5323                     handler.proceed();
5324                 } else {  // Either there isn't a pinned SSL certificate or it doesn't match the current website certificate.
5325                     // Store `handler` so it can be accesses from `onSslErrorCancel()` and `onSslErrorProceed()`.
5326                     sslErrorHandler = handler;
5327
5328                     // Display the SSL error `AlertDialog`.
5329                     DialogFragment sslCertificateErrorDialogFragment = SslCertificateErrorDialog.displayDialog(error);
5330                     sslCertificateErrorDialogFragment.show(fragmentManager, getString(R.string.ssl_certificate_error));
5331                 }
5332             }
5333         });
5334
5335         // Check to see if this is the first page.
5336         if (pageNumber == 0) {
5337             // Set this nested scroll WebView as the current WebView.
5338             currentWebView = nestedScrollWebView;
5339
5340             // Apply the app settings from the shared preferences.
5341             applyAppSettings();
5342
5343             // Load the website if not waiting for Orbot to connect.
5344             if (!waitingForOrbot) {
5345                 loadUrl(formattedUrlString);
5346             }
5347         }
5348     }
5349 }