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