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