]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java
Implement IP Address Pinning. https://redmine.stoutner.com/issues/212
[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                 // Create the open with intent with `ACTION_VIEW`.
2928                 Intent openWithAppIntent = new Intent(Intent.ACTION_VIEW);
2929
2930                 // Set the URI but not the MIME type.  This should open all available apps.
2931                 openWithAppIntent.setData(Uri.parse(formattedUrlString));
2932
2933                 // Flag the intent to open in a new task.
2934                 openWithAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2935
2936                 // Show the chooser.
2937                 startActivity(Intent.createChooser(openWithAppIntent, getString(R.string.open_with)));
2938                 return true;
2939
2940             case R.id.open_with_browser:
2941                 // Create the open with intent with `ACTION_VIEW`.
2942                 Intent openWithBrowserIntent = new Intent(Intent.ACTION_VIEW);
2943
2944                 // Set the URI and the MIME type.  `"text/html"` should load browser options.
2945                 openWithBrowserIntent.setDataAndType(Uri.parse(formattedUrlString), "text/html");
2946
2947                 // Flag the intent to open in a new task.
2948                 openWithBrowserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2949
2950                 // Show the chooser.
2951                 startActivity(Intent.createChooser(openWithBrowserIntent, getString(R.string.open_with)));
2952                 return true;
2953
2954             case R.id.add_to_homescreen:
2955                 // Show the alert dialog.
2956                 AppCompatDialogFragment createHomeScreenShortcutDialogFragment = new CreateHomeScreenShortcutDialog();
2957                 createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getString(R.string.create_shortcut));
2958
2959                 //Everything else will be handled by the alert dialog and the associated listener below.
2960                 return true;
2961
2962             case R.id.proxy_through_orbot:
2963                 // Toggle the proxy through Orbot variable.
2964                 proxyThroughOrbot = !proxyThroughOrbot;
2965
2966                 // Apply the proxy through Orbot settings.
2967                 applyProxyThroughOrbot(true);
2968                 return true;
2969
2970             case R.id.refresh:
2971                 if (menuItem.getTitle().equals(getString(R.string.refresh))) {  // The refresh button was pushed.
2972                     // Reload the WebView.
2973                     mainWebView.reload();
2974                 } else {  // The stop button was pushed.
2975                     // Stop the loading of the WebView.
2976                     mainWebView.stopLoading();
2977                 }
2978                 return true;
2979
2980             case R.id.ad_consent:
2981                 // Display the ad consent dialog.
2982                 DialogFragment adConsentDialogFragment = new AdConsentDialog();
2983                 adConsentDialogFragment.show(getFragmentManager(), getString(R.string.ad_consent));
2984                 return true;
2985
2986             default:
2987                 // Don't consume the event.
2988                 return super.onOptionsItemSelected(menuItem);
2989         }
2990     }
2991
2992     // removeAllCookies is deprecated, but it is required for API < 21.
2993     @SuppressWarnings("deprecation")
2994     @Override
2995     public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
2996         int menuItemId = menuItem.getItemId();
2997
2998         switch (menuItemId) {
2999             case R.id.home:
3000                 loadUrl(homepage);
3001                 break;
3002
3003             case R.id.back:
3004                 if (mainWebView.canGoBack()) {
3005                     // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
3006                     formattedUrlString = "";
3007
3008                     // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
3009                     navigatingHistory = true;
3010
3011                     // Load the previous website in the history.
3012                     mainWebView.goBack();
3013                 }
3014                 break;
3015
3016             case R.id.forward:
3017                 if (mainWebView.canGoForward()) {
3018                     // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
3019                     formattedUrlString = "";
3020
3021                     // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
3022                     navigatingHistory = true;
3023
3024                     // Load the next website in the history.
3025                     mainWebView.goForward();
3026                 }
3027                 break;
3028
3029             case R.id.history:
3030                 // Get the `WebBackForwardList`.
3031                 WebBackForwardList webBackForwardList = mainWebView.copyBackForwardList();
3032
3033                 // Show the `UrlHistoryDialog` `AlertDialog` and name this instance `R.string.history`.  `this` is the `Context`.
3034                 AppCompatDialogFragment urlHistoryDialogFragment = UrlHistoryDialog.loadBackForwardList(this, webBackForwardList);
3035                 urlHistoryDialogFragment.show(getSupportFragmentManager(), getString(R.string.history));
3036                 break;
3037
3038             case R.id.requests:
3039                 // Launch the requests activity.
3040                 Intent requestsIntent = new Intent(this, RequestsActivity.class);
3041                 startActivity(requestsIntent);
3042                 break;
3043
3044             case R.id.downloads:
3045                 // Launch the system Download Manager.
3046                 Intent downloadManagerIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
3047
3048                 // Launch as a new task so that Download Manager and Privacy Browser show as separate windows in the recent tasks list.
3049                 downloadManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
3050
3051                 startActivity(downloadManagerIntent);
3052                 break;
3053
3054             case R.id.domains:
3055                 // Set the flag to reapply the domain settings on restart when returning from Domain Settings.
3056                 reapplyDomainSettingsOnRestart = true;
3057                 currentDomainName = "";
3058
3059                 // Launch the domains activity.
3060                 Intent domainsIntent = new Intent(this, DomainsActivity.class);
3061                 startActivity(domainsIntent);
3062                 break;
3063
3064             case R.id.settings:
3065                 // Set the flag to reapply app settings on restart when returning from Settings.
3066                 reapplyAppSettingsOnRestart = true;
3067
3068                 // Set the flag to reapply the domain settings on restart when returning from Settings.
3069                 reapplyDomainSettingsOnRestart = true;
3070                 currentDomainName = "";
3071
3072                 // Launch the settings activity.
3073                 Intent settingsIntent = new Intent(this, SettingsActivity.class);
3074                 startActivity(settingsIntent);
3075                 break;
3076
3077             case R.id.import_export:
3078                 // Launch the import/export activity.
3079                 Intent importExportIntent = new Intent (this, ImportExportActivity.class);
3080                 startActivity(importExportIntent);
3081                 break;
3082
3083             case R.id.guide:
3084                 // Launch `GuideActivity`.
3085                 Intent guideIntent = new Intent(this, GuideActivity.class);
3086                 startActivity(guideIntent);
3087                 break;
3088
3089             case R.id.about:
3090                 // Launch `AboutActivity`.
3091                 Intent aboutIntent = new Intent(this, AboutActivity.class);
3092                 startActivity(aboutIntent);
3093                 break;
3094
3095             case R.id.clearAndExit:
3096                 // Close the bookmarks cursor and database.
3097                 bookmarksCursor.close();
3098                 bookmarksDatabaseHelper.close();
3099
3100                 // Get a handle for `sharedPreferences`.  `this` references the current context.
3101                 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3102
3103                 boolean clearEverything = sharedPreferences.getBoolean("clear_everything", true);
3104
3105                 // Clear cookies.
3106                 if (clearEverything || sharedPreferences.getBoolean("clear_cookies", true)) {
3107                     // The command to remove cookies changed slightly in API 21.
3108                     if (Build.VERSION.SDK_INT >= 21) {
3109                         cookieManager.removeAllCookies(null);
3110                     } else {
3111                         cookieManager.removeAllCookie();
3112                     }
3113
3114                     // Manually delete the cookies database, as `CookieManager` sometimes will not flush its changes to disk before `System.exit(0)` is run.
3115                     try {
3116                         // We have to use two commands because `Runtime.exec()` does not like `*`.
3117                         privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies");
3118                         privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies-journal");
3119                     } catch (IOException e) {
3120                         // Do nothing if an error is thrown.
3121                     }
3122                 }
3123
3124                 // Clear DOM storage.
3125                 if (clearEverything || sharedPreferences.getBoolean("clear_dom_storage", true)) {
3126                     // Ask `WebStorage` to clear the DOM storage.
3127                     WebStorage webStorage = WebStorage.getInstance();
3128                     webStorage.deleteAllData();
3129
3130                     // Manually delete the DOM storage files and directories, as `WebStorage` sometimes will not flush its changes to disk before `System.exit(0)` is run.
3131                     try {
3132                         // A `String[]` must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
3133                         privacyBrowserRuntime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
3134
3135                         // Multiple commands must be used because `Runtime.exec()` does not like `*`.
3136                         privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
3137                         privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
3138                         privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
3139                         privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
3140                     } catch (IOException e) {
3141                         // Do nothing if an error is thrown.
3142                     }
3143                 }
3144
3145                 // Clear form data if the API < 26.
3146                 if ((Build.VERSION.SDK_INT < 26) && (clearEverything || sharedPreferences.getBoolean("clear_form_data", true))) {
3147                     WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
3148                     webViewDatabase.clearFormData();
3149
3150                     // Manually delete the form data database, as `WebViewDatabase` sometimes will not flush its changes to disk before `System.exit(0)` is run.
3151                     try {
3152                         // We have to use a `String[]` because the database contains a space and `Runtime.exec` will not escape the string correctly otherwise.
3153                         privacyBrowserRuntime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data"});
3154                         privacyBrowserRuntime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data-journal"});
3155                     } catch (IOException e) {
3156                         // Do nothing if an error is thrown.
3157                     }
3158                 }
3159
3160                 // Clear the cache.
3161                 if (clearEverything || sharedPreferences.getBoolean("clear_cache", true)) {
3162                     // `true` includes disk files.
3163                     mainWebView.clearCache(true);
3164
3165                     // Manually delete the cache directories.
3166                     try {
3167                         // Delete the main cache directory.
3168                         privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/cache");
3169
3170                         // Delete the secondary `Service Worker` cache directory.
3171                         // A `String[]` must be used because the directory contains a space and `Runtime.exec` will not escape the string correctly otherwise.
3172                         privacyBrowserRuntime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
3173                     } catch (IOException e) {
3174                         // Do nothing if an error is thrown.
3175                     }
3176                 }
3177
3178                 // Clear SSL certificate preferences.
3179                 mainWebView.clearSslPreferences();
3180
3181                 // Clear the back/forward history.
3182                 mainWebView.clearHistory();
3183
3184                 // Clear `formattedUrlString`.
3185                 formattedUrlString = null;
3186
3187                 // Clear `customHeaders`.
3188                 customHeaders.clear();
3189
3190                 // Detach all views from `mainWebViewRelativeLayout`.
3191                 mainWebViewRelativeLayout.removeAllViews();
3192
3193                 // Destroy the internal state of `mainWebView`.
3194                 mainWebView.destroy();
3195
3196                 // Manually delete the `app_webview` folder, which contains the cookies, DOM storage, form data, and `Service Worker` cache.
3197                 // See `https://code.google.com/p/android/issues/detail?id=233826&thanks=233826&ts=1486670530`.
3198                 if (clearEverything) {
3199                     try {
3200                         privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview");
3201                     } catch (IOException e) {
3202                         // Do nothing if an error is thrown.
3203                     }
3204                 }
3205
3206                 // Close Privacy Browser.  `finishAndRemoveTask` also removes Privacy Browser from the recent app list.
3207                 if (Build.VERSION.SDK_INT >= 21) {
3208                     finishAndRemoveTask();
3209                 } else {
3210                     finish();
3211                 }
3212
3213                 // Remove the terminated program from RAM.  The status code is `0`.
3214                 System.exit(0);
3215                 break;
3216         }
3217
3218         // Close the navigation drawer.
3219         drawerLayout.closeDrawer(GravityCompat.START);
3220         return true;
3221     }
3222
3223     @Override
3224     public void onPostCreate(Bundle savedInstanceState) {
3225         super.onPostCreate(savedInstanceState);
3226
3227         // Sync the state of the DrawerToggle after onRestoreInstanceState has finished.
3228         drawerToggle.syncState();
3229     }
3230
3231     @Override
3232     public void onConfigurationChanged(Configuration newConfig) {
3233         super.onConfigurationChanged(newConfig);
3234
3235         // Get the status bar pixel size.
3236         int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
3237         int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId);
3238
3239         // Get the resource density.
3240         float screenDensity = getResources().getDisplayMetrics().density;
3241
3242         // Recalculate the drawer header padding.
3243         drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity);
3244         drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity);
3245         drawerHeaderPaddingBottom = (int) (8 * screenDensity);
3246
3247         // Reload the ad for the free flavor if not in full screen mode.
3248         if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
3249             // Reload the ad.  The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
3250             AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
3251         }
3252
3253         // `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:
3254         // https://code.google.com/p/android/issues/detail?id=20493#c8
3255         // ActivityCompat.invalidateOptionsMenu(this);
3256     }
3257
3258     @Override
3259     public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
3260         // Store the `HitTestResult`.
3261         final WebView.HitTestResult hitTestResult = mainWebView.getHitTestResult();
3262
3263         // Create strings.
3264         final String imageUrl;
3265         final String linkUrl;
3266
3267         // Get a handle for the `ClipboardManager`.
3268         final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
3269
3270         // Remove the lint errors below that `clipboardManager` might be `null`.
3271         assert clipboardManager != null;
3272
3273         switch (hitTestResult.getType()) {
3274             // `SRC_ANCHOR_TYPE` is a link.
3275             case WebView.HitTestResult.SRC_ANCHOR_TYPE:
3276                 // Get the target URL.
3277                 linkUrl = hitTestResult.getExtra();
3278
3279                 // Set the target URL as the title of the `ContextMenu`.
3280                 menu.setHeaderTitle(linkUrl);
3281
3282                 // Add a Load URL entry.
3283                 menu.add(R.string.load_url).setOnMenuItemClickListener((MenuItem item) -> {
3284                     loadUrl(linkUrl);
3285                     return false;
3286                 });
3287
3288                 // Add a Copy URL entry.
3289                 menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
3290                     // Save the link URL in a `ClipData`.
3291                     ClipData srcAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl);
3292
3293                     // Set the `ClipData` as the clipboard's primary clip.
3294                     clipboardManager.setPrimaryClip(srcAnchorTypeClipData);
3295                     return false;
3296                 });
3297
3298                 // Add a Download URL entry.
3299                 menu.add(R.string.download_url).setOnMenuItemClickListener((MenuItem item) -> {
3300                     // Check if the download should be processed by an external app.
3301                     if (downloadWithExternalApp) {  // Download with an external app.
3302                         openUrlWithExternalApp(linkUrl);
3303                     } else {  // Download with Android's download manager.
3304                         // Check to see if the storage permission has already been granted.
3305                         if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {  // The storage permission needs to be requested.
3306                             // Store the variables for future use by `onRequestPermissionsResult()`.
3307                             downloadUrl = linkUrl;
3308                             downloadContentDisposition = "none";
3309                             downloadContentLength = -1;
3310
3311                             // Show a dialog if the user has previously denied the permission.
3312                             if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
3313                                 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
3314                                 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE);
3315
3316                                 // Show the download location permission alert dialog.  The permission will be requested when the the dialog is closed.
3317                                 downloadLocationPermissionDialogFragment.show(getFragmentManager(), getString(R.string.download_location));
3318                             } else {  // Show the permission request directly.
3319                                 // Request the permission.  The download dialog will be launched by `onRequestPermissionResult()`.
3320                                 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
3321                             }
3322                         } else {  // The storage permission has already been granted.
3323                             // Get a handle for the download file alert dialog.
3324                             AppCompatDialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(linkUrl, "none", -1);
3325
3326                             // Show the download file alert dialog.
3327                             downloadFileDialogFragment.show(getSupportFragmentManager(), getString(R.string.download));
3328                         }
3329                     }
3330                     return false;
3331                 });
3332
3333                 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
3334                 menu.add(R.string.cancel);
3335                 break;
3336
3337             case WebView.HitTestResult.EMAIL_TYPE:
3338                 // Get the target URL.
3339                 linkUrl = hitTestResult.getExtra();
3340
3341                 // Set the target URL as the title of the `ContextMenu`.
3342                 menu.setHeaderTitle(linkUrl);
3343
3344                 // Add a `Write Email` entry.
3345                 menu.add(R.string.write_email).setOnMenuItemClickListener(item -> {
3346                     // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
3347                     Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
3348
3349                     // Parse the url and set it as the data for the `Intent`.
3350                     emailIntent.setData(Uri.parse("mailto:" + linkUrl));
3351
3352                     // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser.
3353                     emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
3354
3355                     // Make it so.
3356                     startActivity(emailIntent);
3357                     return false;
3358                 });
3359
3360                 // Add a `Copy Email Address` entry.
3361                 menu.add(R.string.copy_email_address).setOnMenuItemClickListener(item -> {
3362                     // Save the email address in a `ClipData`.
3363                     ClipData srcEmailTypeClipData = ClipData.newPlainText(getString(R.string.email_address), linkUrl);
3364
3365                     // Set the `ClipData` as the clipboard's primary clip.
3366                     clipboardManager.setPrimaryClip(srcEmailTypeClipData);
3367                     return false;
3368                 });
3369
3370                 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
3371                 menu.add(R.string.cancel);
3372                 break;
3373
3374             // `IMAGE_TYPE` is an image.
3375             case WebView.HitTestResult.IMAGE_TYPE:
3376                 // Get the image URL.
3377                 imageUrl = hitTestResult.getExtra();
3378
3379                 // Set the image URL as the title of the `ContextMenu`.
3380                 menu.setHeaderTitle(imageUrl);
3381
3382                 // Add a `View Image` entry.
3383                 menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
3384                     loadUrl(imageUrl);
3385                     return false;
3386                 });
3387
3388                 // Add a `Download Image` entry.
3389                 menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
3390                     // Check if the download should be processed by an external app.
3391                     if (downloadWithExternalApp) {  // Download with an external app.
3392                         openUrlWithExternalApp(imageUrl);
3393                     } else {  // Download with Android's download manager.
3394                         // Check to see if the storage permission has already been granted.
3395                         if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {  // The storage permission needs to be requested.
3396                             // Store the image URL for use by `onRequestPermissionResult()`.
3397                             downloadImageUrl = imageUrl;
3398
3399                             // Show a dialog if the user has previously denied the permission.
3400                             if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
3401                                 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
3402                                 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
3403
3404                                 // Show the download location permission alert dialog.  The permission will be requested when the dialog is closed.
3405                                 downloadLocationPermissionDialogFragment.show(getFragmentManager(), getString(R.string.download_location));
3406                             } else {  // Show the permission request directly.
3407                                 // Request the permission.  The download dialog will be launched by `onRequestPermissionResult().
3408                                 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
3409                             }
3410                         } else {  // The storage permission has already been granted.
3411                             // Get a handle for the download image alert dialog.
3412                             AppCompatDialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
3413
3414                             // Show the download image alert dialog.
3415                             downloadImageDialogFragment.show(getSupportFragmentManager(), getString(R.string.download));
3416                         }
3417                     }
3418                     return false;
3419                 });
3420
3421                 // Add a `Copy URL` entry.
3422                 menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> {
3423                     // Save the image URL in a `ClipData`.
3424                     ClipData srcImageTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
3425
3426                     // Set the `ClipData` as the clipboard's primary clip.
3427                     clipboardManager.setPrimaryClip(srcImageTypeClipData);
3428                     return false;
3429                 });
3430
3431                 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
3432                 menu.add(R.string.cancel);
3433                 break;
3434
3435
3436             // `SRC_IMAGE_ANCHOR_TYPE` is an image that is also a link.
3437             case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
3438                 // Get the image URL.
3439                 imageUrl = hitTestResult.getExtra();
3440
3441                 // Set the image URL as the title of the `ContextMenu`.
3442                 menu.setHeaderTitle(imageUrl);
3443
3444                 // Add a `View Image` entry.
3445                 menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
3446                     loadUrl(imageUrl);
3447                     return false;
3448                 });
3449
3450                 // Add a `Download Image` entry.
3451                 menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
3452                     // Check if the download should be processed by an external app.
3453                     if (downloadWithExternalApp) {  // Download with an external app.
3454                         openUrlWithExternalApp(imageUrl);
3455                     } else {  // Download with Android's download manager.
3456                         // Check to see if the storage permission has already been granted.
3457                         if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {  // The storage permission needs to be requested.
3458                             // Store the image URL for use by `onRequestPermissionResult()`.
3459                             downloadImageUrl = imageUrl;
3460
3461                             // Show a dialog if the user has previously denied the permission.
3462                             if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
3463                                 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
3464                                 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
3465
3466                                 // Show the download location permission alert dialog.  The permission will be requested when the dialog is closed.
3467                                 downloadLocationPermissionDialogFragment.show(getFragmentManager(), getString(R.string.download_location));
3468                             } else {  // Show the permission request directly.
3469                                 // Request the permission.  The download dialog will be launched by `onRequestPermissionResult().
3470                                 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
3471                             }
3472                         } else {  // The storage permission has already been granted.
3473                             // Get a handle for the download image alert dialog.
3474                             AppCompatDialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
3475
3476                             // Show the download image alert dialog.
3477                             downloadImageDialogFragment.show(getSupportFragmentManager(), getString(R.string.download));
3478                         }
3479                     }
3480                     return false;
3481                 });
3482
3483                 // Add a `Copy URL` entry.
3484                 menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> {
3485                     // Save the image URL in a `ClipData`.
3486                     ClipData srcImageAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
3487
3488                     // Set the `ClipData` as the clipboard's primary clip.
3489                     clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData);
3490                     return false;
3491                 });
3492
3493                 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
3494                 menu.add(R.string.cancel);
3495                 break;
3496         }
3497     }
3498
3499     @Override
3500     public void onCreateBookmark(AppCompatDialogFragment dialogFragment) {
3501         // Get the `EditTexts` from the `dialogFragment`.
3502         EditText createBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_name_edittext);
3503         EditText createBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_url_edittext);
3504
3505         // Extract the strings from the `EditTexts`.
3506         String bookmarkNameString = createBookmarkNameEditText.getText().toString();
3507         String bookmarkUrlString = createBookmarkUrlEditText.getText().toString();
3508
3509         // Convert the favoriteIcon Bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
3510         ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
3511         favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
3512         byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
3513
3514         // Display the new bookmark below the current items in the (0 indexed) list.
3515         int newBookmarkDisplayOrder = bookmarksListView.getCount();
3516
3517         // Create the bookmark.
3518         bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolder, newBookmarkDisplayOrder, favoriteIconByteArray);
3519
3520         // Update the bookmarks cursor with the current contents of this folder.
3521         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
3522
3523         // Update the `ListView`.
3524         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
3525
3526         // Scroll to the new bookmark.
3527         bookmarksListView.setSelection(newBookmarkDisplayOrder);
3528     }
3529
3530     @Override
3531     public void onCreateBookmarkFolder(AppCompatDialogFragment dialogFragment) {
3532         // Get handles for the views in `dialogFragment`.
3533         EditText createFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.create_folder_name_edittext);
3534         RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon_radiobutton);
3535         ImageView folderIconImageView = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon);
3536
3537         // Get new folder name string.
3538         String folderNameString = createFolderNameEditText.getText().toString();
3539
3540         // Get the new folder icon `Bitmap`.
3541         Bitmap folderIconBitmap;
3542         if (defaultFolderIconRadioButton.isChecked()) {  // Use the default folder icon.
3543             // Get the default folder icon and convert it to a `Bitmap`.
3544             Drawable folderIconDrawable = folderIconImageView.getDrawable();
3545             BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
3546             folderIconBitmap = folderIconBitmapDrawable.getBitmap();
3547         } else {  // Use the `WebView` favorite icon.
3548             folderIconBitmap = favoriteIconBitmap;
3549         }
3550
3551         // Convert `folderIconBitmap` to a byte array.  `0` is for lossless compression (the only option for a PNG).
3552         ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream();
3553         folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream);
3554         byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray();
3555
3556         // Move all the bookmarks down one in the display order.
3557         for (int i = 0; i < bookmarksListView.getCount(); i++) {
3558             int databaseId = (int) bookmarksListView.getItemIdAtPosition(i);
3559             bookmarksDatabaseHelper.updateDisplayOrder(databaseId, i + 1);
3560         }
3561
3562         // Create the folder, which will be placed at the top of the `ListView`.
3563         bookmarksDatabaseHelper.createFolder(folderNameString, currentBookmarksFolder, folderIconByteArray);
3564
3565         // Update the bookmarks cursor with the current contents of this folder.
3566         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
3567
3568         // Update the `ListView`.
3569         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
3570
3571         // Scroll to the new folder.
3572         bookmarksListView.setSelection(0);
3573     }
3574
3575     @Override
3576     public void onCreateHomeScreenShortcut(AppCompatDialogFragment dialogFragment) {
3577         // Get the shortcut name.
3578         EditText shortcutNameEditText = dialogFragment.getDialog().findViewById(R.id.shortcut_name_edittext);
3579         String shortcutNameString = shortcutNameEditText.getText().toString();
3580
3581         // Convert the favorite icon bitmap to an `Icon`.  `IconCompat` is required until API >= 26.
3582         IconCompat favoriteIcon = IconCompat.createWithBitmap(favoriteIconBitmap);
3583
3584         // Setup the shortcut intent.
3585         Intent shortcutIntent = new Intent(Intent.ACTION_VIEW);
3586         shortcutIntent.setData(Uri.parse(formattedUrlString));
3587
3588         // Create a shortcut info builder.  The shortcut name becomes the shortcut ID.
3589         ShortcutInfoCompat.Builder shortcutInfoBuilder = new ShortcutInfoCompat.Builder(this, shortcutNameString);
3590
3591         // Add the required fields to the shortcut info builder.
3592         shortcutInfoBuilder.setIcon(favoriteIcon);
3593         shortcutInfoBuilder.setIntent(shortcutIntent);
3594         shortcutInfoBuilder.setShortLabel(shortcutNameString);
3595
3596         // Request the pin.  `ShortcutManagerCompat` can be switched to `ShortcutManager` once API >= 26.
3597         ShortcutManagerCompat.requestPinShortcut(this, shortcutInfoBuilder.build(), null);
3598     }
3599
3600     @Override
3601     public void onCloseDownloadLocationPermissionDialog(int downloadType) {
3602         switch (downloadType) {
3603             case DownloadLocationPermissionDialog.DOWNLOAD_FILE:
3604                 // Request the WRITE_EXTERNAL_STORAGE permission with a file request code.
3605                 ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
3606                 break;
3607
3608             case DownloadLocationPermissionDialog.DOWNLOAD_IMAGE:
3609                 // Request the WRITE_EXTERNAL_STORAGE permission with an image request code.
3610                 ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
3611                 break;
3612         }
3613     }
3614
3615     @Override
3616     public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
3617         switch (requestCode) {
3618             case DOWNLOAD_FILE_REQUEST_CODE:
3619                 // Show the download file alert dialog.  When the dialog closes, the correct command will be used based on the permission status.
3620                 AppCompatDialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(downloadUrl, downloadContentDisposition, downloadContentLength);
3621
3622                 // On API 23, displaying the fragment must be delayed or the app will crash.
3623                 if (Build.VERSION.SDK_INT == 23) {
3624                     new Handler().postDelayed(() -> downloadFileDialogFragment.show(getSupportFragmentManager(), getString(R.string.download)), 500);
3625                 } else {
3626                     downloadFileDialogFragment.show(getSupportFragmentManager(), getString(R.string.download));
3627                 }
3628
3629                 // Reset the download variables.
3630                 downloadUrl = "";
3631                 downloadContentDisposition = "";
3632                 downloadContentLength = 0;
3633                 break;
3634
3635             case DOWNLOAD_IMAGE_REQUEST_CODE:
3636                 // Show the download image alert dialog.  When the dialog closes, the correct command will be used based on the permission status.
3637                 AppCompatDialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(downloadImageUrl);
3638
3639                 // On API 23, displaying the fragment must be delayed or the app will crash.
3640                 if (Build.VERSION.SDK_INT == 23) {
3641                     new Handler().postDelayed(() -> downloadImageDialogFragment.show(getSupportFragmentManager(), getString(R.string.download)), 500);
3642                 } else {
3643                     downloadImageDialogFragment.show(getSupportFragmentManager(), getString(R.string.download));
3644                 }
3645
3646                 // Reset the image URL variable.
3647                 downloadImageUrl = "";
3648                 break;
3649         }
3650     }
3651
3652     @Override
3653     public void onDownloadImage(AppCompatDialogFragment dialogFragment, String imageUrl) {
3654         // Download the image if it has an HTTP or HTTPS URI.
3655         if (imageUrl.startsWith("http")) {
3656             // Get a handle for the system `DOWNLOAD_SERVICE`.
3657             DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
3658
3659             // Parse `imageUrl`.
3660             DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(imageUrl));
3661
3662             // Pass cookies to download manager if cookies are enabled.  This is required to download images from websites that require a login.
3663             // Code contributed 2017 Hendrik Knackstedt.  Copyright assigned to Soren Stoutner <soren@stoutner.com>.
3664             if (firstPartyCookiesEnabled) {
3665                 // Get the cookies for `imageUrl`.
3666                 String cookies = cookieManager.getCookie(imageUrl);
3667
3668                 // Add the cookies to `downloadRequest`.  In the HTTP request header, cookies are named `Cookie`.
3669                 downloadRequest.addRequestHeader("Cookie", cookies);
3670             }
3671
3672             // Get the file name from the dialog fragment.
3673             EditText downloadImageNameEditText = dialogFragment.getDialog().findViewById(R.id.download_image_name);
3674             String imageName = downloadImageNameEditText.getText().toString();
3675
3676             // Specify the download location.
3677             if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {  // External write permission granted.
3678                 // Download to the public download directory.
3679                 downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, imageName);
3680             } else {  // External write permission denied.
3681                 // Download to the app's external download directory.
3682                 downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, imageName);
3683             }
3684
3685             // Allow `MediaScanner` to index the download if it is a media file.
3686             downloadRequest.allowScanningByMediaScanner();
3687
3688             // Add the URL as the description for the download.
3689             downloadRequest.setDescription(imageUrl);
3690
3691             // Show the download notification after the download is completed.
3692             downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
3693
3694             // Remove the lint warning below that `downloadManager` might be `null`.
3695             assert downloadManager != null;
3696
3697             // Initiate the download.
3698             downloadManager.enqueue(downloadRequest);
3699         } else {  // The image is not an HTTP or HTTPS URI.
3700             Snackbar.make(mainWebView, R.string.cannot_download_image, Snackbar.LENGTH_INDEFINITE).show();
3701         }
3702     }
3703
3704     @Override
3705     public void onDownloadFile(AppCompatDialogFragment dialogFragment, String downloadUrl) {
3706         // Download the file if it has an HTTP or HTTPS URI.
3707         if (downloadUrl.startsWith("http")) {
3708             // Get a handle for the system `DOWNLOAD_SERVICE`.
3709             DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
3710
3711             // Parse `downloadUrl`.
3712             DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(downloadUrl));
3713
3714             // Pass cookies to download manager if cookies are enabled.  This is required to download files from websites that require a login.
3715             // Code contributed 2017 Hendrik Knackstedt.  Copyright assigned to Soren Stoutner <soren@stoutner.com>.
3716             if (firstPartyCookiesEnabled) {
3717                 // Get the cookies for `downloadUrl`.
3718                 String cookies = cookieManager.getCookie(downloadUrl);
3719
3720                 // Add the cookies to `downloadRequest`.  In the HTTP request header, cookies are named `Cookie`.
3721                 downloadRequest.addRequestHeader("Cookie", cookies);
3722             }
3723
3724             // Get the file name from the dialog fragment.
3725             EditText downloadFileNameEditText = dialogFragment.getDialog().findViewById(R.id.download_file_name);
3726             String fileName = downloadFileNameEditText.getText().toString();
3727
3728             // Specify the download location.
3729             if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {  // External write permission granted.
3730                 // Download to the public download directory.
3731                 downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
3732             } else {  // External write permission denied.
3733                 // Download to the app's external download directory.
3734                 downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, fileName);
3735             }
3736
3737             // Allow `MediaScanner` to index the download if it is a media file.
3738             downloadRequest.allowScanningByMediaScanner();
3739
3740             // Add the URL as the description for the download.
3741             downloadRequest.setDescription(downloadUrl);
3742
3743             // Show the download notification after the download is completed.
3744             downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
3745
3746             // Remove the lint warning below that `downloadManager` might be `null`.
3747             assert downloadManager != null;
3748
3749             // Initiate the download.
3750             downloadManager.enqueue(downloadRequest);
3751         } else {  // The download is not an HTTP or HTTPS URI.
3752             Snackbar.make(mainWebView, R.string.cannot_download_file, Snackbar.LENGTH_INDEFINITE).show();
3753         }
3754     }
3755
3756     @Override
3757     public void onSaveBookmark(AppCompatDialogFragment dialogFragment, int selectedBookmarkDatabaseId) {
3758         // Get handles for the views from `dialogFragment`.
3759         EditText editBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_name_edittext);
3760         EditText editBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_url_edittext);
3761         RadioButton currentBookmarkIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_current_icon_radiobutton);
3762
3763         // Store the bookmark strings.
3764         String bookmarkNameString = editBookmarkNameEditText.getText().toString();
3765         String bookmarkUrlString = editBookmarkUrlEditText.getText().toString();
3766
3767         // Update the bookmark.
3768         if (currentBookmarkIconRadioButton.isChecked()) {  // Update the bookmark without changing the favorite icon.
3769             bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString);
3770         } else {  // Update the bookmark using the `WebView` favorite icon.
3771             // Convert the favorite icon to a byte array.  `0` is for lossless compression (the only option for a PNG).
3772             ByteArrayOutputStream newFavoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
3773             favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFavoriteIconByteArrayOutputStream);
3774             byte[] newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray();
3775
3776             //  Update the bookmark and the favorite icon.
3777             bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, newFavoriteIconByteArray);
3778         }
3779
3780         // Update the bookmarks cursor with the current contents of this folder.
3781         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
3782
3783         // Update the `ListView`.
3784         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
3785     }
3786
3787     @Override
3788     public void onSaveBookmarkFolder(AppCompatDialogFragment dialogFragment, int selectedFolderDatabaseId) {
3789         // Get handles for the views from `dialogFragment`.
3790         EditText editFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_folder_name_edittext);
3791         RadioButton currentFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_current_icon_radiobutton);
3792         RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_radiobutton);
3793         ImageView folderIconImageView = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_imageview);
3794
3795         // Get the new folder name.
3796         String newFolderNameString = editFolderNameEditText.getText().toString();
3797
3798         // Check if the favorite icon has changed.
3799         if (currentFolderIconRadioButton.isChecked()) {  // Only the name has changed.
3800             // Update the name in the database.
3801             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString);
3802         } else if (!currentFolderIconRadioButton.isChecked() && newFolderNameString.equals(oldFolderNameString)) {  // Only the icon has changed.
3803             // Get the new folder icon `Bitmap`.
3804             Bitmap folderIconBitmap;
3805             if (defaultFolderIconRadioButton.isChecked()) {
3806                 // Get the default folder icon and convert it to a `Bitmap`.
3807                 Drawable folderIconDrawable = folderIconImageView.getDrawable();
3808                 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
3809                 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
3810             } else {  // Use the `WebView` favorite icon.
3811                 folderIconBitmap = favoriteIconBitmap;
3812             }
3813
3814             // Convert the folder `Bitmap` to a byte array.  `0` is for lossless compression (the only option for a PNG).
3815             ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream();
3816             folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream);
3817             byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray();
3818
3819             // Update the folder icon in the database.
3820             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, folderIconByteArray);
3821         } else {  // The folder icon and the name have changed.
3822             // Get the new folder icon `Bitmap`.
3823             Bitmap folderIconBitmap;
3824             if (defaultFolderIconRadioButton.isChecked()) {
3825                 // Get the default folder icon and convert it to a `Bitmap`.
3826                 Drawable folderIconDrawable = folderIconImageView.getDrawable();
3827                 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
3828                 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
3829             } else {  // Use the `WebView` favorite icon.
3830                 folderIconBitmap = MainWebViewActivity.favoriteIconBitmap;
3831             }
3832
3833             // Convert the folder `Bitmap` to a byte array.  `0` is for lossless compression (the only option for a PNG).
3834             ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream();
3835             folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream);
3836             byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray();
3837
3838             // Update the folder name and icon in the database.
3839             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, folderIconByteArray);
3840         }
3841
3842         // Update the bookmarks cursor with the current contents of this folder.
3843         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
3844
3845         // Update the `ListView`.
3846         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
3847     }
3848
3849     @Override
3850     public void onHttpAuthenticationCancel() {
3851         // Cancel the `HttpAuthHandler`.
3852         httpAuthHandler.cancel();
3853     }
3854
3855     @Override
3856     public void onHttpAuthenticationProceed(AppCompatDialogFragment dialogFragment) {
3857         // Get handles for the `EditTexts`.
3858         EditText usernameEditText = dialogFragment.getDialog().findViewById(R.id.http_authentication_username);
3859         EditText passwordEditText = dialogFragment.getDialog().findViewById(R.id.http_authentication_password);
3860
3861         // Proceed with the HTTP authentication.
3862         httpAuthHandler.proceed(usernameEditText.getText().toString(), passwordEditText.getText().toString());
3863     }
3864
3865     public void viewSslCertificate(View view) {
3866         // Show the `ViewSslCertificateDialog` `AlertDialog` and name this instance `@string/view_ssl_certificate`.
3867         DialogFragment viewSslCertificateDialogFragment = new ViewSslCertificateDialog();
3868         viewSslCertificateDialogFragment.show(getFragmentManager(), getString(R.string.view_ssl_certificate));
3869     }
3870
3871     @Override
3872     public void onSslErrorCancel() {
3873         sslErrorHandler.cancel();
3874     }
3875
3876     @Override
3877     public void onSslErrorProceed() {
3878         sslErrorHandler.proceed();
3879     }
3880
3881     @Override
3882     public void onPinnedMismatchBack() {
3883         if (mainWebView.canGoBack()) {  // There is a back page in the history.
3884             // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
3885             formattedUrlString = "";
3886
3887             // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
3888             navigatingHistory = true;
3889
3890             // Go back.
3891             mainWebView.goBack();
3892         } else {  // There are no pages to go back to.
3893             // Load a blank page
3894             loadUrl("");
3895         }
3896     }
3897
3898     @Override
3899     public void onPinnedMismatchProceed() {
3900         // Do not check the pinned information for this domain again until the domain changes.
3901         ignorePinnedDomainInformation = true;
3902     }
3903
3904     @Override
3905     public void onUrlHistoryEntrySelected(int moveBackOrForwardSteps) {
3906         // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
3907         formattedUrlString = "";
3908
3909         // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
3910         navigatingHistory = true;
3911
3912         // Load the history entry.
3913         mainWebView.goBackOrForward(moveBackOrForwardSteps);
3914     }
3915
3916     @Override
3917     public void onClearHistory() {
3918         // Clear the history.
3919         mainWebView.clearHistory();
3920     }
3921
3922     // Override `onBackPressed` to handle the navigation drawer and `mainWebView`.
3923     @Override
3924     public void onBackPressed() {
3925         if (drawerLayout.isDrawerVisible(GravityCompat.START)) {  // The navigation drawer is open.
3926             // Close the navigation drawer.
3927             drawerLayout.closeDrawer(GravityCompat.START);
3928         } else if (drawerLayout.isDrawerVisible(GravityCompat.END)){  // The bookmarks drawer is open.
3929             if (currentBookmarksFolder.isEmpty()) {  // The home folder is displayed.
3930                 // close the bookmarks drawer.
3931                 drawerLayout.closeDrawer(GravityCompat.END);
3932             } else {  // A subfolder is displayed.
3933                 // Place the former parent folder in `currentFolder`.
3934                 currentBookmarksFolder = bookmarksDatabaseHelper.getParentFolderName(currentBookmarksFolder);
3935
3936                 // Load the new folder.
3937                 loadBookmarksFolder();
3938             }
3939
3940         } else if (mainWebView.canGoBack()) {  // There is at least one item in the `WebView` history.
3941             // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
3942             formattedUrlString = "";
3943
3944             // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
3945             navigatingHistory = true;
3946
3947             // Go back.
3948             mainWebView.goBack();
3949         } else {  // There isn't anything to do in Privacy Browser.
3950             // Pass `onBackPressed()` to the system.
3951             super.onBackPressed();
3952         }
3953     }
3954
3955     // 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.
3956     @Override
3957     public void onActivityResult(int requestCode, int resultCode, Intent data) {
3958         // File uploads only work on API >= 21.
3959         if (Build.VERSION.SDK_INT >= 21) {
3960             // Pass the file to the WebView.
3961             fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, data));
3962         }
3963     }
3964
3965     private void loadUrlFromTextBox() {
3966         // Get the text from urlTextBox and convert it to a string.  trim() removes white spaces from the beginning and end of the string.
3967         String unformattedUrlString = urlTextBox.getText().toString().trim();
3968
3969         // Check to see if `unformattedUrlString` is a valid URL.  Otherwise, convert it into a search.
3970         if (unformattedUrlString.startsWith("content://")) {
3971             // Load the entire content URL.
3972             formattedUrlString = unformattedUrlString;
3973         } else if (Patterns.WEB_URL.matcher(unformattedUrlString).matches() || unformattedUrlString.startsWith("http://") || unformattedUrlString.startsWith("https://")
3974                 || unformattedUrlString.startsWith("file://")) {
3975             // Add `https://` at the beginning if there is no protocol.  Otherwise the app will segfault.
3976             if (!unformattedUrlString.startsWith("http") && !unformattedUrlString.startsWith("file://") && !unformattedUrlString.startsWith("content://")) {
3977                 unformattedUrlString = "https://" + unformattedUrlString;
3978             }
3979
3980             // Initialize `unformattedUrl`.
3981             URL unformattedUrl = null;
3982
3983             // 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.
3984             try {
3985                 unformattedUrl = new URL(unformattedUrlString);
3986             } catch (MalformedURLException e) {
3987                 e.printStackTrace();
3988             }
3989
3990             // The ternary operator (? :) makes sure that a null pointer exception is not thrown, which would happen if `.get` was called on a `null` value.
3991             String scheme = unformattedUrl != null ? unformattedUrl.getProtocol() : null;
3992             String authority = unformattedUrl != null ? unformattedUrl.getAuthority() : null;
3993             String path = unformattedUrl != null ? unformattedUrl.getPath() : null;
3994             String query = unformattedUrl != null ? unformattedUrl.getQuery() : null;
3995             String fragment = unformattedUrl != null ? unformattedUrl.getRef() : null;
3996
3997             // Build the URI.
3998             Uri.Builder formattedUri = new Uri.Builder();
3999             formattedUri.scheme(scheme).authority(authority).path(path).query(query).fragment(fragment);
4000
4001             // Decode `formattedUri` as a `String` in `UTF-8`.
4002             try {
4003                 formattedUrlString = URLDecoder.decode(formattedUri.build().toString(), "UTF-8");
4004             } catch (UnsupportedEncodingException exception) {
4005                 // Load a blank string.
4006                 formattedUrlString = "";
4007             }
4008         } else if (unformattedUrlString.isEmpty()){  // Load a blank web site.
4009             // Load a blank string.
4010             formattedUrlString = "";
4011         } else {  // Search for the contents of the URL box.
4012             // Create an encoded URL String.
4013             String encodedUrlString;
4014
4015             // Sanitize the search input.
4016             try {
4017                 encodedUrlString = URLEncoder.encode(unformattedUrlString, "UTF-8");
4018             } catch (UnsupportedEncodingException exception) {
4019                 encodedUrlString = "";
4020             }
4021
4022             // Add the base search URL.
4023             formattedUrlString = searchURL + encodedUrlString;
4024         }
4025
4026         // 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.
4027         urlTextBox.clearFocus();
4028
4029         // Make it so.
4030         loadUrl(formattedUrlString);
4031     }
4032
4033     private void loadUrl(String url) {// Apply any custom domain settings.
4034         // Set the URL as the formatted URL string so that checking third-party requests works correctly.
4035         formattedUrlString = url;
4036
4037         // Apply the domain settings.
4038         applyDomainSettings(url, true, false);
4039
4040         // If loading a website, set `urlIsLoading` to prevent changes in the user agent on websites with redirects from reloading the current website.
4041         urlIsLoading = !url.equals("");
4042
4043         // Load the URL.
4044         mainWebView.loadUrl(url, customHeaders);
4045     }
4046
4047     public void findPreviousOnPage(View view) {
4048         // Go to the previous highlighted phrase on the page.  `false` goes backwards instead of forwards.
4049         mainWebView.findNext(false);
4050     }
4051
4052     public void findNextOnPage(View view) {
4053         // Go to the next highlighted phrase on the page. `true` goes forwards instead of backwards.
4054         mainWebView.findNext(true);
4055     }
4056
4057     public void closeFindOnPage(View view) {
4058         // Delete the contents of `find_on_page_edittext`.
4059         findOnPageEditText.setText(null);
4060
4061         // Clear the highlighted phrases.
4062         mainWebView.clearMatches();
4063
4064         // Hide the Find on Page `RelativeLayout`.
4065         findOnPageLinearLayout.setVisibility(View.GONE);
4066
4067         // Show the URL app bar.
4068         supportAppBar.setVisibility(View.VISIBLE);
4069
4070         // Hide the keyboard so we can see the webpage.  `0` indicates no additional flags.
4071         inputMethodManager.hideSoftInputFromWindow(mainWebView.getWindowToken(), 0);
4072     }
4073
4074     private void applyAppSettings() {
4075         // Get a handle for the shared preferences.
4076         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4077
4078         // Store the values from the shared preferences in variables.
4079         incognitoModeEnabled = sharedPreferences.getBoolean("incognito_mode", false);
4080         boolean doNotTrackEnabled = sharedPreferences.getBoolean("do_not_track", false);
4081         proxyThroughOrbot = sharedPreferences.getBoolean("proxy_through_orbot", false);
4082         fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean("full_screen_browsing_mode", false);
4083         hideSystemBarsOnFullscreen = sharedPreferences.getBoolean("hide_system_bars", false);
4084         translucentNavigationBarOnFullscreen = sharedPreferences.getBoolean("translucent_navigation_bar", true);
4085         downloadWithExternalApp = sharedPreferences.getBoolean("download_with_external_app", false);
4086
4087         // Apply the proxy through Orbot settings.
4088         applyProxyThroughOrbot(false);
4089
4090         // Set Do Not Track status.
4091         if (doNotTrackEnabled) {
4092             customHeaders.put("DNT", "1");
4093         } else {
4094             customHeaders.remove("DNT");
4095         }
4096
4097         // Apply the appropriate full screen mode the `SYSTEM_UI` flags.
4098         if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) {  // Privacy Browser is currently in full screen browsing mode.
4099             if (hideSystemBarsOnFullscreen) {  // Hide everything.
4100                 // Remove the translucent navigation setting if it is currently flagged.
4101                 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
4102
4103                 // Remove the translucent status bar overlay.
4104                 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4105
4106                 // Remove the translucent status bar overlay on the `Drawer Layout`, which is special and needs its own command.
4107                 drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
4108
4109                 /* SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4110                  * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4111                  * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4112                  */
4113                 rootCoordinatorLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
4114             } else {  // Hide everything except the status and navigation bars.
4115                 // Remove any `SYSTEM_UI` flags from `rootCoordinatorLayout`.
4116                 rootCoordinatorLayout.setSystemUiVisibility(0);
4117
4118                 // Add the translucent status flag if it is unset.
4119                 getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4120
4121                 if (translucentNavigationBarOnFullscreen) {
4122                     // Set the navigation bar to be translucent.
4123                     getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
4124                 } else {
4125                     // Set the navigation bar to be black.
4126                     getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
4127                 }
4128             }
4129         } else {  // Privacy Browser is not in full screen browsing mode.
4130             // 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.
4131             inFullScreenBrowsingMode = false;
4132
4133             // Show the `appBar` if `findOnPageLinearLayout` is not visible.
4134             if (findOnPageLinearLayout.getVisibility() == View.GONE) {
4135                 appBar.show();
4136             }
4137
4138             // Show the `BannerAd` in the free flavor.
4139             if (BuildConfig.FLAVOR.contentEquals("free")) {
4140                 // Initialize the ad.  The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
4141                 AdHelper.initializeAds(findViewById(R.id.adview), getApplicationContext(), getFragmentManager(), getString(R.string.google_app_id), getString(R.string.ad_unit_id));
4142             }
4143
4144             // Remove any `SYSTEM_UI` flags from `rootCoordinatorLayout`.
4145             rootCoordinatorLayout.setSystemUiVisibility(0);
4146
4147             // Remove the translucent navigation bar flag if it is set.
4148             getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
4149
4150             // Add the translucent status flag if it is unset.  This also resets `drawerLayout's` `View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN`.
4151             getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4152
4153             // Constrain `rootCoordinatorLayout` inside the status and navigation bars.
4154             rootCoordinatorLayout.setFitsSystemWindows(true);
4155         }
4156     }
4157
4158     // `reloadWebsite` is used if returning from the Domains activity.  Otherwise JavaScript might not function correctly if it is newly enabled.
4159     // The deprecated `.getDrawable()` must be used until the minimum API >= 21.
4160     @SuppressWarnings("deprecation")
4161     private boolean applyDomainSettings(String url, boolean resetFavoriteIcon, boolean reloadWebsite) {
4162         // Get the current user agent.
4163         String initialUserAgent = mainWebView.getSettings().getUserAgentString();
4164
4165         // Initialize a variable to track if the user agent changes.
4166         boolean userAgentChanged = false;
4167
4168         // Parse the URL into a URI.
4169         Uri uri = Uri.parse(url);
4170
4171         // Extract the domain from `uri`.
4172         String hostName = uri.getHost();
4173
4174         // Initialize `loadingNewDomainName`.
4175         boolean loadingNewDomainName;
4176
4177         // If either `hostName` or `currentDomainName` are `null`, run the options for loading a new domain name.
4178         // The lint suggestion to simplify the `if` statement is incorrect, because `hostName.equals(currentDomainName)` can produce a `null object reference.`
4179         //noinspection SimplifiableIfStatement
4180         if ((hostName == null) || (currentDomainName == null)) {
4181             loadingNewDomainName = true;
4182         } else {  // Determine if `hostName` equals `currentDomainName`.
4183             loadingNewDomainName = !hostName.equals(currentDomainName);
4184         }
4185
4186         // Strings don't like to be null.
4187         if (hostName == null) {
4188             hostName = "";
4189         }
4190
4191         // 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.
4192         if (loadingNewDomainName) {
4193             // Set the new `hostname` as the `currentDomainName`.
4194             currentDomainName = hostName;
4195
4196             // Reset the ignoring of pinned domain information.
4197             ignorePinnedDomainInformation = false;
4198
4199             // Reset the favorite icon if specified.
4200             if (resetFavoriteIcon) {
4201                 favoriteIconBitmap = favoriteIconDefaultBitmap;
4202                 favoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(favoriteIconBitmap, 64, 64, true));
4203             }
4204
4205             // Initialize the database handler.  The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
4206             DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
4207
4208             // Get a full cursor from `domainsDatabaseHelper`.
4209             Cursor domainNameCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomain();
4210
4211             // Initialize `domainSettingsSet`.
4212             Set<String> domainSettingsSet = new HashSet<>();
4213
4214             // Get the domain name column index.
4215             int domainNameColumnIndex = domainNameCursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME);
4216
4217             // Populate `domainSettingsSet`.
4218             for (int i = 0; i < domainNameCursor.getCount(); i++) {
4219                 // Move `domainsCursor` to the current row.
4220                 domainNameCursor.moveToPosition(i);
4221
4222                 // Store the domain name in `domainSettingsSet`.
4223                 domainSettingsSet.add(domainNameCursor.getString(domainNameColumnIndex));
4224             }
4225
4226             // Close `domainNameCursor.
4227             domainNameCursor.close();
4228
4229             // Initialize variables to track if domain settings will be applied and, if so, under which name.
4230             domainSettingsApplied = false;
4231             String domainNameInDatabase = null;
4232
4233             // Check the hostname.
4234             if (domainSettingsSet.contains(hostName)) {
4235                 domainSettingsApplied = true;
4236                 domainNameInDatabase = hostName;
4237             }
4238
4239             // Check all the subdomains of the host name against wildcard domains in the domain cursor.
4240             while (!domainSettingsApplied && hostName.contains(".")) {  // Stop checking if domain settings are already applied or there are no more `.` in the host name.
4241                 if (domainSettingsSet.contains("*." + hostName)) {  // Check the host name prepended by `*.`.
4242                     // Apply the domain settings.
4243                     domainSettingsApplied = true;
4244
4245                     // Store the applied domain names as it appears in the database.
4246                     domainNameInDatabase = "*." + hostName;
4247                 }
4248
4249                 // Strip out the lowest subdomain of of the host name.
4250                 hostName = hostName.substring(hostName.indexOf(".") + 1);
4251             }
4252
4253
4254             // Get a handle for the shared preference.
4255             SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4256
4257             // Store the general preference information.
4258             String defaultFontSizeString = sharedPreferences.getString("font_size", getString(R.string.font_size_default_value));
4259             String defaultUserAgentName = sharedPreferences.getString("user_agent", getString(R.string.user_agent_default_value));
4260             defaultCustomUserAgentString = sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value));
4261             boolean defaultSwipeToRefresh = sharedPreferences.getBoolean("swipe_to_refresh", true);
4262             nightMode = sharedPreferences.getBoolean("night_mode", false);
4263             boolean displayWebpageImages = sharedPreferences.getBoolean("display_webpage_images", true);
4264
4265             if (domainSettingsApplied) {  // The url has custom domain settings.
4266                 // Get a cursor for the current host and move it to the first position.
4267                 Cursor currentHostDomainSettingsCursor = domainsDatabaseHelper.getCursorForDomainName(domainNameInDatabase);
4268                 currentHostDomainSettingsCursor.moveToFirst();
4269
4270                 // Get the settings from the cursor.
4271                 domainSettingsDatabaseId = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper._ID)));
4272                 javaScriptEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)) == 1);
4273                 firstPartyCookiesEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FIRST_PARTY_COOKIES)) == 1);
4274                 thirdPartyCookiesEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_THIRD_PARTY_COOKIES)) == 1);
4275                 domStorageEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_DOM_STORAGE)) == 1);
4276                 // Form data can be removed once the minimum API >= 26.
4277                 saveFormDataEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FORM_DATA)) == 1);
4278                 easyListEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYLIST)) == 1);
4279                 easyPrivacyEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYPRIVACY)) == 1);
4280                 fanboysAnnoyanceListEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST)) == 1);
4281                 fanboysSocialBlockingListEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)) == 1);
4282                 ultraPrivacyEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY)) == 1);
4283                 blockAllThirdPartyRequests = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS)) == 1);
4284                 String userAgentName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.USER_AGENT));
4285                 int fontSize = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.FONT_SIZE));
4286                 int swipeToRefreshInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SWIPE_TO_REFRESH));
4287                 int nightModeInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.NIGHT_MODE));
4288                 int displayWebpageImagesInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.DISPLAY_IMAGES));
4289                 pinnedSslCertificate = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)) == 1);
4290                 pinnedSslIssuedToCName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME));
4291                 pinnedSslIssuedToOName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION));
4292                 pinnedSslIssuedToUName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT));
4293                 pinnedSslIssuedByCName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME));
4294                 pinnedSslIssuedByOName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION));
4295                 pinnedSslIssuedByUName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT));
4296                 pinnedIpAddresses = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_IP_ADDRESSES)) == 1);
4297                 pinnedHostIpAddresses = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.IP_ADDRESSES));
4298
4299                 // Set `nightMode` according to `nightModeInt`.  If `nightModeInt` is `DomainsDatabaseHelper.NIGHT_MODE_SYSTEM_DEFAULT` the current setting from `sharedPreferences` will be used.
4300                 switch (nightModeInt) {
4301                     case DomainsDatabaseHelper.NIGHT_MODE_ENABLED:
4302                         nightMode = true;
4303                         break;
4304
4305                     case DomainsDatabaseHelper.NIGHT_MODE_DISABLED:
4306                         nightMode = false;
4307                         break;
4308                 }
4309
4310                 // Store the domain JavaScript status.  This is used by the options menu night mode toggle.
4311                 domainSettingsJavaScriptEnabled = javaScriptEnabled;
4312
4313                 // Enable JavaScript if night mode is enabled.
4314                 if (nightMode) {
4315                     javaScriptEnabled = true;
4316                 }
4317
4318                 // Set the pinned SSL certificate start date to `null` if the saved date `long` is 0.
4319                 if (currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)) == 0) {
4320                     pinnedSslStartDate = null;
4321                 } else {
4322                     pinnedSslStartDate = new Date(currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)));
4323                 }
4324
4325                 // Set the pinned SSL certificate end date to `null` if the saved date `long` is 0.
4326                 if (currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)) == 0) {
4327                     pinnedSslEndDate = null;
4328                 } else {
4329                     pinnedSslEndDate = new Date(currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)));
4330                 }
4331
4332                 // Close `currentHostDomainSettingsCursor`.
4333                 currentHostDomainSettingsCursor.close();
4334
4335                 // Apply the domain settings.
4336                 mainWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled);
4337                 cookieManager.setAcceptCookie(firstPartyCookiesEnabled);
4338                 mainWebView.getSettings().setDomStorageEnabled(domStorageEnabled);
4339
4340                 // Apply the form data setting if the API < 26.
4341                 if (Build.VERSION.SDK_INT < 26) {
4342                     mainWebView.getSettings().setSaveFormData(saveFormDataEnabled);
4343                 }
4344
4345                 // Apply the font size.
4346                 if (fontSize == 0) {  // Apply the default font size.
4347                     mainWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
4348                 } else {  // Apply the specified font size.
4349                     mainWebView.getSettings().setTextZoom(fontSize);
4350                 }
4351
4352                 // Set third-party cookies status if API >= 21.
4353                 if (Build.VERSION.SDK_INT >= 21) {
4354                     cookieManager.setAcceptThirdPartyCookies(mainWebView, thirdPartyCookiesEnabled);
4355                 }
4356
4357                 // 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.
4358                 // <https://redmine.stoutner.com/issues/160>
4359                 if (!urlIsLoading) {
4360                     // Set the user agent.
4361                     if (userAgentName.equals(getString(R.string.system_default_user_agent))) {  // Use the system default user agent.
4362                         // Get the array position of the default user agent name.
4363                         int defaultUserAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
4364
4365                         // Set the user agent according to the system default.
4366                         switch (defaultUserAgentArrayPosition) {
4367                             case UNRECOGNIZED_USER_AGENT:  // The default user agent name is not on the canonical list.
4368                                 // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
4369                                 mainWebView.getSettings().setUserAgentString(defaultUserAgentName);
4370                                 break;
4371
4372                             case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
4373                                 // Set the user agent to `""`, which uses the default value.
4374                                 mainWebView.getSettings().setUserAgentString("");
4375                                 break;
4376
4377                             case SETTINGS_CUSTOM_USER_AGENT:
4378                                 // Set the custom user agent.
4379                                 mainWebView.getSettings().setUserAgentString(defaultCustomUserAgentString);
4380                                 break;
4381
4382                             default:
4383                                 // Get the user agent string from the user agent data array
4384                                 mainWebView.getSettings().setUserAgentString(userAgentDataArray[defaultUserAgentArrayPosition]);
4385                         }
4386                     } else {  // Set the user agent according to the stored name.
4387                         // Get the array position of the user agent name.
4388                         int userAgentArrayPosition = userAgentNamesArray.getPosition(userAgentName);
4389
4390                         switch (userAgentArrayPosition) {
4391                             case UNRECOGNIZED_USER_AGENT:  // The user agent name contains a custom user agent.
4392                                 mainWebView.getSettings().setUserAgentString(userAgentName);
4393                                 break;
4394
4395                             case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
4396                                 // Set the user agent to `""`, which uses the default value.
4397                                 mainWebView.getSettings().setUserAgentString("");
4398                                 break;
4399
4400                             default:
4401                                 // Get the user agent string from the user agent data array.
4402                                 mainWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
4403                         }
4404                     }
4405
4406                     // Store the applied user agent string, which is used in the View Source activity.
4407                     appliedUserAgentString = mainWebView.getSettings().getUserAgentString();
4408
4409                     // Update the user agent change tracker.
4410                     userAgentChanged = !appliedUserAgentString.equals(initialUserAgent);
4411                 }
4412
4413                 // Set swipe to refresh.
4414                 switch (swipeToRefreshInt) {
4415                     case DomainsDatabaseHelper.SWIPE_TO_REFRESH_SYSTEM_DEFAULT:
4416                         // Set swipe to refresh according to the default.
4417                         swipeRefreshLayout.setEnabled(defaultSwipeToRefresh);
4418                         break;
4419
4420                     case DomainsDatabaseHelper.SWIPE_TO_REFRESH_ENABLED:
4421                         // Enable swipe to refresh.
4422                         swipeRefreshLayout.setEnabled(true);
4423                         break;
4424
4425                     case DomainsDatabaseHelper.SWIPE_TO_REFRESH_DISABLED:
4426                         // Disable swipe to refresh.
4427                         swipeRefreshLayout.setEnabled(false);
4428                 }
4429
4430                 // Set the loading of webpage images.
4431                 switch (displayWebpageImagesInt) {
4432                     case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_SYSTEM_DEFAULT:
4433                         mainWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
4434                         break;
4435
4436                     case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_ENABLED:
4437                         mainWebView.getSettings().setLoadsImagesAutomatically(true);
4438                         break;
4439
4440                     case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_DISABLED:
4441                         mainWebView.getSettings().setLoadsImagesAutomatically(false);
4442                         break;
4443                 }
4444
4445                 // 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.
4446                 if (darkTheme) {
4447                     urlAppBarRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_dark_blue));
4448                 } else {
4449                     urlAppBarRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_light_green));
4450                 }
4451             } else {  // The new URL does not have custom domain settings.  Load the defaults.
4452                 // Store the values from `sharedPreferences` in variables.
4453                 javaScriptEnabled = sharedPreferences.getBoolean("javascript", false);
4454                 firstPartyCookiesEnabled = sharedPreferences.getBoolean("first_party_cookies", false);
4455                 thirdPartyCookiesEnabled = sharedPreferences.getBoolean("third_party_cookies", false);
4456                 domStorageEnabled = sharedPreferences.getBoolean("dom_storage", false);
4457                 saveFormDataEnabled = sharedPreferences.getBoolean("save_form_data", false);  // Form data can be removed once the minimum API >= 26.
4458                 easyListEnabled = sharedPreferences.getBoolean("easylist", true);
4459                 easyPrivacyEnabled = sharedPreferences.getBoolean("easyprivacy", true);
4460                 fanboysAnnoyanceListEnabled = sharedPreferences.getBoolean("fanboys_annoyance_list", true);
4461                 fanboysSocialBlockingListEnabled = sharedPreferences.getBoolean("fanboys_social_blocking_list", true);
4462                 ultraPrivacyEnabled = sharedPreferences.getBoolean("ultraprivacy", true);
4463                 blockAllThirdPartyRequests = sharedPreferences.getBoolean("block_all_third_party_requests", false);
4464
4465                 // Set `javaScriptEnabled` to be `true` if `night_mode` is `true`.
4466                 if (nightMode) {
4467                     javaScriptEnabled = true;
4468                 }
4469
4470                 // Apply the default settings.
4471                 mainWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled);
4472                 cookieManager.setAcceptCookie(firstPartyCookiesEnabled);
4473                 mainWebView.getSettings().setDomStorageEnabled(domStorageEnabled);
4474                 mainWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
4475                 swipeRefreshLayout.setEnabled(defaultSwipeToRefresh);
4476
4477                 // Apply the form data setting if the API < 26.
4478                 if (Build.VERSION.SDK_INT < 26) {
4479                     mainWebView.getSettings().setSaveFormData(saveFormDataEnabled);
4480                 }
4481
4482                 // Reset the pinned variables.
4483                 domainSettingsDatabaseId = -1;
4484                 pinnedSslCertificate = false;
4485                 pinnedSslIssuedToCName = "";
4486                 pinnedSslIssuedToOName = "";
4487                 pinnedSslIssuedToUName = "";
4488                 pinnedSslIssuedByCName = "";
4489                 pinnedSslIssuedByOName = "";
4490                 pinnedSslIssuedByUName = "";
4491                 pinnedSslStartDate = null;
4492                 pinnedSslEndDate = null;
4493                 pinnedIpAddresses = false;
4494                 pinnedHostIpAddresses = "";
4495
4496                 // Set third-party cookies status if API >= 21.
4497                 if (Build.VERSION.SDK_INT >= 21) {
4498                     cookieManager.setAcceptThirdPartyCookies(mainWebView, thirdPartyCookiesEnabled);
4499                 }
4500
4501                 // 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.
4502                 // <https://redmine.stoutner.com/issues/160>
4503                 if (!urlIsLoading) {
4504                     // Get the array position of the user agent name.
4505                     int userAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
4506
4507                     // Set the user agent.
4508                     switch (userAgentArrayPosition) {
4509                         case UNRECOGNIZED_USER_AGENT:  // The default user agent name is not on the canonical list.
4510                             // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
4511                             mainWebView.getSettings().setUserAgentString(defaultUserAgentName);
4512                             break;
4513
4514                         case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
4515                             // Set the user agent to `""`, which uses the default value.
4516                             mainWebView.getSettings().setUserAgentString("");
4517                             break;
4518
4519                         case SETTINGS_CUSTOM_USER_AGENT:
4520                             // Set the custom user agent.
4521                             mainWebView.getSettings().setUserAgentString(defaultCustomUserAgentString);
4522                             break;
4523
4524                         default:
4525                             // Get the user agent string from the user agent data array
4526                             mainWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
4527                     }
4528
4529                     // Store the applied user agent string, which is used in the View Source activity.
4530                     appliedUserAgentString = mainWebView.getSettings().getUserAgentString();
4531
4532                     // Update the user agent change tracker.
4533                     userAgentChanged = !appliedUserAgentString.equals(initialUserAgent);
4534                 }
4535
4536                 // Set the loading of webpage images.
4537                 mainWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
4538
4539                 // Set a transparent background on `urlTextBox`.  We have to use the deprecated `.getDrawable()` until the minimum API >= 21.
4540                 urlAppBarRelativeLayout.setBackgroundDrawable(getResources().getDrawable(R.color.transparent));
4541             }
4542
4543             // Close the domains database helper.
4544             domainsDatabaseHelper.close();
4545
4546             // Update the privacy icons, but only if `mainMenu` has already been populated.
4547             if (mainMenu != null) {
4548                 updatePrivacyIcons(true);
4549             }
4550         }
4551
4552         // Reload the website if returning from the Domains activity.
4553         if (reloadWebsite) {
4554             mainWebView.reload();
4555         }
4556
4557         // Return the user agent changed status.
4558         return userAgentChanged;
4559     }
4560
4561     private void applyProxyThroughOrbot(boolean reloadWebsite) {
4562         // Get a handle for the shared preferences.
4563         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4564
4565         // Get the search preferences.
4566         String homepageString = sharedPreferences.getString("homepage", getString(R.string.homepage_default_value));
4567         String torHomepageString = sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value));
4568         String torSearchString = sharedPreferences.getString("tor_search", getString(R.string.tor_search_default_value));
4569         String torSearchCustomUrlString = sharedPreferences.getString("tor_search_custom_url", getString(R.string.tor_search_custom_url_default_value));
4570         String searchString = sharedPreferences.getString("search", getString(R.string.search_default_value));
4571         String searchCustomUrlString = sharedPreferences.getString("search_custom_url", getString(R.string.search_custom_url_default_value));
4572
4573         // Set the homepage, search, and proxy options.
4574         if (proxyThroughOrbot) {  // Set the Tor options.
4575             // Set `torHomepageString` as `homepage`.
4576             homepage = torHomepageString;
4577
4578             // If formattedUrlString is null assign the homepage to it.
4579             if (formattedUrlString == null) {
4580                 formattedUrlString = homepage;
4581             }
4582
4583             // Set the search URL.
4584             if (torSearchString.equals("Custom URL")) {  // Get the custom URL string.
4585                 searchURL = torSearchCustomUrlString;
4586             } else {  // Use the string from the pre-built list.
4587                 searchURL = torSearchString;
4588             }
4589
4590             // Set the proxy.  `this` refers to the current activity where an `AlertDialog` might be displayed.
4591             OrbotProxyHelper.setProxy(getApplicationContext(), this, "localhost", "8118");
4592
4593             // Set the `appBar` background to indicate proxying through Orbot is enabled.  `this` refers to the context.
4594             if (darkTheme) {
4595                 appBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.dark_blue_30));
4596             } else {
4597                 appBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.blue_50));
4598             }
4599
4600             // Check to see if Orbot is ready.
4601             if (!orbotStatus.equals("ON")) {  // Orbot is not ready.
4602                 // Set `waitingForOrbot`.
4603                 waitingForOrbot = true;
4604
4605                 // Disable the wide view port so that the waiting for Orbot text is displayed correctly.
4606                 mainWebView.getSettings().setUseWideViewPort(false);
4607
4608                 // Load a waiting page.  `null` specifies no encoding, which defaults to ASCII.
4609                 mainWebView.loadData(waitingForOrbotHtmlString, "text/html", null);
4610             } else if (reloadWebsite) {  // Orbot is ready and the website should be reloaded.
4611                 // Reload the website.
4612                 mainWebView.reload();
4613             }
4614         } else {  // Set the non-Tor options.
4615             // Set `homepageString` as `homepage`.
4616             homepage = homepageString;
4617
4618             // If formattedUrlString is null assign the homepage to it.
4619             if (formattedUrlString == null) {
4620                 formattedUrlString = homepage;
4621             }
4622
4623             // Set the search URL.
4624             if (searchString.equals("Custom URL")) {  // Get the custom URL string.
4625                 searchURL = searchCustomUrlString;
4626             } else {  // Use the string from the pre-built list.
4627                 searchURL = searchString;
4628             }
4629
4630             // Reset the proxy to default.  The host is `""` and the port is `"0"`.
4631             OrbotProxyHelper.setProxy(getApplicationContext(), this, "", "0");
4632
4633             // Set the default `appBar` background.  `this` refers to the context.
4634             if (darkTheme) {
4635                 appBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.gray_900));
4636             } else {
4637                 appBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.gray_100));
4638             }
4639
4640             // Reset `waitingForOrbot.
4641             waitingForOrbot = false;
4642
4643             // Reload the website if requested.
4644             if (reloadWebsite) {
4645                 mainWebView.reload();
4646             }
4647         }
4648     }
4649
4650     private void updatePrivacyIcons(boolean runInvalidateOptionsMenu) {
4651         // Get handles for the menu items.
4652         MenuItem privacyMenuItem = mainMenu.findItem(R.id.toggle_javascript);
4653         MenuItem firstPartyCookiesMenuItem = mainMenu.findItem(R.id.toggle_first_party_cookies);
4654         MenuItem domStorageMenuItem = mainMenu.findItem(R.id.toggle_dom_storage);
4655         MenuItem refreshMenuItem = mainMenu.findItem(R.id.refresh);
4656
4657         // Update the privacy icon.
4658         if (javaScriptEnabled) {  // JavaScript is enabled.
4659             privacyMenuItem.setIcon(R.drawable.javascript_enabled);
4660         } else if (firstPartyCookiesEnabled) {  // JavaScript is disabled but cookies are enabled.
4661             privacyMenuItem.setIcon(R.drawable.warning);
4662         } else {  // All the dangerous features are disabled.
4663             privacyMenuItem.setIcon(R.drawable.privacy_mode);
4664         }
4665
4666         // Update the first-party cookies icon.
4667         if (firstPartyCookiesEnabled) {  // First-party cookies are enabled.
4668             firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_enabled);
4669         } else {  // First-party cookies are disabled.
4670             if (darkTheme) {
4671                 firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_dark);
4672             } else {
4673                 firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_light);
4674             }
4675         }
4676
4677         // Update the DOM storage icon.
4678         if (javaScriptEnabled && domStorageEnabled) {  // Both JavaScript and DOM storage are enabled.
4679             domStorageMenuItem.setIcon(R.drawable.dom_storage_enabled);
4680         } else if (javaScriptEnabled) {  // JavaScript is enabled but DOM storage is disabled.
4681             if (darkTheme) {
4682                 domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_dark);
4683             } else {
4684                 domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_light);
4685             }
4686         } else {  // JavaScript is disabled, so DOM storage is ghosted.
4687             if (darkTheme) {
4688                 domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_dark);
4689             } else {
4690                 domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_light);
4691             }
4692         }
4693
4694         // Update the refresh icon.
4695         if (darkTheme) {
4696             refreshMenuItem.setIcon(R.drawable.refresh_enabled_dark);
4697         } else {
4698             refreshMenuItem.setIcon(R.drawable.refresh_enabled_light);
4699         }
4700
4701         // `invalidateOptionsMenu` calls `onPrepareOptionsMenu()` and redraws the icons in the `AppBar`.
4702         if (runInvalidateOptionsMenu) {
4703             invalidateOptionsMenu();
4704         }
4705     }
4706
4707     private void openUrlWithExternalApp(String url) {
4708         // Create a download intent.  Not specifying the action type will display the maximum number of options.
4709         Intent downloadIntent = new Intent();
4710
4711         // Set the URI and the MIME type.  Specifying `text/html` displays a good number of options.
4712         downloadIntent.setDataAndType(Uri.parse(url), "text/html");
4713
4714         // Flag the intent to open in a new task.
4715         downloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4716
4717         // Show the chooser.
4718         startActivity(Intent.createChooser(downloadIntent, getString(R.string.open_with)));
4719     }
4720
4721     private void highlightUrlText() {
4722         // Get the URL string.
4723         String urlString = urlTextBox.getText().toString();
4724
4725         // Highlight the URL according to the protocol.
4726         if (urlString.startsWith("file://")) {  // This is a file URL.
4727             // De-emphasize only the protocol.
4728             urlTextBox.getText().setSpan(initialGrayColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4729         } else if (urlString.startsWith("content://")) {
4730             // De-emphasize only the protocol.
4731             urlTextBox.getText().setSpan(initialGrayColorSpan, 0, 10, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4732         } else {  // This is a web URL.
4733             // Get the index of the `/` immediately after the domain name.
4734             int endOfDomainName = urlString.indexOf("/", (urlString.indexOf("//") + 2));
4735
4736             // Create a base URL string.
4737             String baseUrl;
4738
4739             // Get the base URL.
4740             if (endOfDomainName > 0) {  // There is at least one character after the base URL.
4741                 // Get the base URL.
4742                 baseUrl = urlString.substring(0, endOfDomainName);
4743             } else {  // There are no characters after the base URL.
4744                 // Set the base URL to be the entire URL string.
4745                 baseUrl = urlString;
4746             }
4747
4748             // Get the index of the last `.` in the domain.
4749             int lastDotIndex = baseUrl.lastIndexOf(".");
4750
4751             // Get the index of the penultimate `.` in the domain.
4752             int penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1);
4753
4754             // Markup the beginning of the URL.
4755             if (urlString.startsWith("http://")) {  // Highlight the protocol of connections that are not encrypted.
4756                 urlTextBox.getText().setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4757
4758                 // De-emphasize subdomains.
4759                 if (penultimateDotIndex > 0) {  // There is more than one subdomain in the domain name.
4760                     urlTextBox.getText().setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4761                 }
4762             } else if (urlString.startsWith("https://")) {  // De-emphasize the protocol of connections that are encrypted.
4763                 if (penultimateDotIndex > 0) {  // There is more than one subdomain in the domain name.
4764                     // De-emphasize the protocol and the additional subdomains.
4765                     urlTextBox.getText().setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4766                 } else {  // There is only one subdomain in the domain name.
4767                     // De-emphasize only the protocol.
4768                     urlTextBox.getText().setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4769                 }
4770             }
4771
4772             // De-emphasize the text after the domain name.
4773             if (endOfDomainName > 0) {
4774                 urlTextBox.getText().setSpan(finalGrayColorSpan, endOfDomainName, urlString.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4775             }
4776         }
4777     }
4778
4779     private void loadBookmarksFolder() {
4780         // Update the bookmarks cursor with the contents of the bookmarks database for the current folder.
4781         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
4782
4783         // Populate the bookmarks cursor adapter.  `this` specifies the `Context`.  `false` disables `autoRequery`.
4784         bookmarksCursorAdapter = new CursorAdapter(this, bookmarksCursor, false) {
4785             @Override
4786             public View newView(Context context, Cursor cursor, ViewGroup parent) {
4787                 // Inflate the individual item layout.  `false` does not attach it to the root.
4788                 return getLayoutInflater().inflate(R.layout.bookmarks_drawer_item_linearlayout, parent, false);
4789             }
4790
4791             @Override
4792             public void bindView(View view, Context context, Cursor cursor) {
4793                 // Get handles for the views.
4794                 ImageView bookmarkFavoriteIcon = view.findViewById(R.id.bookmark_favorite_icon);
4795                 TextView bookmarkNameTextView = view.findViewById(R.id.bookmark_name);
4796
4797                 // Get the favorite icon byte array from the cursor.
4798                 byte[] favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON));
4799
4800                 // Convert the byte array to a `Bitmap` beginning at the first byte and ending at the last.
4801                 Bitmap favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.length);
4802
4803                 // Display the bitmap in `bookmarkFavoriteIcon`.
4804                 bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap);
4805
4806                 // Get the bookmark name from the cursor and display it in `bookmarkNameTextView`.
4807                 String bookmarkNameString = cursor.getString(cursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
4808                 bookmarkNameTextView.setText(bookmarkNameString);
4809
4810                 // Make the font bold for folders.
4811                 if (cursor.getInt(cursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {
4812                     bookmarkNameTextView.setTypeface(Typeface.DEFAULT_BOLD);
4813                 } else {  // Reset the font to default for normal bookmarks.
4814                     bookmarkNameTextView.setTypeface(Typeface.DEFAULT);
4815                 }
4816             }
4817         };
4818
4819         // Populate the `ListView` with the adapter.
4820         bookmarksListView.setAdapter(bookmarksCursorAdapter);
4821
4822         // Set the bookmarks drawer title.
4823         if (currentBookmarksFolder.isEmpty()) {
4824             bookmarksTitleTextView.setText(R.string.bookmarks);
4825         } else {
4826             bookmarksTitleTextView.setText(currentBookmarksFolder);
4827         }
4828     }
4829
4830     // This must run asynchronously because it involves a network request.  `String` declares the parameters.  `Void` does not declare progress units.  `String` contains the results.
4831     private static class GetHostIpAddresses extends AsyncTask<String, Void, String> {
4832         // The weak references are used to determine if the activity have disappeared while the AsyncTask is running.
4833         private final WeakReference<Activity> activityWeakReference;
4834
4835         GetHostIpAddresses(Activity activity) {
4836             // Populate the weak references.
4837             activityWeakReference = new WeakReference<>(activity);
4838         }
4839
4840         @Override
4841         protected String doInBackground(String... domainName) {
4842             // Get handles for the activity and the alert dialog.
4843             Activity activity = activityWeakReference.get();
4844
4845             // Abort if the activity or the dialog is gone.
4846             if ((activity == null) || activity.isFinishing()) {
4847                 // Return an empty spannable string builder.
4848                 return "";
4849             }
4850
4851             // Initialize an IP address string builder.
4852             StringBuilder ipAddresses = new StringBuilder();
4853
4854             // Get an array with the IP addresses for the host.
4855             try {
4856                 // Get an array with all the IP addresses for the domain.
4857                 InetAddress[] inetAddressesArray = InetAddress.getAllByName(domainName[0]);
4858
4859                 // Add each IP address to the string builder.
4860                 for (InetAddress inetAddress : inetAddressesArray) {
4861                     if (ipAddresses.length() == 0) {  // This is the first IP address.
4862                         // Add the IP address to the string builder.
4863                         ipAddresses.append(inetAddress.getHostAddress());
4864                     } else {  // This is not the first IP address.
4865                         // Add a line break to the string builder first.
4866                         ipAddresses.append("\n");
4867
4868                         // Add the IP address to the string builder.
4869                         ipAddresses.append(inetAddress.getHostAddress());
4870                     }
4871                 }
4872             } catch (UnknownHostException exception) {
4873                 // Do nothing.
4874             }
4875
4876             // Return the string.
4877             return ipAddresses.toString();
4878         }
4879
4880         // `onPostExecute()` operates on the UI thread.
4881         @Override
4882         protected void onPostExecute(String ipAddresses) {
4883             // Get handles for the activity and the alert dialog.
4884             Activity activity = activityWeakReference.get();
4885
4886             // Abort if the activity or the alert dialog is gone.
4887             if ((activity == null) || activity.isFinishing()) {
4888                 return;
4889             }
4890
4891             // Store the IP addresses.
4892             currentHostIpAddresses = ipAddresses;
4893         }
4894     }
4895 }