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