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