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