]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java
Add a logcat activity. https://redmine.stoutner.com/issues/264
[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         // Run the commands that correlate to the selected menu item.
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 array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
2536                                                 Process deleteLocalStorageProcess = 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                                                 Process deleteIndexProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
2540                                                 Process deleteQuotaManagerProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
2541                                                 Process deleteQuotaManagerJournalProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
2542                                                 Process deleteDatabasesProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
2543
2544                                                 // Wait for the processes to finish.
2545                                                 deleteLocalStorageProcess.waitFor();
2546                                                 deleteIndexProcess.waitFor();
2547                                                 deleteQuotaManagerProcess.waitFor();
2548                                                 deleteQuotaManagerJournalProcess.waitFor();
2549                                                 deleteDatabasesProcess.waitFor();
2550                                             } catch (Exception exception) {
2551                                                 // Do nothing if an error is thrown.
2552                                             }
2553                                         };
2554
2555                                         // Manually delete the DOM storage files after 200 milliseconds.
2556                                         deleteDomStorageHandler.postDelayed(deleteDomStorageRunnable, 200);
2557                                 }
2558                             }
2559                         })
2560                         .show();
2561                 return true;
2562
2563             // Form data can be remove once the minimum API >= 26.
2564             case R.id.clear_form_data:
2565                 Snackbar.make(findViewById(R.id.main_webview), R.string.form_data_deleted, Snackbar.LENGTH_LONG)
2566                         .setAction(R.string.undo, v -> {
2567                             // Do nothing because everything will be handled by `onDismissed()` below.
2568                         })
2569                         .addCallback(new Snackbar.Callback() {
2570                             @Override
2571                             public void onDismissed(Snackbar snackbar, int event) {
2572                                 switch (event) {
2573                                     // The user pushed the `Undo` button.
2574                                     case Snackbar.Callback.DISMISS_EVENT_ACTION:
2575                                         // Do nothing.
2576                                         break;
2577
2578                                     // The `Snackbar` was dismissed without the `Undo` button being pushed.
2579                                     default:
2580                                         // Delete the form data.
2581                                         WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(getApplicationContext());
2582                                         mainWebViewDatabase.clearFormData();
2583                                 }
2584                             }
2585                         })
2586                         .show();
2587                 return true;
2588
2589             case R.id.easylist:
2590                 // Toggle the EasyList status.
2591                 easyListEnabled = !easyListEnabled;
2592
2593                 // Update the menu checkbox.
2594                 menuItem.setChecked(easyListEnabled);
2595
2596                 // Reload the main WebView.
2597                 mainWebView.reload();
2598                 return true;
2599
2600             case R.id.easyprivacy:
2601                 // Toggle the EasyPrivacy status.
2602                 easyPrivacyEnabled = !easyPrivacyEnabled;
2603
2604                 // Update the menu checkbox.
2605                 menuItem.setChecked(easyPrivacyEnabled);
2606
2607                 // Reload the main WebView.
2608                 mainWebView.reload();
2609                 return true;
2610
2611             case R.id.fanboys_annoyance_list:
2612                 // Toggle Fanboy's Annoyance List status.
2613                 fanboysAnnoyanceListEnabled = !fanboysAnnoyanceListEnabled;
2614
2615                 // Update the menu checkbox.
2616                 menuItem.setChecked(fanboysAnnoyanceListEnabled);
2617
2618                 // Update the staus of Fanboy's Social Blocking List.
2619                 MenuItem fanboysSocialBlockingListMenuItem = mainMenu.findItem(R.id.fanboys_social_blocking_list);
2620                 fanboysSocialBlockingListMenuItem.setEnabled(!fanboysAnnoyanceListEnabled);
2621
2622                 // Reload the main WebView.
2623                 mainWebView.reload();
2624                 return true;
2625
2626             case R.id.fanboys_social_blocking_list:
2627                 // Toggle Fanboy's Social Blocking List status.
2628                 fanboysSocialBlockingListEnabled = !fanboysSocialBlockingListEnabled;
2629
2630                 // Update the menu checkbox.
2631                 menuItem.setChecked(fanboysSocialBlockingListEnabled);
2632
2633                 // Reload the main WebView.
2634                 mainWebView.reload();
2635                 return true;
2636
2637             case R.id.ultraprivacy:
2638                 // Toggle the UltraPrivacy status.
2639                 ultraPrivacyEnabled = !ultraPrivacyEnabled;
2640
2641                 // Update the menu checkbox.
2642                 menuItem.setChecked(ultraPrivacyEnabled);
2643
2644                 // Reload the main WebView.
2645                 mainWebView.reload();
2646                 return true;
2647
2648             case R.id.block_all_third_party_requests:
2649                 //Toggle the third-party requests blocker status.
2650                 blockAllThirdPartyRequests = !blockAllThirdPartyRequests;
2651
2652                 // Update the menu checkbox.
2653                 menuItem.setChecked(blockAllThirdPartyRequests);
2654
2655                 // Reload the main WebView.
2656                 mainWebView.reload();
2657                 return true;
2658
2659             case R.id.user_agent_privacy_browser:
2660                 // Update the user agent.
2661                 mainWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[0]);
2662
2663                 // Reload the WebView.
2664                 mainWebView.reload();
2665                 return true;
2666
2667             case R.id.user_agent_webview_default:
2668                 // Update the user agent.
2669                 mainWebView.getSettings().setUserAgentString("");
2670
2671                 // Reload the WebView.
2672                 mainWebView.reload();
2673                 return true;
2674
2675             case R.id.user_agent_firefox_on_android:
2676                 // Update the user agent.
2677                 mainWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[2]);
2678
2679                 // Reload the WebView.
2680                 mainWebView.reload();
2681                 return true;
2682
2683             case R.id.user_agent_chrome_on_android:
2684                 // Update the user agent.
2685                 mainWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[3]);
2686
2687                 // Reload the WebView.
2688                 mainWebView.reload();
2689                 return true;
2690
2691             case R.id.user_agent_safari_on_ios:
2692                 // Update the user agent.
2693                 mainWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[4]);
2694
2695                 // Reload the WebView.
2696                 mainWebView.reload();
2697                 return true;
2698
2699             case R.id.user_agent_firefox_on_linux:
2700                 // Update the user agent.
2701                 mainWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[5]);
2702
2703                 // Reload the WebView.
2704                 mainWebView.reload();
2705                 return true;
2706
2707             case R.id.user_agent_chromium_on_linux:
2708                 // Update the user agent.
2709                 mainWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[6]);
2710
2711                 // Reload the WebView.
2712                 mainWebView.reload();
2713                 return true;
2714
2715             case R.id.user_agent_firefox_on_windows:
2716                 // Update the user agent.
2717                 mainWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[7]);
2718
2719                 // Reload the WebView.
2720                 mainWebView.reload();
2721                 return true;
2722
2723             case R.id.user_agent_chrome_on_windows:
2724                 // Update the user agent.
2725                 mainWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[8]);
2726
2727                 // Reload the WebView.
2728                 mainWebView.reload();
2729                 return true;
2730
2731             case R.id.user_agent_edge_on_windows:
2732                 // Update the user agent.
2733                 mainWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[9]);
2734
2735                 // Reload the WebView.
2736                 mainWebView.reload();
2737                 return true;
2738
2739             case R.id.user_agent_internet_explorer_on_windows:
2740                 // Update the user agent.
2741                 mainWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[10]);
2742
2743                 // Reload the WebView.
2744                 mainWebView.reload();
2745                 return true;
2746
2747             case R.id.user_agent_safari_on_macos:
2748                 // Update the user agent.
2749                 mainWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[11]);
2750
2751                 // Reload the WebView.
2752                 mainWebView.reload();
2753                 return true;
2754
2755             case R.id.user_agent_custom:
2756                 // Update the user agent.
2757                 mainWebView.getSettings().setUserAgentString(defaultCustomUserAgentString);
2758
2759                 // Reload the WebView.
2760                 mainWebView.reload();
2761                 return true;
2762
2763             case R.id.font_size_twenty_five_percent:
2764                 mainWebView.getSettings().setTextZoom(25);
2765                 return true;
2766
2767             case R.id.font_size_fifty_percent:
2768                 mainWebView.getSettings().setTextZoom(50);
2769                 return true;
2770
2771             case R.id.font_size_seventy_five_percent:
2772                 mainWebView.getSettings().setTextZoom(75);
2773                 return true;
2774
2775             case R.id.font_size_one_hundred_percent:
2776                 mainWebView.getSettings().setTextZoom(100);
2777                 return true;
2778
2779             case R.id.font_size_one_hundred_twenty_five_percent:
2780                 mainWebView.getSettings().setTextZoom(125);
2781                 return true;
2782
2783             case R.id.font_size_one_hundred_fifty_percent:
2784                 mainWebView.getSettings().setTextZoom(150);
2785                 return true;
2786
2787             case R.id.font_size_one_hundred_seventy_five_percent:
2788                 mainWebView.getSettings().setTextZoom(175);
2789                 return true;
2790
2791             case R.id.font_size_two_hundred_percent:
2792                 mainWebView.getSettings().setTextZoom(200);
2793                 return true;
2794
2795             case R.id.swipe_to_refresh:
2796                 // Toggle swipe to refresh.
2797                 swipeRefreshLayout.setEnabled(!swipeRefreshLayout.isEnabled());
2798                 return true;
2799
2800             case R.id.display_images:
2801                 if (mainWebView.getSettings().getLoadsImagesAutomatically()) {  // Images are currently loaded automatically.
2802                     mainWebView.getSettings().setLoadsImagesAutomatically(false);
2803                     mainWebView.reload();
2804                 } else {  // Images are not currently loaded automatically.
2805                     mainWebView.getSettings().setLoadsImagesAutomatically(true);
2806                 }
2807                 return true;
2808
2809             case R.id.night_mode:
2810                 // Toggle night mode.
2811                 nightMode = !nightMode;
2812
2813                 // Enable or disable JavaScript according to night mode, the global preference, and any domain settings.
2814                 if (nightMode) {  // Night mode is enabled.  Enable JavaScript.
2815                     // Update the global variable.
2816                     javaScriptEnabled = true;
2817                 } else if (domainSettingsApplied) {  // Night mode is disabled and domain settings are applied.  Set JavaScript according to the domain settings.
2818                     // Get the JavaScript preference that was stored the last time domain settings were loaded.
2819                     javaScriptEnabled = domainSettingsJavaScriptEnabled;
2820                 } else {  // Night mode is disabled and domain settings are not applied.  Set JavaScript according to the global preference.
2821                     // Get a handle for the shared preference.
2822                     SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
2823
2824                     // Get the JavaScript preference.
2825                     javaScriptEnabled = sharedPreferences.getBoolean("javascript", false);
2826                 }
2827
2828                 // Apply the JavaScript setting to the WebView.
2829                 mainWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled);
2830
2831                 // Update the privacy icons.
2832                 updatePrivacyIcons(false);
2833
2834                 // Reload the website.
2835                 mainWebView.reload();
2836                 return true;
2837
2838             case R.id.find_on_page:
2839                 // Hide the URL app bar.
2840                 supportAppBar.setVisibility(View.GONE);
2841
2842                 // Show the Find on Page `RelativeLayout`.
2843                 findOnPageLinearLayout.setVisibility(View.VISIBLE);
2844
2845                 // Display the keyboard.  We have to wait 200 ms before running the command to work around a bug in Android.
2846                 // http://stackoverflow.com/questions/5520085/android-show-softkeyboard-with-showsoftinput-is-not-working
2847                 findOnPageEditText.postDelayed(() -> {
2848                     // Set the focus on `findOnPageEditText`.
2849                     findOnPageEditText.requestFocus();
2850
2851                     // Display the keyboard.  `0` sets no input flags.
2852                     inputMethodManager.showSoftInput(findOnPageEditText, 0);
2853                 }, 200);
2854                 return true;
2855
2856             case R.id.view_source:
2857                 // Launch the View Source activity.
2858                 Intent viewSourceIntent = new Intent(this, ViewSourceActivity.class);
2859                 startActivity(viewSourceIntent);
2860                 return true;
2861
2862             case R.id.share_url:
2863                 // Setup the share string.
2864                 String shareString = webViewTitle + " – " + urlTextBox.getText().toString();
2865
2866                 // Create the share intent.
2867                 Intent shareIntent = new Intent(Intent.ACTION_SEND);
2868                 shareIntent.putExtra(Intent.EXTRA_TEXT, shareString);
2869                 shareIntent.setType("text/plain");
2870
2871                 // Make it so.
2872                 startActivity(Intent.createChooser(shareIntent, getString(R.string.share_url)));
2873                 return true;
2874
2875             case R.id.print:
2876                 // Get a `PrintManager` instance.
2877                 PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);
2878
2879                 // Convert `mainWebView` to `printDocumentAdapter`.
2880                 PrintDocumentAdapter printDocumentAdapter = mainWebView.createPrintDocumentAdapter();
2881
2882                 // Remove the lint error below that `printManager` might be `null`.
2883                 assert printManager != null;
2884
2885                 // Print the document.  The print attributes are `null`.
2886                 printManager.print(getString(R.string.privacy_browser_web_page), printDocumentAdapter, null);
2887                 return true;
2888
2889             case R.id.open_with_app:
2890                 openWithApp(formattedUrlString);
2891                 return true;
2892
2893             case R.id.open_with_browser:
2894                 openWithBrowser(formattedUrlString);
2895                 return true;
2896
2897             case R.id.add_to_homescreen:
2898                 // Show the alert dialog.
2899                 AppCompatDialogFragment createHomeScreenShortcutDialogFragment = new CreateHomeScreenShortcutDialog();
2900                 createHomeScreenShortcutDialogFragment.show(supportFragmentManager, getString(R.string.create_shortcut));
2901
2902                 //Everything else will be handled by the alert dialog and the associated listener below.
2903                 return true;
2904
2905             case R.id.proxy_through_orbot:
2906                 // Toggle the proxy through Orbot variable.
2907                 proxyThroughOrbot = !proxyThroughOrbot;
2908
2909                 // Apply the proxy through Orbot settings.
2910                 applyProxyThroughOrbot(true);
2911                 return true;
2912
2913             case R.id.refresh:
2914                 if (menuItem.getTitle().equals(getString(R.string.refresh))) {  // The refresh button was pushed.
2915                     // Reload the WebView.
2916                     mainWebView.reload();
2917                 } else {  // The stop button was pushed.
2918                     // Stop the loading of the WebView.
2919                     mainWebView.stopLoading();
2920                 }
2921                 return true;
2922
2923             case R.id.ad_consent:
2924                 // Display the ad consent dialog.
2925                 DialogFragment adConsentDialogFragment = new AdConsentDialog();
2926                 adConsentDialogFragment.show(getFragmentManager(), getString(R.string.ad_consent));
2927                 return true;
2928
2929             default:
2930                 // Don't consume the event.
2931                 return super.onOptionsItemSelected(menuItem);
2932         }
2933     }
2934
2935     // removeAllCookies is deprecated, but it is required for API < 21.
2936     @SuppressWarnings("deprecation")
2937     @Override
2938     public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
2939         int menuItemId = menuItem.getItemId();
2940
2941         switch (menuItemId) {
2942             case R.id.home:
2943                 loadUrl(homepage);
2944                 break;
2945
2946             case R.id.back:
2947                 if (mainWebView.canGoBack()) {
2948                     // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
2949                     formattedUrlString = "";
2950
2951                     // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
2952                     navigatingHistory = true;
2953
2954                     // Load the previous website in the history.
2955                     mainWebView.goBack();
2956                 }
2957                 break;
2958
2959             case R.id.forward:
2960                 if (mainWebView.canGoForward()) {
2961                     // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
2962                     formattedUrlString = "";
2963
2964                     // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
2965                     navigatingHistory = true;
2966
2967                     // Load the next website in the history.
2968                     mainWebView.goForward();
2969                 }
2970                 break;
2971
2972             case R.id.history:
2973                 // Get the `WebBackForwardList`.
2974                 WebBackForwardList webBackForwardList = mainWebView.copyBackForwardList();
2975
2976                 // Show the `UrlHistoryDialog` `AlertDialog` and name this instance `R.string.history`.  `this` is the `Context`.
2977                 AppCompatDialogFragment urlHistoryDialogFragment = UrlHistoryDialog.loadBackForwardList(this, webBackForwardList);
2978                 urlHistoryDialogFragment.show(supportFragmentManager, getString(R.string.history));
2979                 break;
2980
2981             case R.id.requests:
2982                 // Launch the requests activity.
2983                 Intent requestsIntent = new Intent(this, RequestsActivity.class);
2984                 startActivity(requestsIntent);
2985                 break;
2986
2987             case R.id.downloads:
2988                 // Launch the system Download Manager.
2989                 Intent downloadManagerIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
2990
2991                 // Launch as a new task so that Download Manager and Privacy Browser show as separate windows in the recent tasks list.
2992                 downloadManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2993
2994                 startActivity(downloadManagerIntent);
2995                 break;
2996
2997             case R.id.domains:
2998                 // Set the flag to reapply the domain settings on restart when returning from Domain Settings.
2999                 reapplyDomainSettingsOnRestart = true;
3000                 currentDomainName = "";
3001
3002                 // Launch the domains activity.
3003                 Intent domainsIntent = new Intent(this, DomainsActivity.class);
3004                 startActivity(domainsIntent);
3005                 break;
3006
3007             case R.id.settings:
3008                 // Set the flag to reapply app settings on restart when returning from Settings.
3009                 reapplyAppSettingsOnRestart = true;
3010
3011                 // Set the flag to reapply the domain settings on restart when returning from Settings.
3012                 reapplyDomainSettingsOnRestart = true;
3013                 currentDomainName = "";
3014
3015                 // Launch the settings activity.
3016                 Intent settingsIntent = new Intent(this, SettingsActivity.class);
3017                 startActivity(settingsIntent);
3018                 break;
3019
3020             case R.id.import_export:
3021                 // Launch the import/export activity.
3022                 Intent importExportIntent = new Intent (this, ImportExportActivity.class);
3023                 startActivity(importExportIntent);
3024                 break;
3025
3026             case R.id.logcat:
3027                 // Launch the logcat activity.
3028                 Intent logcatIntent = new Intent(this, LogcatActivity.class);
3029                 startActivity(logcatIntent);
3030                 break;
3031
3032             case R.id.guide:
3033                 // Launch `GuideActivity`.
3034                 Intent guideIntent = new Intent(this, GuideActivity.class);
3035                 startActivity(guideIntent);
3036                 break;
3037
3038             case R.id.about:
3039                 // Launch `AboutActivity`.
3040                 Intent aboutIntent = new Intent(this, AboutActivity.class);
3041                 startActivity(aboutIntent);
3042                 break;
3043
3044             case R.id.clear_and_exit:
3045                 // Close the bookmarks cursor and database.
3046                 bookmarksCursor.close();
3047                 bookmarksDatabaseHelper.close();
3048
3049                 // Get a handle for the shared preferences.
3050                 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3051
3052                 // Get the status of the clear everything preference.
3053                 boolean clearEverything = sharedPreferences.getBoolean("clear_everything", true);
3054
3055                 // Clear cookies.
3056                 if (clearEverything || sharedPreferences.getBoolean("clear_cookies", true)) {
3057                     // The command to remove cookies changed slightly in API 21.
3058                     if (Build.VERSION.SDK_INT >= 21) {
3059                         cookieManager.removeAllCookies(null);
3060                     } else {
3061                         cookieManager.removeAllCookie();
3062                     }
3063
3064                     // Manually delete the cookies database, as `CookieManager` sometimes will not flush its changes to disk before `System.exit(0)` is run.
3065                     try {
3066                         // Two commands must be used because `Runtime.exec()` does not like `*`.
3067                         Process deleteCookiesProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies");
3068                         Process deleteCookiesJournalProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies-journal");
3069
3070                         // Wait until the processes have finished.
3071                         deleteCookiesProcess.waitFor();
3072                         deleteCookiesJournalProcess.waitFor();
3073                     } catch (Exception exception) {
3074                         // Do nothing if an error is thrown.
3075                     }
3076                 }
3077
3078                 // Clear DOM storage.
3079                 if (clearEverything || sharedPreferences.getBoolean("clear_dom_storage", true)) {
3080                     // Ask `WebStorage` to clear the DOM storage.
3081                     WebStorage webStorage = WebStorage.getInstance();
3082                     webStorage.deleteAllData();
3083
3084                     // Manually delete the DOM storage files and directories, as `WebStorage` sometimes will not flush its changes to disk before `System.exit(0)` is run.
3085                     try {
3086                         // A `String[]` must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
3087                         Process deleteLocalStorageProcess = privacyBrowserRuntime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
3088
3089                         // Multiple commands must be used because `Runtime.exec()` does not like `*`.
3090                         Process deleteIndexProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
3091                         Process deleteQuotaManagerProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
3092                         Process deleteQuotaManagerJournalProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
3093                         Process deleteDatabaseProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
3094
3095                         // Wait until the processes have finished.
3096                         deleteLocalStorageProcess.waitFor();
3097                         deleteIndexProcess.waitFor();
3098                         deleteQuotaManagerProcess.waitFor();
3099                         deleteQuotaManagerJournalProcess.waitFor();
3100                         deleteDatabaseProcess.waitFor();
3101                     } catch (Exception exception) {
3102                         // Do nothing if an error is thrown.
3103                     }
3104                 }
3105
3106                 // Clear form data if the API < 26.
3107                 if ((Build.VERSION.SDK_INT < 26) && (clearEverything || sharedPreferences.getBoolean("clear_form_data", true))) {
3108                     WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
3109                     webViewDatabase.clearFormData();
3110
3111                     // Manually delete the form data database, as `WebViewDatabase` sometimes will not flush its changes to disk before `System.exit(0)` is run.
3112                     try {
3113                         // A string array must be used because the database contains a space and `Runtime.exec` will not otherwise escape the string correctly.
3114                         Process deleteWebDataProcess = privacyBrowserRuntime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data"});
3115                         Process deleteWebDataJournalProcess = privacyBrowserRuntime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data-journal"});
3116
3117                         // Wait until the processes have finished.
3118                         deleteWebDataProcess.waitFor();
3119                         deleteWebDataJournalProcess.waitFor();
3120                     } catch (Exception exception) {
3121                         // Do nothing if an error is thrown.
3122                     }
3123                 }
3124
3125                 // Clear the cache.
3126                 if (clearEverything || sharedPreferences.getBoolean("clear_cache", true)) {
3127                     // `true` includes disk files.
3128                     mainWebView.clearCache(true);
3129
3130                     // Manually delete the cache directories.
3131                     try {
3132                         // Delete the main cache directory.
3133                         Process deleteCacheProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/cache");
3134
3135                         // Delete the secondary `Service Worker` cache directory.
3136                         // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
3137                         Process deleteServiceWorkerProcess = privacyBrowserRuntime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
3138
3139                         // Wait until the processes have finished.
3140                         deleteCacheProcess.waitFor();
3141                         deleteServiceWorkerProcess.waitFor();
3142                     } catch (Exception exception) {
3143                         // Do nothing if an error is thrown.
3144                     }
3145                 }
3146
3147                 // Clear SSL certificate preferences.
3148                 mainWebView.clearSslPreferences();
3149
3150                 // Clear the back/forward history.
3151                 mainWebView.clearHistory();
3152
3153                 // Clear `formattedUrlString`.
3154                 formattedUrlString = null;
3155
3156                 // Clear `customHeaders`.
3157                 customHeaders.clear();
3158
3159                 // Detach all views from `mainWebViewRelativeLayout`.
3160                 mainWebViewRelativeLayout.removeAllViews();
3161
3162                 // Destroy the internal state of `mainWebView`.
3163                 mainWebView.destroy();
3164
3165                 // Manually delete the `app_webview` folder, which contains the cookies, DOM storage, form data, and `Service Worker` cache.
3166                 // See `https://code.google.com/p/android/issues/detail?id=233826&thanks=233826&ts=1486670530`.
3167                 if (clearEverything) {
3168                     try {
3169                         // Delete the folder.
3170                         Process deleteAppWebviewProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview");
3171
3172                         // Wait until the process has finished.
3173                         deleteAppWebviewProcess.waitFor();
3174                     } catch (Exception exception) {
3175                         // Do nothing if an error is thrown.
3176                     }
3177                 }
3178
3179                 // Close Privacy Browser.  `finishAndRemoveTask` also removes Privacy Browser from the recent app list.
3180                 if (Build.VERSION.SDK_INT >= 21) {
3181                     finishAndRemoveTask();
3182                 } else {
3183                     finish();
3184                 }
3185
3186                 // Remove the terminated program from RAM.  The status code is `0`.
3187                 System.exit(0);
3188                 break;
3189         }
3190
3191         // Close the navigation drawer.
3192         drawerLayout.closeDrawer(GravityCompat.START);
3193         return true;
3194     }
3195
3196     @Override
3197     public void onPostCreate(Bundle savedInstanceState) {
3198         super.onPostCreate(savedInstanceState);
3199
3200         // Sync the state of the DrawerToggle after onRestoreInstanceState has finished.
3201         drawerToggle.syncState();
3202     }
3203
3204     @Override
3205     public void onConfigurationChanged(Configuration newConfig) {
3206         super.onConfigurationChanged(newConfig);
3207
3208         // Get the status bar pixel size.
3209         int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
3210         int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId);
3211
3212         // Get the resource density.
3213         float screenDensity = getResources().getDisplayMetrics().density;
3214
3215         // Recalculate the drawer header padding.
3216         drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity);
3217         drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity);
3218         drawerHeaderPaddingBottom = (int) (8 * screenDensity);
3219
3220         // Reload the ad for the free flavor if not in full screen mode.
3221         if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
3222             // Reload the ad.  The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
3223             AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
3224         }
3225
3226         // `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:
3227         // https://code.google.com/p/android/issues/detail?id=20493#c8
3228         // ActivityCompat.invalidateOptionsMenu(this);
3229     }
3230
3231     @Override
3232     public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
3233         // Store the `HitTestResult`.
3234         final WebView.HitTestResult hitTestResult = mainWebView.getHitTestResult();
3235
3236         // Create strings.
3237         final String imageUrl;
3238         final String linkUrl;
3239
3240         // Get a handle for the `ClipboardManager`.
3241         final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
3242
3243         // Remove the lint errors below that `clipboardManager` might be `null`.
3244         assert clipboardManager != null;
3245
3246         switch (hitTestResult.getType()) {
3247             // `SRC_ANCHOR_TYPE` is a link.
3248             case WebView.HitTestResult.SRC_ANCHOR_TYPE:
3249                 // Get the target URL.
3250                 linkUrl = hitTestResult.getExtra();
3251
3252                 // Set the target URL as the title of the `ContextMenu`.
3253                 menu.setHeaderTitle(linkUrl);
3254
3255                 // Add a Load URL entry.
3256                 menu.add(R.string.load_url).setOnMenuItemClickListener((MenuItem item) -> {
3257                     loadUrl(linkUrl);
3258                     return false;
3259                 });
3260
3261                 // Add a Copy URL entry.
3262                 menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
3263                     // Save the link URL in a `ClipData`.
3264                     ClipData srcAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl);
3265
3266                     // Set the `ClipData` as the clipboard's primary clip.
3267                     clipboardManager.setPrimaryClip(srcAnchorTypeClipData);
3268                     return false;
3269                 });
3270
3271                 // Add a Download URL entry.
3272                 menu.add(R.string.download_url).setOnMenuItemClickListener((MenuItem item) -> {
3273                     // Check if the download should be processed by an external app.
3274                     if (downloadWithExternalApp) {  // Download with an external app.
3275                         openUrlWithExternalApp(linkUrl);
3276                     } else {  // Download with Android's download manager.
3277                         // Check to see if the storage permission has already been granted.
3278                         if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {  // The storage permission needs to be requested.
3279                             // Store the variables for future use by `onRequestPermissionsResult()`.
3280                             downloadUrl = linkUrl;
3281                             downloadContentDisposition = "none";
3282                             downloadContentLength = -1;
3283
3284                             // Show a dialog if the user has previously denied the permission.
3285                             if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
3286                                 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
3287                                 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE);
3288
3289                                 // Show the download location permission alert dialog.  The permission will be requested when the the dialog is closed.
3290                                 downloadLocationPermissionDialogFragment.show(getFragmentManager(), getString(R.string.download_location));
3291                             } else {  // Show the permission request directly.
3292                                 // Request the permission.  The download dialog will be launched by `onRequestPermissionResult()`.
3293                                 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
3294                             }
3295                         } else {  // The storage permission has already been granted.
3296                             // Get a handle for the download file alert dialog.
3297                             AppCompatDialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(linkUrl, "none", -1);
3298
3299                             // Show the download file alert dialog.
3300                             downloadFileDialogFragment.show(supportFragmentManager, getString(R.string.download));
3301                         }
3302                     }
3303                     return false;
3304                 });
3305
3306                 // Add an Open with App entry.
3307                 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
3308                     openWithApp(linkUrl);
3309                     return false;
3310                 });
3311
3312                 // Add an Open with Browser entry.
3313                 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
3314                     openWithBrowser(linkUrl);
3315                     return false;
3316                 });
3317
3318                 // Add a Cancel entry, which by default closes the context menu.
3319                 menu.add(R.string.cancel);
3320                 break;
3321
3322             case WebView.HitTestResult.EMAIL_TYPE:
3323                 // Get the target URL.
3324                 linkUrl = hitTestResult.getExtra();
3325
3326                 // Set the target URL as the title of the `ContextMenu`.
3327                 menu.setHeaderTitle(linkUrl);
3328
3329                 // Add a Write Email entry.
3330                 menu.add(R.string.write_email).setOnMenuItemClickListener(item -> {
3331                     // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
3332                     Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
3333
3334                     // Parse the url and set it as the data for the `Intent`.
3335                     emailIntent.setData(Uri.parse("mailto:" + linkUrl));
3336
3337                     // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser.
3338                     emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
3339
3340                     // Make it so.
3341                     startActivity(emailIntent);
3342                     return false;
3343                 });
3344
3345                 // Add a Copy Email Address entry.
3346                 menu.add(R.string.copy_email_address).setOnMenuItemClickListener(item -> {
3347                     // Save the email address in a `ClipData`.
3348                     ClipData srcEmailTypeClipData = ClipData.newPlainText(getString(R.string.email_address), linkUrl);
3349
3350                     // Set the `ClipData` as the clipboard's primary clip.
3351                     clipboardManager.setPrimaryClip(srcEmailTypeClipData);
3352                     return false;
3353                 });
3354
3355                 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
3356                 menu.add(R.string.cancel);
3357                 break;
3358
3359             // `IMAGE_TYPE` is an image.
3360             case WebView.HitTestResult.IMAGE_TYPE:
3361                 // Get the image URL.
3362                 imageUrl = hitTestResult.getExtra();
3363
3364                 // Set the image URL as the title of the `ContextMenu`.
3365                 menu.setHeaderTitle(imageUrl);
3366
3367                 // Add a View Image entry.
3368                 menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
3369                     loadUrl(imageUrl);
3370                     return false;
3371                 });
3372
3373                 // Add a Download Image entry.
3374                 menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
3375                     // Check if the download should be processed by an external app.
3376                     if (downloadWithExternalApp) {  // Download with an external app.
3377                         openUrlWithExternalApp(imageUrl);
3378                     } else {  // Download with Android's download manager.
3379                         // Check to see if the storage permission has already been granted.
3380                         if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {  // The storage permission needs to be requested.
3381                             // Store the image URL for use by `onRequestPermissionResult()`.
3382                             downloadImageUrl = imageUrl;
3383
3384                             // Show a dialog if the user has previously denied the permission.
3385                             if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
3386                                 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
3387                                 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
3388
3389                                 // Show the download location permission alert dialog.  The permission will be requested when the dialog is closed.
3390                                 downloadLocationPermissionDialogFragment.show(getFragmentManager(), getString(R.string.download_location));
3391                             } else {  // Show the permission request directly.
3392                                 // Request the permission.  The download dialog will be launched by `onRequestPermissionResult().
3393                                 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
3394                             }
3395                         } else {  // The storage permission has already been granted.
3396                             // Get a handle for the download image alert dialog.
3397                             AppCompatDialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
3398
3399                             // Show the download image alert dialog.
3400                             downloadImageDialogFragment.show(supportFragmentManager, getString(R.string.download));
3401                         }
3402                     }
3403                     return false;
3404                 });
3405
3406                 // Add a Copy URL entry.
3407                 menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> {
3408                     // Save the image URL in a `ClipData`.
3409                     ClipData srcImageTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
3410
3411                     // Set the `ClipData` as the clipboard's primary clip.
3412                     clipboardManager.setPrimaryClip(srcImageTypeClipData);
3413                     return false;
3414                 });
3415
3416                 // Add an Open with App entry.
3417                 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
3418                     openWithApp(imageUrl);
3419                     return false;
3420                 });
3421
3422                 // Add an Open with Browser entry.
3423                 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
3424                     openWithBrowser(imageUrl);
3425                     return false;
3426                 });
3427
3428                 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
3429                 menu.add(R.string.cancel);
3430                 break;
3431
3432
3433             // `SRC_IMAGE_ANCHOR_TYPE` is an image that is also a link.
3434             case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
3435                 // Get the image URL.
3436                 imageUrl = hitTestResult.getExtra();
3437
3438                 // Set the image URL as the title of the `ContextMenu`.
3439                 menu.setHeaderTitle(imageUrl);
3440
3441                 // Add a `View Image` entry.
3442                 menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
3443                     loadUrl(imageUrl);
3444                     return false;
3445                 });
3446
3447                 // Add a `Download Image` entry.
3448                 menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
3449                     // Check if the download should be processed by an external app.
3450                     if (downloadWithExternalApp) {  // Download with an external app.
3451                         openUrlWithExternalApp(imageUrl);
3452                     } else {  // Download with Android's download manager.
3453                         // Check to see if the storage permission has already been granted.
3454                         if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {  // The storage permission needs to be requested.
3455                             // Store the image URL for use by `onRequestPermissionResult()`.
3456                             downloadImageUrl = imageUrl;
3457
3458                             // Show a dialog if the user has previously denied the permission.
3459                             if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
3460                                 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
3461                                 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
3462
3463                                 // Show the download location permission alert dialog.  The permission will be requested when the dialog is closed.
3464                                 downloadLocationPermissionDialogFragment.show(getFragmentManager(), getString(R.string.download_location));
3465                             } else {  // Show the permission request directly.
3466                                 // Request the permission.  The download dialog will be launched by `onRequestPermissionResult().
3467                                 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
3468                             }
3469                         } else {  // The storage permission has already been granted.
3470                             // Get a handle for the download image alert dialog.
3471                             AppCompatDialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
3472
3473                             // Show the download image alert dialog.
3474                             downloadImageDialogFragment.show(supportFragmentManager, getString(R.string.download));
3475                         }
3476                     }
3477                     return false;
3478                 });
3479
3480                 // Add a `Copy URL` entry.
3481                 menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> {
3482                     // Save the image URL in a `ClipData`.
3483                     ClipData srcImageAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
3484
3485                     // Set the `ClipData` as the clipboard's primary clip.
3486                     clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData);
3487                     return false;
3488                 });
3489
3490                 // Add an Open with App entry.
3491                 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
3492                     openWithApp(imageUrl);
3493                     return false;
3494                 });
3495
3496                 // Add an Open with Browser entry.
3497                 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
3498                     openWithBrowser(imageUrl);
3499                     return false;
3500                 });
3501
3502                 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
3503                 menu.add(R.string.cancel);
3504                 break;
3505         }
3506     }
3507
3508     @Override
3509     public void onCreateBookmark(AppCompatDialogFragment dialogFragment) {
3510         // Get the `EditTexts` from the `dialogFragment`.
3511         EditText createBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_name_edittext);
3512         EditText createBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_url_edittext);
3513
3514         // Extract the strings from the `EditTexts`.
3515         String bookmarkNameString = createBookmarkNameEditText.getText().toString();
3516         String bookmarkUrlString = createBookmarkUrlEditText.getText().toString();
3517
3518         // Convert the favoriteIcon Bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
3519         ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
3520         favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
3521         byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
3522
3523         // Display the new bookmark below the current items in the (0 indexed) list.
3524         int newBookmarkDisplayOrder = bookmarksListView.getCount();
3525
3526         // Create the bookmark.
3527         bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolder, newBookmarkDisplayOrder, favoriteIconByteArray);
3528
3529         // Update the bookmarks cursor with the current contents of this folder.
3530         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
3531
3532         // Update the `ListView`.
3533         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
3534
3535         // Scroll to the new bookmark.
3536         bookmarksListView.setSelection(newBookmarkDisplayOrder);
3537     }
3538
3539     @Override
3540     public void onCreateBookmarkFolder(AppCompatDialogFragment dialogFragment) {
3541         // Get handles for the views in `dialogFragment`.
3542         EditText createFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.create_folder_name_edittext);
3543         RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon_radiobutton);
3544         ImageView folderIconImageView = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon);
3545
3546         // Get new folder name string.
3547         String folderNameString = createFolderNameEditText.getText().toString();
3548
3549         // Get the new folder icon `Bitmap`.
3550         Bitmap folderIconBitmap;
3551         if (defaultFolderIconRadioButton.isChecked()) {  // Use the default folder icon.
3552             // Get the default folder icon and convert it to a `Bitmap`.
3553             Drawable folderIconDrawable = folderIconImageView.getDrawable();
3554             BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
3555             folderIconBitmap = folderIconBitmapDrawable.getBitmap();
3556         } else {  // Use the `WebView` favorite icon.
3557             folderIconBitmap = favoriteIconBitmap;
3558         }
3559
3560         // Convert `folderIconBitmap` to a byte array.  `0` is for lossless compression (the only option for a PNG).
3561         ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream();
3562         folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream);
3563         byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray();
3564
3565         // Move all the bookmarks down one in the display order.
3566         for (int i = 0; i < bookmarksListView.getCount(); i++) {
3567             int databaseId = (int) bookmarksListView.getItemIdAtPosition(i);
3568             bookmarksDatabaseHelper.updateDisplayOrder(databaseId, i + 1);
3569         }
3570
3571         // Create the folder, which will be placed at the top of the `ListView`.
3572         bookmarksDatabaseHelper.createFolder(folderNameString, currentBookmarksFolder, folderIconByteArray);
3573
3574         // Update the bookmarks cursor with the current contents of this folder.
3575         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
3576
3577         // Update the `ListView`.
3578         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
3579
3580         // Scroll to the new folder.
3581         bookmarksListView.setSelection(0);
3582     }
3583
3584     @Override
3585     public void onCreateHomeScreenShortcut(AppCompatDialogFragment dialogFragment) {
3586         // Get the shortcut name.
3587         EditText shortcutNameEditText = dialogFragment.getDialog().findViewById(R.id.shortcut_name_edittext);
3588         String shortcutNameString = shortcutNameEditText.getText().toString();
3589
3590         // Convert the favorite icon bitmap to an `Icon`.  `IconCompat` is required until API >= 26.
3591         IconCompat favoriteIcon = IconCompat.createWithBitmap(favoriteIconBitmap);
3592
3593         // Setup the shortcut intent.
3594         Intent shortcutIntent = new Intent(Intent.ACTION_VIEW);
3595         shortcutIntent.setData(Uri.parse(formattedUrlString));
3596
3597         // Create a shortcut info builder.  The shortcut name becomes the shortcut ID.
3598         ShortcutInfoCompat.Builder shortcutInfoBuilder = new ShortcutInfoCompat.Builder(this, shortcutNameString);
3599
3600         // Add the required fields to the shortcut info builder.
3601         shortcutInfoBuilder.setIcon(favoriteIcon);
3602         shortcutInfoBuilder.setIntent(shortcutIntent);
3603         shortcutInfoBuilder.setShortLabel(shortcutNameString);
3604
3605         // Request the pin.  `ShortcutManagerCompat` can be switched to `ShortcutManager` once API >= 26.
3606         ShortcutManagerCompat.requestPinShortcut(this, shortcutInfoBuilder.build(), null);
3607     }
3608
3609     @Override
3610     public void onCloseDownloadLocationPermissionDialog(int downloadType) {
3611         switch (downloadType) {
3612             case DownloadLocationPermissionDialog.DOWNLOAD_FILE:
3613                 // Request the WRITE_EXTERNAL_STORAGE permission with a file request code.
3614                 ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
3615                 break;
3616
3617             case DownloadLocationPermissionDialog.DOWNLOAD_IMAGE:
3618                 // Request the WRITE_EXTERNAL_STORAGE permission with an image request code.
3619                 ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
3620                 break;
3621         }
3622     }
3623
3624     @Override
3625     public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
3626         switch (requestCode) {
3627             case DOWNLOAD_FILE_REQUEST_CODE:
3628                 // Show the download file alert dialog.  When the dialog closes, the correct command will be used based on the permission status.
3629                 AppCompatDialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(downloadUrl, downloadContentDisposition, downloadContentLength);
3630
3631                 // On API 23, displaying the fragment must be delayed or the app will crash.
3632                 if (Build.VERSION.SDK_INT == 23) {
3633                     new Handler().postDelayed(() -> downloadFileDialogFragment.show(supportFragmentManager, getString(R.string.download)), 500);
3634                 } else {
3635                     downloadFileDialogFragment.show(supportFragmentManager, getString(R.string.download));
3636                 }
3637
3638                 // Reset the download variables.
3639                 downloadUrl = "";
3640                 downloadContentDisposition = "";
3641                 downloadContentLength = 0;
3642                 break;
3643
3644             case DOWNLOAD_IMAGE_REQUEST_CODE:
3645                 // Show the download image alert dialog.  When the dialog closes, the correct command will be used based on the permission status.
3646                 AppCompatDialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(downloadImageUrl);
3647
3648                 // On API 23, displaying the fragment must be delayed or the app will crash.
3649                 if (Build.VERSION.SDK_INT == 23) {
3650                     new Handler().postDelayed(() -> downloadImageDialogFragment.show(supportFragmentManager, getString(R.string.download)), 500);
3651                 } else {
3652                     downloadImageDialogFragment.show(supportFragmentManager, getString(R.string.download));
3653                 }
3654
3655                 // Reset the image URL variable.
3656                 downloadImageUrl = "";
3657                 break;
3658         }
3659     }
3660
3661     @Override
3662     public void onDownloadImage(AppCompatDialogFragment dialogFragment, String imageUrl) {
3663         // Download the image if it has an HTTP or HTTPS URI.
3664         if (imageUrl.startsWith("http")) {
3665             // Get a handle for the system `DOWNLOAD_SERVICE`.
3666             DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
3667
3668             // Parse `imageUrl`.
3669             DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(imageUrl));
3670
3671             // Pass cookies to download manager if cookies are enabled.  This is required to download images from websites that require a login.
3672             // Code contributed 2017 Hendrik Knackstedt.  Copyright assigned to Soren Stoutner <soren@stoutner.com>.
3673             if (firstPartyCookiesEnabled) {
3674                 // Get the cookies for `imageUrl`.
3675                 String cookies = cookieManager.getCookie(imageUrl);
3676
3677                 // Add the cookies to `downloadRequest`.  In the HTTP request header, cookies are named `Cookie`.
3678                 downloadRequest.addRequestHeader("Cookie", cookies);
3679             }
3680
3681             // Get the file name from the dialog fragment.
3682             EditText downloadImageNameEditText = dialogFragment.getDialog().findViewById(R.id.download_image_name);
3683             String imageName = downloadImageNameEditText.getText().toString();
3684
3685             // Specify the download location.
3686             if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {  // External write permission granted.
3687                 // Download to the public download directory.
3688                 downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, imageName);
3689             } else {  // External write permission denied.
3690                 // Download to the app's external download directory.
3691                 downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, imageName);
3692             }
3693
3694             // Allow `MediaScanner` to index the download if it is a media file.
3695             downloadRequest.allowScanningByMediaScanner();
3696
3697             // Add the URL as the description for the download.
3698             downloadRequest.setDescription(imageUrl);
3699
3700             // Show the download notification after the download is completed.
3701             downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
3702
3703             // Remove the lint warning below that `downloadManager` might be `null`.
3704             assert downloadManager != null;
3705
3706             // Initiate the download.
3707             downloadManager.enqueue(downloadRequest);
3708         } else {  // The image is not an HTTP or HTTPS URI.
3709             Snackbar.make(mainWebView, R.string.cannot_download_image, Snackbar.LENGTH_INDEFINITE).show();
3710         }
3711     }
3712
3713     @Override
3714     public void onDownloadFile(AppCompatDialogFragment dialogFragment, String downloadUrl) {
3715         // Download the file if it has an HTTP or HTTPS URI.
3716         if (downloadUrl.startsWith("http")) {
3717             // Get a handle for the system `DOWNLOAD_SERVICE`.
3718             DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
3719
3720             // Parse `downloadUrl`.
3721             DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(downloadUrl));
3722
3723             // Pass cookies to download manager if cookies are enabled.  This is required to download files from websites that require a login.
3724             // Code contributed 2017 Hendrik Knackstedt.  Copyright assigned to Soren Stoutner <soren@stoutner.com>.
3725             if (firstPartyCookiesEnabled) {
3726                 // Get the cookies for `downloadUrl`.
3727                 String cookies = cookieManager.getCookie(downloadUrl);
3728
3729                 // Add the cookies to `downloadRequest`.  In the HTTP request header, cookies are named `Cookie`.
3730                 downloadRequest.addRequestHeader("Cookie", cookies);
3731             }
3732
3733             // Get the file name from the dialog fragment.
3734             EditText downloadFileNameEditText = dialogFragment.getDialog().findViewById(R.id.download_file_name);
3735             String fileName = downloadFileNameEditText.getText().toString();
3736
3737             // Specify the download location.
3738             if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {  // External write permission granted.
3739                 // Download to the public download directory.
3740                 downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
3741             } else {  // External write permission denied.
3742                 // Download to the app's external download directory.
3743                 downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, fileName);
3744             }
3745
3746             // Allow `MediaScanner` to index the download if it is a media file.
3747             downloadRequest.allowScanningByMediaScanner();
3748
3749             // Add the URL as the description for the download.
3750             downloadRequest.setDescription(downloadUrl);
3751
3752             // Show the download notification after the download is completed.
3753             downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
3754
3755             // Remove the lint warning below that `downloadManager` might be `null`.
3756             assert downloadManager != null;
3757
3758             // Initiate the download.
3759             downloadManager.enqueue(downloadRequest);
3760         } else {  // The download is not an HTTP or HTTPS URI.
3761             Snackbar.make(mainWebView, R.string.cannot_download_file, Snackbar.LENGTH_INDEFINITE).show();
3762         }
3763     }
3764
3765     @Override
3766     public void onSaveBookmark(AppCompatDialogFragment dialogFragment, int selectedBookmarkDatabaseId) {
3767         // Get handles for the views from `dialogFragment`.
3768         EditText editBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_name_edittext);
3769         EditText editBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_url_edittext);
3770         RadioButton currentBookmarkIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_current_icon_radiobutton);
3771
3772         // Store the bookmark strings.
3773         String bookmarkNameString = editBookmarkNameEditText.getText().toString();
3774         String bookmarkUrlString = editBookmarkUrlEditText.getText().toString();
3775
3776         // Update the bookmark.
3777         if (currentBookmarkIconRadioButton.isChecked()) {  // Update the bookmark without changing the favorite icon.
3778             bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString);
3779         } else {  // Update the bookmark using the `WebView` favorite icon.
3780             // Convert the favorite icon to a byte array.  `0` is for lossless compression (the only option for a PNG).
3781             ByteArrayOutputStream newFavoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
3782             favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFavoriteIconByteArrayOutputStream);
3783             byte[] newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray();
3784
3785             //  Update the bookmark and the favorite icon.
3786             bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, newFavoriteIconByteArray);
3787         }
3788
3789         // Update the bookmarks cursor with the current contents of this folder.
3790         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
3791
3792         // Update the `ListView`.
3793         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
3794     }
3795
3796     @Override
3797     public void onSaveBookmarkFolder(AppCompatDialogFragment dialogFragment, int selectedFolderDatabaseId) {
3798         // Get handles for the views from `dialogFragment`.
3799         EditText editFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_folder_name_edittext);
3800         RadioButton currentFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_current_icon_radiobutton);
3801         RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_radiobutton);
3802         ImageView folderIconImageView = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_imageview);
3803
3804         // Get the new folder name.
3805         String newFolderNameString = editFolderNameEditText.getText().toString();
3806
3807         // Check if the favorite icon has changed.
3808         if (currentFolderIconRadioButton.isChecked()) {  // Only the name has changed.
3809             // Update the name in the database.
3810             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString);
3811         } else if (!currentFolderIconRadioButton.isChecked() && newFolderNameString.equals(oldFolderNameString)) {  // Only the icon has changed.
3812             // Get the new folder icon `Bitmap`.
3813             Bitmap folderIconBitmap;
3814             if (defaultFolderIconRadioButton.isChecked()) {
3815                 // Get the default folder icon and convert it to a `Bitmap`.
3816                 Drawable folderIconDrawable = folderIconImageView.getDrawable();
3817                 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
3818                 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
3819             } else {  // Use the `WebView` favorite icon.
3820                 folderIconBitmap = favoriteIconBitmap;
3821             }
3822
3823             // Convert the folder `Bitmap` to a byte array.  `0` is for lossless compression (the only option for a PNG).
3824             ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream();
3825             folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream);
3826             byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray();
3827
3828             // Update the folder icon in the database.
3829             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, folderIconByteArray);
3830         } else {  // The folder icon and the name have changed.
3831             // Get the new folder icon `Bitmap`.
3832             Bitmap folderIconBitmap;
3833             if (defaultFolderIconRadioButton.isChecked()) {
3834                 // Get the default folder icon and convert it to a `Bitmap`.
3835                 Drawable folderIconDrawable = folderIconImageView.getDrawable();
3836                 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
3837                 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
3838             } else {  // Use the `WebView` favorite icon.
3839                 folderIconBitmap = MainWebViewActivity.favoriteIconBitmap;
3840             }
3841
3842             // Convert the folder `Bitmap` to a byte array.  `0` is for lossless compression (the only option for a PNG).
3843             ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream();
3844             folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream);
3845             byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray();
3846
3847             // Update the folder name and icon in the database.
3848             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, folderIconByteArray);
3849         }
3850
3851         // Update the bookmarks cursor with the current contents of this folder.
3852         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
3853
3854         // Update the `ListView`.
3855         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
3856     }
3857
3858     @Override
3859     public void onHttpAuthenticationCancel() {
3860         // Cancel the `HttpAuthHandler`.
3861         httpAuthHandler.cancel();
3862     }
3863
3864     @Override
3865     public void onHttpAuthenticationProceed(AppCompatDialogFragment dialogFragment) {
3866         // Get handles for the `EditTexts`.
3867         EditText usernameEditText = dialogFragment.getDialog().findViewById(R.id.http_authentication_username);
3868         EditText passwordEditText = dialogFragment.getDialog().findViewById(R.id.http_authentication_password);
3869
3870         // Proceed with the HTTP authentication.
3871         httpAuthHandler.proceed(usernameEditText.getText().toString(), passwordEditText.getText().toString());
3872     }
3873
3874     public void viewSslCertificate(View view) {
3875         // Show the `ViewSslCertificateDialog` `AlertDialog` and name this instance `@string/view_ssl_certificate`.
3876         DialogFragment viewSslCertificateDialogFragment = new ViewSslCertificateDialog();
3877         viewSslCertificateDialogFragment.show(getFragmentManager(), getString(R.string.view_ssl_certificate));
3878     }
3879
3880     @Override
3881     public void onSslErrorCancel() {
3882         sslErrorHandler.cancel();
3883     }
3884
3885     @Override
3886     public void onSslErrorProceed() {
3887         sslErrorHandler.proceed();
3888     }
3889
3890     @Override
3891     public void onPinnedMismatchBack() {
3892         if (mainWebView.canGoBack()) {  // There is a back page in the history.
3893             // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
3894             formattedUrlString = "";
3895
3896             // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
3897             navigatingHistory = true;
3898
3899             // Go back.
3900             mainWebView.goBack();
3901         } else {  // There are no pages to go back to.
3902             // Load a blank page
3903             loadUrl("");
3904         }
3905     }
3906
3907     @Override
3908     public void onPinnedMismatchProceed() {
3909         // Do not check the pinned information for this domain again until the domain changes.
3910         ignorePinnedDomainInformation = true;
3911     }
3912
3913     @Override
3914     public void onUrlHistoryEntrySelected(int moveBackOrForwardSteps) {
3915         // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
3916         formattedUrlString = "";
3917
3918         // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
3919         navigatingHistory = true;
3920
3921         // Load the history entry.
3922         mainWebView.goBackOrForward(moveBackOrForwardSteps);
3923     }
3924
3925     @Override
3926     public void onClearHistory() {
3927         // Clear the history.
3928         mainWebView.clearHistory();
3929     }
3930
3931     // Override `onBackPressed` to handle the navigation drawer and `mainWebView`.
3932     @Override
3933     public void onBackPressed() {
3934         if (drawerLayout.isDrawerVisible(GravityCompat.START)) {  // The navigation drawer is open.
3935             // Close the navigation drawer.
3936             drawerLayout.closeDrawer(GravityCompat.START);
3937         } else if (drawerLayout.isDrawerVisible(GravityCompat.END)){  // The bookmarks drawer is open.
3938             if (currentBookmarksFolder.isEmpty()) {  // The home folder is displayed.
3939                 // close the bookmarks drawer.
3940                 drawerLayout.closeDrawer(GravityCompat.END);
3941             } else {  // A subfolder is displayed.
3942                 // Place the former parent folder in `currentFolder`.
3943                 currentBookmarksFolder = bookmarksDatabaseHelper.getParentFolderName(currentBookmarksFolder);
3944
3945                 // Load the new folder.
3946                 loadBookmarksFolder();
3947             }
3948
3949         } else if (mainWebView.canGoBack()) {  // There is at least one item in the `WebView` history.
3950             // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
3951             formattedUrlString = "";
3952
3953             // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
3954             navigatingHistory = true;
3955
3956             // Go back.
3957             mainWebView.goBack();
3958         } else {  // There isn't anything to do in Privacy Browser.
3959             // Pass `onBackPressed()` to the system.
3960             super.onBackPressed();
3961         }
3962     }
3963
3964     // 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.
3965     @Override
3966     public void onActivityResult(int requestCode, int resultCode, Intent data) {
3967         // File uploads only work on API >= 21.
3968         if (Build.VERSION.SDK_INT >= 21) {
3969             // Pass the file to the WebView.
3970             fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, data));
3971         }
3972     }
3973
3974     private void loadUrlFromTextBox() {
3975         // Get the text from urlTextBox and convert it to a string.  trim() removes white spaces from the beginning and end of the string.
3976         String unformattedUrlString = urlTextBox.getText().toString().trim();
3977
3978         // Check to see if `unformattedUrlString` is a valid URL.  Otherwise, convert it into a search.
3979         if (unformattedUrlString.startsWith("content://")) {
3980             // Load the entire content URL.
3981             formattedUrlString = unformattedUrlString;
3982         } else if (Patterns.WEB_URL.matcher(unformattedUrlString).matches() || unformattedUrlString.startsWith("http://") || unformattedUrlString.startsWith("https://")
3983                 || unformattedUrlString.startsWith("file://")) {
3984             // Add `https://` at the beginning if there is no protocol.  Otherwise the app will segfault.
3985             if (!unformattedUrlString.startsWith("http") && !unformattedUrlString.startsWith("file://") && !unformattedUrlString.startsWith("content://")) {
3986                 unformattedUrlString = "https://" + unformattedUrlString;
3987             }
3988
3989             // Initialize `unformattedUrl`.
3990             URL unformattedUrl = null;
3991
3992             // 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.
3993             try {
3994                 unformattedUrl = new URL(unformattedUrlString);
3995             } catch (MalformedURLException e) {
3996                 e.printStackTrace();
3997             }
3998
3999             // The ternary operator (? :) makes sure that a null pointer exception is not thrown, which would happen if `.get` was called on a `null` value.
4000             String scheme = unformattedUrl != null ? unformattedUrl.getProtocol() : null;
4001             String authority = unformattedUrl != null ? unformattedUrl.getAuthority() : null;
4002             String path = unformattedUrl != null ? unformattedUrl.getPath() : null;
4003             String query = unformattedUrl != null ? unformattedUrl.getQuery() : null;
4004             String fragment = unformattedUrl != null ? unformattedUrl.getRef() : null;
4005
4006             // Build the URI.
4007             Uri.Builder formattedUri = new Uri.Builder();
4008             formattedUri.scheme(scheme).authority(authority).path(path).query(query).fragment(fragment);
4009
4010             // Decode `formattedUri` as a `String` in `UTF-8`.
4011             try {
4012                 formattedUrlString = URLDecoder.decode(formattedUri.build().toString(), "UTF-8");
4013             } catch (UnsupportedEncodingException exception) {
4014                 // Load a blank string.
4015                 formattedUrlString = "";
4016             }
4017         } else if (unformattedUrlString.isEmpty()){  // Load a blank web site.
4018             // Load a blank string.
4019             formattedUrlString = "";
4020         } else {  // Search for the contents of the URL box.
4021             // Create an encoded URL String.
4022             String encodedUrlString;
4023
4024             // Sanitize the search input.
4025             try {
4026                 encodedUrlString = URLEncoder.encode(unformattedUrlString, "UTF-8");
4027             } catch (UnsupportedEncodingException exception) {
4028                 encodedUrlString = "";
4029             }
4030
4031             // Add the base search URL.
4032             formattedUrlString = searchURL + encodedUrlString;
4033         }
4034
4035         // 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.
4036         urlTextBox.clearFocus();
4037
4038         // Make it so.
4039         loadUrl(formattedUrlString);
4040     }
4041
4042     private void loadUrl(String url) {// Apply any custom domain settings.
4043         // Set the URL as the formatted URL string so that checking third-party requests works correctly.
4044         formattedUrlString = url;
4045
4046         // Apply the domain settings.
4047         applyDomainSettings(url, true, false);
4048
4049         // If loading a website, set `urlIsLoading` to prevent changes in the user agent on websites with redirects from reloading the current website.
4050         urlIsLoading = !url.equals("");
4051
4052         // Load the URL.
4053         mainWebView.loadUrl(url, customHeaders);
4054     }
4055
4056     public void findPreviousOnPage(View view) {
4057         // Go to the previous highlighted phrase on the page.  `false` goes backwards instead of forwards.
4058         mainWebView.findNext(false);
4059     }
4060
4061     public void findNextOnPage(View view) {
4062         // Go to the next highlighted phrase on the page. `true` goes forwards instead of backwards.
4063         mainWebView.findNext(true);
4064     }
4065
4066     public void closeFindOnPage(View view) {
4067         // Delete the contents of `find_on_page_edittext`.
4068         findOnPageEditText.setText(null);
4069
4070         // Clear the highlighted phrases.
4071         mainWebView.clearMatches();
4072
4073         // Hide the Find on Page `RelativeLayout`.
4074         findOnPageLinearLayout.setVisibility(View.GONE);
4075
4076         // Show the URL app bar.
4077         supportAppBar.setVisibility(View.VISIBLE);
4078
4079         // Hide the keyboard so we can see the webpage.  `0` indicates no additional flags.
4080         inputMethodManager.hideSoftInputFromWindow(mainWebView.getWindowToken(), 0);
4081     }
4082
4083     private void applyAppSettings() {
4084         // Get a handle for the shared preferences.
4085         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4086
4087         // Store the values from the shared preferences in variables.
4088         incognitoModeEnabled = sharedPreferences.getBoolean("incognito_mode", false);
4089         boolean doNotTrackEnabled = sharedPreferences.getBoolean("do_not_track", false);
4090         proxyThroughOrbot = sharedPreferences.getBoolean("proxy_through_orbot", false);
4091         fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean("full_screen_browsing_mode", false);
4092         hideSystemBarsOnFullscreen = sharedPreferences.getBoolean("hide_system_bars", false);
4093         translucentNavigationBarOnFullscreen = sharedPreferences.getBoolean("translucent_navigation_bar", true);
4094         downloadWithExternalApp = sharedPreferences.getBoolean("download_with_external_app", false);
4095
4096         // Apply the proxy through Orbot settings.
4097         applyProxyThroughOrbot(false);
4098
4099         // Set Do Not Track status.
4100         if (doNotTrackEnabled) {
4101             customHeaders.put("DNT", "1");
4102         } else {
4103             customHeaders.remove("DNT");
4104         }
4105
4106         // Apply the appropriate full screen mode the `SYSTEM_UI` flags.
4107         if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) {  // Privacy Browser is currently in full screen browsing mode.
4108             if (hideSystemBarsOnFullscreen) {  // Hide everything.
4109                 // Remove the translucent navigation setting if it is currently flagged.
4110                 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
4111
4112                 // Remove the translucent status bar overlay.
4113                 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4114
4115                 // Remove the translucent status bar overlay on the `Drawer Layout`, which is special and needs its own command.
4116                 drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
4117
4118                 /* SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4119                  * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4120                  * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4121                  */
4122                 rootCoordinatorLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
4123             } else {  // Hide everything except the status and navigation bars.
4124                 // Remove any `SYSTEM_UI` flags from `rootCoordinatorLayout`.
4125                 rootCoordinatorLayout.setSystemUiVisibility(0);
4126
4127                 // Add the translucent status flag if it is unset.
4128                 getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4129
4130                 if (translucentNavigationBarOnFullscreen) {
4131                     // Set the navigation bar to be translucent.
4132                     getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
4133                 } else {
4134                     // Set the navigation bar to be black.
4135                     getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
4136                 }
4137             }
4138         } else {  // Privacy Browser is not in full screen browsing mode.
4139             // 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.
4140             inFullScreenBrowsingMode = false;
4141
4142             // Show the `appBar` if `findOnPageLinearLayout` is not visible.
4143             if (findOnPageLinearLayout.getVisibility() == View.GONE) {
4144                 appBar.show();
4145             }
4146
4147             // Show the `BannerAd` in the free flavor.
4148             if (BuildConfig.FLAVOR.contentEquals("free")) {
4149                 // Initialize the ad.  The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
4150                 AdHelper.initializeAds(findViewById(R.id.adview), getApplicationContext(), getFragmentManager(), getString(R.string.google_app_id), getString(R.string.ad_unit_id));
4151             }
4152
4153             // Remove any `SYSTEM_UI` flags from `rootCoordinatorLayout`.
4154             rootCoordinatorLayout.setSystemUiVisibility(0);
4155
4156             // Remove the translucent navigation bar flag if it is set.
4157             getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
4158
4159             // Add the translucent status flag if it is unset.  This also resets `drawerLayout's` `View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN`.
4160             getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4161
4162             // Constrain `rootCoordinatorLayout` inside the status and navigation bars.
4163             rootCoordinatorLayout.setFitsSystemWindows(true);
4164         }
4165     }
4166
4167     // `reloadWebsite` is used if returning from the Domains activity.  Otherwise JavaScript might not function correctly if it is newly enabled.
4168     // The deprecated `.getDrawable()` must be used until the minimum API >= 21.
4169     @SuppressWarnings("deprecation")
4170     private boolean applyDomainSettings(String url, boolean resetFavoriteIcon, boolean reloadWebsite) {
4171         // Get the current user agent.
4172         String initialUserAgent = mainWebView.getSettings().getUserAgentString();
4173
4174         // Initialize a variable to track if the user agent changes.
4175         boolean userAgentChanged = false;
4176
4177         // Parse the URL into a URI.
4178         Uri uri = Uri.parse(url);
4179
4180         // Extract the domain from `uri`.
4181         String hostName = uri.getHost();
4182
4183         // Initialize `loadingNewDomainName`.
4184         boolean loadingNewDomainName;
4185
4186         // If either `hostName` or `currentDomainName` are `null`, run the options for loading a new domain name.
4187         // The lint suggestion to simplify the `if` statement is incorrect, because `hostName.equals(currentDomainName)` can produce a `null object reference.`
4188         //noinspection SimplifiableIfStatement
4189         if ((hostName == null) || (currentDomainName == null)) {
4190             loadingNewDomainName = true;
4191         } else {  // Determine if `hostName` equals `currentDomainName`.
4192             loadingNewDomainName = !hostName.equals(currentDomainName);
4193         }
4194
4195         // Strings don't like to be null.
4196         if (hostName == null) {
4197             hostName = "";
4198         }
4199
4200         // 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.
4201         if (loadingNewDomainName) {
4202             // Set the new `hostname` as the `currentDomainName`.
4203             currentDomainName = hostName;
4204
4205             // Reset the ignoring of pinned domain information.
4206             ignorePinnedDomainInformation = false;
4207
4208             // Reset the favorite icon if specified.
4209             if (resetFavoriteIcon) {
4210                 favoriteIconBitmap = favoriteIconDefaultBitmap;
4211                 favoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(favoriteIconBitmap, 64, 64, true));
4212             }
4213
4214             // Initialize the database handler.  The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
4215             DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
4216
4217             // Get a full cursor from `domainsDatabaseHelper`.
4218             Cursor domainNameCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomain();
4219
4220             // Initialize `domainSettingsSet`.
4221             Set<String> domainSettingsSet = new HashSet<>();
4222
4223             // Get the domain name column index.
4224             int domainNameColumnIndex = domainNameCursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME);
4225
4226             // Populate `domainSettingsSet`.
4227             for (int i = 0; i < domainNameCursor.getCount(); i++) {
4228                 // Move `domainsCursor` to the current row.
4229                 domainNameCursor.moveToPosition(i);
4230
4231                 // Store the domain name in `domainSettingsSet`.
4232                 domainSettingsSet.add(domainNameCursor.getString(domainNameColumnIndex));
4233             }
4234
4235             // Close `domainNameCursor.
4236             domainNameCursor.close();
4237
4238             // Initialize variables to track if domain settings will be applied and, if so, under which name.
4239             domainSettingsApplied = false;
4240             String domainNameInDatabase = null;
4241
4242             // Check the hostname.
4243             if (domainSettingsSet.contains(hostName)) {
4244                 domainSettingsApplied = true;
4245                 domainNameInDatabase = hostName;
4246             }
4247
4248             // Check all the subdomains of the host name against wildcard domains in the domain cursor.
4249             while (!domainSettingsApplied && hostName.contains(".")) {  // Stop checking if domain settings are already applied or there are no more `.` in the host name.
4250                 if (domainSettingsSet.contains("*." + hostName)) {  // Check the host name prepended by `*.`.
4251                     // Apply the domain settings.
4252                     domainSettingsApplied = true;
4253
4254                     // Store the applied domain names as it appears in the database.
4255                     domainNameInDatabase = "*." + hostName;
4256                 }
4257
4258                 // Strip out the lowest subdomain of of the host name.
4259                 hostName = hostName.substring(hostName.indexOf(".") + 1);
4260             }
4261
4262
4263             // Get a handle for the shared preference.
4264             SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4265
4266             // Store the general preference information.
4267             String defaultFontSizeString = sharedPreferences.getString("font_size", getString(R.string.font_size_default_value));
4268             String defaultUserAgentName = sharedPreferences.getString("user_agent", getString(R.string.user_agent_default_value));
4269             defaultCustomUserAgentString = sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value));
4270             boolean defaultSwipeToRefresh = sharedPreferences.getBoolean("swipe_to_refresh", true);
4271             nightMode = sharedPreferences.getBoolean("night_mode", false);
4272             boolean displayWebpageImages = sharedPreferences.getBoolean("display_webpage_images", true);
4273
4274             if (domainSettingsApplied) {  // The url has custom domain settings.
4275                 // Get a cursor for the current host and move it to the first position.
4276                 Cursor currentHostDomainSettingsCursor = domainsDatabaseHelper.getCursorForDomainName(domainNameInDatabase);
4277                 currentHostDomainSettingsCursor.moveToFirst();
4278
4279                 // Get the settings from the cursor.
4280                 domainSettingsDatabaseId = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper._ID)));
4281                 javaScriptEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)) == 1);
4282                 firstPartyCookiesEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FIRST_PARTY_COOKIES)) == 1);
4283                 thirdPartyCookiesEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_THIRD_PARTY_COOKIES)) == 1);
4284                 domStorageEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_DOM_STORAGE)) == 1);
4285                 // Form data can be removed once the minimum API >= 26.
4286                 saveFormDataEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FORM_DATA)) == 1);
4287                 easyListEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYLIST)) == 1);
4288                 easyPrivacyEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYPRIVACY)) == 1);
4289                 fanboysAnnoyanceListEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST)) == 1);
4290                 fanboysSocialBlockingListEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)) == 1);
4291                 ultraPrivacyEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY)) == 1);
4292                 blockAllThirdPartyRequests = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS)) == 1);
4293                 String userAgentName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.USER_AGENT));
4294                 int fontSize = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.FONT_SIZE));
4295                 int swipeToRefreshInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SWIPE_TO_REFRESH));
4296                 int nightModeInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.NIGHT_MODE));
4297                 int displayWebpageImagesInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.DISPLAY_IMAGES));
4298                 pinnedSslCertificate = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)) == 1);
4299                 pinnedSslIssuedToCName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME));
4300                 pinnedSslIssuedToOName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION));
4301                 pinnedSslIssuedToUName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT));
4302                 pinnedSslIssuedByCName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME));
4303                 pinnedSslIssuedByOName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION));
4304                 pinnedSslIssuedByUName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT));
4305                 pinnedIpAddresses = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_IP_ADDRESSES)) == 1);
4306                 pinnedHostIpAddresses = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.IP_ADDRESSES));
4307
4308                 // Set `nightMode` according to `nightModeInt`.  If `nightModeInt` is `DomainsDatabaseHelper.NIGHT_MODE_SYSTEM_DEFAULT` the current setting from `sharedPreferences` will be used.
4309                 switch (nightModeInt) {
4310                     case DomainsDatabaseHelper.NIGHT_MODE_ENABLED:
4311                         nightMode = true;
4312                         break;
4313
4314                     case DomainsDatabaseHelper.NIGHT_MODE_DISABLED:
4315                         nightMode = false;
4316                         break;
4317                 }
4318
4319                 // Store the domain JavaScript status.  This is used by the options menu night mode toggle.
4320                 domainSettingsJavaScriptEnabled = javaScriptEnabled;
4321
4322                 // Enable JavaScript if night mode is enabled.
4323                 if (nightMode) {
4324                     javaScriptEnabled = true;
4325                 }
4326
4327                 // Set the pinned SSL certificate start date to `null` if the saved date `long` is 0.
4328                 if (currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)) == 0) {
4329                     pinnedSslStartDate = null;
4330                 } else {
4331                     pinnedSslStartDate = new Date(currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)));
4332                 }
4333
4334                 // Set the pinned SSL certificate end date to `null` if the saved date `long` is 0.
4335                 if (currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)) == 0) {
4336                     pinnedSslEndDate = null;
4337                 } else {
4338                     pinnedSslEndDate = new Date(currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)));
4339                 }
4340
4341                 // Close `currentHostDomainSettingsCursor`.
4342                 currentHostDomainSettingsCursor.close();
4343
4344                 // Apply the domain settings.
4345                 mainWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled);
4346                 cookieManager.setAcceptCookie(firstPartyCookiesEnabled);
4347                 mainWebView.getSettings().setDomStorageEnabled(domStorageEnabled);
4348
4349                 // Apply the form data setting if the API < 26.
4350                 if (Build.VERSION.SDK_INT < 26) {
4351                     mainWebView.getSettings().setSaveFormData(saveFormDataEnabled);
4352                 }
4353
4354                 // Apply the font size.
4355                 if (fontSize == 0) {  // Apply the default font size.
4356                     mainWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
4357                 } else {  // Apply the specified font size.
4358                     mainWebView.getSettings().setTextZoom(fontSize);
4359                 }
4360
4361                 // Set third-party cookies status if API >= 21.
4362                 if (Build.VERSION.SDK_INT >= 21) {
4363                     cookieManager.setAcceptThirdPartyCookies(mainWebView, thirdPartyCookiesEnabled);
4364                 }
4365
4366                 // 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.
4367                 // <https://redmine.stoutner.com/issues/160>
4368                 if (!urlIsLoading) {
4369                     // Set the user agent.
4370                     if (userAgentName.equals(getString(R.string.system_default_user_agent))) {  // Use the system default user agent.
4371                         // Get the array position of the default user agent name.
4372                         int defaultUserAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
4373
4374                         // Set the user agent according to the system default.
4375                         switch (defaultUserAgentArrayPosition) {
4376                             case UNRECOGNIZED_USER_AGENT:  // The default user agent name is not on the canonical list.
4377                                 // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
4378                                 mainWebView.getSettings().setUserAgentString(defaultUserAgentName);
4379                                 break;
4380
4381                             case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
4382                                 // Set the user agent to `""`, which uses the default value.
4383                                 mainWebView.getSettings().setUserAgentString("");
4384                                 break;
4385
4386                             case SETTINGS_CUSTOM_USER_AGENT:
4387                                 // Set the custom user agent.
4388                                 mainWebView.getSettings().setUserAgentString(defaultCustomUserAgentString);
4389                                 break;
4390
4391                             default:
4392                                 // Get the user agent string from the user agent data array
4393                                 mainWebView.getSettings().setUserAgentString(userAgentDataArray[defaultUserAgentArrayPosition]);
4394                         }
4395                     } else {  // Set the user agent according to the stored name.
4396                         // Get the array position of the user agent name.
4397                         int userAgentArrayPosition = userAgentNamesArray.getPosition(userAgentName);
4398
4399                         switch (userAgentArrayPosition) {
4400                             case UNRECOGNIZED_USER_AGENT:  // The user agent name contains a custom user agent.
4401                                 mainWebView.getSettings().setUserAgentString(userAgentName);
4402                                 break;
4403
4404                             case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
4405                                 // Set the user agent to `""`, which uses the default value.
4406                                 mainWebView.getSettings().setUserAgentString("");
4407                                 break;
4408
4409                             default:
4410                                 // Get the user agent string from the user agent data array.
4411                                 mainWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
4412                         }
4413                     }
4414
4415                     // Store the applied user agent string, which is used in the View Source activity.
4416                     appliedUserAgentString = mainWebView.getSettings().getUserAgentString();
4417
4418                     // Update the user agent change tracker.
4419                     userAgentChanged = !appliedUserAgentString.equals(initialUserAgent);
4420                 }
4421
4422                 // Set swipe to refresh.
4423                 switch (swipeToRefreshInt) {
4424                     case DomainsDatabaseHelper.SWIPE_TO_REFRESH_SYSTEM_DEFAULT:
4425                         // Set swipe to refresh according to the default.
4426                         swipeRefreshLayout.setEnabled(defaultSwipeToRefresh);
4427                         break;
4428
4429                     case DomainsDatabaseHelper.SWIPE_TO_REFRESH_ENABLED:
4430                         // Enable swipe to refresh.
4431                         swipeRefreshLayout.setEnabled(true);
4432                         break;
4433
4434                     case DomainsDatabaseHelper.SWIPE_TO_REFRESH_DISABLED:
4435                         // Disable swipe to refresh.
4436                         swipeRefreshLayout.setEnabled(false);
4437                 }
4438
4439                 // Set the loading of webpage images.
4440                 switch (displayWebpageImagesInt) {
4441                     case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_SYSTEM_DEFAULT:
4442                         mainWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
4443                         break;
4444
4445                     case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_ENABLED:
4446                         mainWebView.getSettings().setLoadsImagesAutomatically(true);
4447                         break;
4448
4449                     case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_DISABLED:
4450                         mainWebView.getSettings().setLoadsImagesAutomatically(false);
4451                         break;
4452                 }
4453
4454                 // 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.
4455                 if (darkTheme) {
4456                     urlAppBarRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_dark_blue));
4457                 } else {
4458                     urlAppBarRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_light_green));
4459                 }
4460             } else {  // The new URL does not have custom domain settings.  Load the defaults.
4461                 // Store the values from `sharedPreferences` in variables.
4462                 javaScriptEnabled = sharedPreferences.getBoolean("javascript", false);
4463                 firstPartyCookiesEnabled = sharedPreferences.getBoolean("first_party_cookies", false);
4464                 thirdPartyCookiesEnabled = sharedPreferences.getBoolean("third_party_cookies", false);
4465                 domStorageEnabled = sharedPreferences.getBoolean("dom_storage", false);
4466                 saveFormDataEnabled = sharedPreferences.getBoolean("save_form_data", false);  // Form data can be removed once the minimum API >= 26.
4467                 easyListEnabled = sharedPreferences.getBoolean("easylist", true);
4468                 easyPrivacyEnabled = sharedPreferences.getBoolean("easyprivacy", true);
4469                 fanboysAnnoyanceListEnabled = sharedPreferences.getBoolean("fanboys_annoyance_list", true);
4470                 fanboysSocialBlockingListEnabled = sharedPreferences.getBoolean("fanboys_social_blocking_list", true);
4471                 ultraPrivacyEnabled = sharedPreferences.getBoolean("ultraprivacy", true);
4472                 blockAllThirdPartyRequests = sharedPreferences.getBoolean("block_all_third_party_requests", false);
4473
4474                 // Set `javaScriptEnabled` to be `true` if `night_mode` is `true`.
4475                 if (nightMode) {
4476                     javaScriptEnabled = true;
4477                 }
4478
4479                 // Apply the default settings.
4480                 mainWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled);
4481                 cookieManager.setAcceptCookie(firstPartyCookiesEnabled);
4482                 mainWebView.getSettings().setDomStorageEnabled(domStorageEnabled);
4483                 mainWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
4484                 swipeRefreshLayout.setEnabled(defaultSwipeToRefresh);
4485
4486                 // Apply the form data setting if the API < 26.
4487                 if (Build.VERSION.SDK_INT < 26) {
4488                     mainWebView.getSettings().setSaveFormData(saveFormDataEnabled);
4489                 }
4490
4491                 // Reset the pinned variables.
4492                 domainSettingsDatabaseId = -1;
4493                 pinnedSslCertificate = false;
4494                 pinnedSslIssuedToCName = "";
4495                 pinnedSslIssuedToOName = "";
4496                 pinnedSslIssuedToUName = "";
4497                 pinnedSslIssuedByCName = "";
4498                 pinnedSslIssuedByOName = "";
4499                 pinnedSslIssuedByUName = "";
4500                 pinnedSslStartDate = null;
4501                 pinnedSslEndDate = null;
4502                 pinnedIpAddresses = false;
4503                 pinnedHostIpAddresses = "";
4504
4505                 // Set third-party cookies status if API >= 21.
4506                 if (Build.VERSION.SDK_INT >= 21) {
4507                     cookieManager.setAcceptThirdPartyCookies(mainWebView, thirdPartyCookiesEnabled);
4508                 }
4509
4510                 // 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.
4511                 // <https://redmine.stoutner.com/issues/160>
4512                 if (!urlIsLoading) {
4513                     // Get the array position of the user agent name.
4514                     int userAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
4515
4516                     // Set the user agent.
4517                     switch (userAgentArrayPosition) {
4518                         case UNRECOGNIZED_USER_AGENT:  // The default user agent name is not on the canonical list.
4519                             // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
4520                             mainWebView.getSettings().setUserAgentString(defaultUserAgentName);
4521                             break;
4522
4523                         case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
4524                             // Set the user agent to `""`, which uses the default value.
4525                             mainWebView.getSettings().setUserAgentString("");
4526                             break;
4527
4528                         case SETTINGS_CUSTOM_USER_AGENT:
4529                             // Set the custom user agent.
4530                             mainWebView.getSettings().setUserAgentString(defaultCustomUserAgentString);
4531                             break;
4532
4533                         default:
4534                             // Get the user agent string from the user agent data array
4535                             mainWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
4536                     }
4537
4538                     // Store the applied user agent string, which is used in the View Source activity.
4539                     appliedUserAgentString = mainWebView.getSettings().getUserAgentString();
4540
4541                     // Update the user agent change tracker.
4542                     userAgentChanged = !appliedUserAgentString.equals(initialUserAgent);
4543                 }
4544
4545                 // Set the loading of webpage images.
4546                 mainWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
4547
4548                 // Set a transparent background on `urlTextBox`.  The deprecated `.getDrawable()` must be used until the minimum API >= 21.
4549                 urlAppBarRelativeLayout.setBackgroundDrawable(getResources().getDrawable(R.color.transparent));
4550             }
4551
4552             // Close the domains database helper.
4553             domainsDatabaseHelper.close();
4554
4555             // Update the privacy icons, but only if `mainMenu` has already been populated.
4556             if (mainMenu != null) {
4557                 updatePrivacyIcons(true);
4558             }
4559         }
4560
4561         // Reload the website if returning from the Domains activity.
4562         if (reloadWebsite) {
4563             mainWebView.reload();
4564         }
4565
4566         // Return the user agent changed status.
4567         return userAgentChanged;
4568     }
4569
4570     private void applyProxyThroughOrbot(boolean reloadWebsite) {
4571         // Get a handle for the shared preferences.
4572         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4573
4574         // Get the search preferences.
4575         String homepageString = sharedPreferences.getString("homepage", getString(R.string.homepage_default_value));
4576         String torHomepageString = sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value));
4577         String torSearchString = sharedPreferences.getString("tor_search", getString(R.string.tor_search_default_value));
4578         String torSearchCustomUrlString = sharedPreferences.getString("tor_search_custom_url", getString(R.string.tor_search_custom_url_default_value));
4579         String searchString = sharedPreferences.getString("search", getString(R.string.search_default_value));
4580         String searchCustomUrlString = sharedPreferences.getString("search_custom_url", getString(R.string.search_custom_url_default_value));
4581
4582         // Set the homepage, search, and proxy options.
4583         if (proxyThroughOrbot) {  // Set the Tor options.
4584             // Set `torHomepageString` as `homepage`.
4585             homepage = torHomepageString;
4586
4587             // If formattedUrlString is null assign the homepage to it.
4588             if (formattedUrlString == null) {
4589                 formattedUrlString = homepage;
4590             }
4591
4592             // Set the search URL.
4593             if (torSearchString.equals("Custom URL")) {  // Get the custom URL string.
4594                 searchURL = torSearchCustomUrlString;
4595             } else {  // Use the string from the pre-built list.
4596                 searchURL = torSearchString;
4597             }
4598
4599             // Set the proxy.  `this` refers to the current activity where an `AlertDialog` might be displayed.
4600             OrbotProxyHelper.setProxy(getApplicationContext(), this, "localhost", "8118");
4601
4602             // Set the `appBar` background to indicate proxying through Orbot is enabled.  `this` refers to the context.
4603             if (darkTheme) {
4604                 appBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.dark_blue_30));
4605             } else {
4606                 appBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.blue_50));
4607             }
4608
4609             // Check to see if Orbot is ready.
4610             if (!orbotStatus.equals("ON")) {  // Orbot is not ready.
4611                 // Set `waitingForOrbot`.
4612                 waitingForOrbot = true;
4613
4614                 // Disable the wide view port so that the waiting for Orbot text is displayed correctly.
4615                 mainWebView.getSettings().setUseWideViewPort(false);
4616
4617                 // Load a waiting page.  `null` specifies no encoding, which defaults to ASCII.
4618                 mainWebView.loadData(waitingForOrbotHtmlString, "text/html", null);
4619             } else if (reloadWebsite) {  // Orbot is ready and the website should be reloaded.
4620                 // Reload the website.
4621                 mainWebView.reload();
4622             }
4623         } else {  // Set the non-Tor options.
4624             // Set `homepageString` as `homepage`.
4625             homepage = homepageString;
4626
4627             // If formattedUrlString is null assign the homepage to it.
4628             if (formattedUrlString == null) {
4629                 formattedUrlString = homepage;
4630             }
4631
4632             // Set the search URL.
4633             if (searchString.equals("Custom URL")) {  // Get the custom URL string.
4634                 searchURL = searchCustomUrlString;
4635             } else {  // Use the string from the pre-built list.
4636                 searchURL = searchString;
4637             }
4638
4639             // Reset the proxy to default.  The host is `""` and the port is `"0"`.
4640             OrbotProxyHelper.setProxy(getApplicationContext(), this, "", "0");
4641
4642             // Set the default `appBar` background.  `this` refers to the context.
4643             if (darkTheme) {
4644                 appBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.gray_900));
4645             } else {
4646                 appBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.gray_100));
4647             }
4648
4649             // Reset `waitingForOrbot.
4650             waitingForOrbot = false;
4651
4652             // Reload the website if requested.
4653             if (reloadWebsite) {
4654                 mainWebView.reload();
4655             }
4656         }
4657     }
4658
4659     private void updatePrivacyIcons(boolean runInvalidateOptionsMenu) {
4660         // Get handles for the menu items.
4661         MenuItem privacyMenuItem = mainMenu.findItem(R.id.toggle_javascript);
4662         MenuItem firstPartyCookiesMenuItem = mainMenu.findItem(R.id.toggle_first_party_cookies);
4663         MenuItem domStorageMenuItem = mainMenu.findItem(R.id.toggle_dom_storage);
4664         MenuItem refreshMenuItem = mainMenu.findItem(R.id.refresh);
4665
4666         // Update the privacy icon.
4667         if (javaScriptEnabled) {  // JavaScript is enabled.
4668             privacyMenuItem.setIcon(R.drawable.javascript_enabled);
4669         } else if (firstPartyCookiesEnabled) {  // JavaScript is disabled but cookies are enabled.
4670             privacyMenuItem.setIcon(R.drawable.warning);
4671         } else {  // All the dangerous features are disabled.
4672             privacyMenuItem.setIcon(R.drawable.privacy_mode);
4673         }
4674
4675         // Update the first-party cookies icon.
4676         if (firstPartyCookiesEnabled) {  // First-party cookies are enabled.
4677             firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_enabled);
4678         } else {  // First-party cookies are disabled.
4679             if (darkTheme) {
4680                 firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_dark);
4681             } else {
4682                 firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_light);
4683             }
4684         }
4685
4686         // Update the DOM storage icon.
4687         if (javaScriptEnabled && domStorageEnabled) {  // Both JavaScript and DOM storage are enabled.
4688             domStorageMenuItem.setIcon(R.drawable.dom_storage_enabled);
4689         } else if (javaScriptEnabled) {  // JavaScript is enabled but DOM storage is disabled.
4690             if (darkTheme) {
4691                 domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_dark);
4692             } else {
4693                 domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_light);
4694             }
4695         } else {  // JavaScript is disabled, so DOM storage is ghosted.
4696             if (darkTheme) {
4697                 domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_dark);
4698             } else {
4699                 domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_light);
4700             }
4701         }
4702
4703         // Update the refresh icon.
4704         if (darkTheme) {
4705             refreshMenuItem.setIcon(R.drawable.refresh_enabled_dark);
4706         } else {
4707             refreshMenuItem.setIcon(R.drawable.refresh_enabled_light);
4708         }
4709
4710         // `invalidateOptionsMenu` calls `onPrepareOptionsMenu()` and redraws the icons in the `AppBar`.
4711         if (runInvalidateOptionsMenu) {
4712             invalidateOptionsMenu();
4713         }
4714     }
4715
4716     private void openUrlWithExternalApp(String url) {
4717         // Create a download intent.  Not specifying the action type will display the maximum number of options.
4718         Intent downloadIntent = new Intent();
4719
4720         // Set the URI and the MIME type.  Specifying `text/html` displays a good number of options.
4721         downloadIntent.setDataAndType(Uri.parse(url), "text/html");
4722
4723         // Flag the intent to open in a new task.
4724         downloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4725
4726         // Show the chooser.
4727         startActivity(Intent.createChooser(downloadIntent, getString(R.string.open_with)));
4728     }
4729
4730     private void highlightUrlText() {
4731         // Only highlight the URL text if the box is not currently selected.
4732         if (!urlTextBox.hasFocus()) {
4733             // Get the URL string.
4734             String urlString = urlTextBox.getText().toString();
4735
4736             // Highlight the URL according to the protocol.
4737             if (urlString.startsWith("file://")) {  // This is a file URL.
4738                 // De-emphasize only the protocol.
4739                 urlTextBox.getText().setSpan(initialGrayColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4740             } else if (urlString.startsWith("content://")) {
4741                 // De-emphasize only the protocol.
4742                 urlTextBox.getText().setSpan(initialGrayColorSpan, 0, 10, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4743             } else {  // This is a web URL.
4744                 // Get the index of the `/` immediately after the domain name.
4745                 int endOfDomainName = urlString.indexOf("/", (urlString.indexOf("//") + 2));
4746
4747                 // Create a base URL string.
4748                 String baseUrl;
4749
4750                 // Get the base URL.
4751                 if (endOfDomainName > 0) {  // There is at least one character after the base URL.
4752                     // Get the base URL.
4753                     baseUrl = urlString.substring(0, endOfDomainName);
4754                 } else {  // There are no characters after the base URL.
4755                     // Set the base URL to be the entire URL string.
4756                     baseUrl = urlString;
4757                 }
4758
4759                 // Get the index of the last `.` in the domain.
4760                 int lastDotIndex = baseUrl.lastIndexOf(".");
4761
4762                 // Get the index of the penultimate `.` in the domain.
4763                 int penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1);
4764
4765                 // Markup the beginning of the URL.
4766                 if (urlString.startsWith("http://")) {  // Highlight the protocol of connections that are not encrypted.
4767                     urlTextBox.getText().setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4768
4769                     // De-emphasize subdomains.
4770                     if (penultimateDotIndex > 0) {  // There is more than one subdomain in the domain name.
4771                         urlTextBox.getText().setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4772                     }
4773                 } else if (urlString.startsWith("https://")) {  // De-emphasize the protocol of connections that are encrypted.
4774                     if (penultimateDotIndex > 0) {  // There is more than one subdomain in the domain name.
4775                         // De-emphasize the protocol and the additional subdomains.
4776                         urlTextBox.getText().setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4777                     } else {  // There is only one subdomain in the domain name.
4778                         // De-emphasize only the protocol.
4779                         urlTextBox.getText().setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4780                     }
4781                 }
4782
4783                 // De-emphasize the text after the domain name.
4784                 if (endOfDomainName > 0) {
4785                     urlTextBox.getText().setSpan(finalGrayColorSpan, endOfDomainName, urlString.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4786                 }
4787             }
4788         }
4789     }
4790
4791     private void loadBookmarksFolder() {
4792         // Update the bookmarks cursor with the contents of the bookmarks database for the current folder.
4793         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
4794
4795         // Populate the bookmarks cursor adapter.  `this` specifies the `Context`.  `false` disables `autoRequery`.
4796         bookmarksCursorAdapter = new CursorAdapter(this, bookmarksCursor, false) {
4797             @Override
4798             public View newView(Context context, Cursor cursor, ViewGroup parent) {
4799                 // Inflate the individual item layout.  `false` does not attach it to the root.
4800                 return getLayoutInflater().inflate(R.layout.bookmarks_drawer_item_linearlayout, parent, false);
4801             }
4802
4803             @Override
4804             public void bindView(View view, Context context, Cursor cursor) {
4805                 // Get handles for the views.
4806                 ImageView bookmarkFavoriteIcon = view.findViewById(R.id.bookmark_favorite_icon);
4807                 TextView bookmarkNameTextView = view.findViewById(R.id.bookmark_name);
4808
4809                 // Get the favorite icon byte array from the cursor.
4810                 byte[] favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON));
4811
4812                 // Convert the byte array to a `Bitmap` beginning at the first byte and ending at the last.
4813                 Bitmap favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.length);
4814
4815                 // Display the bitmap in `bookmarkFavoriteIcon`.
4816                 bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap);
4817
4818                 // Get the bookmark name from the cursor and display it in `bookmarkNameTextView`.
4819                 String bookmarkNameString = cursor.getString(cursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
4820                 bookmarkNameTextView.setText(bookmarkNameString);
4821
4822                 // Make the font bold for folders.
4823                 if (cursor.getInt(cursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {
4824                     bookmarkNameTextView.setTypeface(Typeface.DEFAULT_BOLD);
4825                 } else {  // Reset the font to default for normal bookmarks.
4826                     bookmarkNameTextView.setTypeface(Typeface.DEFAULT);
4827                 }
4828             }
4829         };
4830
4831         // Populate the `ListView` with the adapter.
4832         bookmarksListView.setAdapter(bookmarksCursorAdapter);
4833
4834         // Set the bookmarks drawer title.
4835         if (currentBookmarksFolder.isEmpty()) {
4836             bookmarksTitleTextView.setText(R.string.bookmarks);
4837         } else {
4838             bookmarksTitleTextView.setText(currentBookmarksFolder);
4839         }
4840     }
4841
4842     private void openWithApp(String url) {
4843         // Create the open with intent with `ACTION_VIEW`.
4844         Intent openWithAppIntent = new Intent(Intent.ACTION_VIEW);
4845
4846         // Set the URI but not the MIME type.  This should open all available apps.
4847         openWithAppIntent.setData(Uri.parse(url));
4848
4849         // Flag the intent to open in a new task.
4850         openWithAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4851
4852         // Show the chooser.
4853         startActivity(openWithAppIntent);
4854     }
4855
4856     private void openWithBrowser(String url) {
4857         // Create the open with intent with `ACTION_VIEW`.
4858         Intent openWithBrowserIntent = new Intent(Intent.ACTION_VIEW);
4859
4860         // Set the URI and the MIME type.  `"text/html"` should load browser options.
4861         openWithBrowserIntent.setDataAndType(Uri.parse(url), "text/html");
4862
4863         // Flag the intent to open in a new task.
4864         openWithBrowserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4865
4866         // Show the chooser.
4867         startActivity(openWithBrowserIntent);
4868     }
4869
4870     private static void checkPinnedMismatch() {
4871         if ((pinnedSslCertificate || pinnedIpAddresses) && !ignorePinnedDomainInformation) {
4872             // Initialize the current SSL certificate variables.
4873             String currentWebsiteIssuedToCName = "";
4874             String currentWebsiteIssuedToOName = "";
4875             String currentWebsiteIssuedToUName = "";
4876             String currentWebsiteIssuedByCName = "";
4877             String currentWebsiteIssuedByOName = "";
4878             String currentWebsiteIssuedByUName = "";
4879             Date currentWebsiteSslStartDate = null;
4880             Date currentWebsiteSslEndDate = null;
4881
4882
4883             // Extract the individual pieces of information from the current website SSL certificate if it is not null.
4884             if (sslCertificate != null) {
4885                 currentWebsiteIssuedToCName = sslCertificate.getIssuedTo().getCName();
4886                 currentWebsiteIssuedToOName = sslCertificate.getIssuedTo().getOName();
4887                 currentWebsiteIssuedToUName = sslCertificate.getIssuedTo().getUName();
4888                 currentWebsiteIssuedByCName = sslCertificate.getIssuedBy().getCName();
4889                 currentWebsiteIssuedByOName = sslCertificate.getIssuedBy().getOName();
4890                 currentWebsiteIssuedByUName = sslCertificate.getIssuedBy().getUName();
4891                 currentWebsiteSslStartDate = sslCertificate.getValidNotBeforeDate();
4892                 currentWebsiteSslEndDate = sslCertificate.getValidNotAfterDate();
4893             }
4894
4895             // 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`.
4896             String currentWebsiteSslStartDateString = "";
4897             String currentWebsiteSslEndDateString = "";
4898             String pinnedSslStartDateString = "";
4899             String pinnedSslEndDateString = "";
4900
4901             // Convert the `Dates` to `Strings` if they are not `null`.
4902             if (currentWebsiteSslStartDate != null) {
4903                 currentWebsiteSslStartDateString = currentWebsiteSslStartDate.toString();
4904             }
4905
4906             if (currentWebsiteSslEndDate != null) {
4907                 currentWebsiteSslEndDateString = currentWebsiteSslEndDate.toString();
4908             }
4909
4910             if (pinnedSslStartDate != null) {
4911                 pinnedSslStartDateString = pinnedSslStartDate.toString();
4912             }
4913
4914             if (pinnedSslEndDate != null) {
4915                 pinnedSslEndDateString = pinnedSslEndDate.toString();
4916             }
4917
4918             // Check to see if the pinned information matches the current information.
4919             if ((pinnedIpAddresses && !currentHostIpAddresses.equals(pinnedHostIpAddresses)) || (pinnedSslCertificate && (!currentWebsiteIssuedToCName.equals(pinnedSslIssuedToCName) ||
4920                     !currentWebsiteIssuedToOName.equals(pinnedSslIssuedToOName) || !currentWebsiteIssuedToUName.equals(pinnedSslIssuedToUName) ||
4921                     !currentWebsiteIssuedByCName.equals(pinnedSslIssuedByCName) || !currentWebsiteIssuedByOName.equals(pinnedSslIssuedByOName) ||
4922                     !currentWebsiteIssuedByUName.equals(pinnedSslIssuedByUName) || !currentWebsiteSslStartDateString.equals(pinnedSslStartDateString) ||
4923                     !currentWebsiteSslEndDateString.equals(pinnedSslEndDateString)))) {
4924
4925                 // Get a handle for the pinned mismatch alert dialog.
4926                 AppCompatDialogFragment pinnedMismatchDialogFragment = PinnedMismatchDialog.displayDialog(pinnedSslCertificate, pinnedIpAddresses);
4927
4928                 // Show the pinned mismatch alert dialog.
4929                 pinnedMismatchDialogFragment.show(supportFragmentManager, "Pinned Mismatch");
4930             }
4931         }
4932     }
4933
4934     // This must run asynchronously because it involves a network request.  `String` declares the parameters.  `Void` does not declare progress units.  `String` contains the results.
4935     private static class GetHostIpAddresses extends AsyncTask<String, Void, String> {
4936         // The weak references are used to determine if the activity have disappeared while the AsyncTask is running.
4937         private final WeakReference<Activity> activityWeakReference;
4938
4939         GetHostIpAddresses(Activity activity) {
4940             // Populate the weak references.
4941             activityWeakReference = new WeakReference<>(activity);
4942         }
4943
4944         // `onPreExecute()` operates on the UI thread.
4945         @Override
4946         protected void onPreExecute() {
4947             // Get a handle for the activity.
4948             Activity activity = activityWeakReference.get();
4949
4950             // Abort if the activity is gone.
4951             if ((activity == null) || activity.isFinishing()) {
4952                 return;
4953             }
4954
4955             // Set the getting IP addresses tracker.
4956             gettingIpAddresses = true;
4957         }
4958
4959
4960         @Override
4961         protected String doInBackground(String... domainName) {
4962             // Get a handle for the activity.
4963             Activity activity = activityWeakReference.get();
4964
4965             // Abort if the activity is gone.
4966             if ((activity == null) || activity.isFinishing()) {
4967                 // Return an empty spannable string builder.
4968                 return "";
4969             }
4970
4971             // Initialize an IP address string builder.
4972             StringBuilder ipAddresses = new StringBuilder();
4973
4974             // Get an array with the IP addresses for the host.
4975             try {
4976                 // Get an array with all the IP addresses for the domain.
4977                 InetAddress[] inetAddressesArray = InetAddress.getAllByName(domainName[0]);
4978
4979                 // Add each IP address to the string builder.
4980                 for (InetAddress inetAddress : inetAddressesArray) {
4981                     if (ipAddresses.length() == 0) {  // This is the first IP address.
4982                         // Add the IP address to the string builder.
4983                         ipAddresses.append(inetAddress.getHostAddress());
4984                     } else {  // This is not the first IP address.
4985                         // Add a line break to the string builder first.
4986                         ipAddresses.append("\n");
4987
4988                         // Add the IP address to the string builder.
4989                         ipAddresses.append(inetAddress.getHostAddress());
4990                     }
4991                 }
4992             } catch (UnknownHostException exception) {
4993                 // Do nothing.
4994             }
4995
4996             // Return the string.
4997             return ipAddresses.toString();
4998         }
4999
5000         // `onPostExecute()` operates on the UI thread.
5001         @Override
5002         protected void onPostExecute(String ipAddresses) {
5003             // Get a handle for the activity.
5004             Activity activity = activityWeakReference.get();
5005
5006             // Abort if the activity is gone.
5007             if ((activity == null) || activity.isFinishing()) {
5008                 return;
5009             }
5010
5011             // Store the IP addresses.
5012             currentHostIpAddresses = ipAddresses;
5013
5014             if (!urlIsLoading) {
5015                 checkPinnedMismatch();
5016             }
5017
5018             // Reset the getting IP addresses tracker.
5019             gettingIpAddresses = false;
5020         }
5021     }
5022 }