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