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