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