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