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