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