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