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