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