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