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