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