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