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