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