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