Make pinned settings tab aware.
[PrivacyBrowser.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     // TODO.  We are going to have to move this to the NestedScrollWebView.
185     // The URL loading tracker is public static so it can be accessed from `GetHostIpAddresses`.
186     // It is also used in `onCreate()`, `onCreateOptionsMenu()`, `loadUrl()`, `applyDomainSettings()`, and `GetHostIpAddresses`.
187     public static boolean urlIsLoading;
188
189     // `orbotStatus` is public static so it can be accessed from `OrbotProxyHelper`.  It is also used in `onCreate()`, `onResume()`, and `applyProxyThroughOrbot()`.
190     public static String orbotStatus;
191
192     // TODO.
193     // `appliedUserAgentString` is public static so it can be accessed from `ViewSourceActivity`.  It is also used in `applyDomainSettings()`.
194     public static String appliedUserAgentString;
195
196     // The WebView pager adapter is accessed from `PinnedMismatchDialog`.  It is also used in `onCreate()`, `onResume()`, and `addTab()`.
197     public static WebViewPagerAdapter webViewPagerAdapter;
198
199     // `reloadOnRestart` is public static so it can be accessed from `SettingsFragment`.  It is also used in `onRestart()`
200     public static boolean reloadOnRestart;
201
202     // `reloadUrlOnRestart` is public static so it can be accessed from `SettingsFragment` and `BookmarksActivity`.  It is also used in `onRestart()`.
203     public static boolean loadUrlOnRestart;
204
205     // `restartFromBookmarksActivity` is public static so it can be accessed from `BookmarksActivity`.  It is also used in `onRestart()`.
206     public static boolean restartFromBookmarksActivity;
207
208     // The blocklist versions are public static so they can be accessed from `AboutTabFragment`.  They are also used in `onCreate()`.
209     public static String easyListVersion;
210     public static String easyPrivacyVersion;
211     public static String fanboysAnnoyanceVersion;
212     public static String fanboysSocialVersion;
213     public static String ultraPrivacyVersion;
214
215     // `blockAllThirdPartyRequests` is public static so it can be accessed from `RequestsActivity`.
216     // It is also used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyAppSettings()`
217     public static boolean blockAllThirdPartyRequests;
218
219     // `currentBookmarksFolder` is public static so it can be accessed from `BookmarksActivity`.  It is also used in `onCreate()`, `onBackPressed()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`,
220     // `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
221     public static String currentBookmarksFolder;
222
223     // The user agent constants are public static so they can be accessed from `SettingsFragment`, `DomainsActivity`, and `DomainSettingsFragment`.
224     public final static int UNRECOGNIZED_USER_AGENT = -1;
225     public final static int SETTINGS_WEBVIEW_DEFAULT_USER_AGENT = 1;
226     public final static int SETTINGS_CUSTOM_USER_AGENT = 12;
227     public final static int DOMAINS_SYSTEM_DEFAULT_USER_AGENT = 0;
228     public final static int DOMAINS_WEBVIEW_DEFAULT_USER_AGENT = 2;
229     public final static int DOMAINS_CUSTOM_USER_AGENT = 13;
230
231
232
233     // `navigatingHistory` is used in `onCreate()`, `onNavigationItemSelected()`, `onSslMismatchBack()`, and `applyDomainSettings()`.
234     private boolean navigatingHistory;
235
236     // The current WebView is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`,
237     // `findNextOnPage()`, `closeFindOnPage()`, `loadUrlFromTextBox()`, `onSslMismatchBack()`, `applyProxyThroughOrbot()`, and `applyDomainSettings()`.
238     private NestedScrollWebView currentWebView;
239
240     // `fullScreenVideoFrameLayout` is used in `onCreate()` and `onConfigurationChanged()`.
241     private FrameLayout fullScreenVideoFrameLayout;
242
243     // `cookieManager` is used in `onCreate()`, `onOptionsItemSelected()`, and `onNavigationItemSelected()`, `loadUrlFromTextBox()`, `onDownloadImage()`, `onDownloadFile()`, and `onRestart()`.
244     private CookieManager cookieManager;
245
246     // `customHeader` is used in `onCreate()`, `onOptionsItemSelected()`, `onCreateContextMenu()`, and `loadUrl()`.
247     private final Map<String, String> customHeaders = new HashMap<>();
248
249     // `javaScriptEnabled` is also used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `applyDomainSettings()`, and `updatePrivacyIcons()`.
250     private boolean javaScriptEnabled;
251
252     // `firstPartyCookiesEnabled` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onDownloadImage()`, `onDownloadFile()`, and `applyDomainSettings()`.
253     private boolean firstPartyCookiesEnabled;
254
255     // `thirdPartyCookiesEnabled` used in `onCreate()`, `onPrepareOptionsMenu()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyDomainSettings()`.
256     private boolean thirdPartyCookiesEnabled;
257
258     // `domStorageEnabled` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyDomainSettings()`.
259     private boolean domStorageEnabled;
260
261     // `saveFormDataEnabled` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyDomainSettings()`.  It can be removed once the minimum API >= 26.
262     private boolean saveFormDataEnabled;
263
264     // TODO Move to NestedScrollWebView.
265     // `nightMode` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and  `applyDomainSettings()`.
266     private boolean nightMode;
267
268     // 'homepage' is used in `onCreate()`, `onNavigationItemSelected()`, and `applyProxyThroughOrbot()`.
269     private String homepage;
270
271     // `searchURL` is used in `loadURLFromTextBox()` and `applyProxyThroughOrbot()`.
272     private String searchURL;
273
274     // `mainMenu` is used in `onCreateOptionsMenu()` and `updatePrivacyIcons()`.
275     private Menu mainMenu;
276
277     // `refreshMenuItem` is used in `onCreate()` and `onCreateOptionsMenu()`.
278     private MenuItem refreshMenuItem;
279
280     // The navigation requests menu item is used in `onCreate()` and accessed from `WebViewPagerAdapter`.
281     private MenuItem navigationRequestsMenuItem;
282
283     // TODO.  This could probably be removed.
284     // The blocklist helper is used in `onCreate()` and `WebViewPagerAdapter`.
285     private BlockListHelper blockListHelper;
286
287     // The blocklists are populated in `onCreate()` and accessed from `WebViewPagerAdapter`.
288     private ArrayList<List<String[]>> easyList;
289     private ArrayList<List<String[]>> easyPrivacy;
290     private ArrayList<List<String[]>> fanboysAnnoyanceList;
291     private ArrayList<List<String[]>> fanboysSocialList;
292     private ArrayList<List<String[]>> ultraPrivacy;
293
294     // The blocklist menu items are used in `onCreate()`, `onCreateOptionsMenu()`, and `onPrepareOptionsMenu()`.
295     private MenuItem blocklistsMenuItem;
296     private MenuItem easyListMenuItem;
297     private MenuItem easyPrivacyMenuItem;
298     private MenuItem fanboysAnnoyanceListMenuItem;
299     private MenuItem fanboysSocialBlockingListMenuItem;
300     private MenuItem ultraPrivacyMenuItem;
301     private MenuItem blockAllThirdPartyRequestsMenuItem;
302
303     // The blocklist variables are used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyAppSettings()`.
304     private boolean easyListEnabled;
305     private boolean easyPrivacyEnabled;
306     private boolean fanboysAnnoyanceListEnabled;
307     private boolean fanboysSocialBlockingListEnabled;
308     private boolean ultraPrivacyEnabled;
309
310     // `webViewDefaultUserAgent` is used in `onCreate()` and `onPrepareOptionsMenu()`.
311     private String webViewDefaultUserAgent;
312
313     // `defaultCustomUserAgentString` is used in `onPrepareOptionsMenu()` and `applyDomainSettings()`.
314     private String defaultCustomUserAgentString;
315
316     // `privacyBrowserRuntime` is used in `onCreate()`, `onOptionsItemSelected()`, and `applyAppSettings()`.
317     private Runtime privacyBrowserRuntime;
318
319     // `proxyThroughOrbot` is used in `onRestart()`, `onOptionsItemSelected()`, `applyAppSettings()`, and `applyProxyThroughOrbot()`.
320     private boolean proxyThroughOrbot;
321
322     // `incognitoModeEnabled` is used in `onCreate()` and `applyAppSettings()`.
323     private boolean incognitoModeEnabled;
324
325     // `fullScreenBrowsingModeEnabled` is used in `onCreate()` and `applyAppSettings()`.
326     private boolean fullScreenBrowsingModeEnabled;
327
328     // `inFullScreenBrowsingMode` is used in `onCreate()`, `onConfigurationChanged()`, and `applyAppSettings()`.
329     private boolean inFullScreenBrowsingMode;
330
331     // Hide app bar is used in `onCreate()` and `applyAppSettings()`.
332     private boolean hideAppBar;
333
334     // `reapplyDomainSettingsOnRestart` is used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, and `onAddDomain()`, .
335     private boolean reapplyDomainSettingsOnRestart;
336
337     // `reapplyAppSettingsOnRestart` is used in `onNavigationItemSelected()` and `onRestart()`.
338     private boolean reapplyAppSettingsOnRestart;
339
340     // `displayingFullScreenVideo` is used in `onCreate()` and `onResume()`.
341     private boolean displayingFullScreenVideo;
342
343     // `downloadWithExternalApp` is used in `onCreate()`, `onCreateContextMenu()`, and `applyDomainSettings()`.
344     private boolean downloadWithExternalApp;
345
346     // `currentDomainName` is used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onAddDomain()`, and `applyDomainSettings()`.
347     private String currentDomainName;
348
349     // `orbotStatusBroadcastReceiver` is used in `onCreate()` and `onDestroy()`.
350     private BroadcastReceiver orbotStatusBroadcastReceiver;
351
352     // `waitingForOrbot` is used in `onCreate()`, `onResume()`, and `applyProxyThroughOrbot()`.
353     private boolean waitingForOrbot;
354
355     // `domainSettingsJavaScriptEnabled` is used in `onOptionsItemSelected()` and `applyDomainSettings()`.
356     private Boolean domainSettingsJavaScriptEnabled;
357
358     // `waitingForOrbotHtmlString` is used in `onCreate()` and `applyProxyThroughOrbot()`.
359     private String waitingForOrbotHtmlString;
360
361     // `privateDataDirectoryString` is used in `onCreate()`, `onOptionsItemSelected()`, and `onNavigationItemSelected()`.
362     private String privateDataDirectoryString;
363
364     // `findOnPageEditText` is used in `onCreate()`, `onOptionsItemSelected()`, and `closeFindOnPage()`.
365     private EditText findOnPageEditText;
366
367     // `displayAdditionalAppBarIcons` is used in `onCreate()` and `onCreateOptionsMenu()`.
368     private boolean displayAdditionalAppBarIcons;
369
370     // The action bar drawer toggle is initialized in `onCreate()` and used in `onResume()`.
371     private ActionBarDrawerToggle actionBarDrawerToggle;
372
373     // The color spans are used in `onCreate()` and `highlightUrlText()`.
374     private ForegroundColorSpan redColorSpan;
375     private ForegroundColorSpan initialGrayColorSpan;
376     private ForegroundColorSpan finalGrayColorSpan;
377
378     // The drawer header padding variables are used in `onCreate()` and `onConfigurationChanged()`.
379     private int drawerHeaderPaddingLeftAndRight;
380     private int drawerHeaderPaddingTop;
381     private int drawerHeaderPaddingBottom;
382
383     // `sslErrorHandler` is used in `onCreate()`, `onSslErrorCancel()`, and `onSslErrorProceed`.
384     private SslErrorHandler sslErrorHandler;
385
386     // `httpAuthHandler` is used in `onCreate()`, `onHttpAuthenticationCancel()`, and `onHttpAuthenticationProceed()`.
387     private static HttpAuthHandler httpAuthHandler;
388
389     // `inputMethodManager` is used in `onOptionsItemSelected()`, `loadUrlFromTextBox()`, and `closeFindOnPage()`.
390     private InputMethodManager inputMethodManager;
391
392     // `bookmarksDatabaseHelper` is used in `onCreate()`, `onDestroy`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`,
393     // and `loadBookmarksFolder()`.
394     private BookmarksDatabaseHelper bookmarksDatabaseHelper;
395
396     // `bookmarksListView` is used in `onCreate()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, and `loadBookmarksFolder()`.
397     private ListView bookmarksListView;
398
399     // `bookmarksTitleTextView` is used in `onCreate()` and `loadBookmarksFolder()`.
400     private TextView bookmarksTitleTextView;
401
402     // `bookmarksCursor` is used in `onDestroy()`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
403     private Cursor bookmarksCursor;
404
405     // `bookmarksCursorAdapter` is used in `onCreateBookmark()`, `onCreateBookmarkFolder()` `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
406     private CursorAdapter bookmarksCursorAdapter;
407
408     // `oldFolderNameString` is used in `onCreate()` and `onSaveEditBookmarkFolder()`.
409     private String oldFolderNameString;
410
411     // `fileChooserCallback` is used in `onCreate()` and `onActivityResult()`.
412     private ValueCallback<Uri[]> fileChooserCallback;
413
414     // The download strings are used in `onCreate()`, `onRequestPermissionResult()` and `initializeWebView()`.
415     private String downloadUrl;
416     private String downloadContentDisposition;
417     private long downloadContentLength;
418
419     // `downloadImageUrl` is used in `onCreateContextMenu()` and `onRequestPermissionResult()`.
420     private String downloadImageUrl;
421
422     // The user agent variables are used in `onCreate()` and `applyDomainSettings()`.
423     private ArrayAdapter<CharSequence> userAgentNamesArray;
424     private String[] userAgentDataArray;
425
426     // The request codes are used in `onCreate()`, `onCreateContextMenu()`, `onCloseDownloadLocationPermissionDialog()`, `onRequestPermissionResult()`, and `initializeWebView()`.
427     private final int DOWNLOAD_FILE_REQUEST_CODE = 1;
428     private final int DOWNLOAD_IMAGE_REQUEST_CODE = 2;
429
430     @Override
431     // Remove Android Studio's warning about the dangers of using SetJavaScriptEnabled.  The whole premise of Privacy Browser is built around an understanding of these dangers.
432     // Also, remove the warning about needing to override `performClick()` when using an `OnTouchListener` with `WebView`.
433     @SuppressLint({"SetJavaScriptEnabled", "ClickableViewAccessibility"})
434     // Remove Android Studio's warning about deprecations.  The deprecated `getColor()` must be used until API >= 23.
435     @SuppressWarnings("deprecation")
436     protected void onCreate(Bundle savedInstanceState) {
437         // Get a handle for the shared preferences.
438         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
439
440         // Get the theme and screenshot preferences.
441         darkTheme = sharedPreferences.getBoolean("dark_theme", false);
442         allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
443
444         // Disable screenshots if not allowed.
445         if (!allowScreenshots) {
446             getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
447         }
448
449         // Set the activity theme.
450         if (darkTheme) {
451             setTheme(R.style.PrivacyBrowserDark);
452         } else {
453             setTheme(R.style.PrivacyBrowserLight);
454         }
455
456         // Run the default commands.
457         super.onCreate(savedInstanceState);
458
459         // Set the content view.
460         setContentView(R.layout.main_framelayout);
461
462         // Get handles for the input method manager and toolbar.
463         inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
464         Toolbar toolbar = findViewById(R.id.toolbar);
465
466         // Set the action bar.  `SupportActionBar` must be used until the minimum API is >= 21.
467         setSupportActionBar(toolbar);
468         ActionBar actionBar = getSupportActionBar();
469
470         // This is needed to get rid of the Android Studio warning that the action bar might be null.
471         assert actionBar != null;
472
473         // Add the custom layout, which shows the URL text bar.
474         actionBar.setCustomView(R.layout.url_app_bar);
475         actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
476
477         // Initialize the foreground color spans for highlighting the URLs.  We have to use the deprecated `getColor()` until API >= 23.
478         redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_a700));
479         initialGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
480         finalGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
481
482         // Get a handle for `urlTextBox`.
483         EditText urlEditText = findViewById(R.id.url_edittext);
484
485         // Remove the formatting from `urlTextBar` when the user is editing the text.
486         urlEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
487             if (hasFocus) {  // The user is editing the URL text box.
488                 // Remove the highlighting.
489                 urlEditText.getText().removeSpan(redColorSpan);
490                 urlEditText.getText().removeSpan(initialGrayColorSpan);
491                 urlEditText.getText().removeSpan(finalGrayColorSpan);
492             } else {  // The user has stopped editing the URL text box.
493                 // Move to the beginning of the string.
494                 urlEditText.setSelection(0);
495
496                 // Reapply the highlighting.
497                 highlightUrlText();
498             }
499         });
500
501         // Set the go button on the keyboard to load the URL in `urlTextBox`.
502         urlEditText.setOnKeyListener((View v, int keyCode, KeyEvent event) -> {
503             // If the event is a key-down event on the `enter` button, load the URL.
504             if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {
505                 // Load the URL into the mainWebView and consume the event.
506                 loadUrlFromTextBox();
507
508                 // If the enter key was pressed, consume the event.
509                 return true;
510             } else {
511                 // If any other key was pressed, do not consume the event.
512                 return false;
513             }
514         });
515
516         // Set `waitingForOrbotHTMLString`.
517         waitingForOrbotHtmlString = "<html><body><br/><center><h1>" + getString(R.string.waiting_for_orbot) + "</h1></center></body></html>";
518
519         // Initialize `currentDomainName`, `orbotStatus`, and `waitingForOrbot`.
520         currentDomainName = "";
521         orbotStatus = "unknown";
522         waitingForOrbot = false;
523
524         // Create an Orbot status `BroadcastReceiver`.
525         orbotStatusBroadcastReceiver = new BroadcastReceiver() {
526             @Override
527             public void onReceive(Context context, Intent intent) {
528                 // Store the content of the status message in `orbotStatus`.
529                 orbotStatus = intent.getStringExtra("org.torproject.android.intent.extra.STATUS");
530
531                 // If Privacy Browser is waiting on Orbot, load the website now that Orbot is connected.
532                 if (orbotStatus.equals("ON") && waitingForOrbot) {
533                     // Reset `waitingForOrbot`.
534                     waitingForOrbot = false;
535
536                     // Load `formattedUrlString
537                     loadUrl(formattedUrlString);
538                 }
539             }
540         };
541
542         // Register `orbotStatusBroadcastReceiver` on `this` context.
543         this.registerReceiver(orbotStatusBroadcastReceiver, new IntentFilter("org.torproject.android.intent.action.STATUS"));
544
545         // Instantiate the block list helper.
546         blockListHelper = new BlockListHelper();
547
548         // Parse the block lists.
549         easyList = blockListHelper.parseBlockList(getAssets(), "blocklists/easylist.txt");
550         easyPrivacy = blockListHelper.parseBlockList(getAssets(), "blocklists/easyprivacy.txt");
551         fanboysAnnoyanceList = blockListHelper.parseBlockList(getAssets(), "blocklists/fanboy-annoyance.txt");
552         fanboysSocialList = blockListHelper.parseBlockList(getAssets(), "blocklists/fanboy-social.txt");
553         ultraPrivacy = blockListHelper.parseBlockList(getAssets(), "blocklists/ultraprivacy.txt");
554
555         // Store the list versions.
556         easyListVersion = easyList.get(0).get(0)[0];
557         easyPrivacyVersion = easyPrivacy.get(0).get(0)[0];
558         fanboysAnnoyanceVersion = fanboysAnnoyanceList.get(0).get(0)[0];
559         fanboysSocialVersion = fanboysSocialList.get(0).get(0)[0];
560         ultraPrivacyVersion = ultraPrivacy.get(0).get(0)[0];
561
562         // Get handles for views that need to be modified.
563         DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
564         final NavigationView navigationView = findViewById(R.id.navigationview);
565         TabLayout tabLayout = findViewById(R.id.tablayout);
566         SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
567         ViewPager webViewPager = findViewById(R.id.webviewpager);
568         bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
569         bookmarksTitleTextView = findViewById(R.id.bookmarks_title_textview);
570         FloatingActionButton launchBookmarksActivityFab = findViewById(R.id.launch_bookmarks_activity_fab);
571         FloatingActionButton createBookmarkFolderFab = findViewById(R.id.create_bookmark_folder_fab);
572         FloatingActionButton createBookmarkFab = findViewById(R.id.create_bookmark_fab);
573         findOnPageEditText = findViewById(R.id.find_on_page_edittext);
574         fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout);
575
576         // Listen for touches on the navigation menu.
577         navigationView.setNavigationItemSelectedListener(this);
578
579         // Get handles for the navigation menu and the back and forward menu items.  The menu is zero-based.
580         final Menu navigationMenu = navigationView.getMenu();
581         final MenuItem navigationCloseTabMenuItem = navigationMenu.getItem(0);
582         final MenuItem navigationBackMenuItem = navigationMenu.getItem(3);
583         final MenuItem navigationForwardMenuItem = navigationMenu.getItem(4);
584         final MenuItem navigationHistoryMenuItem = navigationMenu.getItem(5);
585         navigationRequestsMenuItem = navigationMenu.getItem(6);
586
587         // Initialize the web view pager adapter.
588         webViewPagerAdapter = new WebViewPagerAdapter(getSupportFragmentManager());
589
590         // Set the pager adapter on the web view pager.
591         webViewPager.setAdapter(webViewPagerAdapter);
592
593         // Store up to 100 tabs in memory.
594         webViewPager.setOffscreenPageLimit(100);
595
596         // Update the web view pager every time a tab is modified.
597         webViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
598             @Override
599             public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
600                 // Do nothing.
601             }
602
603             @Override
604             public void onPageSelected(int position) {
605                 // Get the WebView tab fragment.
606                 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(position);
607
608                 // Get the fragment view.
609                 View fragmentView = webViewTabFragment.getView();
610
611                 // Remove the incorrect lint warning below that the fragment view might be null.
612                 assert fragmentView != null;
613
614                 // Store the current WebView.
615                 currentWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
616
617                 // Store the current formatted URL string.
618                 formattedUrlString = currentWebView.getUrl();
619
620                 // Clear the focus from the URL text box.
621                 urlEditText.clearFocus();
622
623                 // Hide the soft keyboard.
624                 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
625
626                 // Display the current URL in the URL text box.
627                 urlEditText.setText(formattedUrlString);
628
629                 // Highlight the URL text.
630                 highlightUrlText();
631
632                 // Set the background to indicate the domain settings status.
633                 if (currentWebView.getDomainSettingsApplied()) {
634                     // 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.
635                     if (darkTheme) {
636                         urlEditText.setBackground(getResources().getDrawable(R.drawable.url_bar_background_dark_blue));
637                     } else {
638                         urlEditText.setBackground(getResources().getDrawable(R.drawable.url_bar_background_light_green));
639                     }
640                 } else {
641                     urlEditText.setBackgroundDrawable(getResources().getDrawable(R.color.transparent));
642                 }
643
644                 // 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.
645                 if (tabLayout.getSelectedTabPosition() != position) {
646                     // Get a handle for the corresponding tab.
647                     TabLayout.Tab correspondingTab = tabLayout.getTabAt(position);
648
649                     // Assert that the corresponding tab is not null.
650                     assert correspondingTab != null;
651
652                     // Select the corresponding tab.
653                     correspondingTab.select();
654                 }
655             }
656
657             @Override
658             public void onPageScrollStateChanged(int state) {
659                 // Do nothing.
660             }
661         });
662
663         // Display the View SSL Certificate dialog when the currently selected tab is reselected.
664         tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
665             @Override
666             public void onTabSelected(TabLayout.Tab tab) {
667                 // Select the same page in the view pager.
668                 webViewPager.setCurrentItem(tab.getPosition());
669             }
670
671             @Override
672             public void onTabUnselected(TabLayout.Tab tab) {
673                 // Do nothing.
674             }
675
676             @Override
677             public void onTabReselected(TabLayout.Tab tab) {
678                 // Instantiate the View SSL Certificate dialog.
679                 DialogFragment viewSslCertificateDialogFragment = ViewSslCertificateDialog.displayDialog(currentWebView.getWebViewFragmentId());
680
681                 // Display the View SSL Certificate dialog.
682                 viewSslCertificateDialogFragment.show(getSupportFragmentManager(), getString(R.string.view_ssl_certificate));
683             }
684         });
685
686         // Add the first tab.  (It doesn't matter what view is passed.  That is just required as part of the ImageView `onClick()` syntax).
687         addTab(webViewPager);
688
689         // 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.
690         // The deprecated `getResources().getDrawable()` must be used until the minimum API >= 21 and and `getResources().getColor()` must be used until the minimum API >= 23.
691         if (darkTheme) {
692             launchBookmarksActivityFab.setImageDrawable(getResources().getDrawable(R.drawable.bookmarks_dark));
693             createBookmarkFolderFab.setImageDrawable(getResources().getDrawable(R.drawable.create_folder_dark));
694             createBookmarkFab.setImageDrawable(getResources().getDrawable(R.drawable.create_bookmark_dark));
695             bookmarksListView.setBackgroundColor(getResources().getColor(R.color.gray_850));
696         } else {
697             launchBookmarksActivityFab.setImageDrawable(getResources().getDrawable(R.drawable.bookmarks_light));
698             createBookmarkFolderFab.setImageDrawable(getResources().getDrawable(R.drawable.create_folder_light));
699             createBookmarkFab.setImageDrawable(getResources().getDrawable(R.drawable.create_bookmark_light));
700             bookmarksListView.setBackgroundColor(getResources().getColor(R.color.white));
701         }
702
703         // Set the launch bookmarks activity FAB to launch the bookmarks activity.
704         launchBookmarksActivityFab.setOnClickListener(v -> {
705             // Store the current WebView url and title in the bookmarks activity.
706             BookmarksActivity.currentWebViewUrl = currentWebView.getUrl();
707             BookmarksActivity.currentWebViewTitle = currentWebView.getTitle();
708
709             // Create an intent to launch the bookmarks activity.
710             Intent bookmarksIntent = new Intent(getApplicationContext(), BookmarksActivity.class);
711
712             // Include the current folder with the `Intent`.
713             bookmarksIntent.putExtra("Current Folder", currentBookmarksFolder);
714
715             // Make it so.
716             startActivity(bookmarksIntent);
717         });
718
719         // Set the create new bookmark folder FAB to display an alert dialog.
720         createBookmarkFolderFab.setOnClickListener(v -> {
721             // Show the create bookmark folder dialog and name the instance `@string/create_folder`.
722             DialogFragment createBookmarkFolderDialog = new CreateBookmarkFolderDialog();
723             createBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.create_folder));
724         });
725
726         // Set the create new bookmark FAB to display an alert dialog.
727         createBookmarkFab.setOnClickListener(view -> {
728             // Instantiate the create bookmark dialog.
729             DialogFragment createBookmarkDialog = CreateBookmarkDialog.createBookmark(currentWebView.getUrl(), currentWebView.getTitle(), favoriteIconBitmap);
730
731             // Display the create bookmark dialog.
732             createBookmarkDialog.show(getSupportFragmentManager(), getString(R.string.create_bookmark));
733         });
734
735         // Search for the string on the page whenever a character changes in the `findOnPageEditText`.
736         findOnPageEditText.addTextChangedListener(new TextWatcher() {
737             @Override
738             public void beforeTextChanged(CharSequence s, int start, int count, int after) {
739                 // Do nothing.
740             }
741
742             @Override
743             public void onTextChanged(CharSequence s, int start, int before, int count) {
744                 // Do nothing.
745             }
746
747             @Override
748             public void afterTextChanged(Editable s) {
749                 // Search for the text in `mainWebView`.
750                 currentWebView.findAllAsync(findOnPageEditText.getText().toString());
751             }
752         });
753
754         // Set the `check mark` button for the `findOnPageEditText` keyboard to close the soft keyboard.
755         findOnPageEditText.setOnKeyListener((v, keyCode, event) -> {
756             if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {  // The `enter` key was pressed.
757                 // Hide the soft keyboard.
758                 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
759
760                 // Consume the event.
761                 return true;
762             } else {  // A different key was pressed.
763                 // Do not consume the event.
764                 return false;
765             }
766         });
767
768         // Implement swipe to refresh.
769         swipeRefreshLayout.setOnRefreshListener(() -> currentWebView.reload());
770
771         // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
772         swipeRefreshLayout.setProgressViewOffset(false, swipeRefreshLayout.getProgressViewStartOffset() - 10, swipeRefreshLayout.getProgressViewEndOffset());
773
774         // Set the swipe to refresh color according to the theme.
775         if (darkTheme) {
776             swipeRefreshLayout.setColorSchemeResources(R.color.blue_800);
777             swipeRefreshLayout.setProgressBackgroundColorSchemeResource(R.color.gray_850);
778         } else {
779             swipeRefreshLayout.setColorSchemeResources(R.color.blue_500);
780         }
781
782         // `DrawerTitle` identifies the `DrawerLayouts` in accessibility mode.
783         drawerLayout.setDrawerTitle(GravityCompat.START, getString(R.string.navigation_drawer));
784         drawerLayout.setDrawerTitle(GravityCompat.END, getString(R.string.bookmarks));
785
786         // Initialize the bookmarks database helper.  The `0` specifies a database version, but that is ignored and set instead using a constant in `BookmarksDatabaseHelper`.
787         bookmarksDatabaseHelper = new BookmarksDatabaseHelper(this, null, null, 0);
788
789         // Initialize `currentBookmarksFolder`.  `""` is the home folder in the database.
790         currentBookmarksFolder = "";
791
792         // Load the home folder, which is `""` in the database.
793         loadBookmarksFolder();
794
795         bookmarksListView.setOnItemClickListener((parent, view, position, id) -> {
796             // Convert the id from long to int to match the format of the bookmarks database.
797             int databaseID = (int) id;
798
799             // Get the bookmark cursor for this ID and move it to the first row.
800             Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseID);
801             bookmarkCursor.moveToFirst();
802
803             // Act upon the bookmark according to the type.
804             if (bookmarkCursor.getInt(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {  // The selected bookmark is a folder.
805                 // Store the new folder name in `currentBookmarksFolder`.
806                 currentBookmarksFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
807
808                 // Load the new folder.
809                 loadBookmarksFolder();
810             } else {  // The selected bookmark is not a folder.
811                 // Load the bookmark URL.
812                 loadUrl(bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_URL)));
813
814                 // Close the bookmarks drawer.
815                 drawerLayout.closeDrawer(GravityCompat.END);
816             }
817
818             // Close the `Cursor`.
819             bookmarkCursor.close();
820         });
821
822         bookmarksListView.setOnItemLongClickListener((parent, view, position, id) -> {
823             // Convert the database ID from `long` to `int`.
824             int databaseId = (int) id;
825
826             // Find out if the selected bookmark is a folder.
827             boolean isFolder = bookmarksDatabaseHelper.isFolder(databaseId);
828
829             if (isFolder) {
830                 // Save the current folder name, which is used in `onSaveEditBookmarkFolder()`.
831                 oldFolderNameString = bookmarksCursor.getString(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
832
833                 // Show the edit bookmark folder `AlertDialog` and name the instance `@string/edit_folder`.
834                 DialogFragment editBookmarkFolderDialog = EditBookmarkFolderDialog.folderDatabaseId(databaseId);
835                 editBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.edit_folder));
836             } else {
837                 // Show the edit bookmark `AlertDialog` and name the instance `@string/edit_bookmark`.
838                 DialogFragment editBookmarkDialog = EditBookmarkDialog.bookmarkDatabaseId(databaseId);
839                 editBookmarkDialog.show(getSupportFragmentManager(), getString(R.string.edit_bookmark));
840             }
841
842             // Consume the event.
843             return true;
844         });
845
846         // Get the status bar pixel size.
847         int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
848         int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId);
849
850         // Get the resource density.
851         float screenDensity = getResources().getDisplayMetrics().density;
852
853         // Calculate the drawer header padding.  This is used to move the text in the drawer headers below any cutouts.
854         drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity);
855         drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity);
856         drawerHeaderPaddingBottom = (int) (8 * screenDensity);
857
858         // The drawer listener is used to update the navigation menu.`
859         drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() {
860             @Override
861             public void onDrawerSlide(@NonNull View drawerView, float slideOffset) {
862             }
863
864             @Override
865             public void onDrawerOpened(@NonNull View drawerView) {
866             }
867
868             @Override
869             public void onDrawerClosed(@NonNull View drawerView) {
870             }
871
872             @Override
873             public void onDrawerStateChanged(int newState) {
874                 if ((newState == DrawerLayout.STATE_SETTLING) || (newState == DrawerLayout.STATE_DRAGGING)) {  // A drawer is opening or closing.
875                     // Get handles for the drawer headers.
876                     TextView navigationHeaderTextView = findViewById(R.id.navigationText);
877                     TextView bookmarksHeaderTextView = findViewById(R.id.bookmarks_title_textview);
878
879                     // 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.
880                     if (navigationHeaderTextView != null) {
881                         navigationHeaderTextView.setPadding(drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingTop, drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingBottom);
882                     }
883
884                     // 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.
885                     if (bookmarksHeaderTextView != null) {
886                         bookmarksHeaderTextView.setPadding(drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingTop, drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingBottom);
887                     }
888
889                     // Update the navigation menu items.
890                     navigationCloseTabMenuItem.setEnabled(tabLayout.getTabCount() > 1);
891                     navigationBackMenuItem.setEnabled(currentWebView.canGoBack());
892                     navigationForwardMenuItem.setEnabled(currentWebView.canGoForward());
893                     navigationHistoryMenuItem.setEnabled((currentWebView.canGoBack() || currentWebView.canGoForward()));
894                     navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
895
896                     // Hide the keyboard (if displayed).
897                     inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
898
899                     // 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.
900                     urlEditText.clearFocus();
901                     currentWebView.clearFocus();
902                 }
903             }
904         });
905
906         // Create the hamburger icon at the start of the AppBar.
907         actionBarDrawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.open_navigation_drawer, R.string.close_navigation_drawer);
908
909         // Initialize cookieManager.
910         cookieManager = CookieManager.getInstance();
911
912         // 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).
913         customHeaders.put("X-Requested-With", "");
914
915         // Initialize the default preference values the first time the program is run.  `false` keeps this command from resetting any current preferences back to default.
916         PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
917
918         // Get a handle for the `Runtime`.
919         privacyBrowserRuntime = Runtime.getRuntime();
920
921         // Store the application's private data directory.
922         privateDataDirectoryString = getApplicationInfo().dataDir;
923         // `dataDir` will vary, but will be something like `/data/user/0/com.stoutner.privacybrowser.standard`, which links to `/data/data/com.stoutner.privacybrowser.standard`.
924
925         // Initialize `inFullScreenBrowsingMode`, which is always false at this point because Privacy Browser never starts in full screen browsing mode.
926         inFullScreenBrowsingMode = false;
927
928         // Initialize the privacy settings variables.
929         javaScriptEnabled = false;
930         firstPartyCookiesEnabled = false;
931         thirdPartyCookiesEnabled = false;
932         domStorageEnabled = false;
933         saveFormDataEnabled = false;  // Form data can be removed once the minimum API >= 26.
934         nightMode = false;
935
936         // Inflate a bare WebView to get the default user agent.  It is not used to render content on the screen.
937         @SuppressLint("InflateParams") View webViewLayout = getLayoutInflater().inflate(R.layout.bare_webview, null, false);
938
939         // Get a handle for the WebView.
940         WebView bareWebView = webViewLayout.findViewById(R.id.bare_webview);
941
942         // Store the default user agent.
943         webViewDefaultUserAgent = bareWebView.getSettings().getUserAgentString();
944
945         // Destroy the bare WebView.
946         bareWebView.destroy();
947
948         // Initialize the favorite icon bitmap.  `ContextCompat` must be used until API >= 21.
949         Drawable favoriteIconDrawable = ContextCompat.getDrawable(getApplicationContext(), R.drawable.world);
950         BitmapDrawable favoriteIconBitmapDrawable = (BitmapDrawable) favoriteIconDrawable;
951         assert favoriteIconBitmapDrawable != null;
952         favoriteIconDefaultBitmap = favoriteIconBitmapDrawable.getBitmap();
953
954         // If the favorite icon is null, load the default.
955         if (favoriteIconBitmap == null) {
956             favoriteIconBitmap = favoriteIconDefaultBitmap;
957         }
958
959         // Initialize the user agent array adapter and string array.
960         userAgentNamesArray = ArrayAdapter.createFromResource(this, R.array.user_agent_names, R.layout.spinner_item);
961         userAgentDataArray = getResources().getStringArray(R.array.user_agent_data);
962
963         // Get the intent that started the app.
964         Intent launchingIntent = getIntent();
965
966         // Get the information from the intent.
967         String launchingIntentAction = launchingIntent.getAction();
968         Uri launchingIntentUriData = launchingIntent.getData();
969
970         // If the intent action is a web search, perform the search.
971         if ((launchingIntentAction != null) && launchingIntentAction.equals(Intent.ACTION_WEB_SEARCH)) {
972             // Create an encoded URL string.
973             String encodedUrlString;
974
975             // Sanitize the search input and convert it to a search.
976             try {
977                 encodedUrlString = URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8");
978             } catch (UnsupportedEncodingException exception) {
979                 encodedUrlString = "";
980             }
981
982             // Add the base search URL.
983             formattedUrlString = searchURL + encodedUrlString;
984         } else if (launchingIntentUriData != null){  // Check to see if the intent contains a new URL.
985             // Set the formatted URL string.
986             formattedUrlString = launchingIntentUriData.toString();
987         }
988     }
989
990     @Override
991     protected void onNewIntent(Intent intent) {
992         // Sets the new intent as the activity intent, so that any future `getIntent()`s pick up this one instead of creating a new activity.
993         setIntent(intent);
994
995         // Get the information from the intent.
996         String intentAction = intent.getAction();
997         Uri intentUriData = intent.getData();
998
999         // If the intent action is a web search, perform the search.
1000         if ((intentAction != null) && intentAction.equals(Intent.ACTION_WEB_SEARCH)) {
1001             // Create an encoded URL string.
1002             String encodedUrlString;
1003
1004             // Sanitize the search input and convert it to a search.
1005             try {
1006                 encodedUrlString = URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8");
1007             } catch (UnsupportedEncodingException exception) {
1008                 encodedUrlString = "";
1009             }
1010
1011             // Add the base search URL.
1012             formattedUrlString = searchURL + encodedUrlString;
1013         } else if (intentUriData != null){  // Check to see if the intent contains a new URL.
1014             // Set the formatted URL string.
1015             formattedUrlString = intentUriData.toString();
1016         }
1017
1018         // Load the URL.
1019         loadUrl(formattedUrlString);
1020
1021         // Get a handle for the drawer layout.
1022         DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
1023
1024         // Close the navigation drawer if it is open.
1025         if (drawerLayout.isDrawerVisible(GravityCompat.START)) {
1026             drawerLayout.closeDrawer(GravityCompat.START);
1027         }
1028
1029         // Close the bookmarks drawer if it is open.
1030         if (drawerLayout.isDrawerVisible(GravityCompat.END)) {
1031             drawerLayout.closeDrawer(GravityCompat.END);
1032         }
1033
1034         // Clear the keyboard if displayed and remove the focus on the urlTextBar if it has it.
1035         currentWebView.requestFocus();
1036     }
1037
1038     @Override
1039     public void onRestart() {
1040         // Run the default commands.
1041         super.onRestart();
1042
1043         // Make sure Orbot is running if Privacy Browser is proxying through Orbot.
1044         if (proxyThroughOrbot) {
1045             // Request Orbot to start.  If Orbot is already running no hard will be caused by this request.
1046             Intent orbotIntent = new Intent("org.torproject.android.intent.action.START");
1047
1048             // Send the intent to the Orbot package.
1049             orbotIntent.setPackage("org.torproject.android");
1050
1051             // Make it so.
1052             sendBroadcast(orbotIntent);
1053         }
1054
1055         // Apply the app settings if returning from the Settings activity.
1056         if (reapplyAppSettingsOnRestart) {
1057             // Apply the app settings.
1058             applyAppSettings();
1059
1060             // Reload the webpage if displaying of images has been disabled in the Settings activity.
1061             if (reloadOnRestart) {
1062                 // Reload the WebViews.
1063                 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
1064                     // Get the WebView tab fragment.
1065                     WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
1066
1067                     // Get the fragment view.
1068                     View fragmentView = webViewTabFragment.getView();
1069
1070                     // Only reload the WebViews if they exist.
1071                     if (fragmentView != null) {
1072                         // Get the nested scroll WebView from the tab fragment.
1073                         NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
1074
1075                         // Reload the WebView.  This doesn't seem to work if for WebViews that aren't visible.
1076                         nestedScrollWebView.reload();
1077                     }
1078                 }
1079
1080                 // Reset `reloadOnRestartBoolean`.
1081                 reloadOnRestart = false;
1082             }
1083
1084             // Reset the return from settings flag.
1085             reapplyAppSettingsOnRestart = false;
1086         }
1087
1088         // TODO apply to all the tabs.
1089         // Apply the domain settings if returning from the Domains activity.
1090         if (reapplyDomainSettingsOnRestart) {
1091             // Reapply the domain settings.
1092             applyDomainSettings(currentWebView, formattedUrlString, false, true);
1093
1094             // Reset `reapplyDomainSettingsOnRestart`.
1095             reapplyDomainSettingsOnRestart = false;
1096         }
1097
1098         // Load the URL on restart to apply changes to night mode.
1099         if (loadUrlOnRestart) {
1100             // Load the current `formattedUrlString`.
1101             loadUrl(formattedUrlString);
1102
1103             // Reset `loadUrlOnRestart.
1104             loadUrlOnRestart = false;
1105         }
1106
1107         // Update the bookmarks drawer if returning from the Bookmarks activity.
1108         if (restartFromBookmarksActivity) {
1109             // Get a handle for the drawer layout.
1110             DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
1111
1112             // Close the bookmarks drawer.
1113             drawerLayout.closeDrawer(GravityCompat.END);
1114
1115             // Reload the bookmarks drawer.
1116             loadBookmarksFolder();
1117
1118             // Reset `restartFromBookmarksActivity`.
1119             restartFromBookmarksActivity = false;
1120         }
1121
1122         // Update the privacy icon.  `true` runs `invalidateOptionsMenu` as the last step.  This can be important if the screen was rotated.
1123         updatePrivacyIcons(true);
1124     }
1125
1126     // `onResume()` runs after `onStart()`, which runs after `onCreate()` and `onRestart()`.
1127     @Override
1128     public void onResume() {
1129         // Run the default commands.
1130         super.onResume();
1131
1132         for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
1133             // Get the WebView tab fragment.
1134             WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
1135
1136             // Get the fragment view.
1137             View fragmentView = webViewTabFragment.getView();
1138
1139             // Only resume the WebViews if they exist (they won't when the app is first created).
1140             if (fragmentView != null) {
1141                 // Get the nested scroll WebView from the tab fragment.
1142                 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
1143
1144                 // Resume the nested scroll WebView JavaScript timers.
1145                 nestedScrollWebView.resumeTimers();
1146
1147                 // Resume the nested scroll WebView.
1148                 nestedScrollWebView.onResume();
1149             }
1150         }
1151
1152         // Display a message to the user if waiting for Orbot.
1153         if (waitingForOrbot && !orbotStatus.equals("ON")) {
1154             // Disable the wide view port so that the waiting for Orbot text is displayed correctly.
1155             currentWebView.getSettings().setUseWideViewPort(false);
1156
1157             // Load a waiting page.  `null` specifies no encoding, which defaults to ASCII.
1158             currentWebView.loadData(waitingForOrbotHtmlString, "text/html", null);
1159         }
1160
1161         if (displayingFullScreenVideo || inFullScreenBrowsingMode) {
1162             // Get a handle for the root frame layouts.
1163             FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
1164
1165             // Remove the translucent status flag.  This is necessary so the root frame layout can fill the entire screen.
1166             getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
1167
1168             /* Hide the system bars.
1169              * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
1170              * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
1171              * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
1172              * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
1173              */
1174             rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
1175                     View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
1176         } else if (BuildConfig.FLAVOR.contentEquals("free")) {  // Resume the adView for the free flavor.
1177             // Resume the ad.
1178             AdHelper.resumeAd(findViewById(R.id.adview));
1179         }
1180     }
1181
1182     @Override
1183     public void onPause() {
1184         // Run the default commands.
1185         super.onPause();
1186
1187         for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
1188             // Get the WebView tab fragment.
1189             WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
1190
1191             // Get the fragment view.
1192             View fragmentView = webViewTabFragment.getView();
1193
1194             // Only pause the WebViews if they exist (they won't when the app is first created).
1195             if (fragmentView != null) {
1196                 // Get the nested scroll WebView from the tab fragment.
1197                 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
1198
1199                 // Pause the nested scroll WebView.
1200                 nestedScrollWebView.onPause();
1201
1202                 // Pause the nested scroll WebView JavaScript timers.
1203                 nestedScrollWebView.pauseTimers();
1204             }
1205         }
1206
1207         // Pause the ad or it will continue to consume resources in the background on the free flavor.
1208         if (BuildConfig.FLAVOR.contentEquals("free")) {
1209             // Pause the ad.
1210             AdHelper.pauseAd(findViewById(R.id.adview));
1211         }
1212     }
1213
1214     @Override
1215     public void onDestroy() {
1216         // Unregister the Orbot status broadcast receiver.
1217         this.unregisterReceiver(orbotStatusBroadcastReceiver);
1218
1219         // Close the bookmarks cursor and database.
1220         bookmarksCursor.close();
1221         bookmarksDatabaseHelper.close();
1222
1223         // Run the default commands.
1224         super.onDestroy();
1225     }
1226
1227     @Override
1228     public boolean onCreateOptionsMenu(Menu menu) {
1229         // Inflate the menu.  This adds items to the action bar if it is present.
1230         getMenuInflater().inflate(R.menu.webview_options_menu, menu);
1231
1232         // Set mainMenu so it can be used by `onOptionsItemSelected()` and `updatePrivacyIcons`.
1233         mainMenu = menu;
1234
1235         // Set the initial status of the privacy icons.  `false` does not call `invalidateOptionsMenu` as the last step.
1236         updatePrivacyIcons(false);
1237
1238         // Get handles for the menu items.
1239         MenuItem toggleFirstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies);
1240         MenuItem toggleThirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies);
1241         MenuItem toggleDomStorageMenuItem = menu.findItem(R.id.toggle_dom_storage);
1242         MenuItem toggleSaveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data);  // Form data can be removed once the minimum API >= 26.
1243         MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data);  // Form data can be removed once the minimum API >= 26.
1244         refreshMenuItem = menu.findItem(R.id.refresh);
1245         blocklistsMenuItem = menu.findItem(R.id.blocklists);
1246         easyListMenuItem = menu.findItem(R.id.easylist);
1247         easyPrivacyMenuItem = menu.findItem(R.id.easyprivacy);
1248         fanboysAnnoyanceListMenuItem = menu.findItem(R.id.fanboys_annoyance_list);
1249         fanboysSocialBlockingListMenuItem = menu.findItem(R.id.fanboys_social_blocking_list);
1250         ultraPrivacyMenuItem = menu.findItem(R.id.ultraprivacy);
1251         blockAllThirdPartyRequestsMenuItem = menu.findItem(R.id.block_all_third_party_requests);
1252         MenuItem adConsentMenuItem = menu.findItem(R.id.ad_consent);
1253
1254         // Only display third-party cookies if API >= 21
1255         toggleThirdPartyCookiesMenuItem.setVisible(Build.VERSION.SDK_INT >= 21);
1256
1257         // Only display the form data menu items if the API < 26.
1258         toggleSaveFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
1259         clearFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
1260
1261         // Only show Ad Consent if this is the free flavor.
1262         adConsentMenuItem.setVisible(BuildConfig.FLAVOR.contentEquals("free"));
1263
1264         // Get the shared preference values.
1265         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
1266
1267         // Get the status of the additional AppBar icons.
1268         displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false);
1269
1270         // 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.
1271         if (displayAdditionalAppBarIcons) {
1272             toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
1273             toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
1274             refreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
1275         } else { //Do not display the additional icons.
1276             toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
1277             toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
1278             refreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
1279         }
1280
1281         // Replace Refresh with Stop if a URL is already loading.
1282         if (urlIsLoading) {
1283             // Set the title.
1284             refreshMenuItem.setTitle(R.string.stop);
1285
1286             // If the icon is displayed in the AppBar, set it according to the theme.
1287             if (displayAdditionalAppBarIcons) {
1288                 if (darkTheme) {
1289                     refreshMenuItem.setIcon(R.drawable.close_dark);
1290                 } else {
1291                     refreshMenuItem.setIcon(R.drawable.close_light);
1292                 }
1293             }
1294         }
1295
1296         return true;
1297     }
1298
1299     @Override
1300     public boolean onPrepareOptionsMenu(Menu menu) {
1301         // Get a handle for the swipe refresh layout.
1302         SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
1303
1304         // Get handles for the menu items.
1305         MenuItem addOrEditDomain = menu.findItem(R.id.add_or_edit_domain);
1306         MenuItem toggleFirstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies);
1307         MenuItem toggleThirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies);
1308         MenuItem toggleDomStorageMenuItem = menu.findItem(R.id.toggle_dom_storage);
1309         MenuItem toggleSaveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data);  // Form data can be removed once the minimum API >= 26.
1310         MenuItem clearDataMenuItem = menu.findItem(R.id.clear_data);
1311         MenuItem clearCookiesMenuItem = menu.findItem(R.id.clear_cookies);
1312         MenuItem clearDOMStorageMenuItem = menu.findItem(R.id.clear_dom_storage);
1313         MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data);  // Form data can be removed once the minimum API >= 26.
1314         MenuItem fontSizeMenuItem = menu.findItem(R.id.font_size);
1315         MenuItem swipeToRefreshMenuItem = menu.findItem(R.id.swipe_to_refresh);
1316         MenuItem displayImagesMenuItem = menu.findItem(R.id.display_images);
1317         MenuItem nightModeMenuItem = menu.findItem(R.id.night_mode);
1318         MenuItem proxyThroughOrbotMenuItem = menu.findItem(R.id.proxy_through_orbot);
1319
1320         // Initialize the current user agent string and the font size.
1321         String currentUserAgent = getString(R.string.user_agent_privacy_browser);
1322         int fontSize = 100;
1323
1324         // 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.
1325         if (currentWebView != null) {
1326             // Set the add or edit domain text.
1327             if (currentWebView.getDomainSettingsApplied()) {
1328                 addOrEditDomain.setTitle(R.string.edit_domain_settings);
1329             } else {
1330                 addOrEditDomain.setTitle(R.string.add_domain_settings);
1331             }
1332
1333             // Get the current user agent from the WebView.
1334             currentUserAgent = currentWebView.getSettings().getUserAgentString();
1335
1336             // Get the current font size from the
1337             fontSize = currentWebView.getSettings().getTextZoom();
1338
1339             // Set the status of the display images menu item.
1340             displayImagesMenuItem.setChecked(currentWebView.getSettings().getLoadsImagesAutomatically());
1341
1342             // Initialize the display names for the blocklists with the number of blocked requests.
1343             blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
1344             easyListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASY_LIST_BLOCKED_REQUESTS) + " - " + getString(R.string.easylist));
1345             easyPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASY_PRIVACY_BLOCKED_REQUESTS) + " - " + getString(R.string.easyprivacy));
1346             fanboysAnnoyanceListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST_BLOCKED_REQUESTS) + " - " + getString(R.string.fanboys_annoyance_list));
1347             fanboysSocialBlockingListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST_BLOCKED_REQUESTS) + " - " +
1348                     getString(R.string.fanboys_social_blocking_list));
1349             ultraPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.ULTRA_PRIVACY_BLOCKED_REQUESTS) + " - " + getString(R.string.ultraprivacy));
1350             blockAllThirdPartyRequestsMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_BLOCKED_REQUESTS) + " - " + getString(R.string.block_all_third_party_requests));
1351         }
1352
1353         // Set the status of the menu item checkboxes.
1354         toggleFirstPartyCookiesMenuItem.setChecked(firstPartyCookiesEnabled);
1355         toggleThirdPartyCookiesMenuItem.setChecked(thirdPartyCookiesEnabled);
1356         toggleDomStorageMenuItem.setChecked(domStorageEnabled);
1357         toggleSaveFormDataMenuItem.setChecked(saveFormDataEnabled);  // Form data can be removed once the minimum API >= 26.
1358         easyListMenuItem.setChecked(easyListEnabled);
1359         easyPrivacyMenuItem.setChecked(easyPrivacyEnabled);
1360         fanboysAnnoyanceListMenuItem.setChecked(fanboysAnnoyanceListEnabled);
1361         fanboysSocialBlockingListMenuItem.setChecked(fanboysSocialBlockingListEnabled);
1362         ultraPrivacyMenuItem.setChecked(ultraPrivacyEnabled);
1363         blockAllThirdPartyRequestsMenuItem.setChecked(blockAllThirdPartyRequests);
1364         swipeToRefreshMenuItem.setChecked(swipeRefreshLayout.isEnabled());
1365         nightModeMenuItem.setChecked(nightMode);
1366         proxyThroughOrbotMenuItem.setChecked(proxyThroughOrbot);
1367
1368         // Enable third-party cookies if first-party cookies are enabled.
1369         toggleThirdPartyCookiesMenuItem.setEnabled(firstPartyCookiesEnabled);
1370
1371         // Enable DOM Storage if JavaScript is enabled.
1372         toggleDomStorageMenuItem.setEnabled(javaScriptEnabled);
1373
1374         // Enable Clear Cookies if there are any.
1375         clearCookiesMenuItem.setEnabled(cookieManager.hasCookies());
1376
1377         // Get a count of the number of files in the Local Storage directory.
1378         File localStorageDirectory = new File (privateDataDirectoryString + "/app_webview/Local Storage/");
1379         int localStorageDirectoryNumberOfFiles = 0;
1380         if (localStorageDirectory.exists()) {
1381             localStorageDirectoryNumberOfFiles = localStorageDirectory.list().length;
1382         }
1383
1384         // Get a count of the number of files in the IndexedDB directory.
1385         File indexedDBDirectory = new File (privateDataDirectoryString + "/app_webview/IndexedDB");
1386         int indexedDBDirectoryNumberOfFiles = 0;
1387         if (indexedDBDirectory.exists()) {
1388             indexedDBDirectoryNumberOfFiles = indexedDBDirectory.list().length;
1389         }
1390
1391         // Enable Clear DOM Storage if there is any.
1392         clearDOMStorageMenuItem.setEnabled(localStorageDirectoryNumberOfFiles > 0 || indexedDBDirectoryNumberOfFiles > 0);
1393
1394         // Enable Clear Form Data is there is any.  This can be removed once the minimum API >= 26.
1395         if (Build.VERSION.SDK_INT < 26) {
1396             WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(this);
1397             clearFormDataMenuItem.setEnabled(mainWebViewDatabase.hasFormData());
1398         } else {
1399             // Disable clear form data because it is not supported on current version of Android.
1400             clearFormDataMenuItem.setEnabled(false);
1401         }
1402
1403         // Enable Clear Data if any of the submenu items are enabled.
1404         clearDataMenuItem.setEnabled(clearCookiesMenuItem.isEnabled() || clearDOMStorageMenuItem.isEnabled() || clearFormDataMenuItem.isEnabled());
1405
1406         // Disable Fanboy's Social Blocking List if Fanboy's Annoyance List is checked.
1407         fanboysSocialBlockingListMenuItem.setEnabled(!fanboysAnnoyanceListEnabled);
1408
1409         // Select the current user agent menu item.  A switch statement cannot be used because the user agents are not compile time constants.
1410         if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[0])) {  // Privacy Browser.
1411             menu.findItem(R.id.user_agent_privacy_browser).setChecked(true);
1412         } else if (currentUserAgent.equals(webViewDefaultUserAgent)) {  // WebView Default.
1413             menu.findItem(R.id.user_agent_webview_default).setChecked(true);
1414         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[2])) {  // Firefox on Android.
1415             menu.findItem(R.id.user_agent_firefox_on_android).setChecked(true);
1416         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[3])) {  // Chrome on Android.
1417             menu.findItem(R.id.user_agent_chrome_on_android).setChecked(true);
1418         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[4])) {  // Safari on iOS.
1419             menu.findItem(R.id.user_agent_safari_on_ios).setChecked(true);
1420         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[5])) {  // Firefox on Linux.
1421             menu.findItem(R.id.user_agent_firefox_on_linux).setChecked(true);
1422         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[6])) {  // Chromium on Linux.
1423             menu.findItem(R.id.user_agent_chromium_on_linux).setChecked(true);
1424         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[7])) {  // Firefox on Windows.
1425             menu.findItem(R.id.user_agent_firefox_on_windows).setChecked(true);
1426         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[8])) {  // Chrome on Windows.
1427             menu.findItem(R.id.user_agent_chrome_on_windows).setChecked(true);
1428         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[9])) {  // Edge on Windows.
1429             menu.findItem(R.id.user_agent_edge_on_windows).setChecked(true);
1430         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[10])) {  // Internet Explorer on Windows.
1431             menu.findItem(R.id.user_agent_internet_explorer_on_windows).setChecked(true);
1432         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[11])) {  // Safari on macOS.
1433             menu.findItem(R.id.user_agent_safari_on_macos).setChecked(true);
1434         } else {  // Custom user agent.
1435             menu.findItem(R.id.user_agent_custom).setChecked(true);
1436         }
1437
1438         // Instantiate the font size title and the selected font size menu item.
1439         String fontSizeTitle;
1440         MenuItem selectedFontSizeMenuItem;
1441
1442         // Prepare the font size title and current size menu item.
1443         switch (fontSize) {
1444             case 25:
1445                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.twenty_five_percent);
1446                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_twenty_five_percent);
1447                 break;
1448
1449             case 50:
1450                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.fifty_percent);
1451                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_fifty_percent);
1452                 break;
1453
1454             case 75:
1455                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.seventy_five_percent);
1456                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_seventy_five_percent);
1457                 break;
1458
1459             case 100:
1460                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent);
1461                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent);
1462                 break;
1463
1464             case 125:
1465                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_twenty_five_percent);
1466                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_twenty_five_percent);
1467                 break;
1468
1469             case 150:
1470                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_fifty_percent);
1471                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_fifty_percent);
1472                 break;
1473
1474             case 175:
1475                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_seventy_five_percent);
1476                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_seventy_five_percent);
1477                 break;
1478
1479             case 200:
1480                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.two_hundred_percent);
1481                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_two_hundred_percent);
1482                 break;
1483
1484             default:
1485                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent);
1486                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent);
1487                 break;
1488         }
1489
1490         // Set the font size title and select the current size menu item.
1491         fontSizeMenuItem.setTitle(fontSizeTitle);
1492         selectedFontSizeMenuItem.setChecked(true);
1493
1494         // Run all the other default commands.
1495         super.onPrepareOptionsMenu(menu);
1496
1497         // Display the menu.
1498         return true;
1499     }
1500
1501     @Override
1502     // Remove Android Studio's warning about the dangers of using SetJavaScriptEnabled.
1503     @SuppressLint("SetJavaScriptEnabled")
1504     // removeAllCookies is deprecated, but it is required for API < 21.
1505     @SuppressWarnings("deprecation")
1506     public boolean onOptionsItemSelected(MenuItem menuItem) {
1507         // Reenter full screen browsing mode if it was interrupted by the options menu.  <https://redmine.stoutner.com/issues/389>
1508         if (inFullScreenBrowsingMode) {
1509             // Remove the translucent status flag.  This is necessary so the root frame layout can fill the entire screen.
1510             getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
1511
1512             FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
1513
1514             /* Hide the system bars.
1515              * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
1516              * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
1517              * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
1518              * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
1519              */
1520             rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
1521                     View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
1522         }
1523
1524         // Get the selected menu item ID.
1525         int menuItemId = menuItem.getItemId();
1526
1527         // Run the commands that correlate to the selected menu item.
1528         switch (menuItemId) {
1529             case R.id.toggle_javascript:
1530                 // Switch the status of javaScriptEnabled.
1531                 javaScriptEnabled = !javaScriptEnabled;
1532
1533                 // Apply the new JavaScript status.
1534                 currentWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled);
1535
1536                 // Update the privacy icon.  `true` runs `invalidateOptionsMenu` as the last step.
1537                 updatePrivacyIcons(true);
1538
1539                 // Display a `Snackbar`.
1540                 if (javaScriptEnabled) {  // JavaScrip is enabled.
1541                     Snackbar.make(findViewById(R.id.webviewpager), R.string.javascript_enabled, Snackbar.LENGTH_SHORT).show();
1542                 } else if (firstPartyCookiesEnabled) {  // JavaScript is disabled, but first-party cookies are enabled.
1543                     Snackbar.make(findViewById(R.id.webviewpager), R.string.javascript_disabled, Snackbar.LENGTH_SHORT).show();
1544                 } else {  // Privacy mode.
1545                     Snackbar.make(findViewById(R.id.webviewpager), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
1546                 }
1547
1548                 // Reload the current WebView.
1549                 currentWebView.reload();
1550                 return true;
1551
1552             case R.id.add_or_edit_domain:
1553                 if (currentWebView.getDomainSettingsApplied()) {  // Edit the current domain settings.
1554                     // Reapply the domain settings on returning to `MainWebViewActivity`.
1555                     reapplyDomainSettingsOnRestart = true;
1556                     currentDomainName = "";
1557
1558                     // TODO.  Move these to `putExtra`.  The certificate can be stored as strings.
1559                     // Store the current SSL certificate and IP addresses in the domains activity.
1560                     DomainsActivity.currentSslCertificate = currentWebView.getCertificate();
1561                     DomainsActivity.currentIpAddresses = currentWebView.getCurrentIpAddresses();
1562
1563                     // Create an intent to launch the domains activity.
1564                     Intent domainsIntent = new Intent(this, DomainsActivity.class);
1565
1566                     // Put extra information instructing the domains activity to directly load the current domain and close on back instead of returning to the domains list.
1567                     domainsIntent.putExtra("loadDomain", currentWebView.getDomainSettingsDatabaseId());
1568                     domainsIntent.putExtra("closeOnBack", true);
1569
1570                     // Make it so.
1571                     startActivity(domainsIntent);
1572                 } else {  // Add a new domain.
1573                     // Apply the new domain settings on returning to `MainWebViewActivity`.
1574                     reapplyDomainSettingsOnRestart = true;
1575                     currentDomainName = "";
1576
1577                     // Get the current domain
1578                     Uri currentUri = Uri.parse(formattedUrlString);
1579                     String currentDomain = currentUri.getHost();
1580
1581                     // Initialize the database handler.  The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
1582                     DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
1583
1584                     // Create the domain and store the database ID.
1585                     int newDomainDatabaseId = domainsDatabaseHelper.addDomain(currentDomain);
1586
1587                     // TODO.  Move these to `putExtra`.  The certificate can be stored as strings.
1588                     // Store the current SSL certificate and IP addresses in the domains activity.
1589                     DomainsActivity.currentSslCertificate = currentWebView.getCertificate();
1590                     DomainsActivity.currentIpAddresses = currentWebView.getCurrentIpAddresses();
1591
1592                     // Create an intent to launch the domains activity.
1593                     Intent domainsIntent = new Intent(this, DomainsActivity.class);
1594
1595                     // Put extra information instructing the domains activity to directly load the new domain and close on back instead of returning to the domains list.
1596                     domainsIntent.putExtra("loadDomain", newDomainDatabaseId);
1597                     domainsIntent.putExtra("closeOnBack", true);
1598
1599                     // Make it so.
1600                     startActivity(domainsIntent);
1601                 }
1602                 return true;
1603
1604             case R.id.toggle_first_party_cookies:
1605                 // Switch the status of firstPartyCookiesEnabled.
1606                 firstPartyCookiesEnabled = !firstPartyCookiesEnabled;
1607
1608                 // Update the menu checkbox.
1609                 menuItem.setChecked(firstPartyCookiesEnabled);
1610
1611                 // Apply the new cookie status.
1612                 cookieManager.setAcceptCookie(firstPartyCookiesEnabled);
1613
1614                 // Update the privacy icon.  `true` runs `invalidateOptionsMenu` as the last step.
1615                 updatePrivacyIcons(true);
1616
1617                 // Display a `Snackbar`.
1618                 if (firstPartyCookiesEnabled) {  // First-party cookies are enabled.
1619                     Snackbar.make(findViewById(R.id.webviewpager), R.string.first_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
1620                 } else if (javaScriptEnabled) {  // JavaScript is still enabled.
1621                     Snackbar.make(findViewById(R.id.webviewpager), R.string.first_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
1622                 } else {  // Privacy mode.
1623                     Snackbar.make(findViewById(R.id.webviewpager), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
1624                 }
1625
1626                 // Reload the current WebView.
1627                 currentWebView.reload();
1628                 return true;
1629
1630             case R.id.toggle_third_party_cookies:
1631                 if (Build.VERSION.SDK_INT >= 21) {
1632                     // Switch the status of thirdPartyCookiesEnabled.
1633                     thirdPartyCookiesEnabled = !thirdPartyCookiesEnabled;
1634
1635                     // Update the menu checkbox.
1636                     menuItem.setChecked(thirdPartyCookiesEnabled);
1637
1638                     // Apply the new cookie status.
1639                     cookieManager.setAcceptThirdPartyCookies(currentWebView, thirdPartyCookiesEnabled);
1640
1641                     // Display a `Snackbar`.
1642                     if (thirdPartyCookiesEnabled) {
1643                         Snackbar.make(findViewById(R.id.webviewpager), R.string.third_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
1644                     } else {
1645                         Snackbar.make(findViewById(R.id.webviewpager), R.string.third_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
1646                     }
1647
1648                     // Reload the current WebView.
1649                     currentWebView.reload();
1650                 } // Else do nothing because SDK < 21.
1651                 return true;
1652
1653             case R.id.toggle_dom_storage:
1654                 // Switch the status of domStorageEnabled.
1655                 domStorageEnabled = !domStorageEnabled;
1656
1657                 // Update the menu checkbox.
1658                 menuItem.setChecked(domStorageEnabled);
1659
1660                 // Apply the new DOM Storage status.
1661                 currentWebView.getSettings().setDomStorageEnabled(domStorageEnabled);
1662
1663                 // Update the privacy icon.  `true` runs `invalidateOptionsMenu` as the last step.
1664                 updatePrivacyIcons(true);
1665
1666                 // Display a `Snackbar`.
1667                 if (domStorageEnabled) {
1668                     Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_enabled, Snackbar.LENGTH_SHORT).show();
1669                 } else {
1670                     Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_disabled, Snackbar.LENGTH_SHORT).show();
1671                 }
1672
1673                 // Reload the current WebView.
1674                 currentWebView.reload();
1675                 return true;
1676
1677             // Form data can be removed once the minimum API >= 26.
1678             case R.id.toggle_save_form_data:
1679                 // Switch the status of saveFormDataEnabled.
1680                 saveFormDataEnabled = !saveFormDataEnabled;
1681
1682                 // Update the menu checkbox.
1683                 menuItem.setChecked(saveFormDataEnabled);
1684
1685                 // Apply the new form data status.
1686                 currentWebView.getSettings().setSaveFormData(saveFormDataEnabled);
1687
1688                 // Display a `Snackbar`.
1689                 if (saveFormDataEnabled) {
1690                     Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_enabled, Snackbar.LENGTH_SHORT).show();
1691                 } else {
1692                     Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_disabled, Snackbar.LENGTH_SHORT).show();
1693                 }
1694
1695                 // Update the privacy icon.  `true` runs `invalidateOptionsMenu` as the last step.
1696                 updatePrivacyIcons(true);
1697
1698                 // Reload the current WebView.
1699                 currentWebView.reload();
1700                 return true;
1701
1702             case R.id.clear_cookies:
1703                 Snackbar.make(findViewById(R.id.webviewpager), R.string.cookies_deleted, Snackbar.LENGTH_LONG)
1704                         .setAction(R.string.undo, v -> {
1705                             // Do nothing because everything will be handled by `onDismissed()` below.
1706                         })
1707                         .addCallback(new Snackbar.Callback() {
1708                             @SuppressLint("SwitchIntDef")  // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1709                             @Override
1710                             public void onDismissed(Snackbar snackbar, int event) {
1711                                 switch (event) {
1712                                     // The user pushed the undo button.
1713                                     case Snackbar.Callback.DISMISS_EVENT_ACTION:
1714                                         // Do nothing.
1715                                         break;
1716
1717                                     // The snackbar was dismissed without the undo button being pushed.
1718                                     default:
1719                                         // `cookieManager.removeAllCookie()` varies by SDK.
1720                                         if (Build.VERSION.SDK_INT < 21) {
1721                                             cookieManager.removeAllCookie();
1722                                         } else {
1723                                             cookieManager.removeAllCookies(null);
1724                                         }
1725                                 }
1726                             }
1727                         })
1728                         .show();
1729                 return true;
1730
1731             case R.id.clear_dom_storage:
1732                 Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_deleted, Snackbar.LENGTH_LONG)
1733                         .setAction(R.string.undo, v -> {
1734                             // Do nothing because everything will be handled by `onDismissed()` below.
1735                         })
1736                         .addCallback(new Snackbar.Callback() {
1737                             @SuppressLint("SwitchIntDef")  // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1738                             @Override
1739                             public void onDismissed(Snackbar snackbar, int event) {
1740                                 switch (event) {
1741                                     // The user pushed the undo button.
1742                                     case Snackbar.Callback.DISMISS_EVENT_ACTION:
1743                                         // Do nothing.
1744                                         break;
1745
1746                                     // The snackbar was dismissed without the undo button being pushed.
1747                                     default:
1748                                         // Delete the DOM Storage.
1749                                         WebStorage webStorage = WebStorage.getInstance();
1750                                         webStorage.deleteAllData();
1751
1752                                         // Initialize a handler to manually delete the DOM storage files and directories.
1753                                         Handler deleteDomStorageHandler = new Handler();
1754
1755                                         // Setup a runnable to manually delete the DOM storage files and directories.
1756                                         Runnable deleteDomStorageRunnable = () -> {
1757                                             try {
1758                                                 // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
1759                                                 Process deleteLocalStorageProcess = privacyBrowserRuntime.exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
1760
1761                                                 // Multiple commands must be used because `Runtime.exec()` does not like `*`.
1762                                                 Process deleteIndexProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
1763                                                 Process deleteQuotaManagerProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
1764                                                 Process deleteQuotaManagerJournalProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
1765                                                 Process deleteDatabasesProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
1766
1767                                                 // Wait for the processes to finish.
1768                                                 deleteLocalStorageProcess.waitFor();
1769                                                 deleteIndexProcess.waitFor();
1770                                                 deleteQuotaManagerProcess.waitFor();
1771                                                 deleteQuotaManagerJournalProcess.waitFor();
1772                                                 deleteDatabasesProcess.waitFor();
1773                                             } catch (Exception exception) {
1774                                                 // Do nothing if an error is thrown.
1775                                             }
1776                                         };
1777
1778                                         // Manually delete the DOM storage files after 200 milliseconds.
1779                                         deleteDomStorageHandler.postDelayed(deleteDomStorageRunnable, 200);
1780                                 }
1781                             }
1782                         })
1783                         .show();
1784                 return true;
1785
1786             // Form data can be remove once the minimum API >= 26.
1787             case R.id.clear_form_data:
1788                 Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_deleted, Snackbar.LENGTH_LONG)
1789                         .setAction(R.string.undo, v -> {
1790                             // Do nothing because everything will be handled by `onDismissed()` below.
1791                         })
1792                         .addCallback(new Snackbar.Callback() {
1793                             @SuppressLint("SwitchIntDef")  // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1794                             @Override
1795                             public void onDismissed(Snackbar snackbar, int event) {
1796                                 switch (event) {
1797                                     // The user pushed the undo button.
1798                                     case Snackbar.Callback.DISMISS_EVENT_ACTION:
1799                                         // Do nothing.
1800                                         break;
1801
1802                                     // The snackbar was dismissed without the `Undo` button being pushed.
1803                                     default:
1804                                         // Delete the form data.
1805                                         WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(getApplicationContext());
1806                                         mainWebViewDatabase.clearFormData();
1807                                 }
1808                             }
1809                         })
1810                         .show();
1811                 return true;
1812
1813             case R.id.easylist:
1814                 // Toggle the EasyList status.
1815                 easyListEnabled = !easyListEnabled;
1816
1817                 // Update the menu checkbox.
1818                 menuItem.setChecked(easyListEnabled);
1819
1820                 // Reload the current WebView.
1821                 currentWebView.reload();
1822                 return true;
1823
1824             case R.id.easyprivacy:
1825                 // Toggle the EasyPrivacy status.
1826                 easyPrivacyEnabled = !easyPrivacyEnabled;
1827
1828                 // Update the menu checkbox.
1829                 menuItem.setChecked(easyPrivacyEnabled);
1830
1831                 // Reload the current WebView.
1832                 currentWebView.reload();
1833                 return true;
1834
1835             case R.id.fanboys_annoyance_list:
1836                 // Toggle Fanboy's Annoyance List status.
1837                 fanboysAnnoyanceListEnabled = !fanboysAnnoyanceListEnabled;
1838
1839                 // Update the menu checkbox.
1840                 menuItem.setChecked(fanboysAnnoyanceListEnabled);
1841
1842                 // Update the staus of Fanboy's Social Blocking List.
1843                 MenuItem fanboysSocialBlockingListMenuItem = mainMenu.findItem(R.id.fanboys_social_blocking_list);
1844                 fanboysSocialBlockingListMenuItem.setEnabled(!fanboysAnnoyanceListEnabled);
1845
1846                 // Reload the current WebView.
1847                 currentWebView.reload();
1848                 return true;
1849
1850             case R.id.fanboys_social_blocking_list:
1851                 // Toggle Fanboy's Social Blocking List status.
1852                 fanboysSocialBlockingListEnabled = !fanboysSocialBlockingListEnabled;
1853
1854                 // Update the menu checkbox.
1855                 menuItem.setChecked(fanboysSocialBlockingListEnabled);
1856
1857                 // Reload the current WebView.
1858                 currentWebView.reload();
1859                 return true;
1860
1861             case R.id.ultraprivacy:
1862                 // Toggle the UltraPrivacy status.
1863                 ultraPrivacyEnabled = !ultraPrivacyEnabled;
1864
1865                 // Update the menu checkbox.
1866                 menuItem.setChecked(ultraPrivacyEnabled);
1867
1868                 // Reload the current WebView.
1869                 currentWebView.reload();
1870                 return true;
1871
1872             case R.id.block_all_third_party_requests:
1873                 //Toggle the third-party requests blocker status.
1874                 blockAllThirdPartyRequests = !blockAllThirdPartyRequests;
1875
1876                 // Update the menu checkbox.
1877                 menuItem.setChecked(blockAllThirdPartyRequests);
1878
1879                 // Reload the current WebView.
1880                 currentWebView.reload();
1881                 return true;
1882
1883             case R.id.user_agent_privacy_browser:
1884                 // Update the user agent.
1885                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[0]);
1886
1887                 // Reload the current WebView.
1888                 currentWebView.reload();
1889                 return true;
1890
1891             case R.id.user_agent_webview_default:
1892                 // Update the user agent.
1893                 currentWebView.getSettings().setUserAgentString("");
1894
1895                 // Reload the current WebView.
1896                 currentWebView.reload();
1897                 return true;
1898
1899             case R.id.user_agent_firefox_on_android:
1900                 // Update the user agent.
1901                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[2]);
1902
1903                 // Reload the current WebView.
1904                 currentWebView.reload();
1905                 return true;
1906
1907             case R.id.user_agent_chrome_on_android:
1908                 // Update the user agent.
1909                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[3]);
1910
1911                 // Reload the current WebView.
1912                 currentWebView.reload();
1913                 return true;
1914
1915             case R.id.user_agent_safari_on_ios:
1916                 // Update the user agent.
1917                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[4]);
1918
1919                 // Reload the current WebView.
1920                 currentWebView.reload();
1921                 return true;
1922
1923             case R.id.user_agent_firefox_on_linux:
1924                 // Update the user agent.
1925                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[5]);
1926
1927                 // Reload the current WebView.
1928                 currentWebView.reload();
1929                 return true;
1930
1931             case R.id.user_agent_chromium_on_linux:
1932                 // Update the user agent.
1933                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[6]);
1934
1935                 // Reload the current WebView.
1936                 currentWebView.reload();
1937                 return true;
1938
1939             case R.id.user_agent_firefox_on_windows:
1940                 // Update the user agent.
1941                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[7]);
1942
1943                 // Reload the current WebView.
1944                 currentWebView.reload();
1945                 return true;
1946
1947             case R.id.user_agent_chrome_on_windows:
1948                 // Update the user agent.
1949                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[8]);
1950
1951                 // Reload the current WebView.
1952                 currentWebView.reload();
1953                 return true;
1954
1955             case R.id.user_agent_edge_on_windows:
1956                 // Update the user agent.
1957                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[9]);
1958
1959                 // Reload the current WebView.
1960                 currentWebView.reload();
1961                 return true;
1962
1963             case R.id.user_agent_internet_explorer_on_windows:
1964                 // Update the user agent.
1965                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[10]);
1966
1967                 // Reload the current WebView.
1968                 currentWebView.reload();
1969                 return true;
1970
1971             case R.id.user_agent_safari_on_macos:
1972                 // Update the user agent.
1973                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[11]);
1974
1975                 // Reload the current WebView.
1976                 currentWebView.reload();
1977                 return true;
1978
1979             case R.id.user_agent_custom:
1980                 // Update the user agent.
1981                 currentWebView.getSettings().setUserAgentString(defaultCustomUserAgentString);
1982
1983                 // Reload the current WebView.
1984                 currentWebView.reload();
1985                 return true;
1986
1987             case R.id.font_size_twenty_five_percent:
1988                 currentWebView.getSettings().setTextZoom(25);
1989                 return true;
1990
1991             case R.id.font_size_fifty_percent:
1992                 currentWebView.getSettings().setTextZoom(50);
1993                 return true;
1994
1995             case R.id.font_size_seventy_five_percent:
1996                 currentWebView.getSettings().setTextZoom(75);
1997                 return true;
1998
1999             case R.id.font_size_one_hundred_percent:
2000                 currentWebView.getSettings().setTextZoom(100);
2001                 return true;
2002
2003             case R.id.font_size_one_hundred_twenty_five_percent:
2004                 currentWebView.getSettings().setTextZoom(125);
2005                 return true;
2006
2007             case R.id.font_size_one_hundred_fifty_percent:
2008                 currentWebView.getSettings().setTextZoom(150);
2009                 return true;
2010
2011             case R.id.font_size_one_hundred_seventy_five_percent:
2012                 currentWebView.getSettings().setTextZoom(175);
2013                 return true;
2014
2015             case R.id.font_size_two_hundred_percent:
2016                 currentWebView.getSettings().setTextZoom(200);
2017                 return true;
2018
2019             case R.id.swipe_to_refresh:
2020                 // Get a handle for the swipe refresh layout.
2021                 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
2022
2023                 // Toggle swipe to refresh.
2024                 swipeRefreshLayout.setEnabled(!swipeRefreshLayout.isEnabled());
2025                 return true;
2026
2027             case R.id.display_images:
2028                 if (currentWebView.getSettings().getLoadsImagesAutomatically()) {  // Images are currently loaded automatically.
2029                     // Disable loading of images.
2030                     currentWebView.getSettings().setLoadsImagesAutomatically(false);
2031
2032                     // Reload the website to remove existing images.
2033                     currentWebView.reload();
2034                 } else {  // Images are not currently loaded automatically.
2035                     // Enable loading of images.  Missing images will be loaded without the need for a reload.
2036                     currentWebView.getSettings().setLoadsImagesAutomatically(true);
2037                 }
2038                 return true;
2039
2040             case R.id.night_mode:
2041                 // Toggle night mode.
2042                 nightMode = !nightMode;
2043
2044                 // Enable or disable JavaScript according to night mode, the global preference, and any domain settings.
2045                 if (nightMode) {  // Night mode is enabled.  Enable JavaScript.
2046                     // Update the global variable.
2047                     javaScriptEnabled = true;
2048                 } else if (currentWebView.getDomainSettingsApplied()) {  // Night mode is disabled and domain settings are applied.  Set JavaScript according to the domain settings.
2049                     // Get the JavaScript preference that was stored the last time domain settings were loaded.
2050                     javaScriptEnabled = domainSettingsJavaScriptEnabled;
2051                 } else {  // Night mode is disabled and domain settings are not applied.  Set JavaScript according to the global preference.
2052                     // Get a handle for the shared preference.
2053                     SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
2054
2055                     // Get the JavaScript preference.
2056                     javaScriptEnabled = sharedPreferences.getBoolean("javascript", false);
2057                 }
2058
2059                 // Apply the JavaScript setting to the WebView.
2060                 currentWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled);
2061
2062                 // Update the privacy icons.
2063                 updatePrivacyIcons(false);
2064
2065                 // Reload the website.
2066                 currentWebView.reload();
2067                 return true;
2068
2069             case R.id.find_on_page:
2070                 // Get a handle for the views.
2071                 Toolbar toolbar = findViewById(R.id.toolbar);
2072                 LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
2073
2074                 // Hide the toolbar.
2075                 toolbar.setVisibility(View.GONE);
2076
2077                 // Show the find on page linear layout.
2078                 findOnPageLinearLayout.setVisibility(View.VISIBLE);
2079
2080                 // Display the keyboard.  The app must wait 200 ms before running the command to work around a bug in Android.
2081                 // http://stackoverflow.com/questions/5520085/android-show-softkeyboard-with-showsoftinput-is-not-working
2082                 findOnPageEditText.postDelayed(() -> {
2083                     // Set the focus on `findOnPageEditText`.
2084                     findOnPageEditText.requestFocus();
2085
2086                     // Display the keyboard.  `0` sets no input flags.
2087                     inputMethodManager.showSoftInput(findOnPageEditText, 0);
2088                 }, 200);
2089                 return true;
2090
2091             case R.id.view_source:
2092                 // Launch the View Source activity.
2093                 Intent viewSourceIntent = new Intent(this, ViewSourceActivity.class);
2094                 startActivity(viewSourceIntent);
2095                 return true;
2096
2097             case R.id.share_url:
2098                 // Setup the share string.
2099                 String shareString = currentWebView.getTitle() + " – " + formattedUrlString;
2100
2101                 // Create the share intent.
2102                 Intent shareIntent = new Intent(Intent.ACTION_SEND);
2103                 shareIntent.putExtra(Intent.EXTRA_TEXT, shareString);
2104                 shareIntent.setType("text/plain");
2105
2106                 // Make it so.
2107                 startActivity(Intent.createChooser(shareIntent, getString(R.string.share_url)));
2108                 return true;
2109
2110             case R.id.print:
2111                 // Get a `PrintManager` instance.
2112                 PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);
2113
2114                 // Create a print document adapter form the current WebView.
2115                 PrintDocumentAdapter printDocumentAdapter = currentWebView.createPrintDocumentAdapter();
2116
2117                 // Remove the lint error below that `printManager` might be `null`.
2118                 assert printManager != null;
2119
2120                 // Print the document.  The print attributes are `null`.
2121                 printManager.print(getString(R.string.privacy_browser_web_page), printDocumentAdapter, null);
2122                 return true;
2123
2124             case R.id.open_with_app:
2125                 openWithApp(formattedUrlString);
2126                 return true;
2127
2128             case R.id.open_with_browser:
2129                 openWithBrowser(formattedUrlString);
2130                 return true;
2131
2132             case R.id.add_to_homescreen:
2133                 // Instantiate the create home screen shortcut dialog.
2134                 DialogFragment createHomeScreenShortcutDialogFragment = CreateHomeScreenShortcutDialog.createDialog(currentWebView.getTitle(), formattedUrlString, favoriteIconBitmap);
2135
2136                 // Show the create home screen shortcut dialog.
2137                 createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getString(R.string.create_shortcut));
2138                 return true;
2139
2140             case R.id.proxy_through_orbot:
2141                 // Toggle the proxy through Orbot variable.
2142                 proxyThroughOrbot = !proxyThroughOrbot;
2143
2144                 // Apply the proxy through Orbot settings.
2145                 applyProxyThroughOrbot(true);
2146                 return true;
2147
2148             case R.id.refresh:
2149                 if (menuItem.getTitle().equals(getString(R.string.refresh))) {  // The refresh button was pushed.
2150                     // Reload the current WebView.
2151                     currentWebView.reload();
2152                 } else {  // The stop button was pushed.
2153                     // Stop the loading of the WebView.
2154                     currentWebView.stopLoading();
2155                 }
2156                 return true;
2157
2158             case R.id.ad_consent:
2159                 // Display the ad consent dialog.
2160                 DialogFragment adConsentDialogFragment = new AdConsentDialog();
2161                 adConsentDialogFragment.show(getSupportFragmentManager(), getString(R.string.ad_consent));
2162                 return true;
2163
2164             default:
2165                 // Don't consume the event.
2166                 return super.onOptionsItemSelected(menuItem);
2167         }
2168     }
2169
2170     // removeAllCookies is deprecated, but it is required for API < 21.
2171     @SuppressWarnings("deprecation")
2172     @Override
2173     public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
2174         // Get the menu item ID.
2175         int menuItemId = menuItem.getItemId();
2176
2177         // Run the commands that correspond to the selected menu item.
2178         switch (menuItemId) {
2179             case R.id.close_tab:
2180                 // Get a handle for the tab layout.
2181                 TabLayout tabLayout = findViewById(R.id.tablayout);
2182
2183                 // Get the current tab number.
2184                 int currentTabNumber = tabLayout.getSelectedTabPosition();
2185
2186                 // Delete the current tab.
2187                 tabLayout.removeTabAt(currentTabNumber);
2188
2189                 // Delete the current page.
2190                 webViewPagerAdapter.deletePage(currentTabNumber);
2191                 break;
2192
2193             case R.id.clear_and_exit:
2194                 // Close the bookmarks cursor and database.
2195                 bookmarksCursor.close();
2196                 bookmarksDatabaseHelper.close();
2197
2198                 // Get a handle for the shared preferences.
2199                 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
2200
2201                 // Get the status of the clear everything preference.
2202                 boolean clearEverything = sharedPreferences.getBoolean("clear_everything", true);
2203
2204                 // Clear cookies.
2205                 if (clearEverything || sharedPreferences.getBoolean("clear_cookies", true)) {
2206                     // The command to remove cookies changed slightly in API 21.
2207                     if (Build.VERSION.SDK_INT >= 21) {
2208                         cookieManager.removeAllCookies(null);
2209                     } else {
2210                         cookieManager.removeAllCookie();
2211                     }
2212
2213                     // Manually delete the cookies database, as `CookieManager` sometimes will not flush its changes to disk before `System.exit(0)` is run.
2214                     try {
2215                         // Two commands must be used because `Runtime.exec()` does not like `*`.
2216                         Process deleteCookiesProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies");
2217                         Process deleteCookiesJournalProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies-journal");
2218
2219                         // Wait until the processes have finished.
2220                         deleteCookiesProcess.waitFor();
2221                         deleteCookiesJournalProcess.waitFor();
2222                     } catch (Exception exception) {
2223                         // Do nothing if an error is thrown.
2224                     }
2225                 }
2226
2227                 // Clear DOM storage.
2228                 if (clearEverything || sharedPreferences.getBoolean("clear_dom_storage", true)) {
2229                     // Ask `WebStorage` to clear the DOM storage.
2230                     WebStorage webStorage = WebStorage.getInstance();
2231                     webStorage.deleteAllData();
2232
2233                     // Manually delete the DOM storage files and directories, as `WebStorage` sometimes will not flush its changes to disk before `System.exit(0)` is run.
2234                     try {
2235                         // A `String[]` must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
2236                         Process deleteLocalStorageProcess = privacyBrowserRuntime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
2237
2238                         // Multiple commands must be used because `Runtime.exec()` does not like `*`.
2239                         Process deleteIndexProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
2240                         Process deleteQuotaManagerProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
2241                         Process deleteQuotaManagerJournalProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
2242                         Process deleteDatabaseProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
2243
2244                         // Wait until the processes have finished.
2245                         deleteLocalStorageProcess.waitFor();
2246                         deleteIndexProcess.waitFor();
2247                         deleteQuotaManagerProcess.waitFor();
2248                         deleteQuotaManagerJournalProcess.waitFor();
2249                         deleteDatabaseProcess.waitFor();
2250                     } catch (Exception exception) {
2251                         // Do nothing if an error is thrown.
2252                     }
2253                 }
2254
2255                 // Clear form data if the API < 26.
2256                 if ((Build.VERSION.SDK_INT < 26) && (clearEverything || sharedPreferences.getBoolean("clear_form_data", true))) {
2257                     WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
2258                     webViewDatabase.clearFormData();
2259
2260                     // Manually delete the form data database, as `WebViewDatabase` sometimes will not flush its changes to disk before `System.exit(0)` is run.
2261                     try {
2262                         // A string array must be used because the database contains a space and `Runtime.exec` will not otherwise escape the string correctly.
2263                         Process deleteWebDataProcess = privacyBrowserRuntime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data"});
2264                         Process deleteWebDataJournalProcess = privacyBrowserRuntime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data-journal"});
2265
2266                         // Wait until the processes have finished.
2267                         deleteWebDataProcess.waitFor();
2268                         deleteWebDataJournalProcess.waitFor();
2269                     } catch (Exception exception) {
2270                         // Do nothing if an error is thrown.
2271                     }
2272                 }
2273
2274                 // Clear the cache.
2275                 if (clearEverything || sharedPreferences.getBoolean("clear_cache", true)) {
2276                     // Clear the cache from each WebView.
2277                     for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
2278                         // Get the WebView tab fragment.
2279                         WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
2280
2281                         // Get the fragment view.
2282                         View fragmentView = webViewTabFragment.getView();
2283
2284                         // Only clear the cache if the WebView exists.
2285                         if (fragmentView != null) {
2286                             // Get the nested scroll WebView from the tab fragment.
2287                             NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
2288
2289                             // Clear the cache for this WebView.
2290                             nestedScrollWebView.clearCache(true);
2291                         }
2292                     }
2293
2294                     // Manually delete the cache directories.
2295                     try {
2296                         // Delete the main cache directory.
2297                         Process deleteCacheProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/cache");
2298
2299                         // Delete the secondary `Service Worker` cache directory.
2300                         // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
2301                         Process deleteServiceWorkerProcess = privacyBrowserRuntime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
2302
2303                         // Wait until the processes have finished.
2304                         deleteCacheProcess.waitFor();
2305                         deleteServiceWorkerProcess.waitFor();
2306                     } catch (Exception exception) {
2307                         // Do nothing if an error is thrown.
2308                     }
2309                 }
2310
2311                 // Wipe out each WebView.
2312                 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
2313                     // Get the WebView tab fragment.
2314                     WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
2315
2316                     // Get the fragment view.
2317                     View fragmentView = webViewTabFragment.getView();
2318
2319                     // Only wipe out the WebView if it exists.
2320                     if (fragmentView != null) {
2321                         // Get the nested scroll WebView from the tab fragment.
2322                         NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
2323
2324                         // Clear SSL certificate preferences for this WebView.
2325                         nestedScrollWebView.clearSslPreferences();
2326
2327                         // Clear the back/forward history for this WebView.
2328                         nestedScrollWebView.clearHistory();
2329
2330                         // Destroy the internal state of `mainWebView`.
2331                         nestedScrollWebView.destroy();
2332                     }
2333                 }
2334
2335                 // Clear the formatted URL string.
2336                 formattedUrlString = null;
2337
2338                 // Clear the custom headers.
2339                 customHeaders.clear();
2340
2341                 // Manually delete the `app_webview` folder, which contains the cookies, DOM storage, form data, and `Service Worker` cache.
2342                 // See `https://code.google.com/p/android/issues/detail?id=233826&thanks=233826&ts=1486670530`.
2343                 if (clearEverything) {
2344                     try {
2345                         // Delete the folder.
2346                         Process deleteAppWebviewProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview");
2347
2348                         // Wait until the process has finished.
2349                         deleteAppWebviewProcess.waitFor();
2350                     } catch (Exception exception) {
2351                         // Do nothing if an error is thrown.
2352                     }
2353                 }
2354
2355                 // Close Privacy Browser.  `finishAndRemoveTask` also removes Privacy Browser from the recent app list.
2356                 if (Build.VERSION.SDK_INT >= 21) {
2357                     finishAndRemoveTask();
2358                 } else {
2359                     finish();
2360                 }
2361
2362                 // Remove the terminated program from RAM.  The status code is `0`.
2363                 System.exit(0);
2364                 break;
2365
2366             case R.id.home:
2367                 loadUrl(homepage);
2368                 break;
2369
2370             case R.id.back:
2371                 if (currentWebView.canGoBack()) {
2372                     // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
2373                     formattedUrlString = "";
2374
2375                     // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
2376                     navigatingHistory = true;
2377
2378                     // Load the previous website in the history.
2379                     currentWebView.goBack();
2380                 }
2381                 break;
2382
2383             case R.id.forward:
2384                 if (currentWebView.canGoForward()) {
2385                     // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
2386                     formattedUrlString = "";
2387
2388                     // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
2389                     navigatingHistory = true;
2390
2391                     // Load the next website in the history.
2392                     currentWebView.goForward();
2393                 }
2394                 break;
2395
2396             case R.id.history:
2397                 // Get the `WebBackForwardList`.
2398                 WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList();
2399
2400                 // Show the URL history dialog and name this instance `R.string.history`.
2401                 DialogFragment urlHistoryDialogFragment = UrlHistoryDialog.loadBackForwardList(this, webBackForwardList);
2402                 urlHistoryDialogFragment.show(getSupportFragmentManager(), getString(R.string.history));
2403                 break;
2404
2405             case R.id.requests:
2406                 // Populate the resource requests.
2407                 RequestsActivity.resourceRequests = currentWebView.getResourceRequests();
2408
2409                 // Create an intent to launch the Requests activity.
2410                 Intent requestsIntent = new Intent(this, RequestsActivity.class);
2411
2412                 // Make it so.
2413                 startActivity(requestsIntent);
2414                 break;
2415
2416             case R.id.downloads:
2417                 // Launch the system Download Manager.
2418                 Intent downloadManagerIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
2419
2420                 // Launch as a new task so that Download Manager and Privacy Browser show as separate windows in the recent tasks list.
2421                 downloadManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2422
2423                 startActivity(downloadManagerIntent);
2424                 break;
2425
2426             case R.id.domains:
2427                 // Set the flag to reapply the domain settings on restart when returning from Domain Settings.
2428                 reapplyDomainSettingsOnRestart = true;
2429                 currentDomainName = "";
2430
2431                 // TODO.  Move these to `putExtra`.  The certificate can be stored as strings.
2432                 // Store the current SSL certificate and IP addresses in the domains activity.
2433                 DomainsActivity.currentSslCertificate = currentWebView.getCertificate();
2434                 DomainsActivity.currentIpAddresses = currentWebView.getCurrentIpAddresses();
2435
2436                 // Launch the domains activity.
2437                 Intent domainsIntent = new Intent(this, DomainsActivity.class);
2438                 startActivity(domainsIntent);
2439                 break;
2440
2441             case R.id.settings:
2442                 // Set the flag to reapply app settings on restart when returning from Settings.
2443                 reapplyAppSettingsOnRestart = true;
2444
2445                 // Set the flag to reapply the domain settings on restart when returning from Settings.
2446                 reapplyDomainSettingsOnRestart = true;
2447                 currentDomainName = "";
2448
2449                 // Launch the settings activity.
2450                 Intent settingsIntent = new Intent(this, SettingsActivity.class);
2451                 startActivity(settingsIntent);
2452                 break;
2453
2454             case R.id.import_export:
2455                 // Launch the import/export activity.
2456                 Intent importExportIntent = new Intent (this, ImportExportActivity.class);
2457                 startActivity(importExportIntent);
2458                 break;
2459
2460             case R.id.logcat:
2461                 // Launch the logcat activity.
2462                 Intent logcatIntent = new Intent(this, LogcatActivity.class);
2463                 startActivity(logcatIntent);
2464                 break;
2465
2466             case R.id.guide:
2467                 // Launch `GuideActivity`.
2468                 Intent guideIntent = new Intent(this, GuideActivity.class);
2469                 startActivity(guideIntent);
2470                 break;
2471
2472             case R.id.about:
2473                 // Launch `AboutActivity`.
2474                 Intent aboutIntent = new Intent(this, AboutActivity.class);
2475                 startActivity(aboutIntent);
2476                 break;
2477         }
2478
2479         // Get a handle for the drawer layout.
2480         DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
2481
2482         // Close the navigation drawer.
2483         drawerLayout.closeDrawer(GravityCompat.START);
2484         return true;
2485     }
2486
2487     @Override
2488     public void onPostCreate(Bundle savedInstanceState) {
2489         // Run the default commands.
2490         super.onPostCreate(savedInstanceState);
2491
2492         // Sync the state of the DrawerToggle after the default `onRestoreInstanceState()` has finished.  This creates the navigation drawer icon.
2493         actionBarDrawerToggle.syncState();
2494     }
2495
2496     @Override
2497     public void onConfigurationChanged(Configuration newConfig) {
2498         // Run the default commands.
2499         super.onConfigurationChanged(newConfig);
2500
2501         // Get the status bar pixel size.
2502         int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
2503         int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId);
2504
2505         // Get the resource density.
2506         float screenDensity = getResources().getDisplayMetrics().density;
2507
2508         // Recalculate the drawer header padding.
2509         drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity);
2510         drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity);
2511         drawerHeaderPaddingBottom = (int) (8 * screenDensity);
2512
2513         // Reload the ad for the free flavor if not in full screen mode.
2514         if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
2515             // Reload the ad.  The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
2516             AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
2517         }
2518
2519         // `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:
2520         // https://code.google.com/p/android/issues/detail?id=20493#c8
2521         // ActivityCompat.invalidateOptionsMenu(this);
2522     }
2523
2524     @Override
2525     public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
2526         // Store the `HitTestResult`.
2527         final WebView.HitTestResult hitTestResult = currentWebView.getHitTestResult();
2528
2529         // Create strings.
2530         final String imageUrl;
2531         final String linkUrl;
2532
2533         // Get a handle for the the clipboard and fragment managers.
2534         final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
2535         FragmentManager fragmentManager = getSupportFragmentManager();
2536
2537         // Remove the lint errors below that `clipboardManager` might be `null`.
2538         assert clipboardManager != null;
2539
2540         switch (hitTestResult.getType()) {
2541             // `SRC_ANCHOR_TYPE` is a link.
2542             case WebView.HitTestResult.SRC_ANCHOR_TYPE:
2543                 // Get the target URL.
2544                 linkUrl = hitTestResult.getExtra();
2545
2546                 // Set the target URL as the title of the `ContextMenu`.
2547                 menu.setHeaderTitle(linkUrl);
2548
2549                 // Add a Load URL entry.
2550                 menu.add(R.string.load_url).setOnMenuItemClickListener((MenuItem item) -> {
2551                     loadUrl(linkUrl);
2552                     return false;
2553                 });
2554
2555                 // Add a Copy URL entry.
2556                 menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
2557                     // Save the link URL in a `ClipData`.
2558                     ClipData srcAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl);
2559
2560                     // Set the `ClipData` as the clipboard's primary clip.
2561                     clipboardManager.setPrimaryClip(srcAnchorTypeClipData);
2562                     return false;
2563                 });
2564
2565                 // Add a Download URL entry.
2566                 menu.add(R.string.download_url).setOnMenuItemClickListener((MenuItem item) -> {
2567                     // Check if the download should be processed by an external app.
2568                     if (downloadWithExternalApp) {  // Download with an external app.
2569                         openUrlWithExternalApp(linkUrl);
2570                     } else {  // Download with Android's download manager.
2571                         // Check to see if the storage permission has already been granted.
2572                         if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {  // The storage permission needs to be requested.
2573                             // Store the variables for future use by `onRequestPermissionsResult()`.
2574                             downloadUrl = linkUrl;
2575                             downloadContentDisposition = "none";
2576                             downloadContentLength = -1;
2577
2578                             // Show a dialog if the user has previously denied the permission.
2579                             if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
2580                                 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
2581                                 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE);
2582
2583                                 // Show the download location permission alert dialog.  The permission will be requested when the the dialog is closed.
2584                                 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
2585                             } else {  // Show the permission request directly.
2586                                 // Request the permission.  The download dialog will be launched by `onRequestPermissionResult()`.
2587                                 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
2588                             }
2589                         } else {  // The storage permission has already been granted.
2590                             // Get a handle for the download file alert dialog.
2591                             DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(linkUrl, "none", -1);
2592
2593                             // Show the download file alert dialog.
2594                             downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
2595                         }
2596                     }
2597                     return false;
2598                 });
2599
2600                 // Add an Open with App entry.
2601                 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2602                     openWithApp(linkUrl);
2603                     return false;
2604                 });
2605
2606                 // Add an Open with Browser entry.
2607                 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2608                     openWithBrowser(linkUrl);
2609                     return false;
2610                 });
2611
2612                 // Add a Cancel entry, which by default closes the context menu.
2613                 menu.add(R.string.cancel);
2614                 break;
2615
2616             case WebView.HitTestResult.EMAIL_TYPE:
2617                 // Get the target URL.
2618                 linkUrl = hitTestResult.getExtra();
2619
2620                 // Set the target URL as the title of the `ContextMenu`.
2621                 menu.setHeaderTitle(linkUrl);
2622
2623                 // Add a Write Email entry.
2624                 menu.add(R.string.write_email).setOnMenuItemClickListener(item -> {
2625                     // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
2626                     Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
2627
2628                     // Parse the url and set it as the data for the `Intent`.
2629                     emailIntent.setData(Uri.parse("mailto:" + linkUrl));
2630
2631                     // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser.
2632                     emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2633
2634                     // Make it so.
2635                     startActivity(emailIntent);
2636                     return false;
2637                 });
2638
2639                 // Add a Copy Email Address entry.
2640                 menu.add(R.string.copy_email_address).setOnMenuItemClickListener(item -> {
2641                     // Save the email address in a `ClipData`.
2642                     ClipData srcEmailTypeClipData = ClipData.newPlainText(getString(R.string.email_address), linkUrl);
2643
2644                     // Set the `ClipData` as the clipboard's primary clip.
2645                     clipboardManager.setPrimaryClip(srcEmailTypeClipData);
2646                     return false;
2647                 });
2648
2649                 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
2650                 menu.add(R.string.cancel);
2651                 break;
2652
2653             // `IMAGE_TYPE` is an image.
2654             case WebView.HitTestResult.IMAGE_TYPE:
2655                 // Get the image URL.
2656                 imageUrl = hitTestResult.getExtra();
2657
2658                 // Set the image URL as the title of the `ContextMenu`.
2659                 menu.setHeaderTitle(imageUrl);
2660
2661                 // Add a View Image entry.
2662                 menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
2663                     loadUrl(imageUrl);
2664                     return false;
2665                 });
2666
2667                 // Add a Download Image entry.
2668                 menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
2669                     // Check if the download should be processed by an external app.
2670                     if (downloadWithExternalApp) {  // Download with an external app.
2671                         openUrlWithExternalApp(imageUrl);
2672                     } else {  // Download with Android's download manager.
2673                         // Check to see if the storage permission has already been granted.
2674                         if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {  // The storage permission needs to be requested.
2675                             // Store the image URL for use by `onRequestPermissionResult()`.
2676                             downloadImageUrl = imageUrl;
2677
2678                             // Show a dialog if the user has previously denied the permission.
2679                             if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
2680                                 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
2681                                 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
2682
2683                                 // Show the download location permission alert dialog.  The permission will be requested when the dialog is closed.
2684                                 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
2685                             } else {  // Show the permission request directly.
2686                                 // Request the permission.  The download dialog will be launched by `onRequestPermissionResult().
2687                                 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
2688                             }
2689                         } else {  // The storage permission has already been granted.
2690                             // Get a handle for the download image alert dialog.
2691                             DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
2692
2693                             // Show the download image alert dialog.
2694                             downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
2695                         }
2696                     }
2697                     return false;
2698                 });
2699
2700                 // Add a Copy URL entry.
2701                 menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> {
2702                     // Save the image URL in a `ClipData`.
2703                     ClipData srcImageTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
2704
2705                     // Set the `ClipData` as the clipboard's primary clip.
2706                     clipboardManager.setPrimaryClip(srcImageTypeClipData);
2707                     return false;
2708                 });
2709
2710                 // Add an Open with App entry.
2711                 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2712                     openWithApp(imageUrl);
2713                     return false;
2714                 });
2715
2716                 // Add an Open with Browser entry.
2717                 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2718                     openWithBrowser(imageUrl);
2719                     return false;
2720                 });
2721
2722                 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
2723                 menu.add(R.string.cancel);
2724                 break;
2725
2726
2727             // `SRC_IMAGE_ANCHOR_TYPE` is an image that is also a link.
2728             case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
2729                 // Get the image URL.
2730                 imageUrl = hitTestResult.getExtra();
2731
2732                 // Set the image URL as the title of the `ContextMenu`.
2733                 menu.setHeaderTitle(imageUrl);
2734
2735                 // Add a `View Image` entry.
2736                 menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
2737                     loadUrl(imageUrl);
2738                     return false;
2739                 });
2740
2741                 // Add a `Download Image` entry.
2742                 menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
2743                     // Check if the download should be processed by an external app.
2744                     if (downloadWithExternalApp) {  // Download with an external app.
2745                         openUrlWithExternalApp(imageUrl);
2746                     } else {  // Download with Android's download manager.
2747                         // Check to see if the storage permission has already been granted.
2748                         if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {  // The storage permission needs to be requested.
2749                             // Store the image URL for use by `onRequestPermissionResult()`.
2750                             downloadImageUrl = imageUrl;
2751
2752                             // Show a dialog if the user has previously denied the permission.
2753                             if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
2754                                 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
2755                                 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
2756
2757                                 // Show the download location permission alert dialog.  The permission will be requested when the dialog is closed.
2758                                 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
2759                             } else {  // Show the permission request directly.
2760                                 // Request the permission.  The download dialog will be launched by `onRequestPermissionResult().
2761                                 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
2762                             }
2763                         } else {  // The storage permission has already been granted.
2764                             // Get a handle for the download image alert dialog.
2765                             DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
2766
2767                             // Show the download image alert dialog.
2768                             downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
2769                         }
2770                     }
2771                     return false;
2772                 });
2773
2774                 // Add a `Copy URL` entry.
2775                 menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> {
2776                     // Save the image URL in a `ClipData`.
2777                     ClipData srcImageAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
2778
2779                     // Set the `ClipData` as the clipboard's primary clip.
2780                     clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData);
2781                     return false;
2782                 });
2783
2784                 // Add an Open with App entry.
2785                 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2786                     openWithApp(imageUrl);
2787                     return false;
2788                 });
2789
2790                 // Add an Open with Browser entry.
2791                 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2792                     openWithBrowser(imageUrl);
2793                     return false;
2794                 });
2795
2796                 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
2797                 menu.add(R.string.cancel);
2798                 break;
2799         }
2800     }
2801
2802     @Override
2803     public void onCreateBookmark(DialogFragment dialogFragment) {
2804         // Get the views from the dialog fragment.
2805         EditText createBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_name_edittext);
2806         EditText createBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_url_edittext);
2807
2808         // Extract the strings from the edit texts.
2809         String bookmarkNameString = createBookmarkNameEditText.getText().toString();
2810         String bookmarkUrlString = createBookmarkUrlEditText.getText().toString();
2811
2812         // Get a copy of the favorite icon bitmap.
2813         Bitmap favoriteIcon = favoriteIconBitmap;
2814
2815         // Scale the favorite icon bitmap down if it is larger than 256 x 256.  Filtering uses bilinear interpolation.
2816         if ((favoriteIcon.getHeight() > 256) || (favoriteIcon.getWidth() > 256)) {
2817             favoriteIcon = Bitmap.createScaledBitmap(favoriteIcon, 256, 256, true);
2818         }
2819
2820         // Create a favorite icon byte array output stream.
2821         ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
2822
2823         // Convert the favorite icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2824         favoriteIcon.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
2825
2826         // Convert the favorite icon byte array stream to a byte array.
2827         byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
2828
2829         // Display the new bookmark below the current items in the (0 indexed) list.
2830         int newBookmarkDisplayOrder = bookmarksListView.getCount();
2831
2832         // Create the bookmark.
2833         bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolder, newBookmarkDisplayOrder, favoriteIconByteArray);
2834
2835         // Update the bookmarks cursor with the current contents of this folder.
2836         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2837
2838         // Update the list view.
2839         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2840
2841         // Scroll to the new bookmark.
2842         bookmarksListView.setSelection(newBookmarkDisplayOrder);
2843     }
2844
2845     @Override
2846     public void onCreateBookmarkFolder(DialogFragment dialogFragment) {
2847         // Get handles for the views in the dialog fragment.
2848         EditText createFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.create_folder_name_edittext);
2849         RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon_radiobutton);
2850         ImageView folderIconImageView = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon);
2851
2852         // Get new folder name string.
2853         String folderNameString = createFolderNameEditText.getText().toString();
2854
2855         // Create a folder icon bitmap.
2856         Bitmap folderIconBitmap;
2857
2858         // Set the folder icon bitmap according to the dialog.
2859         if (defaultFolderIconRadioButton.isChecked()) {  // Use the default folder icon.
2860             // Get the default folder icon drawable.
2861             Drawable folderIconDrawable = folderIconImageView.getDrawable();
2862
2863             // Convert the folder icon drawable to a bitmap drawable.
2864             BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2865
2866             // Convert the folder icon bitmap drawable to a bitmap.
2867             folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2868         } else {  // Use the WebView favorite icon.
2869             // Get a copy of the favorite icon bitmap.
2870             folderIconBitmap = favoriteIconBitmap;
2871
2872             // Scale the folder icon bitmap down if it is larger than 256 x 256.  Filtering uses bilinear interpolation.
2873             if ((folderIconBitmap.getHeight() > 256) || (folderIconBitmap.getWidth() > 256)) {
2874                 folderIconBitmap = Bitmap.createScaledBitmap(folderIconBitmap, 256, 256, true);
2875             }
2876         }
2877
2878         // Create a folder icon byte array output stream.
2879         ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream();
2880
2881         // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2882         folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream);
2883
2884         // Convert the folder icon byte array stream to a byte array.
2885         byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray();
2886
2887         // Move all the bookmarks down one in the display order.
2888         for (int i = 0; i < bookmarksListView.getCount(); i++) {
2889             int databaseId = (int) bookmarksListView.getItemIdAtPosition(i);
2890             bookmarksDatabaseHelper.updateDisplayOrder(databaseId, i + 1);
2891         }
2892
2893         // Create the folder, which will be placed at the top of the `ListView`.
2894         bookmarksDatabaseHelper.createFolder(folderNameString, currentBookmarksFolder, folderIconByteArray);
2895
2896         // Update the bookmarks cursor with the current contents of this folder.
2897         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2898
2899         // Update the `ListView`.
2900         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2901
2902         // Scroll to the new folder.
2903         bookmarksListView.setSelection(0);
2904     }
2905
2906     @Override
2907     public void onSaveBookmark(DialogFragment dialogFragment, int selectedBookmarkDatabaseId) {
2908         // Get handles for the views from `dialogFragment`.
2909         EditText editBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_name_edittext);
2910         EditText editBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_url_edittext);
2911         RadioButton currentBookmarkIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_current_icon_radiobutton);
2912
2913         // Store the bookmark strings.
2914         String bookmarkNameString = editBookmarkNameEditText.getText().toString();
2915         String bookmarkUrlString = editBookmarkUrlEditText.getText().toString();
2916
2917         // Update the bookmark.
2918         if (currentBookmarkIconRadioButton.isChecked()) {  // Update the bookmark without changing the favorite icon.
2919             bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString);
2920         } else {  // Update the bookmark using the `WebView` favorite icon.
2921             // Get a copy of the favorite icon bitmap.
2922             Bitmap favoriteIcon = favoriteIconBitmap;
2923
2924             // Scale the favorite icon bitmap down if it is larger than 256 x 256.  Filtering uses bilinear interpolation.
2925             if ((favoriteIcon.getHeight() > 256) || (favoriteIcon.getWidth() > 256)) {
2926                 favoriteIcon = Bitmap.createScaledBitmap(favoriteIcon, 256, 256, true);
2927             }
2928
2929             // Create a favorite icon byte array output stream.
2930             ByteArrayOutputStream newFavoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
2931
2932             // Convert the favorite icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2933             favoriteIcon.compress(Bitmap.CompressFormat.PNG, 0, newFavoriteIconByteArrayOutputStream);
2934
2935             // Convert the favorite icon byte array stream to a byte array.
2936             byte[] newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray();
2937
2938             //  Update the bookmark and the favorite icon.
2939             bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, newFavoriteIconByteArray);
2940         }
2941
2942         // Update the bookmarks cursor with the current contents of this folder.
2943         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2944
2945         // Update the list view.
2946         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2947     }
2948
2949     @Override
2950     public void onSaveBookmarkFolder(DialogFragment dialogFragment, int selectedFolderDatabaseId) {
2951         // Get handles for the views from `dialogFragment`.
2952         EditText editFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_folder_name_edittext);
2953         RadioButton currentFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_current_icon_radiobutton);