X-Git-Url: https://gitweb.stoutner.com/?p=PrivacyBrowserAndroid.git;a=blobdiff_plain;f=app%2Fsrc%2Fmain%2Fjava%2Fcom%2Fstoutner%2Fprivacybrowser%2Factivities%2FMainWebViewActivity.java;h=b54888e2b03fe8521a752d578698ccb1da8e9d89;hp=7549f5ca2e1a4b15c3cecbfde3d5fab96b1fb7e6;hb=1a3d457d10c7b6c64d2454834bb0794909e43bd9;hpb=65531f439c8cb610d0e3e0f1e4127dc4d8a006d5 diff --git a/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java b/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java index 7549f5ca..14a2264e 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java @@ -1,5 +1,5 @@ /* - * Copyright © 2015-2017 Soren Stoutner . + * Copyright © 2015-2020 Soren Stoutner . * * Download cookie code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner . * @@ -21,9 +21,13 @@ package com.stoutner.privacybrowser.activities; +import android.Manifest; import android.annotation.SuppressLint; -import android.app.DialogFragment; +import android.app.Activity; +import android.app.Dialog; import android.app.DownloadManager; +import android.app.SearchManager; +import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; import android.content.ClipData; import android.content.ClipboardManager; @@ -31,9 +35,12 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.content.res.Configuration; import android.database.Cursor; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Typeface; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; @@ -42,28 +49,16 @@ import android.net.http.SslError; import android.os.Build; import android.os.Bundle; import android.os.Handler; +import android.os.Message; import android.preference.PreferenceManager; import android.print.PrintDocumentAdapter; import android.print.PrintManager; -import android.support.annotation.NonNull; -import android.support.design.widget.CoordinatorLayout; -import android.support.design.widget.NavigationView; -import android.support.design.widget.Snackbar; -import android.support.v4.app.ActivityCompat; -import android.support.v4.content.ContextCompat; -import android.support.v4.view.GravityCompat; -import android.support.v4.widget.DrawerLayout; -import android.support.v4.widget.SwipeRefreshLayout; -import android.support.v7.app.ActionBar; -import android.support.v7.app.ActionBarDrawerToggle; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.app.AppCompatDialogFragment; -import android.support.v7.widget.Toolbar; import android.text.Editable; import android.text.Spanned; import android.text.TextWatcher; import android.text.style.ForegroundColorSpan; import android.util.Patterns; +import android.util.TypedValue; import android.view.ContextMenu; import android.view.GestureDetector; import android.view.KeyEvent; @@ -71,1650 +66,1847 @@ import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; +import android.view.ViewGroup; import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; import android.webkit.CookieManager; -import android.webkit.DownloadListener; import android.webkit.HttpAuthHandler; import android.webkit.SslErrorHandler; import android.webkit.ValueCallback; import android.webkit.WebBackForwardList; import android.webkit.WebChromeClient; import android.webkit.WebResourceResponse; +import android.webkit.WebSettings; import android.webkit.WebStorage; import android.webkit.WebView; import android.webkit.WebViewClient; import android.webkit.WebViewDatabase; +import android.widget.ArrayAdapter; +import android.widget.CursorAdapter; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; +import android.widget.ListView; import android.widget.ProgressBar; +import android.widget.RadioButton; import android.widget.RelativeLayout; import android.widget.TextView; -import com.stoutner.privacybrowser.BannerAd; +import androidx.annotation.NonNull; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.ActionBarDrawerToggle; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatDelegate; +import androidx.appcompat.widget.Toolbar; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.core.content.res.ResourcesCompat; +import androidx.core.view.GravityCompat; +import androidx.drawerlayout.widget.DrawerLayout; +import androidx.fragment.app.DialogFragment; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import androidx.viewpager.widget.ViewPager; +import androidx.webkit.WebSettingsCompat; +import androidx.webkit.WebViewFeature; + +import com.google.android.material.appbar.AppBarLayout; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.google.android.material.navigation.NavigationView; +import com.google.android.material.snackbar.Snackbar; +import com.google.android.material.tabs.TabLayout; + import com.stoutner.privacybrowser.BuildConfig; import com.stoutner.privacybrowser.R; +import com.stoutner.privacybrowser.adapters.WebViewPagerAdapter; +import com.stoutner.privacybrowser.asynctasks.GetHostIpAddresses; +import com.stoutner.privacybrowser.asynctasks.PopulateBlocklists; +import com.stoutner.privacybrowser.asynctasks.PrepareSaveDialog; +import com.stoutner.privacybrowser.asynctasks.SaveUrl; +import com.stoutner.privacybrowser.asynctasks.SaveWebpageImage; +import com.stoutner.privacybrowser.dialogs.AdConsentDialog; +import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog; +import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog; import com.stoutner.privacybrowser.dialogs.CreateHomeScreenShortcutDialog; -import com.stoutner.privacybrowser.dialogs.DownloadImageDialog; +import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDialog; +import com.stoutner.privacybrowser.dialogs.FontSizeDialog; import com.stoutner.privacybrowser.dialogs.HttpAuthenticationDialog; -import com.stoutner.privacybrowser.dialogs.PinnedSslCertificateMismatchDialog; +import com.stoutner.privacybrowser.dialogs.OpenDialog; +import com.stoutner.privacybrowser.dialogs.ProxyNotInstalledDialog; +import com.stoutner.privacybrowser.dialogs.PinnedMismatchDialog; +import com.stoutner.privacybrowser.dialogs.SaveDialog; +import com.stoutner.privacybrowser.dialogs.SslCertificateErrorDialog; +import com.stoutner.privacybrowser.dialogs.StoragePermissionDialog; import com.stoutner.privacybrowser.dialogs.UrlHistoryDialog; import com.stoutner.privacybrowser.dialogs.ViewSslCertificateDialog; +import com.stoutner.privacybrowser.dialogs.WaitingForProxyDialog; +import com.stoutner.privacybrowser.fragments.WebViewTabFragment; +import com.stoutner.privacybrowser.helpers.AdHelper; +import com.stoutner.privacybrowser.helpers.BlocklistHelper; +import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper; +import com.stoutner.privacybrowser.helpers.CheckPinnedMismatchHelper; import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper; -import com.stoutner.privacybrowser.helpers.OrbotProxyHelper; -import com.stoutner.privacybrowser.dialogs.DownloadFileDialog; -import com.stoutner.privacybrowser.dialogs.SslCertificateErrorDialog; +import com.stoutner.privacybrowser.helpers.FileNameHelper; +import com.stoutner.privacybrowser.helpers.ProxyHelper; +import com.stoutner.privacybrowser.views.NestedScrollWebView; -import java.io.BufferedReader; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; -import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLDecoder; import java.net.URLEncoder; +import java.text.NumberFormat; +import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; -// We need to use AppCompatActivity from android.support.v7.app.AppCompatActivity to have access to the SupportActionBar until the minimum API is >= 21. -public class MainWebViewActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener, CreateHomeScreenShortcutDialog.CreateHomeScreenSchortcutListener, - HttpAuthenticationDialog.HttpAuthenticationListener, PinnedSslCertificateMismatchDialog.PinnedSslCertificateMismatchListener, SslCertificateErrorDialog.SslCertificateErrorListener, DownloadFileDialog.DownloadFileListener, - DownloadImageDialog.DownloadImageListener, UrlHistoryDialog.UrlHistoryListener { - - // `darkTheme` is public static so it can be accessed from `AboutActivity`, `GuideActivity`, `AddDomainDialog`, `SettingsActivity`, `DomainsActivity`, `DomainsListFragment`, `BookmarksActivity`, `BookmarksDatabaseViewActivity`, - // `CreateBookmarkDialog`, `CreateBookmarkFolderDialog`, `DownloadFileDialog`, `DownloadImageDialog`, `EditBookmarkDialog`, `EditBookmarkFolderDialog`, `HttpAuthenticationDialog`, `MoveToFolderDialog`, `SslCertificateErrorDialog`, `UrlHistoryDialog`, - // `ViewSslCertificateDialog`, `CreateHomeScreenShortcutDialog`, and `OrbotProxyHelper`. It is also used in `onCreate()`, `applyAppSettings()`, `applyDomainSettings()`, and `updatePrivacyIcons()`. - public static boolean darkTheme; +public class MainWebViewActivity extends AppCompatActivity implements CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener, + EditBookmarkFolderDialog.EditBookmarkFolderListener, FontSizeDialog.UpdateFontSizeListener, NavigationView.OnNavigationItemSelectedListener, OpenDialog.OpenListener, + PinnedMismatchDialog.PinnedMismatchListener, PopulateBlocklists.PopulateBlocklistsListener, SaveDialog.SaveWebpageListener, StoragePermissionDialog.StoragePermissionDialogListener, + UrlHistoryDialog.NavigateHistoryListener, WebViewTabFragment.NewTabListener { - // `favoriteIconBitmap` is public static so it can be accessed from `CreateHomeScreenShortcutDialog`, `BookmarksActivity`, `CreateBookmarkDialog`, `CreateBookmarkFolderDialog`, `EditBookmarkDialog`, `EditBookmarkFolderDialog`, - // and `ViewSslCertificateDialog`. It is also used in `onCreate()`, `onCreateHomeScreenShortcutCreate()`, and `applyDomainSettings`. - public static Bitmap favoriteIconBitmap; + // The executor service handles background tasks. It is accessed from `ViewSourceActivity`. TODO. Change the number of threads, or create a single thread executor. + public static ExecutorService executorService = Executors.newFixedThreadPool(4); - // `formattedUrlString` is public static so it can be accessed from `BookmarksActivity`, `CreateBookmarkDialog`, and `AddDomainDialog`. - // It is also used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onCreateHomeScreenShortcutCreate()`, and `loadUrlFromTextBox()`. - public static String formattedUrlString; + // `orbotStatus` is public static so it can be accessed from `OrbotProxyHelper`. It is also used in `onCreate()`, `onResume()`, and `applyProxy()`. + public static String orbotStatus = "unknown"; - // `sslCertificate` is public static so it can be accessed from `DomainsActivity`, `DomainsListFragment`, `DomainSettingsFragment`, `PinnedSslCertificateMismatchDialog`, and `ViewSslCertificateDialog`. It is also used in `onCreate()`. - public static SslCertificate sslCertificate; + // The WebView pager adapter is accessed from `HttpAuthenticationDialog`, `PinnedMismatchDialog`, and `SslCertificateErrorDialog`. It is also used in `onCreate()`, `onResume()`, and `addTab()`. + public static WebViewPagerAdapter webViewPagerAdapter; - // `orbotStatus` is public static so it can be accessed from `OrbotProxyHelper`. It is also used in `onCreate()`. - public static String orbotStatus; + // The load URL on restart variables are public static so they can be accessed from `BookmarksActivity`. They are used in `onRestart()`. + public static boolean loadUrlOnRestart; + public static String urlToLoadOnRestart; + + // `restartFromBookmarksActivity` is public static so it can be accessed from `BookmarksActivity`. It is also used in `onRestart()`. + public static boolean restartFromBookmarksActivity; + + // `currentBookmarksFolder` is public static so it can be accessed from `BookmarksActivity`. It is also used in `onCreate()`, `onBackPressed()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, + // `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`. + public static String currentBookmarksFolder; + + // The user agent constants are public static so they can be accessed from `SettingsFragment`, `DomainsActivity`, and `DomainSettingsFragment`. + public final static int UNRECOGNIZED_USER_AGENT = -1; + public final static int SETTINGS_WEBVIEW_DEFAULT_USER_AGENT = 1; + public final static int SETTINGS_CUSTOM_USER_AGENT = 12; + public final static int DOMAINS_SYSTEM_DEFAULT_USER_AGENT = 0; + public final static int DOMAINS_WEBVIEW_DEFAULT_USER_AGENT = 2; + public final static int DOMAINS_CUSTOM_USER_AGENT = 13; + + // Start activity for result request codes. The public static entries are accessed from `OpenDialog()` and `SaveWebpageDialog()`. + public final static int BROWSE_OPEN_REQUEST_CODE = 0; + public final static int BROWSE_SAVE_WEBPAGE_REQUEST_CODE = 1; + private final int BROWSE_FILE_UPLOAD_REQUEST_CODE = 2; + + // The proxy mode is public static so it can be accessed from `ProxyHelper()`. + // It is also used in `onRestart()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `applyAppSettings()`, and `applyProxy()`. + // It will be updated in `applyAppSettings()`, but it needs to be initialized here or the first run of `onPrepareOptionsMenu()` crashes. + public static String proxyMode = ProxyHelper.NONE; + + + // The permission result request codes are used in `onCreateContextMenu()`, `onRequestPermissionResult()`, `onSaveWebpage()`, `onCloseStoragePermissionDialog()`, and `initializeWebView()`. + private final int PERMISSION_OPEN_REQUEST_CODE = 0; + private final int PERMISSION_SAVE_URL_REQUEST_CODE = 1; + private final int PERMISSION_SAVE_AS_ARCHIVE_REQUEST_CODE = 2; + private final int PERMISSION_SAVE_AS_IMAGE_REQUEST_CODE = 3; + + // Define the saved instance state constants. + private final String SAVED_STATE_ARRAY_LIST = "saved_state_array_list"; + private final String SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST = "saved_nested_scroll_webview_state_array_list"; + private final String SAVED_TAB_POSITION = "saved_tab_position"; + private final String PROXY_MODE = "proxy_mode"; + + // Define the saved instance state variables. + private ArrayList savedStateArrayList; + private ArrayList savedNestedScrollWebViewStateArrayList; + private int savedTabPosition; + private String savedProxyMode; + + // Define the class views. + private AppBarLayout appBarLayout; + private TabLayout tabLayout; + private ViewPager webViewPager; + + // The current WebView is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`, + // `findNextOnPage()`, `closeFindOnPage()`, `loadUrlFromTextBox()`, `onSslMismatchBack()`, `applyProxy()`, and `applyDomainSettings()`. + private NestedScrollWebView currentWebView; - // `webViewTitle` is public static so it can be accessed from `CreateBookmarkDialog` and `CreateHomeScreenShortcutDialog`. It is also used in `onCreate()`. - public static String webViewTitle; + // `customHeader` is used in `onCreate()`, `onOptionsItemSelected()`, `onCreateContextMenu()`, and `loadUrl()`. + private final Map customHeaders = new HashMap<>(); - // `displayWebpageImagesBoolean` is public static so it can be accessed from `DomainSettingsFragment`. It is also used in `applyAppSettings()` and `applyDomainSettings()`. - public static boolean displayWebpageImagesBoolean; + // The search URL is set in `applyAppSettings()` and used in `onNewIntent()`, `loadUrlFromTextBox()`, `initializeApp()`, and `initializeWebView()`. + private String searchURL; - // `reloadOnRestart` is public static so it can be accessed from `SettingsFragment`. It is also used in `onRestart()` - public static boolean reloadOnRestart; + // The options menu is set in `onCreateOptionsMenu()` and used in `onOptionsItemSelected()`, `updatePrivacyIcons()`, and `initializeWebView()`. + private Menu optionsMenu; - // `reloadUrlOnRestart` is public static so it can be accessed from `SettingsFragment`. It is also used in `onRestart()`. - public static boolean loadUrlOnRestart; + // The blocklists are populated in `finishedPopulatingBlocklists()` and accessed from `initializeWebView()`. + private ArrayList> easyList; + private ArrayList> easyPrivacy; + private ArrayList> fanboysAnnoyanceList; + private ArrayList> fanboysSocialList; + private ArrayList> ultraList; + private ArrayList> ultraPrivacy; - // The pinned domain SSL Certificate variables are public static so they can be accessed from `PinnedSslCertificateMismatchDialog`. They are also used in `onCreate()` and `applyDomainSettings()`. - public static int domainSettingsDatabaseId; - public static String pinnedDomainSslIssuedToCNameString; - public static String pinnedDomainSslIssuedToONameString; - public static String pinnedDomainSslIssuedToUNameString; - public static String pinnedDomainSslIssuedByCNameString; - public static String pinnedDomainSslIssuedByONameString; - public static String pinnedDomainSslIssuedByUNameString; - public static Date pinnedDomainSslStartDate; - public static Date pinnedDomainSslEndDate; + // `webViewDefaultUserAgent` is used in `onCreate()` and `onPrepareOptionsMenu()`. + private String webViewDefaultUserAgent; + // The incognito mode is set in `applyAppSettings()` and used in `initializeWebView()`. + private boolean incognitoModeEnabled; - // `appBar` is used in `onCreate()`, `onOptionsItemSelected()`, `closeFindOnPage()`, and `applyAppSettings()`. - private ActionBar appBar; + // The full screen browsing mode tracker is set it `applyAppSettings()` and used in `initializeWebView()`. + private boolean fullScreenBrowsingModeEnabled; - // `navigatingHistory` is used in `onCreate()`, `onNavigationItemSelected()`, `onSslMismatchBack()`, and `applyDomainSettings()`. - private boolean navigatingHistory; + // `inFullScreenBrowsingMode` is used in `onCreate()`, `onConfigurationChanged()`, and `applyAppSettings()`. + private boolean inFullScreenBrowsingMode; - // `favoriteIconDefaultBitmap` is used in `onCreate()` and `applyDomainSettings`. - private Bitmap favoriteIconDefaultBitmap; + // The app bar trackers are set in `applyAppSettings()` and used in `initializeWebView()`. + private boolean hideAppBar; + private boolean scrollAppBar; - // `drawerLayout` is used in `onCreate()`, `onNewIntent()`, and `onBackPressed()`. - private DrawerLayout drawerLayout; + // The loading new intent tracker is set in `onNewIntent()` and used in `setCurrentWebView()`. + private boolean loadingNewIntent; - // `rootCoordinatorLayout` is used in `onCreate()` and `applyAppSettings()`. - private CoordinatorLayout rootCoordinatorLayout; + // `reapplyDomainSettingsOnRestart` is used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, and `onAddDomain()`, . + private boolean reapplyDomainSettingsOnRestart; - // `mainWebView` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`, `findNextOnPage()`, `closeFindOnPage()`, `loadUrlFromTextBox()` - // `onSslMismatchBack()`, and `setDisplayWebpageImages()`. - private WebView mainWebView; + // `reapplyAppSettingsOnRestart` is used in `onNavigationItemSelected()` and `onRestart()`. + private boolean reapplyAppSettingsOnRestart; - // `fullScreenVideoFrameLayout` is used in `onCreate()` and `onConfigurationChanged()`. - private FrameLayout fullScreenVideoFrameLayout; + // `displayingFullScreenVideo` is used in `onCreate()` and `onResume()`. + private boolean displayingFullScreenVideo; - // `swipeRefreshLayout` is used in `onCreate()`, `onPrepareOptionsMenu`, and `onRestart()`. - private SwipeRefreshLayout swipeRefreshLayout; + // `orbotStatusBroadcastReceiver` is used in `onCreate()` and `onDestroy()`. + private BroadcastReceiver orbotStatusBroadcastReceiver; - // `urlAppBarRelativeLayout` is used in `onCreate()` and `applyDomainSettings()`. - private RelativeLayout urlAppBarRelativeLayout; + // The waiting for proxy boolean is used in `onResume()`, `initializeApp()` and `applyProxy()`. + private boolean waitingForProxy = false; - // `favoriteIconImageView` is used in `onCreate()` and `applyDomainSettings()` - private ImageView favoriteIconImageView; + // The action bar drawer toggle is initialized in `onCreate()` and used in `onResume()`. + private ActionBarDrawerToggle actionBarDrawerToggle; - // `cookieManager` is used in `onCreate()`, `onOptionsItemSelected()`, and `onNavigationItemSelected()`, `loadUrlFromTextBox()`, `onDownloadImage()`, `onDownloadFile()`, and `onRestart()`. - private CookieManager cookieManager; + // The color spans are used in `onCreate()` and `highlightUrlText()`. + private ForegroundColorSpan redColorSpan; + private ForegroundColorSpan initialGrayColorSpan; + private ForegroundColorSpan finalGrayColorSpan; - // `customHeader` is used in `onCreate()`, `onOptionsItemSelected()`, `onCreateContextMenu()`, and `loadUrl()`. - private final Map customHeaders = new HashMap<>(); + // `bookmarksDatabaseHelper` is used in `onCreate()`, `onDestroy`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, + // and `loadBookmarksFolder()`. + private BookmarksDatabaseHelper bookmarksDatabaseHelper; - // `javaScriptEnabled` is also used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `applyDomainSettings()`, and `updatePrivacyIcons()`. - private boolean javaScriptEnabled; + // `bookmarksCursor` is used in `onDestroy()`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`. + private Cursor bookmarksCursor; - // `firstPartyCookiesEnabled` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onDownloadImage()`, `onDownloadFile()`, and `applyDomainSettings()`. - private boolean firstPartyCookiesEnabled; + // `bookmarksCursorAdapter` is used in `onCreateBookmark()`, `onCreateBookmarkFolder()` `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`. + private CursorAdapter bookmarksCursorAdapter; - // `thirdPartyCookiesEnabled` used in `onCreate()`, `onPrepareOptionsMenu()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyDomainSettings()`. - private boolean thirdPartyCookiesEnabled; + // `oldFolderNameString` is used in `onCreate()` and `onSaveEditBookmarkFolder()`. + private String oldFolderNameString; - // `domStorageEnabled` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyDomainSettings()`. - private boolean domStorageEnabled; + // `fileChooserCallback` is used in `onCreate()` and `onActivityResult()`. + private ValueCallback fileChooserCallback; - // `saveFormDataEnabled` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyDomainSettings()`. - private boolean saveFormDataEnabled; + // The default progress view offsets are set in `onCreate()` and used in `initializeWebView()`. + private int appBarHeight; + private int defaultProgressViewStartOffset; + private int defaultProgressViewEndOffset; - // `nightMode` is used in `onCreate()` and `applyDomainSettings()`. - private boolean nightMode; + // The URL sanitizers are set in `applyAppSettings()` and used in `sanitizeUrl()`. + private boolean sanitizeGoogleAnalytics; + private boolean sanitizeFacebookClickIds; + private boolean sanitizeTwitterAmpRedirects; - // `swipeToRefreshEnabled` is used in `onPrepareOptionsMenu()` and `applyAppSettings()`. - private boolean swipeToRefreshEnabled; + // The file path strings are used in `onSaveWebpage()` and `onRequestPermissionResult()` + private String openFilePath; + private String saveWebpageUrl; + private String saveWebpageFilePath; - // 'homepage' is used in `onCreate()`, `onNavigationItemSelected()`, and `applyAppSettings()`. - private String homepage; + @Override + // Remove the warning about needing to override `performClick()` when using an `OnTouchListener` with `WebView`. + @SuppressLint("ClickableViewAccessibility") + protected void onCreate(Bundle savedInstanceState) { + // Run the default commands. + super.onCreate(savedInstanceState); - // `searchURL` is used in `loadURLFromTextBox()` and `applyAppSettings()`. - private String searchURL; + // Check to see if the activity has been restarted. + if (savedInstanceState != null) { + // Store the saved instance state variables. + savedStateArrayList = savedInstanceState.getParcelableArrayList(SAVED_STATE_ARRAY_LIST); + savedNestedScrollWebViewStateArrayList = savedInstanceState.getParcelableArrayList(SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST); + savedTabPosition = savedInstanceState.getInt(SAVED_TAB_POSITION); + savedProxyMode = savedInstanceState.getString(PROXY_MODE); + } - // `adBlockerEnabled` is used in `onCreate()` and `applyAppSettings()`. - private boolean adBlockerEnabled; + // Initialize the default preference values the first time the program is run. `false` keeps this command from resetting any current preferences back to default. + PreferenceManager.setDefaultValues(this, R.xml.preferences, false); - // `privacyBrowserRuntime` is used in `onCreate()`, `onOptionsItemSelected()`, and `applyAppSettings()`. - private Runtime privacyBrowserRuntime; + // Get a handle for the shared preferences. + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - // `incognitoModeEnabled` is used in `onCreate()` and `applyAppSettings()`. - private boolean incognitoModeEnabled; + // Get the screenshot preference. + String appTheme = sharedPreferences.getString("app_theme", getString(R.string.app_theme_default_value)); + boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false); + + // Get the theme entry values string array. + String[] appThemeEntryValuesStringArray = getResources().getStringArray(R.array.app_theme_entry_values); + + // Set the app theme according to the preference. A switch statement cannot be used because the theme entry values string array is not a compile time constant. + if (appTheme.equals(appThemeEntryValuesStringArray[1])) { // The light theme is selected. + // Apply the light theme. + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); + } else if (appTheme.equals(appThemeEntryValuesStringArray[2])) { // The dark theme is selected. + // Apply the dark theme. + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); + } else { // The system default theme is selected. + if (Build.VERSION.SDK_INT >= 28) { // The system default theme is supported. + // Follow the system default theme. + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); + } else { // The system default theme is not supported. + // Follow the battery saver mode. + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY); + } + } - // `fullScreenBrowsingModeEnabled` is used in `onCreate()` and `applyAppSettings()`. - private boolean fullScreenBrowsingModeEnabled; + // Disable screenshots if not allowed. + if (!allowScreenshots) { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE); + } - // `inFullScreenBrowsingMode` is used in `onCreate()`, `onConfigurationChanged()`, and `applyAppSettings()`. - private boolean inFullScreenBrowsingMode; + // Enable the drawing of the entire webpage. This makes it possible to save a website image. This must be done before anything else happens with the WebView. + if (Build.VERSION.SDK_INT >= 21) { + WebView.enableSlowWholeDocumentDraw(); + } - // `hideSystemBarsOnFullscreen` is used in `onCreate()` and `applyAppSettings()`. - private boolean hideSystemBarsOnFullscreen; + // Set the theme. + setTheme(R.style.PrivacyBrowser); - // `translucentNavigationBarOnFullscreen` is used in `onCreate()` and `applyAppSettings()`. - private boolean translucentNavigationBarOnFullscreen; + // Set the content view. + setContentView(R.layout.main_framelayout); - // `currentDomainName` is used in `onCreate()`, `onNavigationItemSelected()`, `onSslMismatchProceed()`, and `applyDomainSettings()`. - private String currentDomainName; + // Get handles for the views. + DrawerLayout drawerLayout = findViewById(R.id.drawerlayout); + appBarLayout = findViewById(R.id.appbar_layout); + Toolbar toolbar = findViewById(R.id.toolbar); + tabLayout = findViewById(R.id.tablayout); + webViewPager = findViewById(R.id.webviewpager); - // `ignorePinnedSslCertificateForDomain` is used in `onCreate()`, `onSslMismatchProceed()`, and `applyDomainSettings()`. - private boolean ignorePinnedSslCertificate; + // Get a handle for the app compat delegate. + AppCompatDelegate appCompatDelegate = getDelegate(); - // `waitingForOrbot` is used in `onCreate()` and `applyAppSettings()`. - private boolean waitingForOrbot; + // Set the support action bar. + appCompatDelegate.setSupportActionBar(toolbar); - // `domainSettingsApplied` is used in `applyDomainSettings()` and `setDisplayWebpageImages()`. - private boolean domainSettingsApplied; + // Get a handle for the action bar. + ActionBar actionBar = appCompatDelegate.getSupportActionBar(); - // `displayWebpageImagesInt` is used in `applyDomainSettings()` and `setDisplayWebpageImages()`. - private int displayWebpageImagesInt; + // This is needed to get rid of the Android Studio warning that the action bar might be null. + assert actionBar != null; - // `onTheFlyDisplayImagesSet` is used in `applyDomainSettings()` and `setDisplayWebpageImages()`. - private boolean onTheFlyDisplayImagesSet; + // Add the custom layout, which shows the URL text bar. + actionBar.setCustomView(R.layout.url_app_bar); + actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM); - // `waitingForOrbotData` is used in `onCreate()` and `applyAppSettings()`. - private String waitingForOrbotHTMLString; + // Create the hamburger icon at the start of the AppBar. + actionBarDrawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.open_navigation_drawer, R.string.close_navigation_drawer); - // `privateDataDirectoryString` is used in `onCreate()`, `onOptionsItemSelected()`, and `onNavigationItemSelected()`. - private String privateDataDirectoryString; + // Initially disable the sliding drawers. They will be enabled once the blocklists are loaded. + drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); - // `findOnPageLinearLayout` is used in `onCreate()`, `onOptionsItemSelected()`, and `closeFindOnPage()`. - private LinearLayout findOnPageLinearLayout; + // Initialize the web view pager adapter. + webViewPagerAdapter = new WebViewPagerAdapter(getSupportFragmentManager()); - // `findOnPageEditText` is used in `onCreate()`, `onOptionsItemSelected()`, and `closeFindOnPage()`. - private EditText findOnPageEditText; + // Set the pager adapter on the web view pager. + webViewPager.setAdapter(webViewPagerAdapter); - // `mainMenu` is used in `onCreateOptionsMenu()` and `updatePrivacyIcons()`. - private Menu mainMenu; + // Store up to 100 tabs in memory. + webViewPager.setOffscreenPageLimit(100); - // `drawerToggle` is used in `onCreate()`, `onPostCreate()`, `onConfigurationChanged()`, `onNewIntent()`, and `onNavigationItemSelected()`. - private ActionBarDrawerToggle drawerToggle; + // Initialize the app. + initializeApp(); - // `supportAppBar` is used in `onCreate()`, `onOptionsItemSelected()`, and `closeFindOnPage()`. - private Toolbar supportAppBar; + // Apply the app settings from the shared preferences. + applyAppSettings(); - // `urlTextBox` is used in `onCreate()`, `onOptionsItemSelected()`, `loadUrlFromTextBox()`, `loadUrl()`, and `highlightUrlText()`. - private EditText urlTextBox; + // Populate the blocklists. + new PopulateBlocklists(this, this).execute(); + } - // `redColorSpan` is used in `onCreate()` and `highlightUrlText()`. - private ForegroundColorSpan redColorSpan; + @Override + protected void onNewIntent(Intent intent) { + // Run the default commands. + super.onNewIntent(intent); - // `initialGrayColorSpan` is sued in `onCreate()` and `highlightUrlText()`. - private ForegroundColorSpan initialGrayColorSpan; + // Replace the intent that started the app with this one. + setIntent(intent); - // `finalGrayColorSpam` is used in `onCreate()` and `highlightUrlText()`. - private ForegroundColorSpan finalGrayColorSpan; + // Check to see if the app is being restarted. + if (savedStateArrayList == null || savedStateArrayList.size() == 0) { // The activity is running for the first time. + // Get the information from the intent. + String intentAction = intent.getAction(); + Uri intentUriData = intent.getData(); - // `adView` is used in `onCreate()` and `onConfigurationChanged()`. - private View adView; + // Determine if this is a web search. + boolean isWebSearch = ((intentAction != null) && intentAction.equals(Intent.ACTION_WEB_SEARCH)); - // `sslErrorHandler` is used in `onCreate()`, `onSslErrorCancel()`, and `onSslErrorProceed`. - private SslErrorHandler sslErrorHandler; + // 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. + if (intentUriData != null || isWebSearch) { + // Get the shared preferences. + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - // `httpAuthHandler` is used in `onCreate()`, `onHttpAuthenticationCancel()`, and `onHttpAuthenticationProceed()`. - private static HttpAuthHandler httpAuthHandler; + // Create a URL string. + String url; - // `inputMethodManager` is used in `onOptionsItemSelected()`, `loadUrlFromTextBox()`, and `closeFindOnPage()`. - private InputMethodManager inputMethodManager; + // If the intent action is a web search, perform the search. + if (isWebSearch) { // The intent is a web search. + // Create an encoded URL string. + String encodedUrlString; - // `mainWebViewRelativeLayout` is used in `onCreate()` and `onNavigationItemSelected()`. - private RelativeLayout mainWebViewRelativeLayout; + // Sanitize the search input and convert it to a search. + try { + encodedUrlString = URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8"); + } catch (UnsupportedEncodingException exception) { + encodedUrlString = ""; + } - // `urlIsLoading` is used in `onCreate()`, `loadUrl()`, and `applyDomainSettings()`. - private boolean urlIsLoading; + // Add the base search URL. + url = searchURL + encodedUrlString; + } else { // The intent should contain a URL. + // Set the intent data as the URL. + url = intentUriData.toString(); + } - // `pinnedDomainSslCertificate` is used in `onCreate()` and `applyDomainSettings()`. - private boolean pinnedDomainSslCertificate; + // Add a new tab if specified in the preferences. + if (sharedPreferences.getBoolean("open_intents_in_new_tab", true)) { // Load the URL in a new tab. + // Set the loading new intent flag. + loadingNewIntent = true; + // Add a new tab. + addNewTab(url, true); + } else { // Load the URL in the current tab. + // Make it so. + loadUrl(currentWebView, url); + } - @Override - // Remove Android Studio's warning about the dangers of using SetJavaScriptEnabled. The whole premise of Privacy Browser is built around an understanding of these dangers. - @SuppressLint("SetJavaScriptEnabled") - // Remove Android Studio's warning about deprecations. We have to use the deprecated `getColor()` until API >= 23. - @SuppressWarnings("deprecation") - protected void onCreate(Bundle savedInstanceState) { - // Get a handle for `sharedPreferences`. `this` references the current context. - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + // Get a handle for the drawer layout. + DrawerLayout drawerLayout = findViewById(R.id.drawerlayout); - // Get the theme preference. - darkTheme = sharedPreferences.getBoolean("dark_theme", false); + // Close the navigation drawer if it is open. + if (drawerLayout.isDrawerVisible(GravityCompat.START)) { + drawerLayout.closeDrawer(GravityCompat.START); + } - // Set the activity theme. - if (darkTheme) { - setTheme(R.style.PrivacyBrowserDark); - } else { - setTheme(R.style.PrivacyBrowserLight); + // Close the bookmarks drawer if it is open. + if (drawerLayout.isDrawerVisible(GravityCompat.END)) { + drawerLayout.closeDrawer(GravityCompat.END); + } + } } + } + @Override + public void onRestart() { // Run the default commands. - super.onCreate(savedInstanceState); - - // Set the content view. - setContentView(R.layout.main_drawerlayout); + super.onRestart(); - // Get a handle for `inputMethodManager`. - inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + // Apply the app settings if returning from the Settings activity. + if (reapplyAppSettingsOnRestart) { + // Reset the reapply app settings on restart tracker. + reapplyAppSettingsOnRestart = false; - // We need to use the `SupportActionBar` from `android.support.v7.app.ActionBar` until the minimum API is >= 21. - supportAppBar = (Toolbar) findViewById(R.id.app_bar); - setSupportActionBar(supportAppBar); - appBar = getSupportActionBar(); + // Apply the app settings. + applyAppSettings(); + } - // This is needed to get rid of the Android Studio warning that `appBar` might be null. - assert appBar != null; + // Apply the domain settings if returning from the settings or domains activity. + if (reapplyDomainSettingsOnRestart) { + // Reset the reapply domain settings on restart tracker. + reapplyDomainSettingsOnRestart = false; - // Add the custom `url_app_bar` layout, which shows the favorite icon and the URL text bar. - appBar.setCustomView(R.layout.url_app_bar); - appBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM); + // Reapply the domain settings for each tab. + for (int i = 0; i < webViewPagerAdapter.getCount(); i++) { + // Get the WebView tab fragment. + WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i); - // Initialize the `ForegroundColorSpans` and `StyleSpan` for highlighting `urlTextBox`. We have to use the deprecated `getColor()` until API >= 23. - redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_a700)); - initialGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500)); - finalGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500)); + // Get the fragment view. + View fragmentView = webViewTabFragment.getView(); - // Get a handle for `urlTextBox`. - urlTextBox = (EditText) appBar.getCustomView().findViewById(R.id.url_edittext); + // Only reload the WebViews if they exist. + if (fragmentView != null) { + // Get the nested scroll WebView from the tab fragment. + NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview); - // Remove the formatting from `urlTextBar` when the user is editing the text. - urlTextBox.setOnFocusChangeListener(new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View v, boolean hasFocus) { - if (hasFocus) { // The user is editing `urlTextBox`. - // Remove the highlighting. - urlTextBox.getText().removeSpan(redColorSpan); - urlTextBox.getText().removeSpan(initialGrayColorSpan); - urlTextBox.getText().removeSpan(finalGrayColorSpan); - } else { // The user has stopped editing `urlTextBox`. - // Reapply the highlighting. - highlightUrlText(); - } - } - }); + // Reset the current domain name so the domain settings will be reapplied. + nestedScrollWebView.resetCurrentDomainName(); - // Set the `Go` button on the keyboard to load the URL in `urlTextBox`. - urlTextBox.setOnKeyListener(new View.OnKeyListener() { - @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { - // If the event is a key-down event on the `enter` button, load the URL. - if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { - // Load the URL into the mainWebView and consume the event. - try { - loadUrlFromTextBox(); - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); + // Reapply the domain settings if the URL is not null, which can happen if an empty tab is active when returning from settings. + if (nestedScrollWebView.getUrl() != null) { + applyDomainSettings(nestedScrollWebView, nestedScrollWebView.getUrl(), false, true); } - // If the enter key was pressed, consume the event. - return true; - } else { - // If any other key was pressed, do not consume the event. - return false; } } - }); + } - // Set `waitingForOrbotHTMLString`. - waitingForOrbotHTMLString = "

" + getString(R.string.waiting_for_orbot) + "

"; + // Load the URL on restart (used when loading a bookmark). + if (loadUrlOnRestart) { + // Load the specified URL. + loadUrl(currentWebView, urlToLoadOnRestart); - // Initialize `currentDomainName`, `orbotStatus`, and `waitingForOrbot`. - currentDomainName = ""; - orbotStatus = "unknown"; - waitingForOrbot = false; + // Reset the load on restart tracker. + loadUrlOnRestart = false; + } - // Create an Orbot status `BroadcastReceiver`. - BroadcastReceiver orbotStatusBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - // Store the content of the status message in `orbotStatus`. - orbotStatus = intent.getStringExtra("org.torproject.android.intent.extra.STATUS"); + // Update the bookmarks drawer if returning from the Bookmarks activity. + if (restartFromBookmarksActivity) { + // Get a handle for the drawer layout. + DrawerLayout drawerLayout = findViewById(R.id.drawerlayout); - // If we are waiting on Orbot, load the website now that Orbot is connected. - if (orbotStatus.equals("ON") && waitingForOrbot) { - // Reset `waitingForOrbot`. - waitingForOrbot = false; + // Close the bookmarks drawer. + drawerLayout.closeDrawer(GravityCompat.END); - // Load `formattedUrlString - loadUrl(formattedUrlString); - } - } - }; + // Reload the bookmarks drawer. + loadBookmarksFolder(); - // Register `orbotStatusBroadcastReceiver` on `this` context. - this.registerReceiver(orbotStatusBroadcastReceiver, new IntentFilter("org.torproject.android.intent.action.STATUS")); + // Reset `restartFromBookmarksActivity`. + restartFromBookmarksActivity = false; + } - // Get handles for views that need to be accessed. - drawerLayout = (DrawerLayout) findViewById(R.id.drawerlayout); - rootCoordinatorLayout = (CoordinatorLayout) findViewById(R.id.root_coordinatorlayout); - mainWebViewRelativeLayout = (RelativeLayout) findViewById(R.id.main_webview_relativelayout); - mainWebView = (WebView) findViewById(R.id.main_webview); - findOnPageLinearLayout = (LinearLayout) findViewById(R.id.find_on_page_linearlayout); - findOnPageEditText = (EditText) findViewById(R.id.find_on_page_edittext); - fullScreenVideoFrameLayout = (FrameLayout) findViewById(R.id.full_screen_video_framelayout); - urlAppBarRelativeLayout = (RelativeLayout) findViewById(R.id.url_app_bar_relativelayout); - favoriteIconImageView = (ImageView) findViewById(R.id.favorite_icon); - - // Create a double-tap listener to toggle full-screen mode. - final GestureDetector gestureDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() { - // Override `onDoubleTap()`. All other events are handled using the default settings. - @Override - public boolean onDoubleTap(MotionEvent event) { - if (fullScreenBrowsingModeEnabled) { // Only process the double-tap if full screen browsing mode is enabled. - // Toggle `inFullScreenBrowsingMode`. - inFullScreenBrowsingMode = !inFullScreenBrowsingMode; + // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. This can be important if the screen was rotated. + updatePrivacyIcons(true); + } - if (inFullScreenBrowsingMode) { // Switch to full screen mode. - // Hide the `appBar`. - appBar.hide(); + // `onResume()` runs after `onStart()`, which runs after `onCreate()` and `onRestart()`. + @Override + public void onResume() { + // Run the default commands. + super.onResume(); - // Hide the `BannerAd` in the free flavor. - if (BuildConfig.FLAVOR.contentEquals("free")) { - BannerAd.hideAd(adView); - } + // Resume any WebViews. + for (int i = 0; i < webViewPagerAdapter.getCount(); i++) { + // Get the WebView tab fragment. + WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i); - // Modify the system bars. - if (hideSystemBarsOnFullscreen) { // Hide everything. - // Remove the translucent overlays. - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); - - // Remove the translucent status bar overlay on the `Drawer Layout`, which is special and needs its own command. - drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); - - /* SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen. - * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen. - * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically rehides them after they are shown. - */ - rootCoordinatorLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); - - // Set `rootCoordinatorLayout` to fill the whole screen. - rootCoordinatorLayout.setFitsSystemWindows(false); - } else { // Hide everything except the status and navigation bars. - // Set `rootCoordinatorLayout` to fit under the status and navigation bars. - rootCoordinatorLayout.setFitsSystemWindows(false); - - if (translucentNavigationBarOnFullscreen) { // There is an Android Support Library bug that causes a scrim to print on the right side of the `Drawer Layout` when the navigation bar is displayed on the right of the screen. - // Set the navigation bar to be translucent. - getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); - } - } - } else { // Switch to normal viewing mode. - // Show the `appBar`. - appBar.show(); + // Get the fragment view. + View fragmentView = webViewTabFragment.getView(); - // Show the `BannerAd` in the free flavor. - if (BuildConfig.FLAVOR.contentEquals("free")) { - // Reload the ad. Because the screen may have rotated, we need to use `reloadAfterRotate`. - BannerAd.reloadAfterRotate(adView, getApplicationContext(), getString(R.string.ad_id)); + // Only resume the WebViews if they exist (they won't when the app is first created). + if (fragmentView != null) { + // Get the nested scroll WebView from the tab fragment. + NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview); - // Reinitialize the `adView` variable, as the `View` will have been removed and re-added by `BannerAd.reloadAfterRotate()`. - adView = findViewById(R.id.adview); - } + // Resume the nested scroll WebView JavaScript timers. + nestedScrollWebView.resumeTimers(); - // Remove the translucent navigation bar flag if it is set. - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + // Resume the nested scroll WebView. + nestedScrollWebView.onResume(); + } + } - // Add the translucent status flag if it is unset. This also resets `drawerLayout's` `View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN`. - getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + // Reapply the proxy settings if the system is using a proxy. This redisplays the appropriate alert dialog. + if (!proxyMode.equals(ProxyHelper.NONE)) { + applyProxy(false); + } - // Remove any `SYSTEM_UI` flags from `rootCoordinatorLayout`. - rootCoordinatorLayout.setSystemUiVisibility(0); + // Reapply any system UI flags and the ad in the free flavor. + if (displayingFullScreenVideo || inFullScreenBrowsingMode) { // The system is displaying a website or a video in full screen mode. + // Get a handle for the root frame layouts. + FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout); + + /* Hide the system bars. + * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen. + * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar. + * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen. + * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown. + */ + rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + } else if (BuildConfig.FLAVOR.contentEquals("free")) { // The system in not in full screen mode. + // Resume the ad. + AdHelper.resumeAd(findViewById(R.id.adview)); + } + } - // Constrain `rootCoordinatorLayout` inside the status and navigation bars. - rootCoordinatorLayout.setFitsSystemWindows(true); - } + @Override + public void onPause() { + // Run the default commands. + super.onPause(); - // Consume the double-tap. - return true; - } else { // Do not consume the double-tap because full screen browsing mode is disabled. - return false; - } - } - }); + for (int i = 0; i < webViewPagerAdapter.getCount(); i++) { + // Get the WebView tab fragment. + WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i); - // Pass all touch events on `mainWebView` through `gestureDetector` to check for double-taps. - mainWebView.setOnTouchListener(new View.OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent event) { - // Send the `event` to `gestureDetector`. - return gestureDetector.onTouchEvent(event); - } - }); + // Get the fragment view. + View fragmentView = webViewTabFragment.getView(); - // Update `findOnPageCountTextView`. - mainWebView.setFindListener(new WebView.FindListener() { - // Get a handle for `findOnPageCountTextView`. - final TextView findOnPageCountTextView = (TextView) findViewById(R.id.find_on_page_count_textview); + // Only pause the WebViews if they exist (they won't when the app is first created). + if (fragmentView != null) { + // Get the nested scroll WebView from the tab fragment. + NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview); - @Override - public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) { - if ((isDoneCounting) && (numberOfMatches == 0)) { // There are no matches. - // Set `findOnPageCountTextView` to `0/0`. - findOnPageCountTextView.setText(R.string.zero_of_zero); - } else if (isDoneCounting) { // There are matches. - // `activeMatchOrdinal` is zero-based. - int activeMatch = activeMatchOrdinal + 1; + // Pause the nested scroll WebView. + nestedScrollWebView.onPause(); - // Set `findOnPageCountTextView`. - findOnPageCountTextView.setText(activeMatch + "/" + numberOfMatches); - } + // Pause the nested scroll WebView JavaScript timers. + nestedScrollWebView.pauseTimers(); } - }); + } - // Search for the string on the page whenever a character changes in the `findOnPageEditText`. - findOnPageEditText.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - // Do nothing. - } + // Pause the ad or it will continue to consume resources in the background on the free flavor. + if (BuildConfig.FLAVOR.contentEquals("free")) { + // Pause the ad. + AdHelper.pauseAd(findViewById(R.id.adview)); + } + } - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - // Do nothing. - } + @Override + public void onSaveInstanceState(@NonNull Bundle savedInstanceState) { + // Run the default commands. + super.onSaveInstanceState(savedInstanceState); - @Override - public void afterTextChanged(Editable s) { - // Search for the text in `mainWebView`. - mainWebView.findAllAsync(findOnPageEditText.getText().toString()); - } - }); + // Create the saved state array lists. + ArrayList savedStateArrayList = new ArrayList<>(); + ArrayList savedNestedScrollWebViewStateArrayList = new ArrayList<>(); - // Set the `check mark` button for the `findOnPageEditText` keyboard to close the soft keyboard. - findOnPageEditText.setOnKeyListener(new View.OnKeyListener() { - @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { - if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { // The `enter` key was pressed. - // Hide the soft keyboard. `0` indicates no additional flags. - inputMethodManager.hideSoftInputFromWindow(mainWebView.getWindowToken(), 0); + // Get the URLs from each tab. + for (int i = 0; i < webViewPagerAdapter.getCount(); i++) { + // Get the WebView tab fragment. + WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i); - // Consume the event. - return true; - } else { // A different key was pressed. - // Do not consume the event. - return false; - } - } - }); + // Get the fragment view. + View fragmentView = webViewTabFragment.getView(); - // Implement swipe to refresh - swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipe_refreshlayout); - swipeRefreshLayout.setColorSchemeResources(R.color.blue_700); - swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { - @Override - public void onRefresh() { - mainWebView.reload(); + if (fragmentView != null) { + // Get the nested scroll WebView from the tab fragment. + NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview); + + // Create saved state bundle. + Bundle savedStateBundle = new Bundle(); + + // Get the current states. + nestedScrollWebView.saveState(savedStateBundle); + Bundle savedNestedScrollWebViewStateBundle = nestedScrollWebView.saveNestedScrollWebViewState(); + + // Store the saved states in the array lists. + savedStateArrayList.add(savedStateBundle); + savedNestedScrollWebViewStateArrayList.add(savedNestedScrollWebViewStateBundle); } - }); + } - // `DrawerTitle` identifies the `DrawerLayout` in accessibility mode. - drawerLayout.setDrawerTitle(GravityCompat.START, getString(R.string.navigation_drawer)); + // Get the current tab position. + int currentTabPosition = tabLayout.getSelectedTabPosition(); - // Listen for touches on the navigation menu. - final NavigationView navigationView = (NavigationView) findViewById(R.id.navigationview); - navigationView.setNavigationItemSelectedListener(this); + // Store the saved states in the bundle. + savedInstanceState.putParcelableArrayList(SAVED_STATE_ARRAY_LIST, savedStateArrayList); + savedInstanceState.putParcelableArrayList(SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST, savedNestedScrollWebViewStateArrayList); + savedInstanceState.putInt(SAVED_TAB_POSITION, currentTabPosition); + savedInstanceState.putString(PROXY_MODE, proxyMode); + } - // Get handles for `navigationMenu` and the back and forward menu items. The menu is zero-based, so items 1, 2, and 3 are the second, third, and fourth entries in the menu. - final Menu navigationMenu = navigationView.getMenu(); - final MenuItem navigationBackMenuItem = navigationMenu.getItem(1); - final MenuItem navigationForwardMenuItem = navigationMenu.getItem(2); - final MenuItem navigationHistoryMenuItem = navigationMenu.getItem(3); + @Override + public void onDestroy() { + // Unregister the orbot status broadcast receiver if it exists. + if (orbotStatusBroadcastReceiver != null) { + this.unregisterReceiver(orbotStatusBroadcastReceiver); + } - // The `DrawerListener` allows us to update the Navigation Menu. - drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() { - @Override - public void onDrawerSlide(View drawerView, float slideOffset) { - } + // Close the bookmarks cursor if it exists. + if (bookmarksCursor != null) { + bookmarksCursor.close(); + } - @Override - public void onDrawerOpened(View drawerView) { - } + // Close the bookmarks database if it exists. + if (bookmarksDatabaseHelper != null) { + bookmarksDatabaseHelper.close(); + } - @Override - public void onDrawerClosed(View drawerView) { - } + // Run the default commands. + super.onDestroy(); + } - @Override - public void onDrawerStateChanged(int newState) { - if ((newState == DrawerLayout.STATE_SETTLING) || (newState == DrawerLayout.STATE_DRAGGING)) { // The drawer is opening or closing. - // Update the `Back`, `Forward`, and `History` menu items. - navigationBackMenuItem.setEnabled(mainWebView.canGoBack()); - navigationForwardMenuItem.setEnabled(mainWebView.canGoForward()); - navigationHistoryMenuItem.setEnabled((mainWebView.canGoBack() || mainWebView.canGoForward())); + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu. This adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.webview_options_menu, menu); - // Hide the keyboard (if displayed) so we can see the navigation menu. `0` indicates no additional flags. - inputMethodManager.hideSoftInputFromWindow(mainWebView.getWindowToken(), 0); + // Store a handle for the options menu so it can be used by `onOptionsItemSelected()` and `updatePrivacyIcons()`. + optionsMenu = menu; - // Clear the focus from `urlTextBox` if it has it. - urlTextBox.clearFocus(); - } - } - }); + // Set the initial status of the privacy icons. `false` does not call `invalidateOptionsMenu` as the last step. + updatePrivacyIcons(false); - // drawerToggle creates the hamburger icon at the start of the AppBar. - drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, supportAppBar, R.string.open_navigation_drawer, R.string.close_navigation_drawer); + // Get handles for the menu items. + MenuItem toggleFirstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies); + MenuItem toggleThirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies); + MenuItem toggleDomStorageMenuItem = menu.findItem(R.id.toggle_dom_storage); + MenuItem toggleSaveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data); // Form data can be removed once the minimum API >= 26. + MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data); // Form data can be removed once the minimum API >= 26. + MenuItem refreshMenuItem = menu.findItem(R.id.refresh); + MenuItem darkWebViewMenuItem = menu.findItem(R.id.dark_webview); + MenuItem adConsentMenuItem = menu.findItem(R.id.ad_consent); - // Initialize `adServerSet`. - final Set adServersSet = new HashSet<>(); + // Only display third-party cookies if API >= 21 + toggleThirdPartyCookiesMenuItem.setVisible(Build.VERSION.SDK_INT >= 21); - // Load the list of ad servers into memory. - try { - // Load `pgl.yoyo.org_adservers.txt` into a `BufferedReader`. - BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(getAssets().open("pgl.yoyo.org_adservers.txt"))); + // Only display the form data menu items if the API < 26. + toggleSaveFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26); + clearFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26); - // Create a string for storing each ad server. - String adServer; + // Disable the clear form data menu item if the API >= 26 so that the status of the main Clear Data is calculated correctly. + clearFormDataMenuItem.setEnabled(Build.VERSION.SDK_INT < 26); - // Populate `adServersSet`. - while ((adServer = bufferedReader.readLine()) != null) { - adServersSet.add(adServer); - } + // Only display the dark WebView menu item if API >= 21. + darkWebViewMenuItem.setVisible(Build.VERSION.SDK_INT >= 21); - // Close `bufferedReader`. - bufferedReader.close(); - } catch (IOException ioException) { - // We're pretty sure the asset exists, so we don't need to worry about the `IOException` ever being thrown. - } + // Only show Ad Consent if this is the free flavor. + adConsentMenuItem.setVisible(BuildConfig.FLAVOR.contentEquals("free")); - mainWebView.setWebViewClient(new WebViewClient() { - // `shouldOverrideUrlLoading` makes this `WebView` the default handler for URLs inside the app, so that links are not kicked out to other apps. - // We have to use the deprecated `shouldOverrideUrlLoading` until API >= 24. - @SuppressWarnings("deprecation") - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - if (url.startsWith("mailto:")) { // Load the URL in an external email program because it begins with `mailto:`. - // We use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched. - Intent emailIntent = new Intent(Intent.ACTION_SENDTO); + // Get the shared preferences. + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - // Parse the url and set it as the data for the `Intent`. - emailIntent.setData(Uri.parse(url)); + // Get the dark theme and app bar preferences.. + boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false); - // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser. - emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // 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. + if (displayAdditionalAppBarIcons) { + toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); + toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); + refreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); + } else { //Do not display the additional icons. + toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + refreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + } - // Make it so. - startActivity(emailIntent); + // Replace Refresh with Stop if a URL is already loading. + if (currentWebView != null && currentWebView.getProgress() != 100) { + // Set the title. + refreshMenuItem.setTitle(R.string.stop); - // Returning `true` indicates the application is handling the URL. - return true; - } else { // Load the URL in Privacy Browser. - // Apply the domain settings for the new URL. - applyDomainSettings(url); + // Set the icon if it is displayed in the app bar. + if (displayAdditionalAppBarIcons) { + // Get the current theme status. + int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; - // Returning `false` causes the current `WebView` to handle the URL and prevents it from adding redirects to the history list. - return false; + // Set the icon according to the current theme status. + if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { + refreshMenuItem.setIcon(R.drawable.close_day); + } else { + refreshMenuItem.setIcon(R.drawable.close_night); } } + } - // Block ads. We have to use the deprecated `shouldInterceptRequest` until minimum API >= 21. - @SuppressWarnings("deprecation") - @Override - public WebResourceResponse shouldInterceptRequest(WebView view, String url){ - if (adBlockerEnabled) { // Block ads. - // Extract the host from `url`. - Uri requestUri = Uri.parse(url); - String requestHost = requestUri.getHost(); + // Done. + return true; + } - // Initialize a variable to track if this is an ad server. - boolean requestHostIsAdServer = false; + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + // Get handles for the menu items. + MenuItem addOrEditDomain = menu.findItem(R.id.add_or_edit_domain); + MenuItem firstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies); + MenuItem thirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies); + MenuItem domStorageMenuItem = menu.findItem(R.id.toggle_dom_storage); + MenuItem saveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data); // Form data can be removed once the minimum API >= 26. + MenuItem clearDataMenuItem = menu.findItem(R.id.clear_data); + MenuItem clearCookiesMenuItem = menu.findItem(R.id.clear_cookies); + MenuItem clearDOMStorageMenuItem = menu.findItem(R.id.clear_dom_storage); + MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data); // Form data can be removed once the minimum API >= 26. + MenuItem blocklistsMenuItem = menu.findItem(R.id.blocklists); + MenuItem easyListMenuItem = menu.findItem(R.id.easylist); + MenuItem easyPrivacyMenuItem = menu.findItem(R.id.easyprivacy); + MenuItem fanboysAnnoyanceListMenuItem = menu.findItem(R.id.fanboys_annoyance_list); + MenuItem fanboysSocialBlockingListMenuItem = menu.findItem(R.id.fanboys_social_blocking_list); + MenuItem ultraListMenuItem = menu.findItem(R.id.ultralist); + MenuItem ultraPrivacyMenuItem = menu.findItem(R.id.ultraprivacy); + MenuItem blockAllThirdPartyRequestsMenuItem = menu.findItem(R.id.block_all_third_party_requests); + MenuItem proxyMenuItem = menu.findItem(R.id.proxy); + MenuItem userAgentMenuItem = menu.findItem(R.id.user_agent); + MenuItem fontSizeMenuItem = menu.findItem(R.id.font_size); + MenuItem swipeToRefreshMenuItem = menu.findItem(R.id.swipe_to_refresh); + MenuItem wideViewportMenuItem = menu.findItem(R.id.wide_viewport); + MenuItem displayImagesMenuItem = menu.findItem(R.id.display_images); + MenuItem darkWebViewMenuItem = menu.findItem(R.id.dark_webview); - // Check all the subdomains of `requestHost` if it is not `null` against the ad server database. - if (requestHost != null) { - while (requestHost.contains(".") && !requestHostIsAdServer) { // Stop checking if we run out of `.` or if we already know that `requestHostIsAdServer` is `true`. - if (adServersSet.contains(requestHost)) { - requestHostIsAdServer = true; - } + // Get a handle for the cookie manager. + CookieManager cookieManager = CookieManager.getInstance(); - // Strip out the lowest subdomain of `requestHost`. - requestHost = requestHost.substring(requestHost.indexOf(".") + 1); - } - } + // Initialize the current user agent string and the font size. + String currentUserAgent = getString(R.string.user_agent_privacy_browser); + int fontSize = 100; - if (requestHostIsAdServer) { // It is an ad server. - // Return an empty `WebResourceResponse`. - return new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes())); - } else { // It is not an ad server. - // `return null` loads the requested resource. - return null; - } - } else { // Ad blocking is disabled. - // `return null` loads the requested resource. - return null; - } + // 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. + if (currentWebView != null) { + // Set the add or edit domain text. + if (currentWebView.getDomainSettingsApplied()) { + addOrEditDomain.setTitle(R.string.edit_domain_settings); + } else { + addOrEditDomain.setTitle(R.string.add_domain_settings); } - // Handle HTTP authentication requests. - @Override - public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) { - // Store `handler` so it can be accessed from `onHttpAuthenticationCancel()` and `onHttpAuthenticationProceed()`. - httpAuthHandler = handler; + // Get the current user agent from the WebView. + currentUserAgent = currentWebView.getSettings().getUserAgentString(); + + // Get the current font size from the + fontSize = currentWebView.getSettings().getTextZoom(); + + // Set the status of the menu item checkboxes. + domStorageMenuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled()); + saveFormDataMenuItem.setChecked(currentWebView.getSettings().getSaveFormData()); // Form data can be removed once the minimum API >= 26. + easyListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASYLIST)); + easyPrivacyMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASYPRIVACY)); + fanboysAnnoyanceListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST)); + fanboysSocialBlockingListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST)); + ultraListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRALIST)); + ultraPrivacyMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRAPRIVACY)); + blockAllThirdPartyRequestsMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS)); + swipeToRefreshMenuItem.setChecked(currentWebView.getSwipeToRefresh()); + wideViewportMenuItem.setChecked(currentWebView.getSettings().getUseWideViewPort()); + displayImagesMenuItem.setChecked(currentWebView.getSettings().getLoadsImagesAutomatically()); + + // Initialize the display names for the blocklists with the number of blocked requests. + blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); + easyListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASYLIST) + " - " + getString(R.string.easylist)); + easyPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASYPRIVACY) + " - " + getString(R.string.easyprivacy)); + fanboysAnnoyanceListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST) + " - " + getString(R.string.fanboys_annoyance_list)); + fanboysSocialBlockingListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST) + " - " + getString(R.string.fanboys_social_blocking_list)); + ultraListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.ULTRALIST) + " - " + getString(R.string.ultralist)); + ultraPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.ULTRAPRIVACY) + " - " + getString(R.string.ultraprivacy)); + blockAllThirdPartyRequestsMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " + getString(R.string.block_all_third_party_requests)); + + // Only modify third-party cookies if the API >= 21. + if (Build.VERSION.SDK_INT >= 21) { + // Set the status of the third-party cookies checkbox. + thirdPartyCookiesMenuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView)); + + // Enable third-party cookies if first-party cookies are enabled. + thirdPartyCookiesMenuItem.setEnabled(cookieManager.acceptCookie()); + } - // Display the HTTP authentication dialog. - AppCompatDialogFragment httpAuthenticationDialogFragment = HttpAuthenticationDialog.displayDialog(host, realm); - httpAuthenticationDialogFragment.show(getSupportFragmentManager(), getString(R.string.http_authentication)); + // Enable DOM Storage if JavaScript is enabled. + domStorageMenuItem.setEnabled(currentWebView.getSettings().getJavaScriptEnabled()); + + // Set the checkbox status for dark WebView if the WebView supports it. + if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) { + darkWebViewMenuItem.setChecked(WebSettingsCompat.getForceDark(currentWebView.getSettings()) == WebSettingsCompat.FORCE_DARK_ON); } + } - // Update the URL in urlTextBox when the page starts to load. - @Override - public void onPageStarted(WebView view, String url, Bitmap favicon) { - // If night mode is enabled, hide `mainWebView` until after the night mode CSS is applied. - if (nightMode) { - mainWebView.setVisibility(View.INVISIBLE); - } + // Set the checked status of the first party cookies menu item. + firstPartyCookiesMenuItem.setChecked(cookieManager.acceptCookie()); - // Check to see if we are waiting on Orbot. - if (!waitingForOrbot) { // We are not waiting on Orbot, so we need to process the URL. - // We need to update `formattedUrlString` at the beginning of the load, so that if the user toggles JavaScript during the load the new website is reloaded. - formattedUrlString = url; + // Enable Clear Cookies if there are any. + clearCookiesMenuItem.setEnabled(cookieManager.hasCookies()); - // Display the formatted URL text. - urlTextBox.setText(formattedUrlString); + // 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`. + String privateDataDirectoryString = getApplicationInfo().dataDir; - // Apply text highlighting to `urlTextBox`. - highlightUrlText(); + // Get a count of the number of files in the Local Storage directory. + File localStorageDirectory = new File (privateDataDirectoryString + "/app_webview/Local Storage/"); + int localStorageDirectoryNumberOfFiles = 0; + if (localStorageDirectory.exists()) { + // `Objects.requireNonNull` removes a lint warning that `localStorageDirectory.list` might produce a null pointed exception if it is dereferenced. + localStorageDirectoryNumberOfFiles = Objects.requireNonNull(localStorageDirectory.list()).length; + } - // Apply any custom domain settings if the URL was loaded by navigating history. - if (navigatingHistory) { - applyDomainSettings(url); - } + // Get a count of the number of files in the IndexedDB directory. + File indexedDBDirectory = new File (privateDataDirectoryString + "/app_webview/IndexedDB"); + int indexedDBDirectoryNumberOfFiles = 0; + if (indexedDBDirectory.exists()) { + // `Objects.requireNonNull` removes a lint warning that `indexedDBDirectory.list` might produce a null pointed exception if it is dereferenced. + indexedDBDirectoryNumberOfFiles = Objects.requireNonNull(indexedDBDirectory.list()).length; + } - // Set `urlIsLoading` to `true`, so that redirects while loading do not trigger changes in the user agent, which forces another reload of the existing page. - urlIsLoading = true; - } - } + // Enable Clear DOM Storage if there is any. + clearDOMStorageMenuItem.setEnabled(localStorageDirectoryNumberOfFiles > 0 || indexedDBDirectoryNumberOfFiles > 0); - // It is necessary to update `formattedUrlString` and `urlTextBox` after the page finishes loading because the final URL can change during load. - @Override - public void onPageFinished(WebView view, String url) { - // Reset `urlIsLoading`, which is used to prevent reloads on redirect if the user agent changes. - urlIsLoading = false; + // Enable Clear Form Data is there is any. This can be removed once the minimum API >= 26. + if (Build.VERSION.SDK_INT < 26) { + // Get the WebView database. + WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this); - // Clear the cache and history if Incognito Mode is enabled. - if (incognitoModeEnabled) { - // Clear the cache. `true` includes disk files. - mainWebView.clearCache(true); + // Enable the clear form data menu item if there is anything to clear. + clearFormDataMenuItem.setEnabled(webViewDatabase.hasFormData()); + } - // Clear the back/forward history. - mainWebView.clearHistory(); + // Enable Clear Data if any of the submenu items are enabled. + clearDataMenuItem.setEnabled(clearCookiesMenuItem.isEnabled() || clearDOMStorageMenuItem.isEnabled() || clearFormDataMenuItem.isEnabled()); - // Manually delete cache folders. - try { - // Delete the main `cache` folder. - privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/cache"); + // Disable Fanboy's Social Blocking List menu item if Fanboy's Annoyance List is checked. + fanboysSocialBlockingListMenuItem.setEnabled(!fanboysAnnoyanceListMenuItem.isChecked()); - // Delete the `app_webview` folder, which contains an additional `WebView` cache. See `https://code.google.com/p/android/issues/detail?id=233826&thanks=233826&ts=1486670530`. - privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview"); - } catch (IOException e) { - // Do nothing if an error is thrown. - } - } + // Set the proxy title and check the applied proxy. + switch (proxyMode) { + case ProxyHelper.NONE: + // Set the proxy title. + proxyMenuItem.setTitle(getString(R.string.proxy) + " - " + getString(R.string.proxy_none)); - // Update `urlTextBox` and apply domain settings if not waiting on Orbot. - if (!waitingForOrbot) { - // Check to see if `WebView` has set `url` to be `about:blank`. - if (url.equals("about:blank")) { // `WebView` is blank, so `formattedUrlString` should be `""` and `urlTextBox` should display a hint. - // Set `formattedUrlString` to `""`. - formattedUrlString = ""; + // Check the proxy None radio button. + menu.findItem(R.id.proxy_none).setChecked(true); + break; - urlTextBox.setText(formattedUrlString); + case ProxyHelper.TOR: + // Set the proxy title. + proxyMenuItem.setTitle(getString(R.string.proxy) + " - " + getString(R.string.proxy_tor)); - // Request focus for `urlTextBox`. - urlTextBox.requestFocus(); + // Check the proxy Tor radio button. + menu.findItem(R.id.proxy_tor).setChecked(true); + break; - // Display the keyboard. - inputMethodManager.showSoftInput(urlTextBox, 0); + case ProxyHelper.I2P: + // Set the proxy title. + proxyMenuItem.setTitle(getString(R.string.proxy) + " - " + getString(R.string.proxy_i2p)); - // Apply the domain settings. This clears any settings from the previous domain. - applyDomainSettings(formattedUrlString); - } else { // `WebView` has loaded a webpage. - // Set `formattedUrlString`. - formattedUrlString = url; + // Check the proxy I2P radio button. + menu.findItem(R.id.proxy_i2p).setChecked(true); + break; - // Only update `urlTextBox` if the user is not typing in it. - if (!urlTextBox.hasFocus()) { - // Display the formatted URL text. - urlTextBox.setText(formattedUrlString); + case ProxyHelper.CUSTOM: + // Set the proxy title. + proxyMenuItem.setTitle(getString(R.string.proxy) + " - " + getString(R.string.proxy_custom)); - // Apply text highlighting to `urlTextBox`. - highlightUrlText(); - } - } + // Check the proxy Custom radio button. + menu.findItem(R.id.proxy_custom).setChecked(true); + break; + } - // Store the SSL certificate so it can be accessed from `ViewSslCertificateDialog` and `PinnedSslCertificateMismatchDialog`. - sslCertificate = mainWebView.getCertificate(); - - // Check the current website SSL certificate against the pinned SSL certificate if there is a pinned SSL certificate the user has not chosen to ignore it for this session. - if (pinnedDomainSslCertificate && !ignorePinnedSslCertificate) { - // Initialize the current SSL certificate variables. - String currentWebsiteIssuedToCName = ""; - String currentWebsiteIssuedToOName = ""; - String currentWebsiteIssuedToUName = ""; - String currentWebsiteIssuedByCName = ""; - String currentWebsiteIssuedByOName = ""; - String currentWebsiteIssuedByUName = ""; - Date currentWebsiteSslStartDate = null; - Date currentWebsiteSslEndDate = null; - - - // Extract the individual pieces of information from the current website SSL certificate if it is not null. - if (sslCertificate != null) { - currentWebsiteIssuedToCName = sslCertificate.getIssuedTo().getCName(); - currentWebsiteIssuedToOName = sslCertificate.getIssuedTo().getOName(); - currentWebsiteIssuedToUName = sslCertificate.getIssuedTo().getUName(); - currentWebsiteIssuedByCName = sslCertificate.getIssuedBy().getCName(); - currentWebsiteIssuedByOName = sslCertificate.getIssuedBy().getOName(); - currentWebsiteIssuedByUName = sslCertificate.getIssuedBy().getUName(); - currentWebsiteSslStartDate = sslCertificate.getValidNotBeforeDate(); - currentWebsiteSslEndDate = sslCertificate.getValidNotAfterDate(); - } + // Select the current user agent menu item. A switch statement cannot be used because the user agents are not compile time constants. + if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[0])) { // Privacy Browser. + // Update the user agent menu item title. + userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_privacy_browser)); + + // Select the Privacy Browser radio box. + menu.findItem(R.id.user_agent_privacy_browser).setChecked(true); + } else if (currentUserAgent.equals(webViewDefaultUserAgent)) { // WebView Default. + // Update the user agent menu item title. + userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_webview_default)); + + // Select the WebView Default radio box. + menu.findItem(R.id.user_agent_webview_default).setChecked(true); + } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[2])) { // Firefox on Android. + // Update the user agent menu item title. + userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_firefox_on_android)); + + // Select the Firefox on Android radio box. + menu.findItem(R.id.user_agent_firefox_on_android).setChecked(true); + } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[3])) { // Chrome on Android. + // Update the user agent menu item title. + userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_chrome_on_android)); + + // Select the Chrome on Android radio box. + menu.findItem(R.id.user_agent_chrome_on_android).setChecked(true); + } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[4])) { // Safari on iOS. + // Update the user agent menu item title. + userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_safari_on_ios)); + + // Select the Safari on iOS radio box. + menu.findItem(R.id.user_agent_safari_on_ios).setChecked(true); + } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[5])) { // Firefox on Linux. + // Update the user agent menu item title. + userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_firefox_on_linux)); + + // Select the Firefox on Linux radio box. + menu.findItem(R.id.user_agent_firefox_on_linux).setChecked(true); + } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[6])) { // Chromium on Linux. + // Update the user agent menu item title. + userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_chromium_on_linux)); + + // Select the Chromium on Linux radio box. + menu.findItem(R.id.user_agent_chromium_on_linux).setChecked(true); + } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[7])) { // Firefox on Windows. + // Update the user agent menu item title. + userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_firefox_on_windows)); + + // Select the Firefox on Windows radio box. + menu.findItem(R.id.user_agent_firefox_on_windows).setChecked(true); + } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[8])) { // Chrome on Windows. + // Update the user agent menu item title. + userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_chrome_on_windows)); + + // Select the Chrome on Windows radio box. + menu.findItem(R.id.user_agent_chrome_on_windows).setChecked(true); + } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[9])) { // Edge on Windows. + // Update the user agent menu item title. + userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_edge_on_windows)); + + // Select the Edge on Windows radio box. + menu.findItem(R.id.user_agent_edge_on_windows).setChecked(true); + } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[10])) { // Internet Explorer on Windows. + // Update the user agent menu item title. + userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_internet_explorer_on_windows)); + + // Select the Internet on Windows radio box. + menu.findItem(R.id.user_agent_internet_explorer_on_windows).setChecked(true); + } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[11])) { // Safari on macOS. + // Update the user agent menu item title. + userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_safari_on_macos)); + + // Select the Safari on macOS radio box. + menu.findItem(R.id.user_agent_safari_on_macos).setChecked(true); + } else { // Custom user agent. + // Update the user agent menu item title. + userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_custom)); + + // Select the Custom radio box. + menu.findItem(R.id.user_agent_custom).setChecked(true); + } - // Initialize `String` variables to store the SSL certificate dates. `Strings` are needed to compare the values below, which doesn't work with `Dates` if they are `null`. - String currentWebsiteSslStartDateString = ""; - String currentWebsiteSslEndDateString = ""; - String pinnedDomainSslStartDateString = ""; - String pinnedDomainSslEndDateString = ""; + // Set the font size title. + fontSizeMenuItem.setTitle(getString(R.string.font_size) + " - " + fontSize + "%"); - // Convert the `Dates` to `Strings` if they are not `null`. - if (currentWebsiteSslStartDate != null) { - currentWebsiteSslStartDateString = currentWebsiteSslStartDate.toString(); - } + // Run all the other default commands. + super.onPrepareOptionsMenu(menu); - if (currentWebsiteSslEndDate != null) { - currentWebsiteSslEndDateString = currentWebsiteSslEndDate.toString(); - } + // Display the menu. + return true; + } - if (pinnedDomainSslStartDate != null) { - pinnedDomainSslStartDateString = pinnedDomainSslStartDate.toString(); - } + @Override + // Remove Android Studio's warning about the dangers of using SetJavaScriptEnabled. + @SuppressLint("SetJavaScriptEnabled") + public boolean onOptionsItemSelected(MenuItem menuItem) { + // Get the selected menu item ID. + int menuItemId = menuItem.getItemId(); - if (pinnedDomainSslEndDate != null) { - pinnedDomainSslEndDateString = pinnedDomainSslEndDate.toString(); - } + // Get a handle for the shared preferences. + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - // Check to see if the pinned SSL certificate matches the current website certificate. - if (!currentWebsiteIssuedToCName.equals(pinnedDomainSslIssuedToCNameString) || !currentWebsiteIssuedToOName.equals(pinnedDomainSslIssuedToONameString) || !currentWebsiteIssuedToUName.equals(pinnedDomainSslIssuedToUNameString) || - !currentWebsiteIssuedByCName.equals(pinnedDomainSslIssuedByCNameString) || !currentWebsiteIssuedByOName.equals(pinnedDomainSslIssuedByONameString) || !currentWebsiteIssuedByUName.equals(pinnedDomainSslIssuedByUNameString) || - !currentWebsiteSslStartDateString.equals(pinnedDomainSslStartDateString) || !currentWebsiteSslEndDateString.equals(pinnedDomainSslEndDateString)) { // The pinned SSL certificate doesn't match the current domain certificate. - //Display the pinned SSL certificate mismatch `AlertDialog`. - AppCompatDialogFragment pinnedSslCertificateMismatchDialogFragment = new PinnedSslCertificateMismatchDialog(); - pinnedSslCertificateMismatchDialogFragment.show(getSupportFragmentManager(), getString(R.string.ssl_certificate_mismatch)); - } - } - } - } + // Get a handle for the cookie manager. + CookieManager cookieManager = CookieManager.getInstance(); - // Handle SSL Certificate errors. - @Override - public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { - // Get the current website SSL certificate. - SslCertificate currentWebsiteSslCertificate = error.getCertificate(); + // Run the commands that correlate to the selected menu item. + switch (menuItemId) { + case R.id.toggle_javascript: + // Toggle the JavaScript status. + currentWebView.getSettings().setJavaScriptEnabled(!currentWebView.getSettings().getJavaScriptEnabled()); - // Extract the individual pieces of information from the current website SSL certificate. - String currentWebsiteIssuedToCName = currentWebsiteSslCertificate.getIssuedTo().getCName(); - String currentWebsiteIssuedToOName = currentWebsiteSslCertificate.getIssuedTo().getOName(); - String currentWebsiteIssuedToUName = currentWebsiteSslCertificate.getIssuedTo().getUName(); - String currentWebsiteIssuedByCName = currentWebsiteSslCertificate.getIssuedBy().getCName(); - String currentWebsiteIssuedByOName = currentWebsiteSslCertificate.getIssuedBy().getOName(); - String currentWebsiteIssuedByUName = currentWebsiteSslCertificate.getIssuedBy().getUName(); - Date currentWebsiteSslStartDate = currentWebsiteSslCertificate.getValidNotBeforeDate(); - Date currentWebsiteSslEndDate = currentWebsiteSslCertificate.getValidNotAfterDate(); + // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. + updatePrivacyIcons(true); - // Proceed to the website if the current SSL website certificate matches the pinned domain certificate. - if (pinnedDomainSslCertificate && - currentWebsiteIssuedToCName.equals(pinnedDomainSslIssuedToCNameString) && currentWebsiteIssuedToOName.equals(pinnedDomainSslIssuedToONameString) && currentWebsiteIssuedToUName.equals(pinnedDomainSslIssuedToUNameString) && - currentWebsiteIssuedByCName.equals(pinnedDomainSslIssuedByCNameString) && currentWebsiteIssuedByOName.equals(pinnedDomainSslIssuedByONameString) && currentWebsiteIssuedByUName.equals(pinnedDomainSslIssuedByUNameString) && - currentWebsiteSslStartDate.equals(pinnedDomainSslStartDate) && currentWebsiteSslEndDate.equals(pinnedDomainSslEndDate)) { // An SSL certificate is pinned and matches the current domain certificate. - // Proceed to the website without displaying an error. - handler.proceed(); - } else { // Either there isn't a pinned SSL certificate or it doesn't match the current website certificate. - // Store `handler` so it can be accesses from `onSslErrorCancel()` and `onSslErrorProceed()`. - sslErrorHandler = handler; + // Display a `Snackbar`. + if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScrip is enabled. + Snackbar.make(webViewPager, R.string.javascript_enabled, Snackbar.LENGTH_SHORT).show(); + } else if (cookieManager.acceptCookie()) { // JavaScript is disabled, but first-party cookies are enabled. + Snackbar.make(webViewPager, R.string.javascript_disabled, Snackbar.LENGTH_SHORT).show(); + } else { // Privacy mode. + Snackbar.make(webViewPager, R.string.privacy_mode, Snackbar.LENGTH_SHORT).show(); + } - // Display the SSL error `AlertDialog`. - AppCompatDialogFragment sslCertificateErrorDialogFragment = SslCertificateErrorDialog.displayDialog(error); - sslCertificateErrorDialogFragment.show(getSupportFragmentManager(), getString(R.string.ssl_certificate_error)); + // Reload the current WebView. + currentWebView.reload(); + + // Consume the event. + return true; + + case R.id.refresh: + if (menuItem.getTitle().equals(getString(R.string.refresh))) { // The refresh button was pushed. + // Reload the current WebView. + currentWebView.reload(); + } else { // The stop button was pushed. + // Stop the loading of the WebView. + currentWebView.stopLoading(); } - } - }); - // Get a handle for the progress bar. - final ProgressBar progressBar = (ProgressBar) findViewById(R.id.progress_bar); + // Consume the event. + return true; - mainWebView.setWebChromeClient(new WebChromeClient() { - // Update the progress bar when a page is loading. - @Override - public void onProgressChanged(WebView view, int progress) { - progressBar.setProgress(progress); - if (progress < 100) { - // Show the progress bar. - progressBar.setVisibility(View.VISIBLE); - } else { - // Hide the progress bar. - progressBar.setVisibility(View.GONE); + case R.id.bookmarks: + // Get a handle for the drawer layout. + DrawerLayout drawerLayout = findViewById(R.id.drawerlayout); - // Inject the night mode CSS if night mode is enabled. - if (nightMode) { // Night mode is enabled. - // `background-color: #212121` sets the background to be dark gray. `color: #BDBDBD` sets the text color to be light gray. `box-shadow: none` removes a lower underline on links used by WordPress. - // `text-decoration: none` removes all text underlines. `text-shadow: none` removes text shadows, which usually have a hard coded color. `border: none` removes all borders, which can also be used to underline text. - // `a {color: #1565C0}` sets links to be a dark blue. `!important` takes precedent over any existing sub-settings. - mainWebView.evaluateJavascript("(function() {var parent = document.getElementsByTagName('head').item(0); var style = document.createElement('style'); style.type = 'text/css'; style.innerHTML = '" + - "* {background-color: #212121 !important; color: #BDBDBD !important; box-shadow: none !important; text-decoration: none !important; text-shadow: none !important; border: none !important;}" + - "a {color: #1565C0 !important;}" + - "'; parent.appendChild(style)})()", new ValueCallback() { - @Override - public void onReceiveValue(String value) { - // Initialize a `Handler` to display `mainWebView`. - Handler displayWebViewHandler = new Handler(); - - // Setup a `Runnable` to display `mainWebView` after a delay to allow the CSS to be applied. - Runnable displayWebViewRunnable = new Runnable() { - public void run() { - mainWebView.setVisibility(View.VISIBLE); - } - }; + // Open the bookmarks drawer. + drawerLayout.openDrawer(GravityCompat.END); - // Use `displayWebViewHandler` to delay the displaying of `mainWebView` for 500 milliseconds. - displayWebViewHandler.postDelayed(displayWebViewRunnable, 500); - } - }); - } else { // Night mode is disabled. - // Display `mainWebView` in case it was hidden before loading domain settings. - mainWebView.setVisibility(View.VISIBLE); - } + // Consume the event. + return true; - //Stop the `SwipeToRefresh` indicator if it is running - swipeRefreshLayout.setRefreshing(false); - } - } + case R.id.toggle_first_party_cookies: + // Switch the first-party cookie status. + cookieManager.setAcceptCookie(!cookieManager.acceptCookie()); - // Set the favorite icon when it changes. - @Override - public void onReceivedIcon(WebView view, Bitmap icon) { - // Only update the favorite icon if the website has finished loading. - if (progressBar.getVisibility() == View.GONE) { - // Save a copy of the favorite icon. - favoriteIconBitmap = icon; + // Store the first-party cookie status. + currentWebView.setAcceptFirstPartyCookies(cookieManager.acceptCookie()); - // Place the favorite icon in the appBar. - favoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(icon, 64, 64, true)); - } - } + // Update the menu checkbox. + menuItem.setChecked(cookieManager.acceptCookie()); - // Save a copy of the title when it changes. - @Override - public void onReceivedTitle(WebView view, String title) { - // Save a copy of the title. - webViewTitle = title; - } + // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. + updatePrivacyIcons(true); - // Enter full screen video - @Override - public void onShowCustomView(View view, CustomViewCallback callback) { - // Pause the ad if this is the free flavor. - if (BuildConfig.FLAVOR.contentEquals("free")) { - BannerAd.pauseAd(adView); + // Display a snackbar. + if (cookieManager.acceptCookie()) { // First-party cookies are enabled. + Snackbar.make(webViewPager, R.string.first_party_cookies_enabled, Snackbar.LENGTH_SHORT).show(); + } else if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScript is still enabled. + Snackbar.make(webViewPager, R.string.first_party_cookies_disabled, Snackbar.LENGTH_SHORT).show(); + } else { // Privacy mode. + Snackbar.make(webViewPager, R.string.privacy_mode, Snackbar.LENGTH_SHORT).show(); } - // Remove the translucent overlays. - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + // Reload the current WebView. + currentWebView.reload(); - // Remove the translucent status bar overlay on the `Drawer Layout`, which is special and needs its own command. - drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + // Consume the event. + return true; - /* SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen. - * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen. - * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically rehides them after they are shown. - */ - rootCoordinatorLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + case R.id.toggle_third_party_cookies: + if (Build.VERSION.SDK_INT >= 21) { + // Switch the status of thirdPartyCookiesEnabled. + cookieManager.setAcceptThirdPartyCookies(currentWebView, !cookieManager.acceptThirdPartyCookies(currentWebView)); + + // Update the menu checkbox. + menuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView)); - // Set `rootCoordinatorLayout` to fill the entire screen. - rootCoordinatorLayout.setFitsSystemWindows(false); + // Display a snackbar. + if (cookieManager.acceptThirdPartyCookies(currentWebView)) { + Snackbar.make(webViewPager, R.string.third_party_cookies_enabled, Snackbar.LENGTH_SHORT).show(); + } else { + Snackbar.make(webViewPager, R.string.third_party_cookies_disabled, Snackbar.LENGTH_SHORT).show(); + } - // Add `view` to `fullScreenVideoFrameLayout` and display it on the screen. - fullScreenVideoFrameLayout.addView(view); - fullScreenVideoFrameLayout.setVisibility(View.VISIBLE); - } + // Reload the current WebView. + currentWebView.reload(); + } // Else do nothing because SDK < 21. - // Exit full screen video - public void onHideCustomView() { - // Hide `fullScreenVideoFrameLayout`. - fullScreenVideoFrameLayout.removeAllViews(); - fullScreenVideoFrameLayout.setVisibility(View.GONE); + // Consume the event. + return true; - // Add the translucent status flag. This also resets `drawerLayout's` `View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN`. - getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + case R.id.toggle_dom_storage: + // Toggle the status of domStorageEnabled. + currentWebView.getSettings().setDomStorageEnabled(!currentWebView.getSettings().getDomStorageEnabled()); - // Set `rootCoordinatorLayout` to fit inside the status and navigation bars. This also clears the `SYSTEM_UI` flags. - rootCoordinatorLayout.setFitsSystemWindows(true); + // Update the menu checkbox. + menuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled()); - // Show the ad if this is the free flavor. - if (BuildConfig.FLAVOR.contentEquals("free")) { - // Reload the ad. Because the screen may have rotated, we need to use `reloadAfterRotate`. - BannerAd.reloadAfterRotate(adView, getApplicationContext(), getString(R.string.ad_id)); + // Update the privacy icon. `true` refreshes the app bar icons. + updatePrivacyIcons(true); - // Reinitialize the `adView` variable, as the `View` will have been removed and re-added by `BannerAd.reloadAfterRotate()`. - adView = findViewById(R.id.adview); + // Display a snackbar. + if (currentWebView.getSettings().getDomStorageEnabled()) { + Snackbar.make(webViewPager, R.string.dom_storage_enabled, Snackbar.LENGTH_SHORT).show(); + } else { + Snackbar.make(webViewPager, R.string.dom_storage_disabled, Snackbar.LENGTH_SHORT).show(); } - } - }); - // Register `mainWebView` for a context menu. This is used to see link targets and download images. - registerForContextMenu(mainWebView); + // Reload the current WebView. + currentWebView.reload(); - // Allow the downloading of files. - mainWebView.setDownloadListener(new DownloadListener() { - @Override - public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) { - // Show the `DownloadFileDialog` `AlertDialog` and name this instance `@string/download`. - AppCompatDialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(url, contentDisposition, contentLength); - downloadFileDialogFragment.show(getSupportFragmentManager(), getString(R.string.download)); - } - }); + // Consume the event. + return true; - // Allow pinch to zoom. - mainWebView.getSettings().setBuiltInZoomControls(true); + // Form data can be removed once the minimum API >= 26. + case R.id.toggle_save_form_data: + // Switch the status of saveFormDataEnabled. + currentWebView.getSettings().setSaveFormData(!currentWebView.getSettings().getSaveFormData()); - // Hide zoom controls. - mainWebView.getSettings().setDisplayZoomControls(false); + // Update the menu checkbox. + menuItem.setChecked(currentWebView.getSettings().getSaveFormData()); - // Set `mainWebView` to use a wide viewport. Otherwise, some web pages will be scrunched and some content will render outside the screen. - mainWebView.getSettings().setUseWideViewPort(true); + // Display a snackbar. + if (currentWebView.getSettings().getSaveFormData()) { + Snackbar.make(webViewPager, R.string.form_data_enabled, Snackbar.LENGTH_SHORT).show(); + } else { + Snackbar.make(webViewPager, R.string.form_data_disabled, Snackbar.LENGTH_SHORT).show(); + } - // Set `mainWebView` to load in overview mode (zoomed out to the maximum width). - mainWebView.getSettings().setLoadWithOverviewMode(true); + // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. + updatePrivacyIcons(true); - // Explicitly disable geolocation. - mainWebView.getSettings().setGeolocationEnabled(false); + // Reload the current WebView. + currentWebView.reload(); - // Initialize cookieManager. - cookieManager = CookieManager.getInstance(); + // Consume the event. + return true; - // Replace the header that `WebView` creates for `X-Requested-With` with a null value. The default value is the application ID (com.stoutner.privacybrowser.standard). - customHeaders.put("X-Requested-With", ""); + case R.id.clear_cookies: + Snackbar.make(webViewPager, R.string.cookies_deleted, Snackbar.LENGTH_LONG) + .setAction(R.string.undo, v -> { + // Do nothing because everything will be handled by `onDismissed()` below. + }) + .addCallback(new Snackbar.Callback() { + @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`. + @Override + public void onDismissed(Snackbar snackbar, int event) { + if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) { // The snackbar was dismissed without the undo button being pushed. + // Delete the cookies, which command varies by SDK. + if (Build.VERSION.SDK_INT < 21) { + cookieManager.removeAllCookie(); + } else { + cookieManager.removeAllCookies(null); + } + } + } + }) + .show(); - // Initialize the default preference values the first time the program is run. `this` is the context. `false` keeps this command from resetting any current preferences back to default. - PreferenceManager.setDefaultValues(this, R.xml.preferences, false); + // Consume the event. + return true; - // Get the intent that started the app. - final Intent launchingIntent = getIntent(); + case R.id.clear_dom_storage: + Snackbar.make(webViewPager, R.string.dom_storage_deleted, Snackbar.LENGTH_LONG) + .setAction(R.string.undo, v -> { + // Do nothing because everything will be handled by `onDismissed()` below. + }) + .addCallback(new Snackbar.Callback() { + @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`. + @Override + public void onDismissed(Snackbar snackbar, int event) { + if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) { // The snackbar was dismissed without the undo button being pushed. + // Delete the DOM Storage. + WebStorage webStorage = WebStorage.getInstance(); + webStorage.deleteAllData(); - // Extract the launching intent data as `launchingIntentUriData`. - final Uri launchingIntentUriData = launchingIntent.getData(); + // Initialize a handler to manually delete the DOM storage files and directories. + Handler deleteDomStorageHandler = new Handler(); - // Convert the launching intent URI data (if it exists) to a string and store it in `formattedUrlString`. - if (launchingIntentUriData != null) { - formattedUrlString = launchingIntentUriData.toString(); - } + // Setup a runnable to manually delete the DOM storage files and directories. + Runnable deleteDomStorageRunnable = () -> { + try { + // Get a handle for the runtime. + Runtime runtime = Runtime.getRuntime(); + + // 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`. + String privateDataDirectoryString = getApplicationInfo().dataDir; + + // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly. + Process deleteLocalStorageProcess = runtime.exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"}); + + // Multiple commands must be used because `Runtime.exec()` does not like `*`. + Process deleteIndexProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB"); + Process deleteQuotaManagerProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager"); + Process deleteQuotaManagerJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal"); + Process deleteDatabasesProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases"); + + // Wait for the processes to finish. + deleteLocalStorageProcess.waitFor(); + deleteIndexProcess.waitFor(); + deleteQuotaManagerProcess.waitFor(); + deleteQuotaManagerJournalProcess.waitFor(); + deleteDatabasesProcess.waitFor(); + } catch (Exception exception) { + // Do nothing if an error is thrown. + } + }; - // Get a handle for the `Runtime`. - privacyBrowserRuntime = Runtime.getRuntime(); + // Manually delete the DOM storage files after 200 milliseconds. + deleteDomStorageHandler.postDelayed(deleteDomStorageRunnable, 200); + } + } + }) + .show(); - // Store the application's private data directory. - privateDataDirectoryString = getApplicationInfo().dataDir; // `dataDir` will vary, but will be something like `/data/user/0/com.stoutner.privacybrowser.standard`, which links to `/data/data/com.stoutner.privacybrowser.standard`. + // Consume the event. + return true; - // Initialize `inFullScreenBrowsingMode`, which is always false at this point because Privacy Browser never starts in full screen browsing mode. - inFullScreenBrowsingMode = false; + // Form data can be remove once the minimum API >= 26. + case R.id.clear_form_data: + Snackbar.make(webViewPager, R.string.form_data_deleted, Snackbar.LENGTH_LONG) + .setAction(R.string.undo, v -> { + // Do nothing because everything will be handled by `onDismissed()` below. + }) + .addCallback(new Snackbar.Callback() { + @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`. + @Override + public void onDismissed(Snackbar snackbar, int event) { + if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) { // The snackbar was dismissed without the undo button being pushed. + // Delete the form data. + WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(getApplicationContext()); + mainWebViewDatabase.clearFormData(); + } + } + }) + .show(); - // Initialize AdView for the free flavor. - adView = findViewById(R.id.adview); + // Consume the event. + return true; - // Initialize the privacy settings variables. - javaScriptEnabled = false; - firstPartyCookiesEnabled = false; - thirdPartyCookiesEnabled = false; - domStorageEnabled = false; - saveFormDataEnabled = false; - nightMode = false; + case R.id.easylist: + // Toggle the EasyList status. + currentWebView.enableBlocklist(NestedScrollWebView.EASYLIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.EASYLIST)); - // Initialize `webViewTitle`. - webViewTitle = getString(R.string.no_title); + // Update the menu checkbox. + menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASYLIST)); - // Initialize `favoriteIconBitmap`. We have to use `ContextCompat` until API >= 21. - Drawable favoriteIconDrawable = ContextCompat.getDrawable(getApplicationContext(), R.drawable.world); - BitmapDrawable favoriteIconBitmapDrawable = (BitmapDrawable) favoriteIconDrawable; - favoriteIconDefaultBitmap = favoriteIconBitmapDrawable.getBitmap(); + // Reload the current WebView. + currentWebView.reload(); - // If the favorite icon is null, load the default. - if (favoriteIconBitmap == null) { - favoriteIconBitmap = favoriteIconDefaultBitmap; - } + // Consume the event. + return true; - // Apply the app settings from the shared preferences. - applyAppSettings(); + case R.id.easyprivacy: + // Toggle the EasyPrivacy status. + currentWebView.enableBlocklist(NestedScrollWebView.EASYPRIVACY, !currentWebView.isBlocklistEnabled(NestedScrollWebView.EASYPRIVACY)); - // Load `formattedUrlString` if we are not waiting for Orbot to connect. - if (!waitingForOrbot) { - loadUrl(formattedUrlString); - } - } + // Update the menu checkbox. + menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASYPRIVACY)); - @Override - protected void onNewIntent(Intent intent) { - // Sets the new intent as the activity intent, so that any future `getIntent()`s pick up this one instead of creating a new activity. - setIntent(intent); + // Reload the current WebView. + currentWebView.reload(); - if (intent.getData() != null) { - // Get the intent data and convert it to a string. - final Uri intentUriData = intent.getData(); - formattedUrlString = intentUriData.toString(); - } + // Consume the event. + return true; - // Close the navigation drawer if it is open. - if (drawerLayout.isDrawerVisible(GravityCompat.START)) { - drawerLayout.closeDrawer(GravityCompat.START); - } + case R.id.fanboys_annoyance_list: + // Toggle Fanboy's Annoyance List status. + currentWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST)); - // Load the website. - loadUrl(formattedUrlString); + // Update the menu checkbox. + menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST)); - // Clear the keyboard if displayed and remove the focus on the urlTextBar if it has it. - mainWebView.requestFocus(); - } + // Update the staus of Fanboy's Social Blocking List. + MenuItem fanboysSocialBlockingListMenuItem = optionsMenu.findItem(R.id.fanboys_social_blocking_list); + fanboysSocialBlockingListMenuItem.setEnabled(!currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST)); - @Override - public void onRestart() { - super.onRestart(); + // Reload the current WebView. + currentWebView.reload(); - // Apply the app settings, which may have been changed in `SettingsActivity`. - applyAppSettings(); + // Consume the event. + return true; - // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. - updatePrivacyIcons(true); + case R.id.fanboys_social_blocking_list: + // Toggle Fanboy's Social Blocking List status. + currentWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST)); - // Set the display webpage images mode. - setDisplayWebpageImages(); + // Update the menu checkbox. + menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST)); - // Reload the webpage if displaying of images has been disabled in `SettingsFragment`. - if (reloadOnRestart) { - // Reload `mainWebView`. - mainWebView.reload(); + // Reload the current WebView. + currentWebView.reload(); - // Reset `reloadOnRestartBoolean`. - reloadOnRestart = false; - } + // Consume the event. + return true; - // Load the URL on restart to apply changes to night mode. - if (loadUrlOnRestart) { - // Load the current `formattedUrlString`. - loadUrl(formattedUrlString); + case R.id.ultralist: + // Toggle the UltraList status. + currentWebView.enableBlocklist(NestedScrollWebView.ULTRALIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRALIST)); - // Reset `loadUrlOnRestart. - loadUrlOnRestart = false; - } - } + // Update the menu checkbox. + menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRALIST)); - // `onResume()` runs after `onStart()`, which runs after `onCreate()` and `onRestart()`. - @Override - public void onResume() { - super.onResume(); + // Reload the current WebView. + currentWebView.reload(); - // Resume JavaScript (if enabled). - mainWebView.resumeTimers(); + // Consume the event. + return true; - // Resume `mainWebView`. - mainWebView.onResume(); + case R.id.ultraprivacy: + // Toggle the UltraPrivacy status. + currentWebView.enableBlocklist(NestedScrollWebView.ULTRAPRIVACY, !currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRAPRIVACY)); - // Resume the adView for the free flavor. - if (BuildConfig.FLAVOR.contentEquals("free")) { - BannerAd.resumeAd(adView); - } - } + // Update the menu checkbox. + menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRAPRIVACY)); - @Override - public void onPause() { - // Pause `mainWebView`. - mainWebView.onPause(); + // Reload the current WebView. + currentWebView.reload(); - // Stop all JavaScript. - mainWebView.pauseTimers(); + // Consume the event. + return true; - // Pause the adView or it will continue to consume resources in the background on the free flavor. - if (BuildConfig.FLAVOR.contentEquals("free")) { - BannerAd.pauseAd(adView); - } + case R.id.block_all_third_party_requests: + //Toggle the third-party requests blocker status. + currentWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS, !currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS)); - super.onPause(); - } + // Update the menu checkbox. + menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS)); - @Override - public boolean onCreateOptionsMenu(Menu menu) { - // Inflate the menu; this adds items to the action bar if it is present. - getMenuInflater().inflate(R.menu.webview_options_menu, menu); + // Reload the current WebView. + currentWebView.reload(); - // Set mainMenu so it can be used by `onOptionsItemSelected()` and `updatePrivacyIcons`. - mainMenu = menu; + // Consume the event. + return true; - // Set the initial status of the privacy icons. `false` does not call `invalidateOptionsMenu` as the last step. - updatePrivacyIcons(false); + case R.id.proxy_none: + // Update the proxy mode. + proxyMode = ProxyHelper.NONE; - // Get handles for the menu items. - MenuItem toggleFirstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies); - MenuItem toggleThirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies); - MenuItem toggleDomStorageMenuItem = menu.findItem(R.id.toggle_dom_storage); - MenuItem toggleSaveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data); + // Apply the proxy mode. + applyProxy(true); - // Only display third-party cookies if SDK >= 21 - toggleThirdPartyCookiesMenuItem.setVisible(Build.VERSION.SDK_INT >= 21); + // Consume the event. + return true; - // Get the shared preference values. `this` references the current context. - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + case R.id.proxy_tor: + // Update the proxy mode. + proxyMode = ProxyHelper.TOR; - // Set the status of the additional app bar icons. The default is `false`. - if (sharedPreferences.getBoolean("display_additional_app_bar_icons", false)) { - toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); - toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); - toggleSaveFormDataMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); - } else { //Do not display the additional icons. - toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); - toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); - toggleSaveFormDataMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); - } + // Apply the proxy mode. + applyProxy(true); - return true; - } + // Consume the event. + return true; - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - // Get handles for the menu items. - MenuItem toggleFirstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies); - MenuItem toggleThirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies); - MenuItem toggleDomStorageMenuItem = menu.findItem(R.id.toggle_dom_storage); - MenuItem toggleSaveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data); - MenuItem clearCookiesMenuItem = menu.findItem(R.id.clear_cookies); - MenuItem clearDOMStorageMenuItem = menu.findItem(R.id.clear_dom_storage); - MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data); - MenuItem fontSizeMenuItem = menu.findItem(R.id.font_size); - MenuItem displayImagesMenuItem = menu.findItem(R.id.display_images); - MenuItem refreshMenuItem = menu.findItem(R.id.refresh); + case R.id.proxy_i2p: + // Update the proxy mode. + proxyMode = ProxyHelper.I2P; - // Set the status of the menu item checkboxes. - toggleFirstPartyCookiesMenuItem.setChecked(firstPartyCookiesEnabled); - toggleThirdPartyCookiesMenuItem.setChecked(thirdPartyCookiesEnabled); - toggleDomStorageMenuItem.setChecked(domStorageEnabled); - toggleSaveFormDataMenuItem.setChecked(saveFormDataEnabled); - displayImagesMenuItem.setChecked(mainWebView.getSettings().getLoadsImagesAutomatically()); + // Apply the proxy mode. + applyProxy(true); - // Enable third-party cookies if first-party cookies are enabled. - toggleThirdPartyCookiesMenuItem.setEnabled(firstPartyCookiesEnabled); + // Consume the event. + return true; - // Enable `DOM Storage` if JavaScript is enabled. - toggleDomStorageMenuItem.setEnabled(javaScriptEnabled); + case R.id.proxy_custom: + // Update the proxy mode. + proxyMode = ProxyHelper.CUSTOM; - // Enable `Clear Cookies` if there are any. - clearCookiesMenuItem.setEnabled(cookieManager.hasCookies()); + // Apply the proxy mode. + applyProxy(true); - // Get a count of the number of files in the `Local Storage` directory. - File localStorageDirectory = new File (privateDataDirectoryString + "/app_webview/Local Storage/"); - int localStorageDirectoryNumberOfFiles = 0; - if (localStorageDirectory.exists()) { - localStorageDirectoryNumberOfFiles = localStorageDirectory.list().length; - } + // Consume the event. + return true; - // Get a count of the number of files in the `IndexedDB` directory. - File indexedDBDirectory = new File (privateDataDirectoryString + "/app_webview/IndexedDB"); - int indexedDBDirectoryNumberOfFiles = 0; - if (indexedDBDirectory.exists()) { - indexedDBDirectoryNumberOfFiles = indexedDBDirectory.list().length; - } + case R.id.user_agent_privacy_browser: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[0]); - // Enable `Clear DOM Storage` if there is any. - clearDOMStorageMenuItem.setEnabled(localStorageDirectoryNumberOfFiles > 0 || indexedDBDirectoryNumberOfFiles > 0); + // Reload the current WebView. + currentWebView.reload(); - // Enable `Clear Form Data` is there is any. - WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(this); - clearFormDataMenuItem.setEnabled(mainWebViewDatabase.hasFormData()); + // Consume the event. + return true; - // Initialize font size variables. - int fontSize = mainWebView.getSettings().getTextZoom(); - String fontSizeTitle; - MenuItem selectedFontSizeMenuItem; + case R.id.user_agent_webview_default: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(""); - // Prepare the font size title and current size menu item. - switch (fontSize) { - case 25: - fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.twenty_five_percent); - selectedFontSizeMenuItem = menu.findItem(R.id.font_size_twenty_five_percent); - break; + // Reload the current WebView. + currentWebView.reload(); - case 50: - fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.fifty_percent); - selectedFontSizeMenuItem = menu.findItem(R.id.font_size_fifty_percent); - break; + // Consume the event. + return true; - case 75: - fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.seventy_five_percent); - selectedFontSizeMenuItem = menu.findItem(R.id.font_size_seventy_five_percent); - break; + case R.id.user_agent_firefox_on_android: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[2]); - case 100: - fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent); - selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent); - break; + // Reload the current WebView. + currentWebView.reload(); - case 125: - fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_twenty_five_percent); - selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_twenty_five_percent); - break; + // Consume the event. + return true; - case 150: - fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_fifty_percent); - selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_fifty_percent); - break; + case R.id.user_agent_chrome_on_android: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[3]); - case 175: - fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_seventy_five_percent); - selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_seventy_five_percent); - break; + // Reload the current WebView. + currentWebView.reload(); - case 200: - fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.two_hundred_percent); - selectedFontSizeMenuItem = menu.findItem(R.id.font_size_two_hundred_percent); - break; + // Consume the event. + return true; - default: - fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent); - selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent); - break; - } + case R.id.user_agent_safari_on_ios: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[4]); - // Set the font size title and select the current size menu item. - fontSizeMenuItem.setTitle(fontSizeTitle); - selectedFontSizeMenuItem.setChecked(true); + // Reload the current WebView. + currentWebView.reload(); - // Only show `Refresh` if `swipeToRefresh` is disabled. - refreshMenuItem.setVisible(!swipeToRefreshEnabled); + // Consume the event. + return true; - // Run all the other default commands. - super.onPrepareOptionsMenu(menu); + case R.id.user_agent_firefox_on_linux: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[5]); - // `return true` displays the menu. - return true; - } + // Reload the current WebView. + currentWebView.reload(); - @Override - // Remove Android Studio's warning about the dangers of using SetJavaScriptEnabled. - @SuppressLint("SetJavaScriptEnabled") - // removeAllCookies is deprecated, but it is required for API < 21. - @SuppressWarnings("deprecation") - public boolean onOptionsItemSelected(MenuItem menuItem) { - int menuItemId = menuItem.getItemId(); + // Consume the event. + return true; - // Set the commands that relate to the menu entries. - switch (menuItemId) { - case R.id.toggle_javascript: - // Switch the status of javaScriptEnabled. - javaScriptEnabled = !javaScriptEnabled; + case R.id.user_agent_chromium_on_linux: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[6]); - // Apply the new JavaScript status. - mainWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled); + // Reload the current WebView. + currentWebView.reload(); - // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. - updatePrivacyIcons(true); + // Consume the event. + return true; - // Display a `Snackbar`. - if (javaScriptEnabled) { // JavaScrip is enabled. - Snackbar.make(findViewById(R.id.main_webview), R.string.javascript_enabled, Snackbar.LENGTH_SHORT).show(); - } else if (firstPartyCookiesEnabled) { // JavaScript is disabled, but first-party cookies are enabled. - Snackbar.make(findViewById(R.id.main_webview), R.string.javascript_disabled, Snackbar.LENGTH_SHORT).show(); - } else { // Privacy mode. - Snackbar.make(findViewById(R.id.main_webview), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show(); - } + case R.id.user_agent_firefox_on_windows: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[7]); + + // Reload the current WebView. + currentWebView.reload(); - // Reload the WebView. - mainWebView.reload(); + // Consume the event. return true; - case R.id.toggle_first_party_cookies: - // Switch the status of firstPartyCookiesEnabled. - firstPartyCookiesEnabled = !firstPartyCookiesEnabled; + case R.id.user_agent_chrome_on_windows: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[8]); - // Update the menu checkbox. - menuItem.setChecked(firstPartyCookiesEnabled); + // Reload the current WebView. + currentWebView.reload(); - // Apply the new cookie status. - cookieManager.setAcceptCookie(firstPartyCookiesEnabled); + // Consume the event. + return true; - // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. - updatePrivacyIcons(true); + case R.id.user_agent_edge_on_windows: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[9]); - // Display a `Snackbar`. - if (firstPartyCookiesEnabled) { // First-party cookies are enabled. - Snackbar.make(findViewById(R.id.main_webview), R.string.first_party_cookies_enabled, Snackbar.LENGTH_SHORT).show(); - } else if (javaScriptEnabled){ // JavaScript is still enabled. - Snackbar.make(findViewById(R.id.main_webview), R.string.first_party_cookies_disabled, Snackbar.LENGTH_SHORT).show(); - } else { // Privacy mode. - Snackbar.make(findViewById(R.id.main_webview), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show(); - } + // Reload the current WebView. + currentWebView.reload(); - // Reload the WebView. - mainWebView.reload(); + // Consume the event. return true; - case R.id.toggle_third_party_cookies: - if (Build.VERSION.SDK_INT >= 21) { - // Switch the status of thirdPartyCookiesEnabled. - thirdPartyCookiesEnabled = !thirdPartyCookiesEnabled; + case R.id.user_agent_internet_explorer_on_windows: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[10]); - // Update the menu checkbox. - menuItem.setChecked(thirdPartyCookiesEnabled); + // Reload the current WebView. + currentWebView.reload(); - // Apply the new cookie status. - cookieManager.setAcceptThirdPartyCookies(mainWebView, thirdPartyCookiesEnabled); + // Consume the event. + return true; - // Display a `Snackbar`. - if (thirdPartyCookiesEnabled) { - Snackbar.make(findViewById(R.id.main_webview), R.string.third_party_cookies_enabled, Snackbar.LENGTH_SHORT).show(); - } else { - Snackbar.make(findViewById(R.id.main_webview), R.string.third_party_cookies_disabled, Snackbar.LENGTH_SHORT).show(); - } + case R.id.user_agent_safari_on_macos: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[11]); - // Reload the WebView. - mainWebView.reload(); - } // Else do nothing because SDK < 21. + // Reload the current WebView. + currentWebView.reload(); + + // Consume the event. return true; - case R.id.toggle_dom_storage: - // Switch the status of domStorageEnabled. - domStorageEnabled = !domStorageEnabled; + case R.id.user_agent_custom: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value))); - // Update the menu checkbox. - menuItem.setChecked(domStorageEnabled); + // Reload the current WebView. + currentWebView.reload(); - // Apply the new DOM Storage status. - mainWebView.getSettings().setDomStorageEnabled(domStorageEnabled); + // Consume the event. + return true; - // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. - updatePrivacyIcons(true); + case R.id.font_size: + // Instantiate the font size dialog. + DialogFragment fontSizeDialogFragment = FontSizeDialog.displayDialog(currentWebView.getSettings().getTextZoom()); - // Display a `Snackbar`. - if (domStorageEnabled) { - Snackbar.make(findViewById(R.id.main_webview), R.string.dom_storage_enabled, Snackbar.LENGTH_SHORT).show(); - } else { - Snackbar.make(findViewById(R.id.main_webview), R.string.dom_storage_disabled, Snackbar.LENGTH_SHORT).show(); - } + // Show the font size dialog. + fontSizeDialogFragment.show(getSupportFragmentManager(), getString(R.string.font_size)); - // Reload the WebView. - mainWebView.reload(); + // Consume the event. return true; - case R.id.toggle_save_form_data: - // Switch the status of saveFormDataEnabled. - saveFormDataEnabled = !saveFormDataEnabled; - - // Update the menu checkbox. - menuItem.setChecked(saveFormDataEnabled); + case R.id.swipe_to_refresh: + // Toggle the stored status of swipe to refresh. + currentWebView.setSwipeToRefresh(!currentWebView.getSwipeToRefresh()); - // Apply the new form data status. - mainWebView.getSettings().setSaveFormData(saveFormDataEnabled); + // Get a handle for the swipe refresh layout. + SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout); - // Display a `Snackbar`. - if (saveFormDataEnabled) { - Snackbar.make(findViewById(R.id.main_webview), R.string.form_data_enabled, Snackbar.LENGTH_SHORT).show(); - } else { - Snackbar.make(findViewById(R.id.main_webview), R.string.form_data_disabled, Snackbar.LENGTH_SHORT).show(); + // Update the swipe refresh layout. + if (currentWebView.getSwipeToRefresh()) { // Swipe to refresh is enabled. + // Only enable the swipe refresh layout if the WebView is scrolled to the top. It is updated every time the scroll changes. + swipeRefreshLayout.setEnabled(currentWebView.getY() == 0); + } else { // Swipe to refresh is disabled. + // Disable the swipe refresh layout. + swipeRefreshLayout.setEnabled(false); } - // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. - updatePrivacyIcons(true); - - // Reload the WebView. - mainWebView.reload(); + // Consume the event. return true; - case R.id.clear_cookies: - Snackbar.make(findViewById(R.id.main_webview), R.string.cookies_deleted, Snackbar.LENGTH_LONG) - .setAction(R.string.undo, new View.OnClickListener() { - @Override - public void onClick(View v) { - // Do nothing because everything will be handled by `onDismissed()` below. - } - }) - .addCallback(new Snackbar.Callback() { - @Override - public void onDismissed(Snackbar snackbar, int event) { - switch (event) { - // The user pushed the `Undo` button. - case Snackbar.Callback.DISMISS_EVENT_ACTION: - // Do nothing. - break; - - // The `Snackbar` was dismissed without the `Undo` button being pushed. - default: - // `cookieManager.removeAllCookie()` varies by SDK. - if (Build.VERSION.SDK_INT < 21) { - cookieManager.removeAllCookie(); - } else { - // `null` indicates no callback. - cookieManager.removeAllCookies(null); - } - } - } - }) - .show(); - return true; + case R.id.wide_viewport: + // Toggle the viewport. + currentWebView.getSettings().setUseWideViewPort(!currentWebView.getSettings().getUseWideViewPort()); - case R.id.clear_dom_storage: - Snackbar.make(findViewById(R.id.main_webview), R.string.dom_storage_deleted, Snackbar.LENGTH_LONG) - .setAction(R.string.undo, new View.OnClickListener() { - @Override - public void onClick(View v) { - // Do nothing because everything will be handled by `onDismissed()` below. - } - }) - .addCallback(new Snackbar.Callback() { - @Override - public void onDismissed(Snackbar snackbar, int event) { - switch (event) { - // The user pushed the `Undo` button. - case Snackbar.Callback.DISMISS_EVENT_ACTION: - // Do nothing. - break; - - // The `Snackbar` was dismissed without the `Undo` button being pushed. - default: - // Delete the DOM Storage. - WebStorage webStorage = WebStorage.getInstance(); - webStorage.deleteAllData(); - - // Manually remove `IndexedDB` if it exists. - try { - privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB"); - } catch (IOException e) { - // Do nothing if an error is thrown. - } - } - } - }) - .show(); + // Consume the event. return true; - case R.id.clear_form_data: - Snackbar.make(findViewById(R.id.main_webview), R.string.form_data_deleted, Snackbar.LENGTH_LONG) - .setAction(R.string.undo, new View.OnClickListener() { - @Override - public void onClick(View v) { - // Do nothing because everything will be handled by `onDismissed()` below. - } - }) - .addCallback(new Snackbar.Callback() { - @Override - public void onDismissed(Snackbar snackbar, int event) { - switch (event) { - // The user pushed the `Undo` button. - case Snackbar.Callback.DISMISS_EVENT_ACTION: - // Do nothing. - break; - - // The `Snackbar` was dismissed without the `Undo` button being pushed. - default: - // Delete the form data. - WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(getApplicationContext()); - mainWebViewDatabase.clearFormData(); - } - } - }) - .show(); - return true; + case R.id.display_images: + if (currentWebView.getSettings().getLoadsImagesAutomatically()) { // Images are currently loaded automatically. + // Disable loading of images. + currentWebView.getSettings().setLoadsImagesAutomatically(false); + + // Reload the website to remove existing images. + currentWebView.reload(); + } else { // Images are not currently loaded automatically. + // Enable loading of images. Missing images will be loaded without the need for a reload. + currentWebView.getSettings().setLoadsImagesAutomatically(true); + } - case R.id.font_size_twenty_five_percent: - mainWebView.getSettings().setTextZoom(25); + // Consume the event. return true; - case R.id.font_size_fifty_percent: - mainWebView.getSettings().setTextZoom(50); + case R.id.dark_webview: + // Check to see if dark WebView is supported by this WebView. + if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) { + // Toggle the dark WebView setting. + if (WebSettingsCompat.getForceDark(currentWebView.getSettings()) == WebSettingsCompat.FORCE_DARK_ON) { // Dark WebView is currently enabled. + // Turn off dark WebView. + WebSettingsCompat.setForceDark(currentWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF); + } else { // Dark WebView is currently disabled. + // turn on dark WebView. + WebSettingsCompat.setForceDark(currentWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON); + } + } + + // Consume the event. return true; - case R.id.font_size_seventy_five_percent: - mainWebView.getSettings().setTextZoom(75); + case R.id.find_on_page: + // Get a handle for the views. + Toolbar toolbar = findViewById(R.id.toolbar); + LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout); + EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext); + + // Set the minimum height of the find on page linear layout to match the toolbar. + findOnPageLinearLayout.setMinimumHeight(toolbar.getHeight()); + + // Hide the toolbar. + toolbar.setVisibility(View.GONE); + + // Show the find on page linear layout. + findOnPageLinearLayout.setVisibility(View.VISIBLE); + + // Display the keyboard. The app must wait 200 ms before running the command to work around a bug in Android. + // http://stackoverflow.com/questions/5520085/android-show-softkeyboard-with-showsoftinput-is-not-working + findOnPageEditText.postDelayed(() -> { + // Set the focus on `findOnPageEditText`. + findOnPageEditText.requestFocus(); + + // Get a handle for the input method manager. + InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + + // Remove the lint warning below that the input method manager might be null. + assert inputMethodManager != null; + + // Display the keyboard. `0` sets no input flags. + inputMethodManager.showSoftInput(findOnPageEditText, 0); + }, 200); + + // Consume the event. return true; - case R.id.font_size_one_hundred_percent: - mainWebView.getSettings().setTextZoom(100); + case R.id.print: + // Get a print manager instance. + PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE); + + // Remove the lint error below that print manager might be null. + assert printManager != null; + + // Create a print document adapter from the current WebView. + PrintDocumentAdapter printDocumentAdapter = currentWebView.createPrintDocumentAdapter(); + + // Print the document. + printManager.print(getString(R.string.privacy_browser_web_page), printDocumentAdapter, null); + + // Consume the event. return true; - case R.id.font_size_one_hundred_twenty_five_percent: - mainWebView.getSettings().setTextZoom(125); + case R.id.save_url: + // Prepare the save dialog. The dialog will be displayed once the file size and the content disposition have been acquired. + new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(), + currentWebView.getAcceptFirstPartyCookies()).execute(currentWebView.getCurrentUrl()); + + // Consume the event. return true; - case R.id.font_size_one_hundred_fifty_percent: - mainWebView.getSettings().setTextZoom(150); + case R.id.save_as_archive: + // Prepare the save dialog. The dialog will be displayed once the file size and the content disposition have been acquired. + new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_AS_ARCHIVE, currentWebView.getSettings().getUserAgentString(), + currentWebView.getAcceptFirstPartyCookies()).execute(currentWebView.getCurrentUrl()); + + // Consume the event. return true; - case R.id.font_size_one_hundred_seventy_five_percent: - mainWebView.getSettings().setTextZoom(175); + case R.id.save_as_image: + // Prepare the save dialog. The dialog will be displayed once the file size adn the content disposition have been acquired. + new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_AS_IMAGE, currentWebView.getSettings().getUserAgentString(), + currentWebView.getAcceptFirstPartyCookies()).execute(currentWebView.getCurrentUrl()); + + // Consume the event. return true; - case R.id.font_size_two_hundred_percent: - mainWebView.getSettings().setTextZoom(200); + case R.id.add_to_homescreen: + // Instantiate the create home screen shortcut dialog. + DialogFragment createHomeScreenShortcutDialogFragment = CreateHomeScreenShortcutDialog.createDialog(currentWebView.getTitle(), currentWebView.getUrl(), + currentWebView.getFavoriteOrDefaultIcon()); + + // Show the create home screen shortcut dialog. + createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getString(R.string.create_shortcut)); + + // Consume the event. return true; - case R.id.display_images: - if (mainWebView.getSettings().getLoadsImagesAutomatically()) { // Images are currently loaded automatically. - mainWebView.getSettings().setLoadsImagesAutomatically(false); - mainWebView.reload(); - } else { // Images are not currently loaded automatically. - mainWebView.getSettings().setLoadsImagesAutomatically(true); - } + case R.id.view_source: + // Create an intent to launch the view source activity. + Intent viewSourceIntent = new Intent(this, ViewSourceActivity.class); + + // Add the variables to the intent. + viewSourceIntent.putExtra("user_agent", currentWebView.getSettings().getUserAgentString()); + viewSourceIntent.putExtra("current_url", currentWebView.getUrl()); + + // Make it so. + startActivity(viewSourceIntent); - // Set `onTheFlyDisplayImagesSet`. - onTheFlyDisplayImagesSet = true; + // Consume the event. return true; - case R.id.share: + case R.id.share_url: // Setup the share string. - String shareString = webViewTitle + " – " + urlTextBox.getText().toString(); + String shareString = currentWebView.getTitle() + " – " + currentWebView.getUrl(); // Create the share intent. - Intent shareIntent = new Intent(); - shareIntent.setAction(Intent.ACTION_SEND); + Intent shareIntent = new Intent(Intent.ACTION_SEND); shareIntent.putExtra(Intent.EXTRA_TEXT, shareString); shareIntent.setType("text/plain"); // Make it so. - startActivity(Intent.createChooser(shareIntent, "Share URL")); + startActivity(Intent.createChooser(shareIntent, getString(R.string.share_url))); + + // Consume the event. return true; - case R.id.find_on_page: - // Hide the URL app bar. - supportAppBar.setVisibility(View.GONE); + case R.id.open_with_app: + // Open the URL with an outside app. + openWithApp(currentWebView.getUrl()); - // Show the Find on Page `RelativeLayout`. - findOnPageLinearLayout.setVisibility(View.VISIBLE); + // Consume the event. + return true; - // Display the keyboard. We have to wait 200 ms before running the command to work around a bug in Android. http://stackoverflow.com/questions/5520085/android-show-softkeyboard-with-showsoftinput-is-not-working - findOnPageEditText.postDelayed(new Runnable() { - @Override - public void run() - { - // Set the focus on `findOnPageEditText`. - findOnPageEditText.requestFocus(); + case R.id.open_with_browser: + // Open the URL with an outside browser. + openWithBrowser(currentWebView.getUrl()); - // Display the keyboard. `0` sets no input flags. - inputMethodManager.showSoftInput(findOnPageEditText, 0); - } - }, 200); + // Consume the event. return true; - case R.id.print: - // Get a `PrintManager` instance. - PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE); + case R.id.add_or_edit_domain: + if (currentWebView.getDomainSettingsApplied()) { // Edit the current domain settings. + // Reapply the domain settings on returning to `MainWebViewActivity`. + reapplyDomainSettingsOnRestart = true; + + // Create an intent to launch the domains activity. + Intent domainsIntent = new Intent(this, DomainsActivity.class); + + // Add the extra information to the intent. + domainsIntent.putExtra("load_domain", currentWebView.getDomainSettingsDatabaseId()); + domainsIntent.putExtra("close_on_back", true); + domainsIntent.putExtra("current_url", currentWebView.getUrl()); + + // Get the current certificate. + SslCertificate sslCertificate = currentWebView.getCertificate(); + + // Check to see if the SSL certificate is populated. + if (sslCertificate != null) { + // Extract the certificate to strings. + String issuedToCName = sslCertificate.getIssuedTo().getCName(); + String issuedToOName = sslCertificate.getIssuedTo().getOName(); + String issuedToUName = sslCertificate.getIssuedTo().getUName(); + String issuedByCName = sslCertificate.getIssuedBy().getCName(); + String issuedByOName = sslCertificate.getIssuedBy().getOName(); + String issuedByUName = sslCertificate.getIssuedBy().getUName(); + long startDateLong = sslCertificate.getValidNotBeforeDate().getTime(); + long endDateLong = sslCertificate.getValidNotAfterDate().getTime(); + + // Add the certificate to the intent. + domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName); + domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName); + domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName); + domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName); + domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName); + domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName); + domainsIntent.putExtra("ssl_start_date", startDateLong); + domainsIntent.putExtra("ssl_end_date", endDateLong); + } - // Convert `mainWebView` to `printDocumentAdapter`. - PrintDocumentAdapter printDocumentAdapter = mainWebView.createPrintDocumentAdapter(); + // Check to see if the current IP addresses have been received. + if (currentWebView.hasCurrentIpAddresses()) { + // Add the current IP addresses to the intent. + domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses()); + } - // Print the document. The print attributes are `null`. - printManager.print(getString(R.string.privacy_browser_web_page), printDocumentAdapter, null); - return true; + // Make it so. + startActivity(domainsIntent); + } else { // Add a new domain. + // Apply the new domain settings on returning to `MainWebViewActivity`. + reapplyDomainSettingsOnRestart = true; + + // Get the current domain + Uri currentUri = Uri.parse(currentWebView.getUrl()); + String currentDomain = currentUri.getHost(); + + // Initialize the database handler. The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`. + DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0); + + // Create the domain and store the database ID. + int newDomainDatabaseId = domainsDatabaseHelper.addDomain(currentDomain); + + // Create an intent to launch the domains activity. + Intent domainsIntent = new Intent(this, DomainsActivity.class); + + // Add the extra information to the intent. + domainsIntent.putExtra("load_domain", newDomainDatabaseId); + domainsIntent.putExtra("close_on_back", true); + domainsIntent.putExtra("current_url", currentWebView.getUrl()); + + // Get the current certificate. + SslCertificate sslCertificate = currentWebView.getCertificate(); + + // Check to see if the SSL certificate is populated. + if (sslCertificate != null) { + // Extract the certificate to strings. + String issuedToCName = sslCertificate.getIssuedTo().getCName(); + String issuedToOName = sslCertificate.getIssuedTo().getOName(); + String issuedToUName = sslCertificate.getIssuedTo().getUName(); + String issuedByCName = sslCertificate.getIssuedBy().getCName(); + String issuedByOName = sslCertificate.getIssuedBy().getOName(); + String issuedByUName = sslCertificate.getIssuedBy().getUName(); + long startDateLong = sslCertificate.getValidNotBeforeDate().getTime(); + long endDateLong = sslCertificate.getValidNotAfterDate().getTime(); + + // Add the certificate to the intent. + domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName); + domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName); + domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName); + domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName); + domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName); + domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName); + domainsIntent.putExtra("ssl_start_date", startDateLong); + domainsIntent.putExtra("ssl_end_date", endDateLong); + } - case R.id.add_to_homescreen: - // Show the `CreateHomeScreenShortcutDialog` `AlertDialog` and name this instance `R.string.create_shortcut`. - AppCompatDialogFragment createHomeScreenShortcutDialogFragment = new CreateHomeScreenShortcutDialog(); - createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getString(R.string.create_shortcut)); + // Check to see if the current IP addresses have been received. + if (currentWebView.hasCurrentIpAddresses()) { + // Add the current IP addresses to the intent. + domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses()); + } + + // Make it so. + startActivity(domainsIntent); + } - //Everything else will be handled by `CreateHomeScreenShortcutDialog` and the associated listener below. + // Consume the event. return true; - case R.id.refresh: - mainWebView.reload(); + case R.id.ad_consent: + // Instantiate the ad consent dialog. + DialogFragment adConsentDialogFragment = new AdConsentDialog(); + + // Display the ad consent dialog. + adConsentDialogFragment.show(getSupportFragmentManager(), getString(R.string.ad_consent)); + + // Consume the event. return true; default: @@ -1724,49 +1916,86 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation } // removeAllCookies is deprecated, but it is required for API < 21. - @SuppressWarnings("deprecation") @Override public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) { + // Get the menu item ID. int menuItemId = menuItem.getItemId(); + // Get a handle for the shared preferences. + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + + // Run the commands that correspond to the selected menu item. switch (menuItemId) { + case R.id.clear_and_exit: + // Clear and exit Privacy Browser. + clearAndExit(); + break; + case R.id.home: - loadUrl(homepage); + // Load the homepage. + loadUrl(currentWebView, sharedPreferences.getString("homepage", getString(R.string.homepage_default_value))); break; case R.id.back: - if (mainWebView.canGoBack()) { - // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded. - navigatingHistory = true; + if (currentWebView.canGoBack()) { + // Get the current web back forward list. + WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList(); + + // Get the previous entry URL. + String previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.getCurrentIndex() - 1).getUrl(); + + // Apply the domain settings. + applyDomainSettings(currentWebView, previousUrl, false, false); // Load the previous website in the history. - mainWebView.goBack(); + currentWebView.goBack(); } break; case R.id.forward: - if (mainWebView.canGoForward()) { - // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded. - navigatingHistory = true; + if (currentWebView.canGoForward()) { + // Get the current web back forward list. + WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList(); + + // Get the next entry URL. + String nextUrl = webBackForwardList.getItemAtIndex(webBackForwardList.getCurrentIndex() + 1).getUrl(); + + // Apply the domain settings. + applyDomainSettings(currentWebView, nextUrl, false, false); // Load the next website in the history. - mainWebView.goForward(); + currentWebView.goForward(); } break; case R.id.history: - // Get the `WebBackForwardList`. - WebBackForwardList webBackForwardList = mainWebView.copyBackForwardList(); + // Instantiate the URL history dialog. + DialogFragment urlHistoryDialogFragment = UrlHistoryDialog.loadBackForwardList(currentWebView.getWebViewFragmentId()); - // Show the `UrlHistoryDialog` `AlertDialog` and name this instance `R.string.history`. `this` is the `Context`. - AppCompatDialogFragment urlHistoryDialogFragment = UrlHistoryDialog.loadBackForwardList(this, webBackForwardList); + // Show the URL history dialog. urlHistoryDialogFragment.show(getSupportFragmentManager(), getString(R.string.history)); break; - case R.id.bookmarks: - // Launch BookmarksActivity. - Intent bookmarksIntent = new Intent(this, BookmarksActivity.class); - startActivity(bookmarksIntent); + case R.id.open: + // Instantiate the open file dialog. + DialogFragment openDialogFragment = new OpenDialog(); + + // Show the open file dialog. + openDialogFragment.show(getSupportFragmentManager(), getString(R.string.open)); + break; + + case R.id.requests: + // Populate the resource requests. + RequestsActivity.resourceRequests = currentWebView.getResourceRequests(); + + // Create an intent to launch the Requests activity. + Intent requestsIntent = new Intent(this, RequestsActivity.class); + + // Add the block third-party requests status to the intent. + requestsIntent.putExtra("block_all_third_party_requests", currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS)); + + // Make it so. + startActivity(requestsIntent); break; case R.id.downloads: @@ -1776,25 +2005,78 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // Launch as a new task so that Download Manager and Privacy Browser show as separate windows in the recent tasks list. downloadManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // Make it so. startActivity(downloadManagerIntent); break; + case R.id.domains: + // Set the flag to reapply the domain settings on restart when returning from Domain Settings. + reapplyDomainSettingsOnRestart = true; + + // Launch the domains activity. + Intent domainsIntent = new Intent(this, DomainsActivity.class); + + // Add the extra information to the intent. + domainsIntent.putExtra("current_url", currentWebView.getUrl()); + + // Get the current certificate. + SslCertificate sslCertificate = currentWebView.getCertificate(); + + // Check to see if the SSL certificate is populated. + if (sslCertificate != null) { + // Extract the certificate to strings. + String issuedToCName = sslCertificate.getIssuedTo().getCName(); + String issuedToOName = sslCertificate.getIssuedTo().getOName(); + String issuedToUName = sslCertificate.getIssuedTo().getUName(); + String issuedByCName = sslCertificate.getIssuedBy().getCName(); + String issuedByOName = sslCertificate.getIssuedBy().getOName(); + String issuedByUName = sslCertificate.getIssuedBy().getUName(); + long startDateLong = sslCertificate.getValidNotBeforeDate().getTime(); + long endDateLong = sslCertificate.getValidNotAfterDate().getTime(); + + // Add the certificate to the intent. + domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName); + domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName); + domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName); + domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName); + domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName); + domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName); + domainsIntent.putExtra("ssl_start_date", startDateLong); + domainsIntent.putExtra("ssl_end_date", endDateLong); + } + + // Check to see if the current IP addresses have been received. + if (currentWebView.hasCurrentIpAddresses()) { + // Add the current IP addresses to the intent. + domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses()); + } + + // Make it so. + startActivity(domainsIntent); + break; + case R.id.settings: - // Reset `currentDomainName` so that domain settings are reapplied after returning to `MainWebViewActivity`. - currentDomainName = ""; + // Set the flag to reapply app settings on restart when returning from Settings. + reapplyAppSettingsOnRestart = true; - // Launch `SettingsActivity`. + // Set the flag to reapply the domain settings on restart when returning from Settings. + reapplyDomainSettingsOnRestart = true; + + // Launch the settings activity. Intent settingsIntent = new Intent(this, SettingsActivity.class); startActivity(settingsIntent); break; - case R.id.domains: - // Reset `currentDomainName` so that domain settings are reapplied after returning to `MainWebViewActivity`. - currentDomainName = ""; + case R.id.import_export: + // Launch the import/export activity. + Intent importExportIntent = new Intent (this, ImportExportActivity.class); + startActivity(importExportIntent); + break; - // Launch `DomainsActivity`. - Intent domainsIntent = new Intent(this, DomainsActivity.class); - startActivity(domainsIntent); + case R.id.logcat: + // Launch the logcat activity. + Intent logcatIntent = new Intent(this, LogcatActivity.class); + startActivity(logcatIntent); break; case R.id.guide: @@ -1804,1167 +2086,4368 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation break; case R.id.about: - // Launch `AboutActivity`. + // Create an intent to launch the about activity. Intent aboutIntent = new Intent(this, AboutActivity.class); + + // Create a string array for the blocklist versions. + 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], + ultraList.get(0).get(0)[0], ultraPrivacy.get(0).get(0)[0]}; + + // Add the blocklist versions to the intent. + aboutIntent.putExtra("blocklist_versions", blocklistVersions); + + // Make it so. startActivity(aboutIntent); break; + } - case R.id.clearAndExit: - // Get a handle for `sharedPreferences`. `this` references the current context. - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + // Get a handle for the drawer layout. + DrawerLayout drawerLayout = findViewById(R.id.drawerlayout); - boolean clearEverything = sharedPreferences.getBoolean("clear_everything", true); + // Close the navigation drawer. + drawerLayout.closeDrawer(GravityCompat.START); + return true; + } - // Clear cookies. - if (clearEverything || sharedPreferences.getBoolean("clear_cookies", true)) { - // The command to remove cookies changed slightly in API 21. - if (Build.VERSION.SDK_INT >= 21) { - cookieManager.removeAllCookies(null); - } else { - cookieManager.removeAllCookie(); - } + @Override + public void onPostCreate(Bundle savedInstanceState) { + // Run the default commands. + super.onPostCreate(savedInstanceState); - // Manually delete the cookies database, as `CookieManager` sometimes will not flush its changes to disk before `System.exit(0)` is run. - try { - // We have to use two commands because `Runtime.exec()` does not like `*`. - privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies"); - privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies-journal"); - } catch (IOException e) { - // Do nothing if an error is thrown. - } + // Sync the state of the DrawerToggle after the default `onRestoreInstanceState()` has finished. This creates the navigation drawer icon. + actionBarDrawerToggle.syncState(); + } + + @Override + public void onConfigurationChanged(@NonNull Configuration newConfig) { + // Run the default commands. + super.onConfigurationChanged(newConfig); + + // Reload the ad for the free flavor if not in full screen mode. + if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) { + // Reload the ad. The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations. + AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id)); + } + + // `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: + // https://code.google.com/p/android/issues/detail?id=20493#c8 + // ActivityCompat.invalidateOptionsMenu(this); + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) { + // Store the hit test result. + final WebView.HitTestResult hitTestResult = currentWebView.getHitTestResult(); + + // Define the URL strings. + final String imageUrl; + final String linkUrl; + + // Get handles for the system managers. + final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); + + // Remove the lint errors below that the clipboard manager might be null. + assert clipboardManager != null; + + // Process the link according to the type. + switch (hitTestResult.getType()) { + // `SRC_ANCHOR_TYPE` is a link. + case WebView.HitTestResult.SRC_ANCHOR_TYPE: + // Get the target URL. + linkUrl = hitTestResult.getExtra(); + + // Set the target URL as the title of the `ContextMenu`. + menu.setHeaderTitle(linkUrl); + + // Add an Open in New Tab entry. + menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> { + // Load the link URL in a new tab and move to it. + addNewTab(linkUrl, true); + + // Consume the event. + return true; + }); + + // Add an Open in Background entry. + menu.add(R.string.open_in_background).setOnMenuItemClickListener((MenuItem item) -> { + // Load the link URL in a new tab but do not move to it. + addNewTab(linkUrl, false); + + // Consume the event. + return true; + }); + + // Add an Open with App entry. + menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> { + openWithApp(linkUrl); + + // Consume the event. + return true; + }); + + // Add an Open with Browser entry. + menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> { + openWithBrowser(linkUrl); + + // Consume the event. + return true; + }); + + // Add a Copy URL entry. + menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> { + // Save the link URL in a `ClipData`. + ClipData srcAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl); + + // Set the `ClipData` as the clipboard's primary clip. + clipboardManager.setPrimaryClip(srcAnchorTypeClipData); + + // Consume the event. + return true; + }); + + // Add a Save URL entry. + menu.add(R.string.save_url).setOnMenuItemClickListener((MenuItem item) -> { + // Prepare the save dialog. The dialog will be displayed once the file size and the content disposition have been acquired. + new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(), + currentWebView.getAcceptFirstPartyCookies()).execute(linkUrl); + + // Consume the event. + return true; + }); + + // Add an empty Cancel entry, which by default closes the context menu. + menu.add(R.string.cancel); + break; + + // `IMAGE_TYPE` is an image. + case WebView.HitTestResult.IMAGE_TYPE: + // Get the image URL. + imageUrl = hitTestResult.getExtra(); + + // Set the image URL as the title of the context menu. + menu.setHeaderTitle(imageUrl); + + // Add an Open in New Tab entry. + menu.add(R.string.open_image_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> { + // Load the image in a new tab. + addNewTab(imageUrl, true); + + // Consume the event. + return true; + }); + + // Add an Open with App entry. + menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> { + // Open the image URL with an external app. + openWithApp(imageUrl); + + // Consume the event. + return true; + }); + + // Add an Open with Browser entry. + menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> { + // Open the image URL with an external browser. + openWithBrowser(imageUrl); + + // Consume the event. + return true; + }); + + // Add a View Image entry. + menu.add(R.string.view_image).setOnMenuItemClickListener(item -> { + // Load the image in the current tab. + loadUrl(currentWebView, imageUrl); + + // Consume the event. + return true; + }); + + // Add a Save Image entry. + menu.add(R.string.save_image).setOnMenuItemClickListener((MenuItem item) -> { + // Prepare the save dialog. The dialog will be displayed once the file size and the content disposition have been acquired. + new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(), + currentWebView.getAcceptFirstPartyCookies()).execute(imageUrl); + + // Consume the event. + return true; + }); + + // Add a Copy URL entry. + menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> { + // Save the image URL in a clip data. + ClipData imageTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl); + + // Set the clip data as the clipboard's primary clip. + clipboardManager.setPrimaryClip(imageTypeClipData); + + // Consume the event. + return true; + }); + + // Add an empty Cancel entry, which by default closes the context menu. + menu.add(R.string.cancel); + break; + + // `SRC_IMAGE_ANCHOR_TYPE` is an image that is also a link. + case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE: + // Get the image URL. + imageUrl = hitTestResult.getExtra(); + + // Instantiate a handler. + Handler handler = new Handler(); + + // Get a message from the handler. + Message message = handler.obtainMessage(); + + // Request the image details from the last touched node be returned in the message. + currentWebView.requestFocusNodeHref(message); + + // Get the link URL from the message data. + linkUrl = message.getData().getString("url"); + + // Set the link URL as the title of the context menu. + menu.setHeaderTitle(linkUrl); + + // Add an Open in New Tab entry. + menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> { + // Load the link URL in a new tab and move to it. + addNewTab(linkUrl, true); + + // Consume the event. + return true; + }); + + // Add an Open in Background entry. + menu.add(R.string.open_in_background).setOnMenuItemClickListener((MenuItem item) -> { + // Lod the link URL in a new tab but do not move to it. + addNewTab(linkUrl, false); + + // Consume the event. + return true; + }); + + // Add an Open Image in New Tab entry. + menu.add(R.string.open_image_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> { + // Load the image in a new tab and move to it. + addNewTab(imageUrl, true); + + // Consume the event. + return true; + }); + + // Add an Open with App entry. + menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> { + // Open the link URL with an external app. + openWithApp(linkUrl); + + // Consume the event. + return true; + }); + + // Add an Open with Browser entry. + menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> { + // Open the link URL with an external browser. + openWithBrowser(linkUrl); + + // Consume the event. + return true; + }); + + // Add a View Image entry. + menu.add(R.string.view_image).setOnMenuItemClickListener((MenuItem item) -> { + // View the image in the current tab. + loadUrl(currentWebView, imageUrl); + + // Consume the event. + return true; + }); + + // Add a Save Image entry. + menu.add(R.string.save_image).setOnMenuItemClickListener((MenuItem item) -> { + // Prepare the save dialog. The dialog will be displayed once the file size and the content disposition have been acquired. + new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(), + currentWebView.getAcceptFirstPartyCookies()).execute(imageUrl); + + // Consume the event. + return true; + }); + + // Add a Copy URL entry. + menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> { + // Save the link URL in a clip data. + ClipData srcImageAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl); + + // Set the clip data as the clipboard's primary clip. + clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData); + + // Consume the event. + return true; + }); + + // Add a Save URL entry. + menu.add(R.string.save_url).setOnMenuItemClickListener((MenuItem item) -> { + // Prepare the save dialog. The dialog will be displayed once the file size and the content disposition have been acquired. + new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(), + currentWebView.getAcceptFirstPartyCookies()).execute(linkUrl); + + // Consume the event. + return true; + }); + + // Add an empty Cancel entry, which by default closes the context menu. + menu.add(R.string.cancel); + break; + + case WebView.HitTestResult.EMAIL_TYPE: + // Get the target URL. + linkUrl = hitTestResult.getExtra(); + + // Set the target URL as the title of the `ContextMenu`. + menu.setHeaderTitle(linkUrl); + + // Add a Write Email entry. + menu.add(R.string.write_email).setOnMenuItemClickListener(item -> { + // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched. + Intent emailIntent = new Intent(Intent.ACTION_SENDTO); + + // Parse the url and set it as the data for the `Intent`. + emailIntent.setData(Uri.parse("mailto:" + linkUrl)); + + // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser. + emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + // Make it so. + startActivity(emailIntent); + + // Consume the event. + return true; + }); + + // Add a Copy Email Address entry. + menu.add(R.string.copy_email_address).setOnMenuItemClickListener(item -> { + // Save the email address in a `ClipData`. + ClipData srcEmailTypeClipData = ClipData.newPlainText(getString(R.string.email_address), linkUrl); + + // Set the `ClipData` as the clipboard's primary clip. + clipboardManager.setPrimaryClip(srcEmailTypeClipData); + + // Consume the event. + return true; + }); + + // Add an empty Cancel entry, which by default closes the context menu. + menu.add(R.string.cancel); + break; + } + } + + @Override + public void onCreateBookmark(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) { + // Get a handle for the bookmarks list view. + ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview); + + // Get the dialog. + Dialog dialog = dialogFragment.getDialog(); + + // Remove the incorrect lint warning below that the dialog might be null. + assert dialog != null; + + // Get the views from the dialog fragment. + EditText createBookmarkNameEditText = dialog.findViewById(R.id.create_bookmark_name_edittext); + EditText createBookmarkUrlEditText = dialog.findViewById(R.id.create_bookmark_url_edittext); + + // Extract the strings from the edit texts. + String bookmarkNameString = createBookmarkNameEditText.getText().toString(); + String bookmarkUrlString = createBookmarkUrlEditText.getText().toString(); + + // Create a favorite icon byte array output stream. + ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream(); + + // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG). + favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream); + + // Convert the favorite icon byte array stream to a byte array. + byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray(); + + // Display the new bookmark below the current items in the (0 indexed) list. + int newBookmarkDisplayOrder = bookmarksListView.getCount(); + + // Create the bookmark. + bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolder, newBookmarkDisplayOrder, favoriteIconByteArray); + + // Update the bookmarks cursor with the current contents of this folder. + bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder); + + // Update the list view. + bookmarksCursorAdapter.changeCursor(bookmarksCursor); + + // Scroll to the new bookmark. + bookmarksListView.setSelection(newBookmarkDisplayOrder); + } + + @Override + public void onCreateBookmarkFolder(DialogFragment dialogFragment, @NonNull Bitmap favoriteIconBitmap) { + // Get a handle for the bookmarks list view. + ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview); + + // Get the dialog. + Dialog dialog = dialogFragment.getDialog(); + + // Remove the incorrect lint warning below that the dialog might be null. + assert dialog != null; + + // Get handles for the views in the dialog fragment. + EditText createFolderNameEditText = dialog.findViewById(R.id.create_folder_name_edittext); + RadioButton defaultFolderIconRadioButton = dialog.findViewById(R.id.create_folder_default_icon_radiobutton); + ImageView folderIconImageView = dialog.findViewById(R.id.create_folder_default_icon); + + // Get new folder name string. + String folderNameString = createFolderNameEditText.getText().toString(); + + // Create a folder icon bitmap. + Bitmap folderIconBitmap; + + // Set the folder icon bitmap according to the dialog. + if (defaultFolderIconRadioButton.isChecked()) { // Use the default folder icon. + // Get the default folder icon drawable. + Drawable folderIconDrawable = folderIconImageView.getDrawable(); + + // Convert the folder icon drawable to a bitmap drawable. + BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable; + + // Convert the folder icon bitmap drawable to a bitmap. + folderIconBitmap = folderIconBitmapDrawable.getBitmap(); + } else { // Use the WebView favorite icon. + // Copy the favorite icon bitmap to the folder icon bitmap. + folderIconBitmap = favoriteIconBitmap; + } + + // Create a folder icon byte array output stream. + ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream(); + + // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG). + folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream); + + // Convert the folder icon byte array stream to a byte array. + byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray(); + + // Move all the bookmarks down one in the display order. + for (int i = 0; i < bookmarksListView.getCount(); i++) { + int databaseId = (int) bookmarksListView.getItemIdAtPosition(i); + bookmarksDatabaseHelper.updateDisplayOrder(databaseId, i + 1); + } + + // Create the folder, which will be placed at the top of the `ListView`. + bookmarksDatabaseHelper.createFolder(folderNameString, currentBookmarksFolder, folderIconByteArray); + + // Update the bookmarks cursor with the current contents of this folder. + bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder); + + // Update the `ListView`. + bookmarksCursorAdapter.changeCursor(bookmarksCursor); + + // Scroll to the new folder. + bookmarksListView.setSelection(0); + } + + @Override + public void onSaveBookmarkFolder(DialogFragment dialogFragment, int selectedFolderDatabaseId, Bitmap favoriteIconBitmap) { + // Get the dialog. + Dialog dialog = dialogFragment.getDialog(); + + // Remove the incorrect lint warning below that the dialog might be null. + assert dialog != null; + + // Get handles for the views from `dialogFragment`. + EditText editFolderNameEditText = dialog.findViewById(R.id.edit_folder_name_edittext); + RadioButton currentFolderIconRadioButton = dialog.findViewById(R.id.edit_folder_current_icon_radiobutton); + RadioButton defaultFolderIconRadioButton = dialog.findViewById(R.id.edit_folder_default_icon_radiobutton); + ImageView defaultFolderIconImageView = dialog.findViewById(R.id.edit_folder_default_icon_imageview); + + // Get the new folder name. + String newFolderNameString = editFolderNameEditText.getText().toString(); + + // Check if the favorite icon has changed. + if (currentFolderIconRadioButton.isChecked()) { // Only the name has changed. + // Update the name in the database. + bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString); + } else if (!currentFolderIconRadioButton.isChecked() && newFolderNameString.equals(oldFolderNameString)) { // Only the icon has changed. + // Create the new folder icon Bitmap. + Bitmap folderIconBitmap; + + // Populate the new folder icon bitmap. + if (defaultFolderIconRadioButton.isChecked()) { + // Get the default folder icon drawable. + Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable(); + + // Convert the folder icon drawable to a bitmap drawable. + BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable; + + // Convert the folder icon bitmap drawable to a bitmap. + folderIconBitmap = folderIconBitmapDrawable.getBitmap(); + } else { // Use the `WebView` favorite icon. + // Copy the favorite icon bitmap to the folder icon bitmap. + folderIconBitmap = favoriteIconBitmap; + } + + // Create a folder icon byte array output stream. + ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream(); + + // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG). + folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream); + + // Convert the folder icon byte array stream to a byte array. + byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray(); + + // Update the folder icon in the database. + bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderIconByteArray); + } else { // The folder icon and the name have changed. + // Get the new folder icon `Bitmap`. + Bitmap folderIconBitmap; + if (defaultFolderIconRadioButton.isChecked()) { + // Get the default folder icon drawable. + Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable(); + + // Convert the folder icon drawable to a bitmap drawable. + BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable; + + // Convert the folder icon bitmap drawable to a bitmap. + folderIconBitmap = folderIconBitmapDrawable.getBitmap(); + } else { // Use the `WebView` favorite icon. + // Copy the favorite icon bitmap to the folder icon bitmap. + folderIconBitmap = favoriteIconBitmap; + } + + // Create a folder icon byte array output stream. + ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream(); + + // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG). + folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream); + + // Convert the folder icon byte array stream to a byte array. + byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray(); + + // Update the folder name and icon in the database. + bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, newFolderIconByteArray); + } + + // Update the bookmarks cursor with the current contents of this folder. + bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder); + + // Update the `ListView`. + bookmarksCursorAdapter.changeCursor(bookmarksCursor); + } + + // Override `onBackPressed` to handle the navigation drawer and and the WebViews. + @Override + public void onBackPressed() { + // Get a handle for the drawer layout. + DrawerLayout drawerLayout = findViewById(R.id.drawerlayout); + + if (drawerLayout.isDrawerVisible(GravityCompat.START)) { // The navigation drawer is open. + // Close the navigation drawer. + drawerLayout.closeDrawer(GravityCompat.START); + } else if (drawerLayout.isDrawerVisible(GravityCompat.END)){ // The bookmarks drawer is open. + if (currentBookmarksFolder.isEmpty()) { // The home folder is displayed. + // close the bookmarks drawer. + drawerLayout.closeDrawer(GravityCompat.END); + } else { // A subfolder is displayed. + // Place the former parent folder in `currentFolder`. + currentBookmarksFolder = bookmarksDatabaseHelper.getParentFolderName(currentBookmarksFolder); + + // Load the new folder. + loadBookmarksFolder(); + } + } else if (displayingFullScreenVideo) { // A full screen video is shown. + // Get a handle for the layouts. + FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout); + RelativeLayout mainContentRelativeLayout = findViewById(R.id.main_content_relativelayout); + FrameLayout fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout); + + // Re-enable the screen timeout. + fullScreenVideoFrameLayout.setKeepScreenOn(false); + + // Unset the full screen video flag. + displayingFullScreenVideo = false; + + // Remove all the views from the full screen video frame layout. + fullScreenVideoFrameLayout.removeAllViews(); + + // Hide the full screen video frame layout. + fullScreenVideoFrameLayout.setVisibility(View.GONE); + + // Enable the sliding drawers. + drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED); + + // Show the main content relative layout. + mainContentRelativeLayout.setVisibility(View.VISIBLE); + + // Apply the appropriate full screen mode flags. + if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode. + // Hide the banner ad in the free flavor. + if (BuildConfig.FLAVOR.contentEquals("free")) { + AdHelper.hideAd(findViewById(R.id.adview)); + } + + /* Hide the system bars. + * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen. + * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar. + * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen. + * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown. + */ + rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + + // Reload the website if the app bar is hidden. Otherwise, there is some bug in Android that causes the WebView to be entirely black. + if (hideAppBar) { + // Reload the WebView. + currentWebView.reload(); + } + } else { // Switch to normal viewing mode. + // Remove the `SYSTEM_UI` flags from the root frame layout. + rootFrameLayout.setSystemUiVisibility(0); + } + + // Reload the ad for the free flavor if not in full screen mode. + if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) { + // Reload the ad. + AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id)); + } + } else if (currentWebView.canGoBack()) { // There is at least one item in the current WebView history. + // Get the current web back forward list. + WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList(); + + // Get the previous entry URL. + String previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.getCurrentIndex() - 1).getUrl(); + + // Apply the domain settings. + applyDomainSettings(currentWebView, previousUrl, false, false); + + // Go back. + currentWebView.goBack(); + } else if (tabLayout.getTabCount() > 1) { // There are at least two tabs. + // Close the current tab. + closeCurrentTab(); + } else { // There isn't anything to do in Privacy Browser. + // Close Privacy Browser. `finishAndRemoveTask()` also removes Privacy Browser from the recent app list. + if (Build.VERSION.SDK_INT >= 21) { + finishAndRemoveTask(); + } else { + finish(); + } + + // Manually kill Privacy Browser. Otherwise, it is glitchy when restarted. + System.exit(0); + } + } + + // Process the results of a file browse. + @Override + public void onActivityResult(int requestCode, int resultCode, Intent returnedIntent) { + // Run the default commands. + super.onActivityResult(requestCode, resultCode, returnedIntent); + + // Run the commands that correlate to the specified request code. + switch (requestCode) { + case BROWSE_FILE_UPLOAD_REQUEST_CODE: + // File uploads only work on API >= 21. + if (Build.VERSION.SDK_INT >= 21) { + // Pass the file to the WebView. + fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, returnedIntent)); + } + break; + + case BROWSE_SAVE_WEBPAGE_REQUEST_CODE: + // Don't do anything if the user pressed back from the file picker. + if (resultCode == Activity.RESULT_OK) { + // Get a handle for the save dialog fragment. + DialogFragment saveWebpageDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.save_dialog)); + + // Only update the file name if the dialog still exists. + if (saveWebpageDialogFragment != null) { + // Get a handle for the save webpage dialog. + Dialog saveWebpageDialog = saveWebpageDialogFragment.getDialog(); + + // Remove the incorrect lint warning below that the dialog might be null. + assert saveWebpageDialog != null; + + // Get a handle for the file name edit text. + EditText fileNameEditText = saveWebpageDialog.findViewById(R.id.file_name_edittext); + TextView fileExistsWarningTextView = saveWebpageDialog.findViewById(R.id.file_exists_warning_textview); + + // Instantiate the file name helper. + FileNameHelper fileNameHelper = new FileNameHelper(); + + // Get the file path if it isn't null. + if (returnedIntent.getData() != null) { + // Convert the file name URI to a file name path. + String fileNamePath = fileNameHelper.convertUriToFileNamePath(returnedIntent.getData()); + + // Set the file name path as the text of the file name edit text. + fileNameEditText.setText(fileNamePath); + + // Move the cursor to the end of the file name edit text. + fileNameEditText.setSelection(fileNamePath.length()); + + // Hide the file exists warning. + fileExistsWarningTextView.setVisibility(View.GONE); + } + } + } + break; + + case BROWSE_OPEN_REQUEST_CODE: + // Don't do anything if the user pressed back from the file picker. + if (resultCode == Activity.RESULT_OK) { + // Get a handle for the open dialog fragment. + DialogFragment openDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.open)); + + // Only update the file name if the dialog still exists. + if (openDialogFragment != null) { + // Get a handle for the open dialog. + Dialog openDialog = openDialogFragment.getDialog(); + + // Remove the incorrect lint warning below that the dialog might be null. + assert openDialog != null; + + // Get a handle for the file name edit text. + EditText fileNameEditText = openDialog.findViewById(R.id.file_name_edittext); + + // Instantiate the file name helper. + FileNameHelper fileNameHelper = new FileNameHelper(); + + // Get the file path if it isn't null. + if (returnedIntent.getData() != null) { + // Convert the file name URI to a file name path. + String fileNamePath = fileNameHelper.convertUriToFileNamePath(returnedIntent.getData()); + + // Set the file name path as the text of the file name edit text. + fileNameEditText.setText(fileNamePath); + + // Move the cursor to the end of the file name edit text. + fileNameEditText.setSelection(fileNamePath.length()); + } + } + } + break; + } + } + + private void loadUrlFromTextBox() { + // Get a handle for the URL edit text. + EditText urlEditText = findViewById(R.id.url_edittext); + + // Get the text from urlTextBox and convert it to a string. trim() removes white spaces from the beginning and end of the string. + String unformattedUrlString = urlEditText.getText().toString().trim(); + + // Initialize the formatted URL string. + String url = ""; + + // Check to see if `unformattedUrlString` is a valid URL. Otherwise, convert it into a search. + if (unformattedUrlString.startsWith("content://")) { // This is a Content URL. + // Load the entire content URL. + url = unformattedUrlString; + } else if (Patterns.WEB_URL.matcher(unformattedUrlString).matches() || unformattedUrlString.startsWith("http://") || unformattedUrlString.startsWith("https://") || + unformattedUrlString.startsWith("file://")) { // This is a standard URL. + // Add `https://` at the beginning if there is no protocol. Otherwise the app will segfault. + if (!unformattedUrlString.startsWith("http") && !unformattedUrlString.startsWith("file://") && !unformattedUrlString.startsWith("content://")) { + unformattedUrlString = "https://" + unformattedUrlString; + } + + // Initialize `unformattedUrl`. + URL unformattedUrl = null; + + // 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. + try { + unformattedUrl = new URL(unformattedUrlString); + } catch (MalformedURLException e) { + e.printStackTrace(); + } + + // The ternary operator (? :) makes sure that a null pointer exception is not thrown, which would happen if `.get` was called on a `null` value. + String scheme = unformattedUrl != null ? unformattedUrl.getProtocol() : null; + String authority = unformattedUrl != null ? unformattedUrl.getAuthority() : null; + String path = unformattedUrl != null ? unformattedUrl.getPath() : null; + String query = unformattedUrl != null ? unformattedUrl.getQuery() : null; + String fragment = unformattedUrl != null ? unformattedUrl.getRef() : null; + + // Build the URI. + Uri.Builder uri = new Uri.Builder(); + uri.scheme(scheme).authority(authority).path(path).query(query).fragment(fragment); + + // Decode the URI as a UTF-8 string in. + try { + url = URLDecoder.decode(uri.build().toString(), "UTF-8"); + } catch (UnsupportedEncodingException exception) { + // Do nothing. The formatted URL string will remain blank. + } + } else if (!unformattedUrlString.isEmpty()){ // This is not a URL, but rather a search string. + // Create an encoded URL String. + String encodedUrlString; + + // Sanitize the search input. + try { + encodedUrlString = URLEncoder.encode(unformattedUrlString, "UTF-8"); + } catch (UnsupportedEncodingException exception) { + encodedUrlString = ""; + } + + // Add the base search URL. + url = searchURL + encodedUrlString; + } + + // 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. + urlEditText.clearFocus(); + + // Make it so. + loadUrl(currentWebView, url); + } + + private void loadUrl(NestedScrollWebView nestedScrollWebView, String url) { + // Sanitize the URL. + url = sanitizeUrl(url); + + // Apply the domain settings. + applyDomainSettings(nestedScrollWebView, url, true, false); + + // Load the URL. + nestedScrollWebView.loadUrl(url, customHeaders); + } + + public void findPreviousOnPage(View view) { + // Go to the previous highlighted phrase on the page. `false` goes backwards instead of forwards. + currentWebView.findNext(false); + } + + public void findNextOnPage(View view) { + // Go to the next highlighted phrase on the page. `true` goes forwards instead of backwards. + currentWebView.findNext(true); + } + + public void closeFindOnPage(View view) { + // Get a handle for the views. + Toolbar toolbar = findViewById(R.id.toolbar); + LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout); + EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext); + + // Delete the contents of `find_on_page_edittext`. + findOnPageEditText.setText(null); + + // Clear the highlighted phrases if the WebView is not null. + if (currentWebView != null) { + currentWebView.clearMatches(); + } + + // Hide the find on page linear layout. + findOnPageLinearLayout.setVisibility(View.GONE); + + // Show the toolbar. + toolbar.setVisibility(View.VISIBLE); + + // Get a handle for the input method manager. + InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + + // Remove the lint warning below that the input method manager might be null. + assert inputMethodManager != null; + + // Hide the keyboard. + inputMethodManager.hideSoftInputFromWindow(toolbar.getWindowToken(), 0); + } + + @Override + public void onApplyNewFontSize(DialogFragment dialogFragment) { + // Get the dialog. + Dialog dialog = dialogFragment.getDialog(); + + // Remove the incorrect lint warning below tha the dialog might be null. + assert dialog != null; + + // Get a handle for the font size edit text. + EditText fontSizeEditText = dialog.findViewById(R.id.font_size_edittext); + + // Initialize the new font size variable with the current font size. + int newFontSize = currentWebView.getSettings().getTextZoom(); + + // Get the font size from the edit text. + try { + newFontSize = Integer.parseInt(fontSizeEditText.getText().toString()); + } catch (Exception exception) { + // If the edit text does not contain a valid font size do nothing. + } + + // Apply the new font size. + currentWebView.getSettings().setTextZoom(newFontSize); + } + + @Override + public void onOpen(DialogFragment dialogFragment) { + // Get the dialog. + Dialog dialog = dialogFragment.getDialog(); + + // Remove the incorrect lint warning below that the dialog might be null. + assert dialog != null; + + // Get a handle for the file name edit text. + EditText fileNameEditText = dialog.findViewById(R.id.file_name_edittext); + + // Get the file path string. + openFilePath = fileNameEditText.getText().toString(); + + // Check to see if the storage permission is needed. + if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // The storage permission has been granted. + // Open the file. + currentWebView.loadUrl("file://" + openFilePath); + } else { // The storage permission has not been granted. + // Get the external private directory file. + File externalPrivateDirectoryFile = getExternalFilesDir(null); + + // Remove the incorrect lint error below that the file might be null. + assert externalPrivateDirectoryFile != null; + + // Get the external private directory string. + String externalPrivateDirectory = externalPrivateDirectoryFile.toString(); + + // Check to see if the file path is in the external private directory. + if (openFilePath.startsWith(externalPrivateDirectory)) { // the file path is in the external private directory. + // Open the file. + currentWebView.loadUrl("file://" + openFilePath); + } else { // The file path is in a public directory. + // Check if the user has previously denied the storage permission. + if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first. + // Instantiate the storage permission alert dialog. + DialogFragment storagePermissionDialogFragment = StoragePermissionDialog.displayDialog(StoragePermissionDialog.OPEN); + + // Show the storage permission alert dialog. The permission will be requested the the dialog is closed. + storagePermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.storage_permission)); + } else { // Show the permission request directly. + // Request the write external storage permission. The file will be opened when it finishes. + ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_OPEN_REQUEST_CODE); + } + } + } + } + + @Override + public void onSaveWebpage(int saveType, DialogFragment dialogFragment) { + // Get the dialog. + Dialog dialog = dialogFragment.getDialog(); + + // Remove the incorrect lint warning below that the dialog might be null. + assert dialog != null; + + // Get a handle for the edit texts. + EditText urlEditText = dialog.findViewById(R.id.url_edittext); + EditText fileNameEditText = dialog.findViewById(R.id.file_name_edittext); + + // Get the strings from the edit texts. + saveWebpageUrl = urlEditText.getText().toString(); + saveWebpageFilePath = fileNameEditText.getText().toString(); + + // Check to see if the storage permission is needed. + if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // The storage permission has been granted. + //Save the webpage according to the save type. + switch (saveType) { + case StoragePermissionDialog.SAVE_URL: + // Save the URL. + new SaveUrl(this, this, saveWebpageFilePath, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptFirstPartyCookies()).execute(saveWebpageUrl); + break; + + case StoragePermissionDialog.SAVE_AS_ARCHIVE: + // Save the webpage archive. + currentWebView.saveWebArchive(saveWebpageFilePath); + break; + + case StoragePermissionDialog.SAVE_AS_IMAGE: + // Save the webpage image. + new SaveWebpageImage(this, currentWebView).execute(saveWebpageFilePath); + break; + } + } else { // The storage permission has not been granted. + // Get the external private directory file. + File externalPrivateDirectoryFile = getExternalFilesDir(null); + + // Remove the incorrect lint error below that the file might be null. + assert externalPrivateDirectoryFile != null; + + // Get the external private directory string. + String externalPrivateDirectory = externalPrivateDirectoryFile.toString(); + + // Check to see if the file path is in the external private directory. + if (saveWebpageFilePath.startsWith(externalPrivateDirectory)) { // The file path is in the external private directory. + // Save the webpage according to the save type. + switch (saveType) { + case StoragePermissionDialog.SAVE_URL: + // Save the URL. + new SaveUrl(this, this, saveWebpageFilePath, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptFirstPartyCookies()).execute(saveWebpageUrl); + break; + + case StoragePermissionDialog.SAVE_AS_ARCHIVE: + // Save the webpage archive. + currentWebView.saveWebArchive(saveWebpageFilePath); + break; + + case StoragePermissionDialog.SAVE_AS_IMAGE: + // Save the webpage image. + new SaveWebpageImage(this, currentWebView).execute(saveWebpageFilePath); + break; + } + } else { // The file path is in a public directory. + // Check if the user has previously denied the storage permission. + if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first. + // Instantiate the storage permission alert dialog. + DialogFragment storagePermissionDialogFragment = StoragePermissionDialog.displayDialog(saveType); + + // Show the storage permission alert dialog. The permission will be requested when the dialog is closed. + storagePermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.storage_permission)); + } else { // Show the permission request directly. + switch (saveType) { + case StoragePermissionDialog.SAVE_URL: + // Request the write external storage permission. The URL will be saved when it finishes. + ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_URL_REQUEST_CODE); + + case StoragePermissionDialog.SAVE_AS_ARCHIVE: + // Request the write external storage permission. The webpage archive will be saved when it finishes. + ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_AS_ARCHIVE_REQUEST_CODE); + break; + + case StoragePermissionDialog.SAVE_AS_IMAGE: + // Request the write external storage permission. The webpage image will be saved when it finishes. + ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_AS_IMAGE_REQUEST_CODE); + break; + } + } + } + } + } + + @Override + public void onCloseStoragePermissionDialog(int requestType) { + switch (requestType) { + case StoragePermissionDialog.OPEN: + // Request the write external storage permission. The file will be opened when it finishes. + ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_OPEN_REQUEST_CODE); + break; + + case StoragePermissionDialog.SAVE_URL: + // Request the write external storage permission. The URL will be saved when it finishes. + ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_URL_REQUEST_CODE); + break; + + case StoragePermissionDialog.SAVE_AS_ARCHIVE: + // Request the write external storage permission. The webpage archive will be saved when it finishes. + ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_AS_ARCHIVE_REQUEST_CODE); + break; + + case StoragePermissionDialog.SAVE_AS_IMAGE: + // Request the write external storage permission. The webpage image will be saved when it finishes. + ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_AS_IMAGE_REQUEST_CODE); + break; + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + //Only process the results if they exist (this method is triggered when a dialog is presented the first time for an app, but no grant results are included). + if (grantResults.length > 0) { + switch (requestCode) { + case PERMISSION_OPEN_REQUEST_CODE: + // Check to see if the storage permission was granted. If the dialog was canceled the grant results will be empty. + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { // The storage permission was granted. + // Load the file. + currentWebView.loadUrl("file://" + openFilePath); + } else { // The storage permission was not granted. + // Display an error snackbar. + Snackbar.make(currentWebView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show(); + } + + // Reset the open file path. + openFilePath = ""; + break; + + case PERMISSION_SAVE_URL_REQUEST_CODE: + // Check to see if the storage permission was granted. If the dialog was canceled the grant results will be empty. + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { // The storage permission was granted. + // Save the raw URL. + new SaveUrl(this, this, saveWebpageFilePath, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptFirstPartyCookies()).execute(saveWebpageUrl); + } else { // The storage permission was not granted. + // Display an error snackbar. + Snackbar.make(currentWebView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show(); + } + + // Reset the save strings. + saveWebpageUrl = ""; + saveWebpageFilePath = ""; + break; + + case PERMISSION_SAVE_AS_ARCHIVE_REQUEST_CODE: + // Check to see if the storage permission was granted. If the dialog was canceled the grant results will be empty. + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { // The storage permission was granted. + // Save the webpage archive. + currentWebView.saveWebArchive(saveWebpageFilePath); + } else { // The storage permission was not granted. + // Display an error snackbar. + Snackbar.make(currentWebView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show(); + } + + // Reset the save webpage file path. + saveWebpageFilePath = ""; + break; + + case PERMISSION_SAVE_AS_IMAGE_REQUEST_CODE: + // Check to see if the storage permission was granted. If the dialog was canceled the grant results will be empty. + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { // The storage permission was granted. + // Save the webpage image. + new SaveWebpageImage(this, currentWebView).execute(saveWebpageFilePath); + } else { // The storage permission was not granted. + // Display an error snackbar. + Snackbar.make(currentWebView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show(); + } + + // Reset the save webpage file path. + saveWebpageFilePath = ""; + break; + } + } + } + + private void initializeApp() { + // Get a handle for the input method. + InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + + // Remove the lint warning below that the input method manager might be null. + assert inputMethodManager != null; + + // Initialize the gray foreground color spans for highlighting the URLs. The deprecated `getResources()` must be used until API >= 23. + initialGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500)); + finalGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500)); + + // Get the current theme status. + int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; + + // Set the red color span according to the theme. + if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { + redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_a700)); + } else { + redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_900)); + } + + // Get handles for the URL views. + EditText urlEditText = findViewById(R.id.url_edittext); + + // Remove the formatting from the URL edit text when the user is editing the text. + urlEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> { + if (hasFocus) { // The user is editing the URL text box. + // Remove the highlighting. + urlEditText.getText().removeSpan(redColorSpan); + urlEditText.getText().removeSpan(initialGrayColorSpan); + urlEditText.getText().removeSpan(finalGrayColorSpan); + } else { // The user has stopped editing the URL text box. + // Move to the beginning of the string. + urlEditText.setSelection(0); + + // Reapply the highlighting. + highlightUrlText(); + } + }); + + // Set the go button on the keyboard to load the URL in `urlTextBox`. + urlEditText.setOnKeyListener((View v, int keyCode, KeyEvent event) -> { + // If the event is a key-down event on the `enter` button, load the URL. + if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { + // Load the URL into the mainWebView and consume the event. + loadUrlFromTextBox(); + + // If the enter key was pressed, consume the event. + return true; + } else { + // If any other key was pressed, do not consume the event. + return false; + } + }); + + // Create an Orbot status broadcast receiver. + orbotStatusBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + // Store the content of the status message in `orbotStatus`. + orbotStatus = intent.getStringExtra("org.torproject.android.intent.extra.STATUS"); + + // If Privacy Browser is waiting on the proxy, load the website now that Orbot is connected. + if ((orbotStatus != null) && orbotStatus.equals("ON") && waitingForProxy) { + // Reset the waiting for proxy status. + waitingForProxy = false; + + // Get a handle for the waiting for proxy dialog. + DialogFragment waitingForProxyDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.waiting_for_proxy_dialog)); + + // Dismiss the waiting for proxy dialog if it is displayed. + if (waitingForProxyDialogFragment != null) { + waitingForProxyDialogFragment.dismiss(); + } + + // Reload existing URLs and load any URLs that are waiting for the proxy. + for (int i = 0; i < webViewPagerAdapter.getCount(); i++) { + // Get the WebView tab fragment. + WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i); + + // Get the fragment view. + View fragmentView = webViewTabFragment.getView(); + + // Only process the WebViews if they exist. + if (fragmentView != null) { + // Get the nested scroll WebView from the tab fragment. + NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview); + + // Get the waiting for proxy URL string. + String waitingForProxyUrlString = nestedScrollWebView.getWaitingForProxyUrlString(); + + // Load the pending URL if it exists. + if (!waitingForProxyUrlString.isEmpty()) { // A URL is waiting to be loaded. + // Load the URL. + loadUrl(nestedScrollWebView, waitingForProxyUrlString); + + // Reset the waiting for proxy URL string. + nestedScrollWebView.resetWaitingForProxyUrlString(); + } else { // No URL is waiting to be loaded. + // Reload the existing URL. + nestedScrollWebView.reload(); + } + } + } + } + } + }; + + // Register the Orbot status broadcast receiver on `this` context. + this.registerReceiver(orbotStatusBroadcastReceiver, new IntentFilter("org.torproject.android.intent.action.STATUS")); + + // Get handles for views that need to be modified. + DrawerLayout drawerLayout = findViewById(R.id.drawerlayout); + NavigationView navigationView = findViewById(R.id.navigationview); + SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout); + ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview); + FloatingActionButton launchBookmarksActivityFab = findViewById(R.id.launch_bookmarks_activity_fab); + FloatingActionButton createBookmarkFolderFab = findViewById(R.id.create_bookmark_folder_fab); + FloatingActionButton createBookmarkFab = findViewById(R.id.create_bookmark_fab); + EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext); + + // Listen for touches on the navigation menu. + navigationView.setNavigationItemSelectedListener(this); + + // Get handles for the navigation menu and the back and forward menu items. + Menu navigationMenu = navigationView.getMenu(); + MenuItem navigationBackMenuItem = navigationMenu.findItem(R.id.back); + MenuItem navigationForwardMenuItem = navigationMenu.findItem(R.id.forward); + MenuItem navigationHistoryMenuItem = navigationMenu.findItem(R.id.history); + MenuItem navigationRequestsMenuItem = navigationMenu.findItem(R.id.requests); + + // Update the web view pager every time a tab is modified. + webViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + // Do nothing. + } + + @Override + public void onPageSelected(int position) { + // Close the find on page bar if it is open. + closeFindOnPage(null); + + // Set the current WebView. + setCurrentWebView(position); + + // Select the corresponding tab if it does not match the currently selected page. This will happen if the page was scrolled by creating a new tab. + if (tabLayout.getSelectedTabPosition() != position) { + // Create a handler to select the tab. + Handler selectTabHandler = new Handler(); + + // Create a runnable to select the tab. + Runnable selectTabRunnable = () -> { + // Get a handle for the tab. + TabLayout.Tab tab = tabLayout.getTabAt(position); + + // Assert that the tab is not null. + assert tab != null; + + // Select the tab. + tab.select(); + }; + + // Select the tab layout after 150 milliseconds, which leaves enough time for a new tab to be inflated. + selectTabHandler.postDelayed(selectTabRunnable, 150); + } + } + + @Override + public void onPageScrollStateChanged(int state) { + // Do nothing. + } + }); + + // Display the View SSL Certificate dialog when the currently selected tab is reselected. + tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { + @Override + public void onTabSelected(TabLayout.Tab tab) { + // Select the same page in the view pager. + webViewPager.setCurrentItem(tab.getPosition()); + } + + @Override + public void onTabUnselected(TabLayout.Tab tab) { + // Do nothing. + } + + @Override + public void onTabReselected(TabLayout.Tab tab) { + // Instantiate the View SSL Certificate dialog. + DialogFragment viewSslCertificateDialogFragment = ViewSslCertificateDialog.displayDialog(currentWebView.getWebViewFragmentId()); + + // Display the View SSL Certificate dialog. + viewSslCertificateDialogFragment.show(getSupportFragmentManager(), getString(R.string.view_ssl_certificate)); + } + }); + + // Set the launch bookmarks activity FAB to launch the bookmarks activity. + launchBookmarksActivityFab.setOnClickListener(v -> { + // Get a copy of the favorite icon bitmap. + Bitmap favoriteIconBitmap = currentWebView.getFavoriteOrDefaultIcon(); + + // Create a favorite icon byte array output stream. + ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream(); + + // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG). + favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream); + + // Convert the favorite icon byte array stream to a byte array. + byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray(); + + // Create an intent to launch the bookmarks activity. + Intent bookmarksIntent = new Intent(getApplicationContext(), BookmarksActivity.class); + + // Add the extra information to the intent. + bookmarksIntent.putExtra("current_url", currentWebView.getUrl()); + bookmarksIntent.putExtra("current_title", currentWebView.getTitle()); + bookmarksIntent.putExtra("current_folder", currentBookmarksFolder); + bookmarksIntent.putExtra("favorite_icon_byte_array", favoriteIconByteArray); + + // Make it so. + startActivity(bookmarksIntent); + }); + + // Set the create new bookmark folder FAB to display an alert dialog. + createBookmarkFolderFab.setOnClickListener(v -> { + // Create a create bookmark folder dialog. + DialogFragment createBookmarkFolderDialog = CreateBookmarkFolderDialog.createBookmarkFolder(currentWebView.getFavoriteOrDefaultIcon()); + + // Show the create bookmark folder dialog. + createBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.create_folder)); + }); + + // Set the create new bookmark FAB to display an alert dialog. + createBookmarkFab.setOnClickListener(view -> { + // Instantiate the create bookmark dialog. + DialogFragment createBookmarkDialog = CreateBookmarkDialog.createBookmark(currentWebView.getUrl(), currentWebView.getTitle(), currentWebView.getFavoriteOrDefaultIcon()); + + // Display the create bookmark dialog. + createBookmarkDialog.show(getSupportFragmentManager(), getString(R.string.create_bookmark)); + }); + + // Search for the string on the page whenever a character changes in the `findOnPageEditText`. + findOnPageEditText.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // Do nothing. + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + // Do nothing. + } + + @Override + public void afterTextChanged(Editable s) { + // Search for the text in the WebView if it is not null. Sometimes on resume after a period of non-use the WebView will be null. + if (currentWebView != null) { + currentWebView.findAllAsync(findOnPageEditText.getText().toString()); + } + } + }); + + // Set the `check mark` button for the `findOnPageEditText` keyboard to close the soft keyboard. + findOnPageEditText.setOnKeyListener((v, keyCode, event) -> { + if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { // The `enter` key was pressed. + // Hide the soft keyboard. + inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0); + + // Consume the event. + return true; + } else { // A different key was pressed. + // Do not consume the event. + return false; + } + }); + + // Implement swipe to refresh. + swipeRefreshLayout.setOnRefreshListener(() -> currentWebView.reload()); + + // Store the default progress view offsets for use later in `initializeWebView()`. + defaultProgressViewStartOffset = swipeRefreshLayout.getProgressViewStartOffset(); + defaultProgressViewEndOffset = swipeRefreshLayout.getProgressViewEndOffset(); + + // Set the refresh color scheme according to the theme. + if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { + swipeRefreshLayout.setColorSchemeResources(R.color.blue_700); + } else { + swipeRefreshLayout.setColorSchemeResources(R.color.violet_500); + } + + // Initialize a color background typed value. + TypedValue colorBackgroundTypedValue = new TypedValue(); + + // Get the color background from the theme. + getTheme().resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true); + + // Get the color background int from the typed value. + int colorBackgroundInt = colorBackgroundTypedValue.data; + + // Set the swipe refresh background color. + swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt); + + // The drawer titles identify the drawer layouts in accessibility mode. + drawerLayout.setDrawerTitle(GravityCompat.START, getString(R.string.navigation_drawer)); + drawerLayout.setDrawerTitle(GravityCompat.END, getString(R.string.bookmarks)); + + // Initialize the bookmarks database helper. The `0` specifies a database version, but that is ignored and set instead using a constant in `BookmarksDatabaseHelper`. + bookmarksDatabaseHelper = new BookmarksDatabaseHelper(this, null, null, 0); + + // Initialize `currentBookmarksFolder`. `""` is the home folder in the database. + currentBookmarksFolder = ""; + + // Load the home folder, which is `""` in the database. + loadBookmarksFolder(); + + bookmarksListView.setOnItemClickListener((parent, view, position, id) -> { + // Convert the id from long to int to match the format of the bookmarks database. + int databaseId = (int) id; + + // Get the bookmark cursor for this ID. + Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseId); + + // Move the bookmark cursor to the first row. + bookmarkCursor.moveToFirst(); + + // Act upon the bookmark according to the type. + if (bookmarkCursor.getInt(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) { // The selected bookmark is a folder. + // Store the new folder name in `currentBookmarksFolder`. + currentBookmarksFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME)); + + // Load the new folder. + loadBookmarksFolder(); + } else { // The selected bookmark is not a folder. + // Load the bookmark URL. + loadUrl(currentWebView, bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_URL))); + + // Close the bookmarks drawer. + drawerLayout.closeDrawer(GravityCompat.END); + } + + // Close the `Cursor`. + bookmarkCursor.close(); + }); + + bookmarksListView.setOnItemLongClickListener((parent, view, position, id) -> { + // Convert the database ID from `long` to `int`. + int databaseId = (int) id; + + // Find out if the selected bookmark is a folder. + boolean isFolder = bookmarksDatabaseHelper.isFolder(databaseId); + + if (isFolder) { + // Save the current folder name, which is used in `onSaveEditBookmarkFolder()`. + oldFolderNameString = bookmarksCursor.getString(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME)); + + // Instantiate the edit folder bookmark dialog. + DialogFragment editBookmarkFolderDialog = EditBookmarkFolderDialog.folderDatabaseId(databaseId, currentWebView.getFavoriteOrDefaultIcon()); + + // Show the edit folder bookmark dialog. + editBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.edit_folder)); + } else { + // Get the bookmark cursor for this ID. + Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseId); + + // Move the bookmark cursor to the first row. + bookmarkCursor.moveToFirst(); + + // Load the bookmark in a new tab but do not switch to the tab or close the drawer. + addNewTab(bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_URL)), false); + } + + // Consume the event. + return true; + }); + + // The drawer listener is used to update the navigation menu. + drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() { + @Override + public void onDrawerSlide(@NonNull View drawerView, float slideOffset) { + } + + @Override + public void onDrawerOpened(@NonNull View drawerView) { + } + + @Override + public void onDrawerClosed(@NonNull View drawerView) { + } + + @Override + public void onDrawerStateChanged(int newState) { + if ((newState == DrawerLayout.STATE_SETTLING) || (newState == DrawerLayout.STATE_DRAGGING)) { // A drawer is opening or closing. + // Update the navigation menu items if the WebView is not null. + if (currentWebView != null) { + navigationBackMenuItem.setEnabled(currentWebView.canGoBack()); + navigationForwardMenuItem.setEnabled(currentWebView.canGoForward()); + navigationHistoryMenuItem.setEnabled((currentWebView.canGoBack() || currentWebView.canGoForward())); + navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); + + // Hide the keyboard (if displayed). + inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0); + } + + // Clear the focus from from the URL text box and the WebView. This removes any text selection markers and context menus, which otherwise draw above the open drawers. + urlEditText.clearFocus(); + currentWebView.clearFocus(); + } + } + }); + + // Replace the header that `WebView` creates for `X-Requested-With` with a null value. The default value is the application ID (com.stoutner.privacybrowser.standard). + customHeaders.put("X-Requested-With", ""); + + // Inflate a bare WebView to get the default user agent. It is not used to render content on the screen. + @SuppressLint("InflateParams") View webViewLayout = getLayoutInflater().inflate(R.layout.bare_webview, null, false); + + // Get a handle for the WebView. + WebView bareWebView = webViewLayout.findViewById(R.id.bare_webview); + + // Store the default user agent. + webViewDefaultUserAgent = bareWebView.getSettings().getUserAgentString(); + + // Destroy the bare WebView. + bareWebView.destroy(); + } + + private void applyAppSettings() { + // Get a handle for the shared preferences. + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + + // Store the values from the shared preferences in variables. + incognitoModeEnabled = sharedPreferences.getBoolean("incognito_mode", false); + boolean doNotTrackEnabled = sharedPreferences.getBoolean("do_not_track", false); + sanitizeGoogleAnalytics = sharedPreferences.getBoolean("google_analytics", true); + sanitizeFacebookClickIds = sharedPreferences.getBoolean("facebook_click_ids", true); + sanitizeTwitterAmpRedirects = sharedPreferences.getBoolean("twitter_amp_redirects", true); + proxyMode = sharedPreferences.getString("proxy", getString(R.string.proxy_default_value)); + fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean("full_screen_browsing_mode", false); + hideAppBar = sharedPreferences.getBoolean("hide_app_bar", true); + scrollAppBar = sharedPreferences.getBoolean("scroll_app_bar", true); + + // Apply the saved proxy mode if the app has been restarted. + if (savedProxyMode != null) { + // Apply the saved proxy mode. + proxyMode = savedProxyMode; + + // Reset the saved proxy mode. + savedProxyMode = null; + } + + // Get the search string. + String searchString = sharedPreferences.getString("search", getString(R.string.search_default_value)); + + // Set the search string. + if (searchString.equals("Custom URL")) { // A custom search string is used. + searchURL = sharedPreferences.getString("search_custom_url", getString(R.string.search_custom_url_default_value)); + } else { // A custom search string is not used. + searchURL = searchString; + } + + // Get a handle for the app compat delegate. + AppCompatDelegate appCompatDelegate = getDelegate(); + + // Get handles for the views that need to be modified. + FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout); + ActionBar actionBar = appCompatDelegate.getSupportActionBar(); + Toolbar toolbar = findViewById(R.id.toolbar); + LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout); + LinearLayout tabsLinearLayout = findViewById(R.id.tabs_linearlayout); + SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout); + + // Remove the incorrect lint warning below that the action bar might be null. + assert actionBar != null; + + // Apply the proxy. + applyProxy(false); + + // Set Do Not Track status. + if (doNotTrackEnabled) { + customHeaders.put("DNT", "1"); + } else { + customHeaders.remove("DNT"); + } + + // Get the current layout parameters. Using coordinator layout parameters allows the `setBehavior()` command and using app bar layout parameters allows the `setScrollFlags()` command. + CoordinatorLayout.LayoutParams swipeRefreshLayoutParams = (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams(); + AppBarLayout.LayoutParams toolbarLayoutParams = (AppBarLayout.LayoutParams) toolbar.getLayoutParams(); + AppBarLayout.LayoutParams findOnPageLayoutParams = (AppBarLayout.LayoutParams) findOnPageLinearLayout.getLayoutParams(); + AppBarLayout.LayoutParams tabsLayoutParams = (AppBarLayout.LayoutParams) tabsLinearLayout.getLayoutParams(); + + // Add the scrolling behavior to the layout parameters. + if (scrollAppBar) { + // Enable scrolling of the app bar. + swipeRefreshLayoutParams.setBehavior(new AppBarLayout.ScrollingViewBehavior()); + toolbarLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP); + findOnPageLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP); + tabsLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP); + } else { + // Disable scrolling of the app bar. + swipeRefreshLayoutParams.setBehavior(null); + toolbarLayoutParams.setScrollFlags(0); + findOnPageLayoutParams.setScrollFlags(0); + tabsLayoutParams.setScrollFlags(0); + + // Expand the app bar if it is currently collapsed. + appBarLayout.setExpanded(true); + } + + // Apply the modified layout parameters. + swipeRefreshLayout.setLayoutParams(swipeRefreshLayoutParams); + toolbar.setLayoutParams(toolbarLayoutParams); + findOnPageLinearLayout.setLayoutParams(findOnPageLayoutParams); + tabsLinearLayout.setLayoutParams(tabsLayoutParams); + + // Set the app bar scrolling for each WebView. + for (int i = 0; i < webViewPagerAdapter.getCount(); i++) { + // Get the WebView tab fragment. + WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i); + + // Get the fragment view. + View fragmentView = webViewTabFragment.getView(); + + // Only modify the WebViews if they exist. + if (fragmentView != null) { + // Get the nested scroll WebView from the tab fragment. + NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview); + + // Set the app bar scrolling. + nestedScrollWebView.setNestedScrollingEnabled(scrollAppBar); + } + } + + // Update the full screen browsing mode settings. + if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode. + // Update the visibility of the app bar, which might have changed in the settings. + if (hideAppBar) { + // Hide the tab linear layout. + tabsLinearLayout.setVisibility(View.GONE); + + // Hide the action bar. + actionBar.hide(); + } else { + // Show the tab linear layout. + tabsLinearLayout.setVisibility(View.VISIBLE); + + // Show the action bar. + actionBar.show(); + } + + // Hide the banner ad in the free flavor. + if (BuildConfig.FLAVOR.contentEquals("free")) { + AdHelper.hideAd(findViewById(R.id.adview)); + } + + /* Hide the system bars. + * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen. + * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar. + * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen. + * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown. + */ + rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + } else { // Privacy Browser is not in full screen browsing mode. + // Reset the full screen tracker, which could be true if Privacy Browser was in full screen mode before entering settings and full screen browsing was disabled. + inFullScreenBrowsingMode = false; + + // Show the tab linear layout. + tabsLinearLayout.setVisibility(View.VISIBLE); + + // Show the action bar. + actionBar.show(); + + // Show the banner ad in the free flavor. + if (BuildConfig.FLAVOR.contentEquals("free")) { + // Initialize the ads. If this isn't the first run, `loadAd()` will be automatically called instead. + AdHelper.initializeAds(findViewById(R.id.adview), getApplicationContext(), getSupportFragmentManager(), getString(R.string.google_app_id), getString(R.string.ad_unit_id)); + } + + // Remove the `SYSTEM_UI` flags from the root frame layout. + rootFrameLayout.setSystemUiVisibility(0); + } + } + + @Override + public void navigateHistory(String url, int steps) { + // Apply the domain settings. + applyDomainSettings(currentWebView, url, false, false); + + // Load the history entry. + currentWebView.goBackOrForward(steps); + } + + @Override + public void pinnedErrorGoBack() { + // Get the current web back forward list. + WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList(); + + // Get the previous entry URL. + String previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.getCurrentIndex() - 1).getUrl(); + + // Apply the domain settings. + applyDomainSettings(currentWebView, previousUrl, false, false); + + // Go back. + currentWebView.goBack(); + } + + // `reloadWebsite` is used if returning from the Domains activity. Otherwise JavaScript might not function correctly if it is newly enabled. + @SuppressLint("SetJavaScriptEnabled") + private void applyDomainSettings(NestedScrollWebView nestedScrollWebView, String url, boolean resetTab, boolean reloadWebsite) { + // Store the current URL. + nestedScrollWebView.setCurrentUrl(url); + + // Parse the URL into a URI. + Uri uri = Uri.parse(url); + + // Extract the domain from `uri`. + String newHostName = uri.getHost(); + + // Strings don't like to be null. + if (newHostName == null) { + newHostName = ""; + } + + // Apply the domain settings if a new domain is being loaded or if the new domain is blank. This allows the user to set temporary settings for JavaScript, cookies, DOM storage, etc. + if (!nestedScrollWebView.getCurrentDomainName().equals(newHostName) || newHostName.equals("")) { + // Set the new host name as the current domain name. + nestedScrollWebView.setCurrentDomainName(newHostName); + + // Reset the ignoring of pinned domain information. + nestedScrollWebView.setIgnorePinnedDomainInformation(false); + + // Clear any pinned SSL certificate or IP addresses. + nestedScrollWebView.clearPinnedSslCertificate(); + nestedScrollWebView.clearPinnedIpAddresses(); + + // Reset the favorite icon if specified. + if (resetTab) { + // Initialize the favorite icon. + nestedScrollWebView.initializeFavoriteIcon(); + + // Get the current page position. + int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId()); + + // Get the corresponding tab. + TabLayout.Tab tab = tabLayout.getTabAt(currentPagePosition); + + // Update the tab if it isn't null, which sometimes happens when restarting from the background. + if (tab != null) { + // Get the tab custom view. + View tabCustomView = tab.getCustomView(); + + // Remove the warning below that the tab custom view might be null. + assert tabCustomView != null; + + // Get the tab views. + ImageView tabFavoriteIconImageView = tabCustomView.findViewById(R.id.favorite_icon_imageview); + TextView tabTitleTextView = tabCustomView.findViewById(R.id.title_textview); + + // Set the default favorite icon as the favorite icon for this tab. + tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(nestedScrollWebView.getFavoriteOrDefaultIcon(), 64, 64, true)); + + // Set the loading title text. + tabTitleTextView.setText(R.string.loading); + } + } + + // Initialize the database handler. The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`. + DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0); + + // Get a full cursor from `domainsDatabaseHelper`. + Cursor domainNameCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomain(); + + // Initialize `domainSettingsSet`. + Set domainSettingsSet = new HashSet<>(); + + // Get the domain name column index. + int domainNameColumnIndex = domainNameCursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME); + + // Populate `domainSettingsSet`. + for (int i = 0; i < domainNameCursor.getCount(); i++) { + // Move `domainsCursor` to the current row. + domainNameCursor.moveToPosition(i); + + // Store the domain name in `domainSettingsSet`. + domainSettingsSet.add(domainNameCursor.getString(domainNameColumnIndex)); + } + + // Close `domainNameCursor. + domainNameCursor.close(); + + // Initialize the domain name in database variable. + String domainNameInDatabase = null; + + // Check the hostname against the domain settings set. + if (domainSettingsSet.contains(newHostName)) { // The hostname is contained in the domain settings set. + // Record the domain name in the database. + domainNameInDatabase = newHostName; + + // Set the domain settings applied tracker to true. + nestedScrollWebView.setDomainSettingsApplied(true); + } else { // The hostname is not contained in the domain settings set. + // Set the domain settings applied tracker to false. + nestedScrollWebView.setDomainSettingsApplied(false); + } + + // Check all the subdomains of the host name against wildcard domains in the domain cursor. + while (!nestedScrollWebView.getDomainSettingsApplied() && newHostName.contains(".")) { // Stop checking if domain settings are already applied or there are no more `.` in the host name. + if (domainSettingsSet.contains("*." + newHostName)) { // Check the host name prepended by `*.`. + // Set the domain settings applied tracker to true. + nestedScrollWebView.setDomainSettingsApplied(true); + + // Store the applied domain names as it appears in the database. + domainNameInDatabase = "*." + newHostName; + } + + // Strip out the lowest subdomain of of the host name. + newHostName = newHostName.substring(newHostName.indexOf(".") + 1); + } + + + // Get a handle for the shared preferences. + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + + // Store the general preference information. + String defaultFontSizeString = sharedPreferences.getString("font_size", getString(R.string.font_size_default_value)); + String defaultUserAgentName = sharedPreferences.getString("user_agent", getString(R.string.user_agent_default_value)); + boolean defaultSwipeToRefresh = sharedPreferences.getBoolean("swipe_to_refresh", true); + boolean wideViewport = sharedPreferences.getBoolean("wide_viewport", true); + boolean displayWebpageImages = sharedPreferences.getBoolean("display_webpage_images", true); + + // Get a handle for the cookie manager. + CookieManager cookieManager = CookieManager.getInstance(); + + // Get handles for the views. + RelativeLayout urlRelativeLayout = findViewById(R.id.url_relativelayout); + SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout); + + // Initialize the user agent array adapter and string array. + ArrayAdapter userAgentNamesArray = ArrayAdapter.createFromResource(this, R.array.user_agent_names, R.layout.spinner_item); + String[] userAgentDataArray = getResources().getStringArray(R.array.user_agent_data); + + if (nestedScrollWebView.getDomainSettingsApplied()) { // The url has custom domain settings. + // Get a cursor for the current host and move it to the first position. + Cursor currentDomainSettingsCursor = domainsDatabaseHelper.getCursorForDomainName(domainNameInDatabase); + currentDomainSettingsCursor.moveToFirst(); + + // Get the settings from the cursor. + nestedScrollWebView.setDomainSettingsDatabaseId(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper._ID))); + nestedScrollWebView.getSettings().setJavaScriptEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)) == 1); + nestedScrollWebView.setAcceptFirstPartyCookies(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FIRST_PARTY_COOKIES)) == 1); + boolean domainThirdPartyCookiesEnabled = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_THIRD_PARTY_COOKIES)) == 1); + nestedScrollWebView.getSettings().setDomStorageEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_DOM_STORAGE)) == 1); + // Form data can be removed once the minimum API >= 26. + boolean saveFormData = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FORM_DATA)) == 1); + nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASYLIST, + currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYLIST)) == 1); + nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASYPRIVACY, + currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYPRIVACY)) == 1); + nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST, + currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST)) == 1); + nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST, + currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)) == 1); + nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRALIST, currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ULTRALIST)) == 1); + nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRAPRIVACY, + currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY)) == 1); + nestedScrollWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS, + currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS)) == 1); + String userAgentName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.USER_AGENT)); + int fontSize = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.FONT_SIZE)); + int swipeToRefreshInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SWIPE_TO_REFRESH)); + int webViewThemeInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.WEBVIEW_THEME)); + int wideViewportInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.WIDE_VIEWPORT)); + int displayWebpageImagesInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.DISPLAY_IMAGES)); + boolean pinnedSslCertificate = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)) == 1); + String pinnedSslIssuedToCName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME)); + String pinnedSslIssuedToOName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION)); + String pinnedSslIssuedToUName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT)); + String pinnedSslIssuedByCName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME)); + String pinnedSslIssuedByOName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION)); + String pinnedSslIssuedByUName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT)); + boolean pinnedIpAddresses = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_IP_ADDRESSES)) == 1); + String pinnedHostIpAddresses = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.IP_ADDRESSES)); + + // Get the pinned SSL date longs. + long pinnedSslStartDateLong = currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)); + long pinnedSslEndDateLong = currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)); + + // Define the pinned SSL date variables. + Date pinnedSslStartDate; + Date pinnedSslEndDate; + + // Set the pinned SSL certificate start date to `null` if the saved date long is 0 because creating a new date results in an error if the input is 0. + if (pinnedSslStartDateLong == 0) { + pinnedSslStartDate = null; + } else { + pinnedSslStartDate = new Date(pinnedSslStartDateLong); + } + + // Set the pinned SSL certificate end date to `null` if the saved date long is 0 because creating a new date results in an error if the input is 0. + if (pinnedSslEndDateLong == 0) { + pinnedSslEndDate = null; + } else { + pinnedSslEndDate = new Date(pinnedSslEndDateLong); + } + + // Close the current host domain settings cursor. + currentDomainSettingsCursor.close(); + + // If there is a pinned SSL certificate, store it in the WebView. + if (pinnedSslCertificate) { + nestedScrollWebView.setPinnedSslCertificate(pinnedSslIssuedToCName, pinnedSslIssuedToOName, pinnedSslIssuedToUName, pinnedSslIssuedByCName, pinnedSslIssuedByOName, pinnedSslIssuedByUName, + pinnedSslStartDate, pinnedSslEndDate); + } + + // If there is a pinned IP address, store it in the WebView. + if (pinnedIpAddresses) { + nestedScrollWebView.setPinnedIpAddresses(pinnedHostIpAddresses); + } + + // Apply the cookie domain settings. + cookieManager.setAcceptCookie(nestedScrollWebView.getAcceptFirstPartyCookies()); + + // Set third-party cookies status if API >= 21. + if (Build.VERSION.SDK_INT >= 21) { + cookieManager.setAcceptThirdPartyCookies(nestedScrollWebView, domainThirdPartyCookiesEnabled); + } + + // Apply the form data setting if the API < 26. + if (Build.VERSION.SDK_INT < 26) { + nestedScrollWebView.getSettings().setSaveFormData(saveFormData); + } + + // Apply the font size. + try { // Try the specified font size to see if it is valid. + if (fontSize == 0) { // Apply the default font size. + // Try to set the font size from the value in the app settings. + nestedScrollWebView.getSettings().setTextZoom(Integer.parseInt(defaultFontSizeString)); + } else { // Apply the font size from domain settings. + nestedScrollWebView.getSettings().setTextZoom(fontSize); + } + } catch (Exception exception) { // The specified font size is invalid + // Set the font size to be 100% + nestedScrollWebView.getSettings().setTextZoom(100); + } + + // Set the user agent. + if (userAgentName.equals(getString(R.string.system_default_user_agent))) { // Use the system default user agent. + // Get the array position of the default user agent name. + int defaultUserAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName); + + // Set the user agent according to the system default. + switch (defaultUserAgentArrayPosition) { + case UNRECOGNIZED_USER_AGENT: // The default user agent name is not on the canonical list. + // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names. + nestedScrollWebView.getSettings().setUserAgentString(defaultUserAgentName); + break; + + case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT: + // Set the user agent to `""`, which uses the default value. + nestedScrollWebView.getSettings().setUserAgentString(""); + break; + + case SETTINGS_CUSTOM_USER_AGENT: + // Set the default custom user agent. + nestedScrollWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value))); + break; + + default: + // Get the user agent string from the user agent data array + nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[defaultUserAgentArrayPosition]); + } + } else { // Set the user agent according to the stored name. + // Get the array position of the user agent name. + int userAgentArrayPosition = userAgentNamesArray.getPosition(userAgentName); + + switch (userAgentArrayPosition) { + case UNRECOGNIZED_USER_AGENT: // The user agent name contains a custom user agent. + nestedScrollWebView.getSettings().setUserAgentString(userAgentName); + break; + + case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT: + // Set the user agent to `""`, which uses the default value. + nestedScrollWebView.getSettings().setUserAgentString(""); + break; + + default: + // Get the user agent string from the user agent data array. + nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]); + } + } + + // Set swipe to refresh. + switch (swipeToRefreshInt) { + case DomainsDatabaseHelper.SYSTEM_DEFAULT: + // Store the swipe to refresh status in the nested scroll WebView. + nestedScrollWebView.setSwipeToRefresh(defaultSwipeToRefresh); + + // Update the swipe refresh layout. + if (defaultSwipeToRefresh) { // Swipe to refresh is enabled. + // Only enable the swipe refresh layout if the WebView is scrolled to the top. It is updated every time the scroll changes. + swipeRefreshLayout.setEnabled(currentWebView.getY() == 0); + } else { // Swipe to refresh is disabled. + // Disable the swipe refresh layout. + swipeRefreshLayout.setEnabled(false); + } + break; + + case DomainsDatabaseHelper.ENABLED: + // Store the swipe to refresh status in the nested scroll WebView. + nestedScrollWebView.setSwipeToRefresh(true); + + // Only enable the swipe refresh layout if the WebView is scrolled to the top. It is updated every time the scroll changes. + swipeRefreshLayout.setEnabled(currentWebView.getY() == 0); + break; + + case DomainsDatabaseHelper.DISABLED: + // Store the swipe to refresh status in the nested scroll WebView. + nestedScrollWebView.setSwipeToRefresh(false); + + // Disable swipe to refresh. + swipeRefreshLayout.setEnabled(false); + } + + // Check to see if WebView themes are supported. + if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) { + // Set the WebView theme. + switch (webViewThemeInt) { + case DomainsDatabaseHelper.SYSTEM_DEFAULT: + // // Ge the current system theme status. + int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; + + // Set the WebView theme according to the current system theme status. + if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { // The system is in day mode. + // Turn off the WebView dark mode. + WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF); + } else { // The system is in night mode. + // Turn on the WebView dark mode. + WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON); + } + break; + + case DomainsDatabaseHelper.LIGHT_THEME: + // Turn off the WebView dark mode. + WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF); + break; + + case DomainsDatabaseHelper.DARK_THEME: + // Turn on the WebView dark mode. + WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON); + break; + } + } + + // Set the viewport. + switch (wideViewportInt) { + case DomainsDatabaseHelper.SYSTEM_DEFAULT: + nestedScrollWebView.getSettings().setUseWideViewPort(wideViewport); + break; + + case DomainsDatabaseHelper.ENABLED: + nestedScrollWebView.getSettings().setUseWideViewPort(true); + break; + + case DomainsDatabaseHelper.DISABLED: + nestedScrollWebView.getSettings().setUseWideViewPort(false); + break; + } + + // Set the loading of webpage images. + switch (displayWebpageImagesInt) { + case DomainsDatabaseHelper.SYSTEM_DEFAULT: + nestedScrollWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages); + break; + + case DomainsDatabaseHelper.ENABLED: + nestedScrollWebView.getSettings().setLoadsImagesAutomatically(true); + break; + + case DomainsDatabaseHelper.DISABLED: + nestedScrollWebView.getSettings().setLoadsImagesAutomatically(false); + break; + } + + // Get the current theme status. + int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; + + // Set a background on the URL relative layout to indicate that custom domain settings are being used. + if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { + urlRelativeLayout.setBackground(ResourcesCompat.getDrawable(getResources(), R.drawable.url_bar_background_light_green, null)); + } else { + urlRelativeLayout.setBackground(ResourcesCompat.getDrawable(getResources(), R.drawable.url_bar_background_dark_blue, null)); + } + } else { // The new URL does not have custom domain settings. Load the defaults. + // Store the values from the shared preferences. + nestedScrollWebView.getSettings().setJavaScriptEnabled(sharedPreferences.getBoolean("javascript", false)); + nestedScrollWebView.setAcceptFirstPartyCookies(sharedPreferences.getBoolean("first_party_cookies", false)); + boolean defaultThirdPartyCookiesEnabled = sharedPreferences.getBoolean("third_party_cookies", false); + nestedScrollWebView.getSettings().setDomStorageEnabled(sharedPreferences.getBoolean("dom_storage", false)); + boolean saveFormData = sharedPreferences.getBoolean("save_form_data", false); // Form data can be removed once the minimum API >= 26. + nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASYLIST, sharedPreferences.getBoolean("easylist", true)); + nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASYPRIVACY, sharedPreferences.getBoolean("easyprivacy", true)); + nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST, sharedPreferences.getBoolean("fanboys_annoyance_list", true)); + nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST, sharedPreferences.getBoolean("fanboys_social_blocking_list", true)); + nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRALIST, sharedPreferences.getBoolean("ultralist", true)); + nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRAPRIVACY, sharedPreferences.getBoolean("ultraprivacy", true)); + nestedScrollWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS, sharedPreferences.getBoolean("block_all_third_party_requests", false)); + String webViewTheme = sharedPreferences.getString("webview_theme", getString(R.string.webview_theme_default_value)); + + // Apply the default first-party cookie setting. + cookieManager.setAcceptCookie(nestedScrollWebView.getAcceptFirstPartyCookies()); + + // Apply the default font size setting. + try { + // Try to set the font size from the value in the app settings. + nestedScrollWebView.getSettings().setTextZoom(Integer.parseInt(defaultFontSizeString)); + } catch (Exception exception) { + // If the app settings value is invalid, set the font size to 100%. + nestedScrollWebView.getSettings().setTextZoom(100); + } + + // Apply the form data setting if the API < 26. + if (Build.VERSION.SDK_INT < 26) { + nestedScrollWebView.getSettings().setSaveFormData(saveFormData); + } + + // Store the swipe to refresh status in the nested scroll WebView. + nestedScrollWebView.setSwipeToRefresh(defaultSwipeToRefresh); + + // Update the swipe refresh layout. + if (defaultSwipeToRefresh) { // Swipe to refresh is enabled. + // Only enable the swipe refresh layout if the WebView is scrolled to the top. It is updated every time the scroll changes. + swipeRefreshLayout.setEnabled(currentWebView.getY() == 0); + } else { // Swipe to refresh is disabled. + // Disable the swipe refresh layout. + swipeRefreshLayout.setEnabled(false); + } + + // Reset the pinned variables. + nestedScrollWebView.setDomainSettingsDatabaseId(-1); + + // Set third-party cookies status if API >= 21. + if (Build.VERSION.SDK_INT >= 21) { + cookieManager.setAcceptThirdPartyCookies(nestedScrollWebView, defaultThirdPartyCookiesEnabled); + } + + // Get the array position of the user agent name. + int userAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName); + + // Set the user agent. + switch (userAgentArrayPosition) { + case UNRECOGNIZED_USER_AGENT: // The default user agent name is not on the canonical list. + // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names. + nestedScrollWebView.getSettings().setUserAgentString(defaultUserAgentName); + break; + + case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT: + // Set the user agent to `""`, which uses the default value. + nestedScrollWebView.getSettings().setUserAgentString(""); + break; + + case SETTINGS_CUSTOM_USER_AGENT: + // Set the default custom user agent. + nestedScrollWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value))); + break; + + default: + // Get the user agent string from the user agent data array + nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]); + } + + // Get the WebView theme entry values string array. + String[] webViewThemeEntryValuesStringArray = getResources().getStringArray(R.array.webview_theme_entry_values); + + // Apply the WebView theme if supported by the installed WebView. + if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) { + // Set the WebView theme. A switch statement cannot be used because the WebView theme entry values string array is not a compile time constant. + if (webViewTheme.equals(webViewThemeEntryValuesStringArray[1])) { // The light theme is selected. + // Turn off the WebView dark mode. + WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF); + } else if (webViewTheme.equals(webViewThemeEntryValuesStringArray[2])) { // The dark theme is selected. + // Turn on the WebView dark mode. + WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON); + } else { // The system default theme is selected. + // Get the current system theme status. + int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; + + // Set the WebView theme according to the current system theme status. + if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { // The system is in day mode. + // Turn off the WebView dark mode. + WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF); + } else { // The system is in night mode. + // Turn on the WebView dark mode. + WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON); + } + } + } + + // Set the viewport. + nestedScrollWebView.getSettings().setUseWideViewPort(wideViewport); + + // Set the loading of webpage images. + nestedScrollWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages); + + // Set a transparent background on URL edit text. + urlRelativeLayout.setBackground(ResourcesCompat.getDrawable(getResources(), R.color.transparent, null)); + } + + // Close the domains database helper. + domainsDatabaseHelper.close(); + + // Update the privacy icons. + updatePrivacyIcons(true); + } + + // Reload the website if returning from the Domains activity. + if (reloadWebsite) { + nestedScrollWebView.reload(); + } + } + + private void applyProxy(boolean reloadWebViews) { + // Set the proxy according to the mode. `this` refers to the current activity where an alert dialog might be displayed. + ProxyHelper.setProxy(getApplicationContext(), appBarLayout, proxyMode); + + // Reset the waiting for proxy tracker. + waitingForProxy = false; + + // Get the current theme status. + int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; + + // Update the user interface and reload the WebViews if requested. + switch (proxyMode) { + case ProxyHelper.NONE: + // Initialize a color background typed value. + TypedValue colorBackgroundTypedValue = new TypedValue(); + + // Get the color background from the theme. + getTheme().resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true); + + // Get the color background int from the typed value. + int colorBackgroundInt = colorBackgroundTypedValue.data; + + // Set the default app bar layout background. + appBarLayout.setBackgroundColor(colorBackgroundInt); + break; + + case ProxyHelper.TOR: + // Set the app bar background to indicate proxying through Orbot is enabled. + if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { + appBarLayout.setBackgroundResource(R.color.blue_50); + } else { + appBarLayout.setBackgroundResource(R.color.dark_blue_30); + } + + // Check to see if Orbot is installed. + try { + // Get the package manager. + PackageManager packageManager = getPackageManager(); + + // Check to see if Orbot is in the list. This will throw an error and drop to the catch section if it isn't installed. + packageManager.getPackageInfo("org.torproject.android", 0); + + // Check to see if the proxy is ready. + if (!orbotStatus.equals("ON")) { // Orbot is not ready. + // Set the waiting for proxy status. + waitingForProxy = true; + + // Show the waiting for proxy dialog if it isn't already displayed. + if (getSupportFragmentManager().findFragmentByTag(getString(R.string.waiting_for_proxy_dialog)) == null) { + // Get a handle for the waiting for proxy alert dialog. + DialogFragment waitingForProxyDialogFragment = new WaitingForProxyDialog(); + + // Display the waiting for proxy alert dialog. + waitingForProxyDialogFragment.show(getSupportFragmentManager(), getString(R.string.waiting_for_proxy_dialog)); + } + } + } catch (PackageManager.NameNotFoundException exception) { // Orbot is not installed. + // Show the Orbot not installed dialog if it is not already displayed. + if (getSupportFragmentManager().findFragmentByTag(getString(R.string.proxy_not_installed_dialog)) == null) { + // Get a handle for the Orbot not installed alert dialog. + DialogFragment orbotNotInstalledDialogFragment = ProxyNotInstalledDialog.displayDialog(proxyMode); + + // Display the Orbot not installed alert dialog. + orbotNotInstalledDialogFragment.show(getSupportFragmentManager(), getString(R.string.proxy_not_installed_dialog)); + } + } + break; + + case ProxyHelper.I2P: + // Set the app bar background to indicate proxying through Orbot is enabled. + if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { + appBarLayout.setBackgroundResource(R.color.blue_50); + } else { + appBarLayout.setBackgroundResource(R.color.dark_blue_30); + } + + // Check to see if I2P is installed. + try { + // Get the package manager. + PackageManager packageManager = getPackageManager(); + + // Check to see if I2P is in the list. This will throw an error and drop to the catch section if it isn't installed. + packageManager.getPackageInfo("org.torproject.android", 0); + } catch (PackageManager.NameNotFoundException exception) { // I2P is not installed. + // Sow the I2P not installed dialog if it is not already displayed. + if (getSupportFragmentManager().findFragmentByTag(getString(R.string.proxy_not_installed_dialog)) == null) { + // Get a handle for the waiting for proxy alert dialog. + DialogFragment i2pNotInstalledDialogFragment = ProxyNotInstalledDialog.displayDialog(proxyMode); + + // Display the I2P not installed alert dialog. + i2pNotInstalledDialogFragment.show(getSupportFragmentManager(), getString(R.string.proxy_not_installed_dialog)); + } + } + break; + + case ProxyHelper.CUSTOM: + // Set the app bar background to indicate proxying through Orbot is enabled. + if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { + appBarLayout.setBackgroundResource(R.color.blue_50); + } else { + appBarLayout.setBackgroundResource(R.color.dark_blue_30); + } + break; + } + + // Reload the WebViews if requested and not waiting for the proxy. + if (reloadWebViews && !waitingForProxy) { + // Reload the WebViews. + for (int i = 0; i < webViewPagerAdapter.getCount(); i++) { + // Get the WebView tab fragment. + WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i); + + // Get the fragment view. + View fragmentView = webViewTabFragment.getView(); + + // Only reload the WebViews if they exist. + if (fragmentView != null) { + // Get the nested scroll WebView from the tab fragment. + NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview); + + // Reload the WebView. + nestedScrollWebView.reload(); + } + } + } + } + + private void updatePrivacyIcons(boolean runInvalidateOptionsMenu) { + // Only update the privacy icons if the options menu and the current WebView have already been populated. + if ((optionsMenu != null) && (currentWebView != null)) { + // Get handles for the menu items. + MenuItem privacyMenuItem = optionsMenu.findItem(R.id.toggle_javascript); + MenuItem firstPartyCookiesMenuItem = optionsMenu.findItem(R.id.toggle_first_party_cookies); + MenuItem domStorageMenuItem = optionsMenu.findItem(R.id.toggle_dom_storage); + MenuItem refreshMenuItem = optionsMenu.findItem(R.id.refresh); + + // Update the privacy icon. + if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScript is enabled. + privacyMenuItem.setIcon(R.drawable.javascript_enabled); + } else if (currentWebView.getAcceptFirstPartyCookies()) { // JavaScript is disabled but cookies are enabled. + privacyMenuItem.setIcon(R.drawable.warning); + } else { // All the dangerous features are disabled. + privacyMenuItem.setIcon(R.drawable.privacy_mode); + } + + // Get the current theme status. + int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; + + // Update the first-party cookies icon. + if (currentWebView.getAcceptFirstPartyCookies()) { // First-party cookies are enabled. + firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_enabled); + } else { // First-party cookies are disabled. + if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { + firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_day); + } else { + firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_night); + } + } + + // Update the DOM storage icon. + if (currentWebView.getSettings().getJavaScriptEnabled() && currentWebView.getSettings().getDomStorageEnabled()) { // Both JavaScript and DOM storage are enabled. + domStorageMenuItem.setIcon(R.drawable.dom_storage_enabled); + } else if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScript is enabled but DOM storage is disabled. + if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { + domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_day); + } else { + domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_night); + } + } else { // JavaScript is disabled, so DOM storage is ghosted. + if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { + domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_day); + } else { + domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_night); + } + } + + // Update the refresh icon. + if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { + refreshMenuItem.setIcon(R.drawable.refresh_enabled_day); + } else { + refreshMenuItem.setIcon(R.drawable.refresh_enabled_night); + } + + // `invalidateOptionsMenu()` calls `onPrepareOptionsMenu()` and redraws the icons in the app bar. + if (runInvalidateOptionsMenu) { + invalidateOptionsMenu(); + } + } + } + + private void highlightUrlText() { + // Get a handle for the URL edit text. + EditText urlEditText = findViewById(R.id.url_edittext); + + // Only highlight the URL text if the box is not currently selected. + if (!urlEditText.hasFocus()) { + // Get the URL string. + String urlString = urlEditText.getText().toString(); + + // Highlight the URL according to the protocol. + if (urlString.startsWith("file://") || urlString.startsWith("content://")) { // This is a file or content URL. + // De-emphasize everything before the file name. + urlEditText.getText().setSpan(initialGrayColorSpan, 0, urlString.lastIndexOf("/") + 1,Spanned.SPAN_INCLUSIVE_INCLUSIVE); + } else { // This is a web URL. + // Get the index of the `/` immediately after the domain name. + int endOfDomainName = urlString.indexOf("/", (urlString.indexOf("//") + 2)); + + // Create a base URL string. + String baseUrl; + + // Get the base URL. + if (endOfDomainName > 0) { // There is at least one character after the base URL. + // Get the base URL. + baseUrl = urlString.substring(0, endOfDomainName); + } else { // There are no characters after the base URL. + // Set the base URL to be the entire URL string. + baseUrl = urlString; + } + + // Get the index of the last `.` in the domain. + int lastDotIndex = baseUrl.lastIndexOf("."); + + // Get the index of the penultimate `.` in the domain. + int penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1); + + // Markup the beginning of the URL. + if (urlString.startsWith("http://")) { // Highlight the protocol of connections that are not encrypted. + urlEditText.getText().setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE); + + // De-emphasize subdomains. + if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name. + urlEditText.getText().setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE); + } + } else if (urlString.startsWith("https://")) { // De-emphasize the protocol of connections that are encrypted. + if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name. + // De-emphasize the protocol and the additional subdomains. + urlEditText.getText().setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE); + } else { // There is only one subdomain in the domain name. + // De-emphasize only the protocol. + urlEditText.getText().setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE); + } + } + + // De-emphasize the text after the domain name. + if (endOfDomainName > 0) { + urlEditText.getText().setSpan(finalGrayColorSpan, endOfDomainName, urlString.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + } + } + } + } + + private void loadBookmarksFolder() { + // Update the bookmarks cursor with the contents of the bookmarks database for the current folder. + bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder); + + // Populate the bookmarks cursor adapter. `this` specifies the `Context`. `false` disables `autoRequery`. + bookmarksCursorAdapter = new CursorAdapter(this, bookmarksCursor, false) { + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + // Inflate the individual item layout. `false` does not attach it to the root. + return getLayoutInflater().inflate(R.layout.bookmarks_drawer_item_linearlayout, parent, false); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + // Get handles for the views. + ImageView bookmarkFavoriteIcon = view.findViewById(R.id.bookmark_favorite_icon); + TextView bookmarkNameTextView = view.findViewById(R.id.bookmark_name); + + // Get the favorite icon byte array from the cursor. + byte[] favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON)); + + // Convert the byte array to a `Bitmap` beginning at the first byte and ending at the last. + Bitmap favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.length); + + // Display the bitmap in `bookmarkFavoriteIcon`. + bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap); + + // Get the bookmark name from the cursor and display it in `bookmarkNameTextView`. + String bookmarkNameString = cursor.getString(cursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME)); + bookmarkNameTextView.setText(bookmarkNameString); + + // Make the font bold for folders. + if (cursor.getInt(cursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) { + bookmarkNameTextView.setTypeface(Typeface.DEFAULT_BOLD); + } else { // Reset the font to default for normal bookmarks. + bookmarkNameTextView.setTypeface(Typeface.DEFAULT); + } + } + }; + + // Get a handle for the bookmarks list view. + ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview); + + // Populate the list view with the adapter. + bookmarksListView.setAdapter(bookmarksCursorAdapter); + + // Get a handle for the bookmarks title text view. + TextView bookmarksTitleTextView = findViewById(R.id.bookmarks_title_textview); + + // Set the bookmarks drawer title. + if (currentBookmarksFolder.isEmpty()) { + bookmarksTitleTextView.setText(R.string.bookmarks); + } else { + bookmarksTitleTextView.setText(currentBookmarksFolder); + } + } + + private void openWithApp(String url) { + // Create an open with app intent with `ACTION_VIEW`. + Intent openWithAppIntent = new Intent(Intent.ACTION_VIEW); + + // Set the URI but not the MIME type. This should open all available apps. + openWithAppIntent.setData(Uri.parse(url)); + + // Flag the intent to open in a new task. + openWithAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + // Try the intent. + try { + // Show the chooser. + startActivity(openWithAppIntent); + } catch (ActivityNotFoundException exception) { // There are no apps available to open the URL. + // Show a snackbar with the error. + Snackbar.make(currentWebView, getString(R.string.error) + " " + exception, Snackbar.LENGTH_INDEFINITE).show(); + } + } + + private void openWithBrowser(String url) { + // Create an open with browser intent with `ACTION_VIEW`. + Intent openWithBrowserIntent = new Intent(Intent.ACTION_VIEW); + + // Set the URI and the MIME type. `"text/html"` should load browser options. + openWithBrowserIntent.setDataAndType(Uri.parse(url), "text/html"); + + // Flag the intent to open in a new task. + openWithBrowserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + // Try the intent. + try { + // Show the chooser. + startActivity(openWithBrowserIntent); + } catch (ActivityNotFoundException exception) { // There are no browsers available to open the URL. + // Show a snackbar with the error. + Snackbar.make(currentWebView, getString(R.string.error) + " " + exception, Snackbar.LENGTH_INDEFINITE).show(); + } + } + + private String sanitizeUrl(String url) { + // Sanitize Google Analytics. + if (sanitizeGoogleAnalytics) { + // Remove `?utm_`. + if (url.contains("?utm_")) { + url = url.substring(0, url.indexOf("?utm_")); + } + + // Remove `&utm_`. + if (url.contains("&utm_")) { + url = url.substring(0, url.indexOf("&utm_")); + } + } + + // Sanitize Facebook Click IDs. + if (sanitizeFacebookClickIds) { + // Remove `?fbclid=`. + if (url.contains("?fbclid=")) { + url = url.substring(0, url.indexOf("?fbclid=")); + } + + // Remove `&fbclid=`. + if (url.contains("&fbclid=")) { + url = url.substring(0, url.indexOf("&fbclid=")); + } + + // Remove `?fbadid=`. + if (url.contains("?fbadid=")) { + url = url.substring(0, url.indexOf("?fbadid=")); + } + + // Remove `&fbadid=`. + if (url.contains("&fbadid=")) { + url = url.substring(0, url.indexOf("&fbadid=")); + } + } + + // Sanitize Twitter AMP redirects. + if (sanitizeTwitterAmpRedirects) { + // Remove `?amp=1`. + if (url.contains("?amp=1")) { + url = url.substring(0, url.indexOf("?amp=1")); + } + } + + // Return the sanitized URL. + return url; + } + + public void finishedPopulatingBlocklists(ArrayList>> combinedBlocklists) { + // Store the blocklists. + easyList = combinedBlocklists.get(0); + easyPrivacy = combinedBlocklists.get(1); + fanboysAnnoyanceList = combinedBlocklists.get(2); + fanboysSocialList = combinedBlocklists.get(3); + ultraList = combinedBlocklists.get(4); + ultraPrivacy = combinedBlocklists.get(5); + + // Check to see if the activity has been restarted. + if ((savedStateArrayList == null) || (savedStateArrayList.size() == 0)) { // The activity has not been restarted or it was restarted on start to force the night theme. + // Add the first tab. + addNewTab("", true); + } else { // The activity has been restarted. + // Restore each tab. Once the minimum API >= 24, a `forEach()` command can be used. + for (int i = 0; i < savedStateArrayList.size(); i++) { + // Add a new tab. + tabLayout.addTab(tabLayout.newTab()); + + // Get the new tab. + TabLayout.Tab newTab = tabLayout.getTabAt(i); + + // Remove the lint warning below that the current tab might be null. + assert newTab != null; + + // Set a custom view on the new tab. + newTab.setCustomView(R.layout.tab_custom_view); + + // Add the new page. + webViewPagerAdapter.restorePage(savedStateArrayList.get(i), savedNestedScrollWebViewStateArrayList.get(i)); + } + + // Reset the saved state variables. + savedStateArrayList = null; + savedNestedScrollWebViewStateArrayList = null; + + // Restore the selected tab position. + if (savedTabPosition == 0) { // The first tab is selected. + // Set the first page as the current WebView. + setCurrentWebView(0); + } else { // the first tab is not selected. + // Move to the selected tab. + webViewPager.setCurrentItem(savedTabPosition); + } + + // Get the intent that started the app. + Intent intent = getIntent(); + + // Get the information from the intent. + String intentAction = intent.getAction(); + Uri intentUriData = intent.getData(); + + // Determine if this is a web search. + boolean isWebSearch = ((intentAction != null) && intentAction.equals(Intent.ACTION_WEB_SEARCH)); + + // 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. + if (intentUriData != null || isWebSearch) { + // Get the shared preferences. + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + + // Create a URL string. + String url; + + // If the intent action is a web search, perform the search. + if (isWebSearch) { // The intent is a web search. + // Create an encoded URL string. + String encodedUrlString; + + // Sanitize the search input and convert it to a search. + try { + encodedUrlString = URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8"); + } catch (UnsupportedEncodingException exception) { + encodedUrlString = ""; + } + + // Add the base search URL. + url = searchURL + encodedUrlString; + } else { // The intent should contain a URL. + // Set the intent data as the url. + url = intentUriData.toString(); + } + + // Add a new tab if specified in the preferences. + if (sharedPreferences.getBoolean("open_intents_in_new_tab", true)) { // Load the URL in a new tab. + // Set the loading new intent flag. + loadingNewIntent = true; + + // Add a new tab. + addNewTab(url, true); + } else { // Load the URL in the current tab. + // Make it so. + loadUrl(currentWebView, url); + } + } + } + } + + public void addTab(View view) { + // Add a new tab with a blank URL. + addNewTab("", true); + } + + private void addNewTab(String url, boolean moveToTab) { + // Get the new page number. The page numbers are 0 indexed, so the new page number will match the current count. + int newTabNumber = tabLayout.getTabCount(); + + // Add a new tab. + tabLayout.addTab(tabLayout.newTab()); + + // Get the new tab. + TabLayout.Tab newTab = tabLayout.getTabAt(newTabNumber); + + // Remove the lint warning below that the current tab might be null. + assert newTab != null; + + // Set a custom view on the new tab. + newTab.setCustomView(R.layout.tab_custom_view); + + // Add the new WebView page. + webViewPagerAdapter.addPage(newTabNumber, webViewPager, url, moveToTab); + } + + public void closeTab(View view) { + // Run the command according to the number of tabs. + if (tabLayout.getTabCount() > 1) { // There is more than one tab open. + // Close the current tab. + closeCurrentTab(); + } else { // There is only one tab open. + clearAndExit(); + } + } + + private void closeCurrentTab() { + // Get the current tab number. + int currentTabNumber = tabLayout.getSelectedTabPosition(); + + // Delete the current tab. + tabLayout.removeTabAt(currentTabNumber); + + // Delete the current page. If the selected page number did not change during the delete, it will return true, meaning that the current WebView must be reset. + if (webViewPagerAdapter.deletePage(currentTabNumber, webViewPager)) { + setCurrentWebView(currentTabNumber); + } + + // Expand the app bar if it is currently collapsed. + appBarLayout.setExpanded(true); + } + + private void clearAndExit() { + // Get a handle for the shared preferences. + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + + // Close the bookmarks cursor and database. + bookmarksCursor.close(); + bookmarksDatabaseHelper.close(); + + // Get the status of the clear everything preference. + boolean clearEverything = sharedPreferences.getBoolean("clear_everything", true); + + // Get a handle for the runtime. + Runtime runtime = Runtime.getRuntime(); + + // 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`. + String privateDataDirectoryString = getApplicationInfo().dataDir; + + // Clear cookies. + if (clearEverything || sharedPreferences.getBoolean("clear_cookies", true)) { + // The command to remove cookies changed slightly in API 21. + if (Build.VERSION.SDK_INT >= 21) { + CookieManager.getInstance().removeAllCookies(null); + } else { + CookieManager.getInstance().removeAllCookie(); + } + + // Manually delete the cookies database, as `CookieManager` sometimes will not flush its changes to disk before `System.exit(0)` is run. + try { + // Two commands must be used because `Runtime.exec()` does not like `*`. + Process deleteCookiesProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies"); + Process deleteCookiesJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies-journal"); + + // Wait until the processes have finished. + deleteCookiesProcess.waitFor(); + deleteCookiesJournalProcess.waitFor(); + } catch (Exception exception) { + // Do nothing if an error is thrown. + } + } + + // Clear DOM storage. + if (clearEverything || sharedPreferences.getBoolean("clear_dom_storage", true)) { + // Ask `WebStorage` to clear the DOM storage. + WebStorage webStorage = WebStorage.getInstance(); + webStorage.deleteAllData(); + + // Manually delete the DOM storage files and directories, as `WebStorage` sometimes will not flush its changes to disk before `System.exit(0)` is run. + try { + // A `String[]` must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly. + Process deleteLocalStorageProcess = runtime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"}); + + // Multiple commands must be used because `Runtime.exec()` does not like `*`. + Process deleteIndexProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB"); + Process deleteQuotaManagerProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager"); + Process deleteQuotaManagerJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal"); + Process deleteDatabaseProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases"); + + // Wait until the processes have finished. + deleteLocalStorageProcess.waitFor(); + deleteIndexProcess.waitFor(); + deleteQuotaManagerProcess.waitFor(); + deleteQuotaManagerJournalProcess.waitFor(); + deleteDatabaseProcess.waitFor(); + } catch (Exception exception) { + // Do nothing if an error is thrown. + } + } + + // Clear form data if the API < 26. + if ((Build.VERSION.SDK_INT < 26) && (clearEverything || sharedPreferences.getBoolean("clear_form_data", true))) { + WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this); + webViewDatabase.clearFormData(); + + // Manually delete the form data database, as `WebViewDatabase` sometimes will not flush its changes to disk before `System.exit(0)` is run. + try { + // A string array must be used because the database contains a space and `Runtime.exec` will not otherwise escape the string correctly. + Process deleteWebDataProcess = runtime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data"}); + Process deleteWebDataJournalProcess = runtime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data-journal"}); + + // Wait until the processes have finished. + deleteWebDataProcess.waitFor(); + deleteWebDataJournalProcess.waitFor(); + } catch (Exception exception) { + // Do nothing if an error is thrown. + } + } + + // Clear the cache. + if (clearEverything || sharedPreferences.getBoolean("clear_cache", true)) { + // Clear the cache from each WebView. + for (int i = 0; i < webViewPagerAdapter.getCount(); i++) { + // Get the WebView tab fragment. + WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i); + + // Get the fragment view. + View fragmentView = webViewTabFragment.getView(); + + // Only clear the cache if the WebView exists. + if (fragmentView != null) { + // Get the nested scroll WebView from the tab fragment. + NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview); + + // Clear the cache for this WebView. + nestedScrollWebView.clearCache(true); + } + } + + // Manually delete the cache directories. + try { + // Delete the main cache directory. + Process deleteCacheProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/cache"); + + // Delete the secondary `Service Worker` cache directory. + // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly. + Process deleteServiceWorkerProcess = runtime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"}); + + // Wait until the processes have finished. + deleteCacheProcess.waitFor(); + deleteServiceWorkerProcess.waitFor(); + } catch (Exception exception) { + // Do nothing if an error is thrown. + } + } + + // Wipe out each WebView. + for (int i = 0; i < webViewPagerAdapter.getCount(); i++) { + // Get the WebView tab fragment. + WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i); + + // Get the fragment view. + View fragmentView = webViewTabFragment.getView(); + + // Only wipe out the WebView if it exists. + if (fragmentView != null) { + // Get the nested scroll WebView from the tab fragment. + NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview); + + // Clear SSL certificate preferences for this WebView. + nestedScrollWebView.clearSslPreferences(); + + // Clear the back/forward history for this WebView. + nestedScrollWebView.clearHistory(); + + // Destroy the internal state of `mainWebView`. + nestedScrollWebView.destroy(); + } + } + + // Clear the custom headers. + customHeaders.clear(); + + // Manually delete the `app_webview` folder, which contains the cookies, DOM storage, form data, and `Service Worker` cache. + // See `https://code.google.com/p/android/issues/detail?id=233826&thanks=233826&ts=1486670530`. + if (clearEverything) { + try { + // Delete the folder. + Process deleteAppWebviewProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview"); + + // Wait until the process has finished. + deleteAppWebviewProcess.waitFor(); + } catch (Exception exception) { + // Do nothing if an error is thrown. + } + } + + // Close Privacy Browser. `finishAndRemoveTask` also removes Privacy Browser from the recent app list. + if (Build.VERSION.SDK_INT >= 21) { + finishAndRemoveTask(); + } else { + finish(); + } + + // Remove the terminated program from RAM. The status code is `0`. + System.exit(0); + } + + private void setCurrentWebView(int pageNumber) { + // Get handles for the URL views. + RelativeLayout urlRelativeLayout = findViewById(R.id.url_relativelayout); + EditText urlEditText = findViewById(R.id.url_edittext); + SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout); + + // Stop the swipe to refresh indicator if it is running + swipeRefreshLayout.setRefreshing(false); + + // Get the WebView tab fragment. + WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(pageNumber); + + // Get the fragment view. + View fragmentView = webViewTabFragment.getView(); + + // Set the current WebView if the fragment view is not null. + if (fragmentView != null) { // The fragment has been populated. + // Store the current WebView. + currentWebView = fragmentView.findViewById(R.id.nestedscroll_webview); + + // Update the status of swipe to refresh. + if (currentWebView.getSwipeToRefresh()) { // Swipe to refresh is enabled. + // Enable the swipe refresh layout if the WebView is scrolled all the way to the top. It is updated every time the scroll changes. + swipeRefreshLayout.setEnabled(currentWebView.getY() == 0); + } else { // Swipe to refresh is disabled. + // Disable the swipe refresh layout. + swipeRefreshLayout.setEnabled(false); + } + + // Get a handle for the cookie manager. + CookieManager cookieManager = CookieManager.getInstance(); + + // Set the first-party cookie status. + cookieManager.setAcceptCookie(currentWebView.getAcceptFirstPartyCookies()); + + // Update the privacy icons. `true` redraws the icons in the app bar. + updatePrivacyIcons(true); + + // Get a handle for the input method manager. + InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + + // Remove the lint warning below that the input method manager might be null. + assert inputMethodManager != null; + + // Get the current URL. + String url = currentWebView.getUrl(); + + // Update the URL edit text if not loading a new intent. Otherwise, this will be handled by `onPageStarted()` (if called) and `onPageFinished()`. + if (!loadingNewIntent) { // A new intent is not being loaded. + if ((url == null) || url.equals("about:blank")) { // The WebView is blank. + // Display the hint in the URL edit text. + urlEditText.setText(""); + + // Request focus for the URL text box. + urlEditText.requestFocus(); + + // Display the keyboard. + inputMethodManager.showSoftInput(urlEditText, 0); + } else { // The WebView has a loaded URL. + // Clear the focus from the URL text box. + urlEditText.clearFocus(); + + // Hide the soft keyboard. + inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0); + + // Display the current URL in the URL text box. + urlEditText.setText(url); + + // Highlight the URL text. + highlightUrlText(); } + } else { // A new intent is being loaded. + // Reset the loading new intent tracker. + loadingNewIntent = false; + } - // Clear DOM storage. - if (clearEverything || sharedPreferences.getBoolean("clear_dom_storage", true)) { - // Ask `WebStorage` to clear the DOM storage. - WebStorage webStorage = WebStorage.getInstance(); - webStorage.deleteAllData(); + // Set the background to indicate the domain settings status. + if (currentWebView.getDomainSettingsApplied()) { + // Get the current theme status. + int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; - // Manually delete the DOM storage files and directories, as `WebStorage` sometimes will not flush its changes to disk before `System.exit(0)` is run. - try { - // We have to use a `String[]` because the directory contains a space and `Runtime.exec` will not escape the string correctly otherwise. - privacyBrowserRuntime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"}); - - // We have to use multiple commands because `Runtime.exec()` does not like `*`. - privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB"); - privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager"); - privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal"); - privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases"); - } catch (IOException e) { - // Do nothing if an error is thrown. - } + // Set a green background on the URL relative layout to indicate that custom domain settings are being used. + if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { + urlRelativeLayout.setBackground(ResourcesCompat.getDrawable(getResources(), R.drawable.url_bar_background_light_green, null)); + } else { + urlRelativeLayout.setBackground(ResourcesCompat.getDrawable(getResources(), R.drawable.url_bar_background_dark_blue, null)); } + } else { + urlRelativeLayout.setBackground(ResourcesCompat.getDrawable(getResources(), R.color.transparent, null)); + } + } else { // The fragment has not been populated. Try again in 100 milliseconds. + // Create a handler to set the current WebView. + Handler setCurrentWebViewHandler = new Handler(); + + // Create a runnable to set the current WebView. + Runnable setCurrentWebWebRunnable = () -> { + // Set the current WebView. + setCurrentWebView(pageNumber); + }; + + // Try setting the current WebView again after 100 milliseconds. + setCurrentWebViewHandler.postDelayed(setCurrentWebWebRunnable, 100); + } + } - // Clear form data. - if (clearEverything || sharedPreferences.getBoolean("clear_form_data", true)) { - WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this); - webViewDatabase.clearFormData(); + @Override + public void initializeWebView(NestedScrollWebView nestedScrollWebView, int pageNumber, ProgressBar progressBar, String url, Boolean restoringState) { + // Get a handle for the shared preferences. + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - // Manually delete the form data database, as `WebViewDatabase` sometimes will not flush its changes to disk before `System.exit(0)` is run. - try { - // We have to use a `String[]` because the database contains a space and `Runtime.exec` will not escape the string correctly otherwise. - privacyBrowserRuntime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data"}); - privacyBrowserRuntime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data-journal"}); - } catch (IOException e) { - // Do nothing if an error is thrown. - } + // Get the WebView theme. + String webViewTheme = sharedPreferences.getString("webview_theme", getString(R.string.webview_theme_default_value)); + + // Get the WebView theme entry values string array. + String[] webViewThemeEntryValuesStringArray = getResources().getStringArray(R.array.webview_theme_entry_values); + + // Apply the WebView theme if supported by the installed WebView. + if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) { + // Set the WebView theme. A switch statement cannot be used because the WebView theme entry values string array is not a compile time constant. + if (webViewTheme.equals(webViewThemeEntryValuesStringArray[1])) { // The light theme is selected. + // Turn off the WebView dark mode. + WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF); + + // Make the WebView visible. The WebView was created invisible in `webview_framelayout` to prevent a white background splash in night mode. + // If the system is currently in night mode, showing the WebView will be handled in `onProgressChanged()`. + nestedScrollWebView.setVisibility(View.VISIBLE); + } else if (webViewTheme.equals(webViewThemeEntryValuesStringArray[2])) { // The dark theme is selected. + // Turn on the WebView dark mode. + WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON); + } else { // The system default theme is selected. + // Get the current system theme status. + int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; + + // Set the WebView theme according to the current system theme status. + if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { // The system is in day mode. + // Turn off the WebView dark mode. + WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF); + + // Make the WebView visible. The WebView was created invisible in `webview_framelayout` to prevent a white background splash in night mode. + // If the system is currently in night mode, showing the WebView will be handled in `onProgressChanged()`. + nestedScrollWebView.setVisibility(View.VISIBLE); + } else { // The system is in night mode. + // Turn on the WebView dark mode. + WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON); } + } + } - // Clear the cache. - if (clearEverything || sharedPreferences.getBoolean("clear_cache", true)) { - // `true` includes disk files. - mainWebView.clearCache(true); + // Get a handle for the app compat delegate. + AppCompatDelegate appCompatDelegate = getDelegate(); - // Manually delete the cache directories. - try { - // Delete the main cache directory. - privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/cache"); + // Get handles for the activity views. + FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout); + DrawerLayout drawerLayout = findViewById(R.id.drawerlayout); + RelativeLayout mainContentRelativeLayout = findViewById(R.id.main_content_relativelayout); + ActionBar actionBar = appCompatDelegate.getSupportActionBar(); + LinearLayout tabsLinearLayout = findViewById(R.id.tabs_linearlayout); + EditText urlEditText = findViewById(R.id.url_edittext); + SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout); - // Delete the secondary `Service Worker` cache directory. We have to use a `String[]` because the directory contains a space and `Runtime.exec` will not escape the string correctly otherwise. - privacyBrowserRuntime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"}); - } catch (IOException e) { - // Do nothing if an error is thrown. - } - } + // Remove the incorrect lint warning below that the action bar might be null. + assert actionBar != null; - // Clear SSL certificate preferences. - mainWebView.clearSslPreferences(); + // Get a handle for the activity + Activity activity = this; - // Clear the back/forward history. - mainWebView.clearHistory(); + // Get a handle for the input method manager. + InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - // Clear `formattedUrlString`. - formattedUrlString = null; + // Instantiate the blocklist helper. + BlocklistHelper blocklistHelper = new BlocklistHelper(); - // Clear `customHeaders`. - customHeaders.clear(); + // Remove the lint warning below that the input method manager might be null. + assert inputMethodManager != null; - // Detach all views from `mainWebViewRelativeLayout`. - mainWebViewRelativeLayout.removeAllViews(); + // Initialize the favorite icon. + nestedScrollWebView.initializeFavoriteIcon(); - // Destroy the internal state of `mainWebView`. - mainWebView.destroy(); + // Set the app bar scrolling. + nestedScrollWebView.setNestedScrollingEnabled(sharedPreferences.getBoolean("scroll_app_bar", true)); - // Manually delete the `app_webview` folder, which contains the cookies, DOM storage, form data, and `Service Worker` cache. - // See `https://code.google.com/p/android/issues/detail?id=233826&thanks=233826&ts=1486670530`. - if (clearEverything) { - try { - privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview"); - } catch (IOException e) { - // Do nothing if an error is thrown. - } - } + // Allow pinch to zoom. + nestedScrollWebView.getSettings().setBuiltInZoomControls(true); - // Close Privacy Browser. `finishAndRemoveTask` also removes Privacy Browser from the recent app list. - if (Build.VERSION.SDK_INT >= 21) { - finishAndRemoveTask(); - } else { - finish(); - } + // Hide zoom controls. + nestedScrollWebView.getSettings().setDisplayZoomControls(false); - // Remove the terminated program from RAM. The status code is `0`. - System.exit(0); - break; + // Don't allow mixed content (HTTP and HTTPS) on the same website. + if (Build.VERSION.SDK_INT >= 21) { + nestedScrollWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW); } - // Close the navigation drawer. - drawerLayout.closeDrawer(GravityCompat.START); - return true; - } + // Set the WebView to load in overview mode (zoomed out to the maximum width). + nestedScrollWebView.getSettings().setLoadWithOverviewMode(true); - @Override - public void onPostCreate(Bundle savedInstanceState) { - super.onPostCreate(savedInstanceState); + // Explicitly disable geolocation. + nestedScrollWebView.getSettings().setGeolocationEnabled(false); - // Sync the state of the DrawerToggle after onRestoreInstanceState has finished. - drawerToggle.syncState(); - } + // Create a double-tap gesture detector to toggle full-screen mode. + GestureDetector doubleTapGestureDetector = new GestureDetector(getApplicationContext(), new GestureDetector.SimpleOnGestureListener() { + // Override `onDoubleTap()`. All other events are handled using the default settings. + @Override + public boolean onDoubleTap(MotionEvent event) { + if (fullScreenBrowsingModeEnabled) { // Only process the double-tap if full screen browsing mode is enabled. + // Toggle the full screen browsing mode tracker. + inFullScreenBrowsingMode = !inFullScreenBrowsingMode; - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); + // Toggle the full screen browsing mode. + if (inFullScreenBrowsingMode) { // Switch to full screen mode. + // Hide the app bar if specified. + if (hideAppBar) { + // Close the find on page bar if it is visible. + closeFindOnPage(null); - // Reload the ad for the free flavor if we are not in full screen mode. - if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) { - // Reload the ad. - BannerAd.reloadAfterRotate(adView, getApplicationContext(), getString(R.string.ad_id)); + // Hide the tab linear layout. + tabsLinearLayout.setVisibility(View.GONE); - // Reinitialize the `adView` variable, as the `View` will have been removed and re-added by `BannerAd.reloadAfterRotate()`. - adView = findViewById(R.id.adview); - } + // Hide the action bar. + actionBar.hide(); - // `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: https://code.google.com/p/android/issues/detail?id=20493#c8 - // ActivityCompat.invalidateOptionsMenu(this); - } + // If the app bar is not being scrolled, the swipe refresh layout needs to be adjusted. + if (!scrollAppBar) { + // Remove the padding from the top of the swipe refresh layout. + swipeRefreshLayout.setPadding(0, 0, 0, 0); - @Override - public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) { - // Store the `HitTestResult`. - final WebView.HitTestResult hitTestResult = mainWebView.getHitTestResult(); + // The swipe refresh circle must be moved above the now removed status bar location. + swipeRefreshLayout.setProgressViewOffset(false, -200, defaultProgressViewEndOffset); + } + } - // Create strings. - final String imageUrl; - final String linkUrl; + // Hide the banner ad in the free flavor. + if (BuildConfig.FLAVOR.contentEquals("free")) { + AdHelper.hideAd(findViewById(R.id.adview)); + } - // Get a handle for the `ClipboardManager`. - final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); + /* Hide the system bars. + * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen. + * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar. + * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen. + * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown. + */ + rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + } else { // Switch to normal viewing mode. + // Show the app bar if it was hidden. + if (hideAppBar) { + // Show the tab linear layout. + tabsLinearLayout.setVisibility(View.VISIBLE); - switch (hitTestResult.getType()) { - // `SRC_ANCHOR_TYPE` is a link. - case WebView.HitTestResult.SRC_ANCHOR_TYPE: - // Get the target URL. - linkUrl = hitTestResult.getExtra(); + // Show the action bar. + actionBar.show(); - // Set the target URL as the title of the `ContextMenu`. - menu.setHeaderTitle(linkUrl); + // If the app bar is not being scrolled, the swipe refresh layout needs to be adjusted. + if (!scrollAppBar) { + // The swipe refresh layout must be manually moved below the app bar layout. + swipeRefreshLayout.setPadding(0, appBarHeight, 0, 0); - // Add a `Load URL` entry. - menu.add(R.string.load_url).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - loadUrl(linkUrl); - return false; - } - }); + // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels. + swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10 + appBarHeight, defaultProgressViewEndOffset + appBarHeight); + } + } - // Add a `Copy URL` entry. - menu.add(R.string.copy_url).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - // Save the link URL in a `ClipData`. - ClipData srcAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl); + // Show the banner ad in the free flavor. + if (BuildConfig.FLAVOR.contentEquals("free")) { + // Reload the ad. + AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id)); + } - // Set the `ClipData` as the clipboard's primary clip. - clipboardManager.setPrimaryClip(srcAnchorTypeClipData); - return false; + // Remove the `SYSTEM_UI` flags from the root frame layout. + rootFrameLayout.setSystemUiVisibility(0); } - }); - // Add a `Cancel` entry, which by default closes the `ContextMenu`. - menu.add(R.string.cancel); - break; + // Consume the double-tap. + return true; + } else { // Do not consume the double-tap because full screen browsing mode is disabled. + return false; + } + } + }); - case WebView.HitTestResult.EMAIL_TYPE: - // Get the target URL. - linkUrl = hitTestResult.getExtra(); + // Pass all touch events on the WebView through the double-tap gesture detector. + nestedScrollWebView.setOnTouchListener((View view, MotionEvent event) -> { + // Call `performClick()` on the view, which is required for accessibility. + view.performClick(); - // Set the target URL as the title of the `ContextMenu`. - menu.setHeaderTitle(linkUrl); + // Send the event to the gesture detector. + return doubleTapGestureDetector.onTouchEvent(event); + }); - // Add a `Write Email` entry. - menu.add(R.string.write_email).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - // We use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched. - Intent emailIntent = new Intent(Intent.ACTION_SENDTO); + // Register the WebView for a context menu. This is used to see link targets and download images. + registerForContextMenu(nestedScrollWebView); - // Parse the url and set it as the data for the `Intent`. - emailIntent.setData(Uri.parse("mailto:" + linkUrl)); + // Allow the downloading of files. + nestedScrollWebView.setDownloadListener((String downloadUrl, String userAgent, String contentDisposition, String mimetype, long contentLength) -> { + // Define a formatted file size string. + String formattedFileSizeString; + + // Process the content length if it contains data. + if (contentLength > 0) { // The content length is greater than 0. + // Format the content length as a string. + formattedFileSizeString = NumberFormat.getInstance().format(contentLength) + " " + getString(R.string.bytes); + } else { // The content length is not greater than 0. + // Set the formatted file size string to be `unknown size`. + formattedFileSizeString = getString(R.string.unknown_size); + } - // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser. - emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // Get the file name from the content disposition. + String fileNameString = PrepareSaveDialog.getFileNameFromContentDisposition(this, contentDisposition, downloadUrl); - // Make it so. - startActivity(emailIntent); - return false; - } - }); + // Instantiate the save dialog. + DialogFragment saveDialogFragment = SaveDialog.saveUrl(StoragePermissionDialog.SAVE_URL, downloadUrl, formattedFileSizeString, fileNameString, userAgent, + nestedScrollWebView.getAcceptFirstPartyCookies()); - // Add a `Copy Email Address` entry. - menu.add(R.string.copy_email_address).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - // Save the email address in a `ClipData`. - ClipData srcEmailTypeClipData = ClipData.newPlainText(getString(R.string.email_address), linkUrl); + // Show the save dialog. It must be named `save_dialog` so that the file picker can update the file name. + saveDialogFragment.show(getSupportFragmentManager(), getString(R.string.save_dialog)); + }); - // Set the `ClipData` as the clipboard's primary clip. - clipboardManager.setPrimaryClip(srcEmailTypeClipData); - return false; - } - }); + // Update the find on page count. + nestedScrollWebView.setFindListener(new WebView.FindListener() { + // Get a handle for `findOnPageCountTextView`. + final TextView findOnPageCountTextView = findViewById(R.id.find_on_page_count_textview); - // Add a `Cancel` entry, which by default closes the `ContextMenu`. - menu.add(R.string.cancel); - break; + @Override + public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) { + if ((isDoneCounting) && (numberOfMatches == 0)) { // There are no matches. + // Set `findOnPageCountTextView` to `0/0`. + findOnPageCountTextView.setText(R.string.zero_of_zero); + } else if (isDoneCounting) { // There are matches. + // `activeMatchOrdinal` is zero-based. + int activeMatch = activeMatchOrdinal + 1; - // `IMAGE_TYPE` is an image. - case WebView.HitTestResult.IMAGE_TYPE: - // Get the image URL. - imageUrl = hitTestResult.getExtra(); + // Build the match string. + String matchString = activeMatch + "/" + numberOfMatches; - // Set the image URL as the title of the `ContextMenu`. - menu.setHeaderTitle(imageUrl); + // Set `findOnPageCountTextView`. + findOnPageCountTextView.setText(matchString); + } + } + }); - // Add a `View Image` entry. - menu.add(R.string.view_image).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - loadUrl(imageUrl); - return false; - } - }); + // Update the status of swipe to refresh based on the scroll position of the nested scroll WebView. Also reinforce full screen browsing mode. + // On API < 23, `getViewTreeObserver().addOnScrollChangedListener()` must be used, but it is a little bit buggy and appears to get garbage collected from time to time. + if (Build.VERSION.SDK_INT >= 23) { + nestedScrollWebView.setOnScrollChangeListener((view, i, i1, i2, i3) -> { + if (nestedScrollWebView.getSwipeToRefresh()) { + // Only enable swipe to refresh if the WebView is scrolled to the top. + swipeRefreshLayout.setEnabled(nestedScrollWebView.getScrollY() == 0); + } else { + // Disable swipe to refresh. + swipeRefreshLayout.setEnabled(false); + } - // Add a `Download Image` entry. - menu.add(R.string.download_image).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - // Show the `DownloadImageDialog` `AlertDialog` and name this instance `@string/download`. - AppCompatDialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl); - downloadImageDialogFragment.show(getSupportFragmentManager(), getString(R.string.download)); - return false; - } - }); + // Reinforce the system UI visibility flags if in full screen browsing mode. + // This hides the status and navigation bars, which are displayed if other elements are shown, like dialog boxes, the options menu, or the keyboard. + if (inFullScreenBrowsingMode) { + /* Hide the system bars. + * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen. + * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar. + * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen. + * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown. + */ + rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + } + }); + } else { + nestedScrollWebView.getViewTreeObserver().addOnScrollChangedListener(() -> { + if (nestedScrollWebView.getSwipeToRefresh()) { + // Only enable swipe to refresh if the WebView is scrolled to the top. + swipeRefreshLayout.setEnabled(nestedScrollWebView.getScrollY() == 0); + } else { + // Disable swipe to refresh. + swipeRefreshLayout.setEnabled(false); + } - // Add a `Copy URL` entry. - menu.add(R.string.copy_url).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - // Save the image URL in a `ClipData`. - ClipData srcImageTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl); - // Set the `ClipData` as the clipboard's primary clip. - clipboardManager.setPrimaryClip(srcImageTypeClipData); - return false; - } - }); + // Reinforce the system UI visibility flags if in full screen browsing mode. + // This hides the status and navigation bars, which are displayed if other elements are shown, like dialog boxes, the options menu, or the keyboard. + if (inFullScreenBrowsingMode) { + /* Hide the system bars. + * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen. + * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar. + * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen. + * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown. + */ + rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + } + }); + } - // Add a `Cancel` entry, which by default closes the `ContextMenu`. - menu.add(R.string.cancel); - break; + // Set the web chrome client. + nestedScrollWebView.setWebChromeClient(new WebChromeClient() { + // Update the progress bar when a page is loading. + @Override + public void onProgressChanged(WebView view, int progress) { + // Update the progress bar. + progressBar.setProgress(progress); + // Set the visibility of the progress bar. + if (progress < 100) { + // Show the progress bar. + progressBar.setVisibility(View.VISIBLE); + } else { + // Hide the progress bar. + progressBar.setVisibility(View.GONE); - // `SRC_IMAGE_ANCHOR_TYPE` is an image that is also a link. - case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE: - // Get the image URL. - imageUrl = hitTestResult.getExtra(); + //Stop the swipe to refresh indicator if it is running + swipeRefreshLayout.setRefreshing(false); - // Set the image URL as the title of the `ContextMenu`. - menu.setHeaderTitle(imageUrl); + // Make the current WebView visible. If this is a new tab, the current WebView would have been created invisible in `webview_framelayout` to prevent a white background splash in night mode. + nestedScrollWebView.setVisibility(View.VISIBLE); + } + } - // Add a `View Image` entry. - menu.add(R.string.view_image).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - loadUrl(imageUrl); - return false; - } - }); + // Set the favorite icon when it changes. + @Override + public void onReceivedIcon(WebView view, Bitmap icon) { + // Only update the favorite icon if the website has finished loading. + if (progressBar.getVisibility() == View.GONE) { + // Store the new favorite icon. + nestedScrollWebView.setFavoriteOrDefaultIcon(icon); - // Add a `Download Image` entry. - menu.add(R.string.download_image).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - // Show the `DownloadImageDialog` `AlertDialog` and name this instance `@string/download`. - AppCompatDialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl); - downloadImageDialogFragment.show(getSupportFragmentManager(), getString(R.string.download)); - return false; - } - }); + // Get the current page position. + int currentPosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId()); + + // Get the current tab. + TabLayout.Tab tab = tabLayout.getTabAt(currentPosition); + + // Check to see if the tab has been populated. + if (tab != null) { + // Get the custom view from the tab. + View tabView = tab.getCustomView(); - // Add a `Copy URL` entry. - menu.add(R.string.copy_url).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - // Save the image URL in a `ClipData`. - ClipData srcImageAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl); + // Check to see if the custom tab view has been populated. + if (tabView != null) { + // Get the favorite icon image view from the tab. + ImageView tabFavoriteIconImageView = tabView.findViewById(R.id.favorite_icon_imageview); - // Set the `ClipData` as the clipboard's primary clip. - clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData); - return false; + // Display the favorite icon in the tab. + tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(icon, 64, 64, true)); + } } - }); + } + } - // Add a `Cancel` entry, which by default closes the `ContextMenu`. - menu.add(R.string.cancel); - break; - } - } + // Save a copy of the title when it changes. + @Override + public void onReceivedTitle(WebView view, String title) { + // Get the current page position. + int currentPosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId()); + + // Get the current tab. + TabLayout.Tab tab = tabLayout.getTabAt(currentPosition); + + // Only populate the title text view if the tab has been fully created. + if (tab != null) { + // Get the custom view from the tab. + View tabView = tab.getCustomView(); + + // Only populate the title text view if the tab view has been fully populated. + if (tabView != null) { + // Get the title text view from the tab. + TextView tabTitleTextView = tabView.findViewById(R.id.title_textview); + + // Set the title according to the URL. + if (title.equals("about:blank")) { + // Set the title to indicate a new tab. + tabTitleTextView.setText(R.string.new_tab); + } else { + // Set the title as the tab text. + tabTitleTextView.setText(title); + } + } + } + } - @Override - public void onCreateHomeScreenShortcut(AppCompatDialogFragment dialogFragment) { - // Get shortcutNameEditText from the alert dialog. - EditText shortcutNameEditText = (EditText) dialogFragment.getDialog().findViewById(R.id.shortcut_name_edittext); - - // Create the bookmark shortcut based on formattedUrlString. - Intent bookmarkShortcut = new Intent(); - bookmarkShortcut.setAction(Intent.ACTION_VIEW); - bookmarkShortcut.setData(Uri.parse(formattedUrlString)); - - // Place the bookmark shortcut on the home screen. - Intent placeBookmarkShortcut = new Intent(); - placeBookmarkShortcut.putExtra("android.intent.extra.shortcut.INTENT", bookmarkShortcut); - placeBookmarkShortcut.putExtra("android.intent.extra.shortcut.NAME", shortcutNameEditText.getText().toString()); - placeBookmarkShortcut.putExtra("android.intent.extra.shortcut.ICON", favoriteIconBitmap); - placeBookmarkShortcut.setAction("com.android.launcher.action.INSTALL_SHORTCUT"); - sendBroadcast(placeBookmarkShortcut); - } + // Enter full screen video. + @Override + public void onShowCustomView(View video, CustomViewCallback callback) { + // Get a handle for the full screen video frame layout. + FrameLayout fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout); - @Override - public void onDownloadImage(AppCompatDialogFragment dialogFragment, String imageUrl) { - // Download the image if it has an HTTP or HTTPS URI. - if (imageUrl.startsWith("http")) { - // Get a handle for the system `DOWNLOAD_SERVICE`. - DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE); + // Set the full screen video flag. + displayingFullScreenVideo = true; + + // Pause the ad if this is the free flavor. + if (BuildConfig.FLAVOR.contentEquals("free")) { + // The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations. + AdHelper.pauseAd(findViewById(R.id.adview)); + } - // Parse `imageUrl`. - DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(imageUrl)); + // Hide the keyboard. + inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0); - // Pass cookies to download manager if cookies are enabled. This is required to download images from websites that require a login. - // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner . - if (firstPartyCookiesEnabled) { - // Get the cookies for `imageUrl`. - String cookies = cookieManager.getCookie(imageUrl); + // Hide the main content relative layout. + mainContentRelativeLayout.setVisibility(View.GONE); - // Add the cookies to `downloadRequest`. In the HTTP request header, cookies are named `Cookie`. - downloadRequest.addRequestHeader("Cookie", cookies); - } + /* Hide the system bars. + * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen. + * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar. + * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen. + * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown. + */ + rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); - // Get the file name from `dialogFragment`. - EditText downloadImageNameEditText = (EditText) dialogFragment.getDialog().findViewById(R.id.download_image_name); - String imageName = downloadImageNameEditText.getText().toString(); + // Disable the sliding drawers. + drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); - // Once we have `WRITE_EXTERNAL_STORAGE` permissions we can use `setDestinationInExternalPublicDir`. - if (Build.VERSION.SDK_INT >= 23) { // If API >= 23, set the download save in the the `DIRECTORY_DOWNLOADS` using `imageName`. - downloadRequest.setDestinationInExternalFilesDir(this, "/", imageName); - } else { // Only set the title using `imageName`. - downloadRequest.setTitle(imageName); - } + // Add the video view to the full screen video frame layout. + fullScreenVideoFrameLayout.addView(video); - // Allow `MediaScanner` to index the download if it is a media file. - downloadRequest.allowScanningByMediaScanner(); + // Show the full screen video frame layout. + fullScreenVideoFrameLayout.setVisibility(View.VISIBLE); - // Add the URL as the description for the download. - downloadRequest.setDescription(imageUrl); + // Disable the screen timeout while the video is playing. YouTube does this automatically, but not all other videos do. + fullScreenVideoFrameLayout.setKeepScreenOn(true); + } - // Show the download notification after the download is completed. - downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); + // Exit full screen video. + @Override + public void onHideCustomView() { + // Get a handle for the full screen video frame layout. + FrameLayout fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout); - // Initiate the download. - downloadManager.enqueue(downloadRequest); - } else { // The image is not an HTTP or HTTPS URI. - Snackbar.make(mainWebView, R.string.cannot_download_image, Snackbar.LENGTH_INDEFINITE).show(); - } - } + // Re-enable the screen timeout. + fullScreenVideoFrameLayout.setKeepScreenOn(false); - @Override - public void onDownloadFile(AppCompatDialogFragment dialogFragment, String downloadUrl) { - // Download the file if it has an HTTP or HTTPS URI. - if (downloadUrl.startsWith("http")) { + // Unset the full screen video flag. + displayingFullScreenVideo = false; - // Get a handle for the system `DOWNLOAD_SERVICE`. - DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE); + // Remove all the views from the full screen video frame layout. + fullScreenVideoFrameLayout.removeAllViews(); - // Parse `downloadUrl`. - DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(downloadUrl)); + // Hide the full screen video frame layout. + fullScreenVideoFrameLayout.setVisibility(View.GONE); - // Pass cookies to download manager if cookies are enabled. This is required to download files from websites that require a login. - // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner . - if (firstPartyCookiesEnabled) { - // Get the cookies for `downloadUrl`. - String cookies = cookieManager.getCookie(downloadUrl); + // Enable the sliding drawers. + drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED); - // Add the cookies to `downloadRequest`. In the HTTP request header, cookies are named `Cookie`. - downloadRequest.addRequestHeader("Cookie", cookies); - } + // Show the main content relative layout. + mainContentRelativeLayout.setVisibility(View.VISIBLE); - // Get the file name from `dialogFragment`. - EditText downloadFileNameEditText = (EditText) dialogFragment.getDialog().findViewById(R.id.download_file_name); - String fileName = downloadFileNameEditText.getText().toString(); + // Apply the appropriate full screen mode flags. + if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode. + // Hide the app bar if specified. + if (hideAppBar) { + // Hide the tab linear layout. + tabsLinearLayout.setVisibility(View.GONE); - // Once we have `WRITE_EXTERNAL_STORAGE` permissions we can use `setDestinationInExternalPublicDir`. - if (Build.VERSION.SDK_INT >= 23) { // If API >= 23, set the download location to `/sdcard/Android/data/com.stoutner.privacybrowser.standard/files` named `fileName`. - downloadRequest.setDestinationInExternalFilesDir(this, "/", fileName); - } else { // Only set the title using `fileName`. - downloadRequest.setTitle(fileName); - } + // Hide the action bar. + actionBar.hide(); + } - // Allow `MediaScanner` to index the download if it is a media file. - downloadRequest.allowScanningByMediaScanner(); + // Hide the banner ad in the free flavor. + if (BuildConfig.FLAVOR.contentEquals("free")) { + AdHelper.hideAd(findViewById(R.id.adview)); + } - // Add the URL as the description for the download. - downloadRequest.setDescription(downloadUrl); + /* Hide the system bars. + * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen. + * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar. + * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen. + * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown. + */ + rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + } else { // Switch to normal viewing mode. + // Remove the `SYSTEM_UI` flags from the root frame layout. + rootFrameLayout.setSystemUiVisibility(0); + } - // Show the download notification after the download is completed. - downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); + // Reload the ad for the free flavor if not in full screen mode. + if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) { + // Reload the ad. + AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id)); + } + } - // Initiate the download. - downloadManager.enqueue(downloadRequest); - } else { // The download is not an HTTP or HTTPS URI. - Snackbar.make(mainWebView, R.string.cannot_download_file, Snackbar.LENGTH_INDEFINITE).show(); - } - } + // Upload files. + @Override + public boolean onShowFileChooser(WebView webView, ValueCallback filePathCallback, FileChooserParams fileChooserParams) { + // Show the file chooser if the device is running API >= 21. + if (Build.VERSION.SDK_INT >= 21) { + // Store the file path callback. + fileChooserCallback = filePathCallback; - @Override - public void onHttpAuthenticationCancel() { - // Cancel the `HttpAuthHandler`. - httpAuthHandler.cancel(); - } + // Create an intent to open a chooser based on the file chooser parameters. + Intent fileChooserIntent = fileChooserParams.createIntent(); - @Override - public void onHttpAuthenticationProceed(AppCompatDialogFragment dialogFragment) { - // Get handles for the `EditTexts`. - EditText usernameEditText = (EditText) dialogFragment.getDialog().findViewById(R.id.http_authentication_username); - EditText passwordEditText = (EditText) dialogFragment.getDialog().findViewById(R.id.http_authentication_password); + // Get a handle for the package manager. + PackageManager packageManager = getPackageManager(); - // Proceed with the HTTP authentication. - httpAuthHandler.proceed(usernameEditText.getText().toString(), passwordEditText.getText().toString()); - } + // Check to see if the file chooser intent resolves to an installed package. + if (fileChooserIntent.resolveActivity(packageManager) != null) { // The file chooser intent is fine. + // Start the file chooser intent. + startActivityForResult(fileChooserIntent, BROWSE_FILE_UPLOAD_REQUEST_CODE); + } else { // The file chooser intent will cause a crash. + // Create a generic intent to open a chooser. + Intent genericFileChooserIntent = new Intent(Intent.ACTION_GET_CONTENT); - public void viewSslCertificate(View view) { - // Show the `ViewSslCertificateDialog` `AlertDialog` and name this instance `@string/view_ssl_certificate`. - DialogFragment viewSslCertificateDialogFragment = new ViewSslCertificateDialog(); - viewSslCertificateDialogFragment.show(getFragmentManager(), getString(R.string.view_ssl_certificate)); - } + // Request an openable file. + genericFileChooserIntent.addCategory(Intent.CATEGORY_OPENABLE); - @Override - public void onSslErrorCancel() { - sslErrorHandler.cancel(); - } + // Set the file type to everything. + genericFileChooserIntent.setType("*/*"); - @Override - public void onSslErrorProceed() { - sslErrorHandler.proceed(); - } + // Start the generic file chooser intent. + startActivityForResult(genericFileChooserIntent, BROWSE_FILE_UPLOAD_REQUEST_CODE); + } + } + return true; + } + }); - @Override - public void onSslMismatchBack() { - if (mainWebView.canGoBack()) { // There is a back page in the history. - // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded. - navigatingHistory = true; + nestedScrollWebView.setWebViewClient(new WebViewClient() { + // `shouldOverrideUrlLoading` makes this WebView the default handler for URLs inside the app, so that links are not kicked out to other apps. + // The deprecated `shouldOverrideUrlLoading` must be used until API >= 24. + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + // Sanitize the url. + url = sanitizeUrl(url); - // Go back. - mainWebView.goBack(); - } else { // There are no pages to go back to. - // Load a blank page - loadUrl(""); - } - } + // Handle the URL according to the type. + if (url.startsWith("http")) { // Load the URL in Privacy Browser. + // Apply the domain settings for the new URL. This doesn't do anything if the domain has not changed. + applyDomainSettings(nestedScrollWebView, url, true, false); - @Override - public void onSslMismatchProceed() { - // Do not check the pinned SSL certificate for this domain again until the domain changes. - ignorePinnedSslCertificate = true; - } + // Load the URL. By using `loadUrl()`, instead of `loadUrlFromBase()`, the Referer header will never be sent. + nestedScrollWebView.loadUrl(url, customHeaders); - @Override - public void onUrlHistoryEntrySelected(int moveBackOrForwardSteps) { - // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded. - navigatingHistory = true; + // Returning true indicates that Privacy Browser is manually handling the loading of the URL. + // Custom headers cannot be added if false is returned and the WebView handles the loading of the URL. + return true; + } else if (url.startsWith("mailto:")) { // Load the email address in an external email program. + // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched. + Intent emailIntent = new Intent(Intent.ACTION_SENDTO); - // Load the history entry. - mainWebView.goBackOrForward(moveBackOrForwardSteps); - } + // Parse the url and set it as the data for the intent. + emailIntent.setData(Uri.parse(url)); - @Override - public void onClearHistory() { - // Clear the history. - mainWebView.clearHistory(); - } + // Open the email program in a new task instead of as part of Privacy Browser. + emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - // Override `onBackPressed` to handle the navigation drawer and `mainWebView`. - @Override - public void onBackPressed() { - // Close the navigation drawer if it is available. GravityCompat.START is the drawer on the left on Left-to-Right layout text. - if (drawerLayout.isDrawerVisible(GravityCompat.START)) { - drawerLayout.closeDrawer(GravityCompat.START); - } else { - // Load the previous URL if available. - if (mainWebView.canGoBack()) { - // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded. - navigatingHistory = true; + // Make it so. + startActivity(emailIntent); - // Go back. - mainWebView.goBack(); - } else { - // Pass `onBackPressed()` to the system. - super.onBackPressed(); - } - } - } + // Returning true indicates Privacy Browser is handling the URL by creating an intent. + return true; + } else if (url.startsWith("tel:")) { // Load the phone number in the dialer. + // Open the dialer and load the phone number, but wait for the user to place the call. + Intent dialIntent = new Intent(Intent.ACTION_DIAL); - private void loadUrlFromTextBox() throws UnsupportedEncodingException { - // Get the text from urlTextBox and convert it to a string. trim() removes white spaces from the beginning and end of the string. - String unformattedUrlString = urlTextBox.getText().toString().trim(); + // Add the phone number to the intent. + dialIntent.setData(Uri.parse(url)); - // Check to see if `unformattedUrlString` is a valid URL. Otherwise, convert it into a search. - if ((Patterns.WEB_URL.matcher(unformattedUrlString).matches()) || (unformattedUrlString.startsWith("http://")) || (unformattedUrlString.startsWith("https://"))) { - // Add `http://` at the beginning if it is missing. Otherwise the app will segfault. - if (!unformattedUrlString.startsWith("http")) { - unformattedUrlString = "http://" + unformattedUrlString; - } + // Open the dialer in a new task instead of as part of Privacy Browser. + dialIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - // Initialize `unformattedUrl`. - URL unformattedUrl = null; + // Make it so. + startActivity(dialIntent); - // 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. - try { - unformattedUrl = new URL(unformattedUrlString); - } catch (MalformedURLException e) { - e.printStackTrace(); - } + // Returning true indicates Privacy Browser is handling the URL by creating an intent. + return true; + } else { // Load a system chooser to select an app that can handle the URL. + // Open an app that can handle the URL. + Intent genericIntent = new Intent(Intent.ACTION_VIEW); - // The ternary operator (? :) makes sure that a null pointer exception is not thrown, which would happen if `.get` was called on a `null` value. - final String scheme = unformattedUrl != null ? unformattedUrl.getProtocol() : null; - final String authority = unformattedUrl != null ? unformattedUrl.getAuthority() : null; - final String path = unformattedUrl != null ? unformattedUrl.getPath() : null; - final String query = unformattedUrl != null ? unformattedUrl.getQuery() : null; - final String fragment = unformattedUrl != null ? unformattedUrl.getRef() : null; + // Add the URL to the intent. + genericIntent.setData(Uri.parse(url)); - // Build the URI. - Uri.Builder formattedUri = new Uri.Builder(); - formattedUri.scheme(scheme).authority(authority).path(path).query(query).fragment(fragment); + // List all apps that can handle the URL instead of just opening the first one. + genericIntent.addCategory(Intent.CATEGORY_BROWSABLE); - // Decode `formattedUri` as a `String` in `UTF-8`. - formattedUrlString = URLDecoder.decode(formattedUri.build().toString(), "UTF-8"); - } else { - // Sanitize the search input and convert it to a search. - final String encodedUrlString = URLEncoder.encode(unformattedUrlString, "UTF-8"); + // Open the app in a new task instead of as part of Privacy Browser. + genericIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - // Add the base search URL. - formattedUrlString = searchURL + encodedUrlString; - } + // Start the app or display a snackbar if no app is available to handle the URL. + try { + startActivity(genericIntent); + } catch (ActivityNotFoundException exception) { + Snackbar.make(nestedScrollWebView, getString(R.string.unrecognized_url) + " " + url, Snackbar.LENGTH_SHORT).show(); + } - loadUrl(formattedUrlString); + // Returning true indicates Privacy Browser is handling the URL by creating an intent. + return true; + } + } - // Hide the keyboard so we can see the webpage. `0` indicates no additional flags. - inputMethodManager.hideSoftInputFromWindow(mainWebView.getWindowToken(), 0); - } + // Check requests against the block lists. The deprecated `shouldInterceptRequest()` must be used until minimum API >= 21. + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, String url) { + // Check to see if the resource request is for the main URL. + if (url.equals(nestedScrollWebView.getCurrentUrl())) { + // `return null` loads the resource request, which should never be blocked if it is the main URL. + return null; + } + // Wait until the blocklists have been populated. When Privacy Browser is being resumed after having the process killed in the background it will try to load the URLs immediately. + while (ultraPrivacy == null) { + // The wait must be synchronized, which only lets one thread run on it at a time, or `java.lang.IllegalMonitorStateException` is thrown. + synchronized (this) { + try { + // Check to see if the blocklists have been populated after 100 ms. + wait(100); + } catch (InterruptedException exception) { + // Do nothing. + } + } + } - private void loadUrl(String url) { - // Apply any custom domain settings. - applyDomainSettings(url); + // Sanitize the URL. + url = sanitizeUrl(url); - // Load the URL. - mainWebView.loadUrl(url, customHeaders); + // Get a handle for the navigation view. + NavigationView navigationView = findViewById(R.id.navigationview); - // Set `urlIsLoading` to prevent changes in the user agent on websites with redirects from reloading the current website. - urlIsLoading = true; - } + // Get a handle for the navigation menu. + Menu navigationMenu = navigationView.getMenu(); - public void findPreviousOnPage(View view) { - // Go to the previous highlighted phrase on the page. `false` goes backwards instead of forwards. - mainWebView.findNext(false); - } + // Get a handle for the navigation requests menu item. + MenuItem navigationRequestsMenuItem = navigationMenu.findItem(R.id.requests); - public void findNextOnPage(View view) { - // Go to the next highlighted phrase on the page. `true` goes forwards instead of backwards. - mainWebView.findNext(true); - } + // Create an empty web resource response to be used if the resource request is blocked. + WebResourceResponse emptyWebResourceResponse = new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes())); - public void closeFindOnPage(View view) { - // Delete the contents of `find_on_page_edittext`. - findOnPageEditText.setText(null); + // Reset the whitelist results tracker. + String[] whitelistResultStringArray = null; - // Clear the highlighted phrases. - mainWebView.clearMatches(); + // Initialize the third party request tracker. + boolean isThirdPartyRequest = false; - // Hide the Find on Page `RelativeLayout`. - findOnPageLinearLayout.setVisibility(View.GONE); + // Get the current URL. `.getUrl()` throws an error because operations on the WebView cannot be made from this thread. + String currentBaseDomain = nestedScrollWebView.getCurrentDomainName(); - // Show the URL app bar. - supportAppBar.setVisibility(View.VISIBLE); + // Store a copy of the current domain for use in later requests. + String currentDomain = currentBaseDomain; - // Hide the keyboard so we can see the webpage. `0` indicates no additional flags. - inputMethodManager.hideSoftInputFromWindow(mainWebView.getWindowToken(), 0); - } + // Nobody is happy when comparing null strings. + if ((currentBaseDomain != null) && (url != null)) { + // Convert the request URL to a URI. + Uri requestUri = Uri.parse(url); - private void applyAppSettings() { - // Get a handle for `sharedPreferences`. `this` references the current context. - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + // Get the request host name. + String requestBaseDomain = requestUri.getHost(); - // Store the values from `sharedPreferences` in variables. - String homepageString = sharedPreferences.getString("homepage", "https://start.duckduckgo.com"); - String torHomepageString = sharedPreferences.getString("tor_homepage", "https://3g2upl4pq6kufc4m.onion"); - String torSearchString = sharedPreferences.getString("tor_search", "https://3g2upl4pq6kufc4m.onion/html/?q="); - String torSearchCustomURLString = sharedPreferences.getString("tor_search_custom_url", ""); - String searchString = sharedPreferences.getString("search", "https://duckduckgo.com/html/?q="); - String searchCustomURLString = sharedPreferences.getString("search_custom_url", ""); - adBlockerEnabled = sharedPreferences.getBoolean("block_ads", true); - incognitoModeEnabled = sharedPreferences.getBoolean("incognito_mode", false); - boolean doNotTrackEnabled = sharedPreferences.getBoolean("do_not_track", false); - boolean proxyThroughOrbot = sharedPreferences.getBoolean("proxy_through_orbot", false); - fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean("full_screen_browsing_mode", false); - hideSystemBarsOnFullscreen = sharedPreferences.getBoolean("hide_system_bars", false); - translucentNavigationBarOnFullscreen = sharedPreferences.getBoolean("translucent_navigation_bar", true); - swipeToRefreshEnabled = sharedPreferences.getBoolean("swipe_to_refresh", false); - displayWebpageImagesBoolean = sharedPreferences.getBoolean("display_webpage_images", true); + // Only check for third-party requests if the current base domain is not empty and the request domain is not null. + if (!currentBaseDomain.isEmpty() && (requestBaseDomain != null)) { + // Determine the current base domain. + while (currentBaseDomain.indexOf(".", currentBaseDomain.indexOf(".") + 1) > 0) { // There is at least one subdomain. + // Remove the first subdomain. + currentBaseDomain = currentBaseDomain.substring(currentBaseDomain.indexOf(".") + 1); + } - // Set the homepage, search, and proxy options. - if (proxyThroughOrbot) { // Set the Tor options. - // Set `torHomepageString` as `homepage`. - homepage = torHomepageString; + // Determine the request base domain. + while (requestBaseDomain.indexOf(".", requestBaseDomain.indexOf(".") + 1) > 0) { // There is at least one subdomain. + // Remove the first subdomain. + requestBaseDomain = requestBaseDomain.substring(requestBaseDomain.indexOf(".") + 1); + } - // If formattedUrlString is null assign the homepage to it. - if (formattedUrlString == null) { - formattedUrlString = homepage; - } + // Update the third party request tracker. + isThirdPartyRequest = !currentBaseDomain.equals(requestBaseDomain); + } + } - // Set the search URL. - if (torSearchString.equals("Custom URL")) { // Get the custom URL string. - searchURL = torSearchCustomURLString; - } else { // Use the string from the pre-built list. - searchURL = torSearchString; - } + // Get the current WebView page position. + int webViewPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId()); + + // Determine if the WebView is currently displayed. + boolean webViewDisplayed = (webViewPagePosition == tabLayout.getSelectedTabPosition()); + + // Block third-party requests if enabled. + if (isThirdPartyRequest && nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS)) { + // Add the result to the resource requests. + nestedScrollWebView.addResourceRequest(new String[]{BlocklistHelper.REQUEST_THIRD_PARTY, url}); + + // Increment the blocked requests counters. + nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS); + nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS); + + // Update the titles of the blocklist menu items if the WebView is currently displayed. + if (webViewDisplayed) { + // Updating the UI must be run from the UI thread. + activity.runOnUiThread(() -> { + // Update the menu item titles. + navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); + + // Update the options menu if it has been populated. + if (optionsMenu != null) { + optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); + optionsMenu.findItem(R.id.block_all_third_party_requests).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " + + getString(R.string.block_all_third_party_requests)); + } + }); + } - // Set the proxy. `this` refers to the current activity where an `AlertDialog` might be displayed. - OrbotProxyHelper.setProxy(getApplicationContext(), this, "localhost", "8118"); + // Return an empty web resource response. + return emptyWebResourceResponse; + } - // Set the `appBar` background to indicate proxying through Orbot is enabled. `this` refers to the context. - if (darkTheme) { - appBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.dark_blue_30)); - } else { - appBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.blue_50)); - } + // Check UltraList if it is enabled. + if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.ULTRALIST)) { + // Check the URL against UltraList. + String[] ultraListResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, ultraList); + + // Process the UltraList results. + if (ultraListResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) { // The resource request matched UltraLists's blacklist. + // Add the result to the resource requests. + nestedScrollWebView.addResourceRequest(new String[] {ultraListResults[0], ultraListResults[1], ultraListResults[2], ultraListResults[3], ultraListResults[4], ultraListResults[5]}); + + // Increment the blocked requests counters. + nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS); + nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.ULTRALIST); + + // Update the titles of the blocklist menu items if the WebView is currently displayed. + if (webViewDisplayed) { + // Updating the UI must be run from the UI thread. + activity.runOnUiThread(() -> { + // Update the menu item titles. + navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); + + // Update the options menu if it has been populated. + if (optionsMenu != null) { + optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); + optionsMenu.findItem(R.id.ultralist).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.ULTRALIST) + " - " + getString(R.string.ultralist)); + } + }); + } - // Display a message to the user if we are waiting on Orbot. - if (!orbotStatus.equals("ON")) { - // Set `waitingForOrbot`. - waitingForOrbot = true; + // The resource request was blocked. Return an empty web resource response. + return emptyWebResourceResponse; + } else if (ultraListResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) { // The resource request matched UltraList's whitelist. + // Add a whitelist entry to the resource requests array. + nestedScrollWebView.addResourceRequest(new String[] {ultraListResults[0], ultraListResults[1], ultraListResults[2], ultraListResults[3], ultraListResults[4], ultraListResults[5]}); - // Load a waiting page. `null` specifies no encoding, which defaults to ASCII. - mainWebView.loadData(waitingForOrbotHTMLString, "text/html", null); - } - } else { // Set the non-Tor options. - // Set `homepageString` as `homepage`. - homepage = homepageString; + // The resource request has been allowed by UltraPrivacy. `return null` loads the requested resource. + return null; + } + } - // If formattedUrlString is null assign the homepage to it. - if (formattedUrlString == null) { - formattedUrlString = homepage; - } + // Check UltraPrivacy if it is enabled. + if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.ULTRAPRIVACY)) { + // Check the URL against UltraPrivacy. + String[] ultraPrivacyResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, ultraPrivacy); + + // Process the UltraPrivacy results. + if (ultraPrivacyResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) { // The resource request matched UltraPrivacy's blacklist. + // Add the result to the resource requests. + nestedScrollWebView.addResourceRequest(new String[] {ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4], + ultraPrivacyResults[5]}); + + // Increment the blocked requests counters. + nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS); + nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.ULTRAPRIVACY); + + // Update the titles of the blocklist menu items if the WebView is currently displayed. + if (webViewDisplayed) { + // Updating the UI must be run from the UI thread. + activity.runOnUiThread(() -> { + // Update the menu item titles. + navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); + + // Update the options menu if it has been populated. + if (optionsMenu != null) { + optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); + optionsMenu.findItem(R.id.ultraprivacy).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.ULTRAPRIVACY) + " - " + getString(R.string.ultraprivacy)); + } + }); + } - // Set the search URL. - if (searchString.equals("Custom URL")) { // Get the custom URL string. - searchURL = searchCustomURLString; - } else { // Use the string from the pre-built list. - searchURL = searchString; - } + // The resource request was blocked. Return an empty web resource response. + return emptyWebResourceResponse; + } else if (ultraPrivacyResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) { // The resource request matched UltraPrivacy's whitelist. + // Add a whitelist entry to the resource requests array. + nestedScrollWebView.addResourceRequest(new String[] {ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4], + ultraPrivacyResults[5]}); - // Reset the proxy to default. The host is `""` and the port is `"0"`. - OrbotProxyHelper.setProxy(getApplicationContext(), this, "", "0"); + // The resource request has been allowed by UltraPrivacy. `return null` loads the requested resource. + return null; + } + } - // Set the default `appBar` background. `this` refers to the context. - if (darkTheme) { - appBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.gray_900)); - } else { - appBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.gray_100)); - } + // Check EasyList if it is enabled. + if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.EASYLIST)) { + // Check the URL against EasyList. + String[] easyListResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, easyList); + + // Process the EasyList results. + if (easyListResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) { // The resource request matched EasyList's blacklist. + // Add the result to the resource requests. + nestedScrollWebView.addResourceRequest(new String[] {easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5]}); + + // Increment the blocked requests counters. + nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS); + nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.EASYLIST); + + // Update the titles of the blocklist menu items if the WebView is currently displayed. + if (webViewDisplayed) { + // Updating the UI must be run from the UI thread. + activity.runOnUiThread(() -> { + // Update the menu item titles. + navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); + + // Update the options menu if it has been populated. + if (optionsMenu != null) { + optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); + optionsMenu.findItem(R.id.easylist).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.EASYLIST) + " - " + getString(R.string.easylist)); + } + }); + } - // Reset `waitingForOrbot. - waitingForOrbot = false; - } + // The resource request was blocked. Return an empty web resource response. + return emptyWebResourceResponse; + } else if (easyListResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) { // The resource request matched EasyList's whitelist. + // Update the whitelist result string array tracker. + whitelistResultStringArray = new String[] {easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5]}; + } + } - // Set swipe to refresh. - swipeRefreshLayout.setEnabled(swipeToRefreshEnabled); + // Check EasyPrivacy if it is enabled. + if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.EASYPRIVACY)) { + // Check the URL against EasyPrivacy. + String[] easyPrivacyResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, easyPrivacy); + + // Process the EasyPrivacy results. + if (easyPrivacyResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) { // The resource request matched EasyPrivacy's blacklist. + // Add the result to the resource requests. + nestedScrollWebView.addResourceRequest(new String[] {easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[4], + easyPrivacyResults[5]}); + + // Increment the blocked requests counters. + nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS); + nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.EASYPRIVACY); + + // Update the titles of the blocklist menu items if the WebView is currently displayed. + if (webViewDisplayed) { + // Updating the UI must be run from the UI thread. + activity.runOnUiThread(() -> { + // Update the menu item titles. + navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); + + // Update the options menu if it has been populated. + if (optionsMenu != null) { + optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); + optionsMenu.findItem(R.id.easyprivacy).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.EASYPRIVACY) + " - " + getString(R.string.easyprivacy)); + } + }); + } - // Set Do Not Track status. - if (doNotTrackEnabled) { - customHeaders.put("DNT", "1"); - } else { - customHeaders.remove("DNT"); - } + // The resource request was blocked. Return an empty web resource response. + return emptyWebResourceResponse; + } else if (easyPrivacyResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) { // The resource request matched EasyPrivacy's whitelist. + // Update the whitelist result string array tracker. + whitelistResultStringArray = new String[] {easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[4], easyPrivacyResults[5]}; + } + } - // Apply the appropriate full screen mode the `SYSTEM_UI` flags. - if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { - if (hideSystemBarsOnFullscreen) { // Hide everything. - // Remove the translucent navigation setting if it is currently flagged. - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + // Check Fanboy’s Annoyance List if it is enabled. + if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST)) { + // Check the URL against Fanboy's Annoyance List. + String[] fanboysAnnoyanceListResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, fanboysAnnoyanceList); + + // Process the Fanboy's Annoyance List results. + if (fanboysAnnoyanceListResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) { // The resource request matched Fanboy's Annoyance List's blacklist. + // Add the result to the resource requests. + nestedScrollWebView.addResourceRequest(new String[] {fanboysAnnoyanceListResults[0], fanboysAnnoyanceListResults[1], fanboysAnnoyanceListResults[2], fanboysAnnoyanceListResults[3], + fanboysAnnoyanceListResults[4], fanboysAnnoyanceListResults[5]}); + + // Increment the blocked requests counters. + nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS); + nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST); + + // Update the titles of the blocklist menu items if the WebView is currently displayed. + if (webViewDisplayed) { + // Updating the UI must be run from the UI thread. + activity.runOnUiThread(() -> { + // Update the menu item titles. + navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); + + // Update the options menu if it has been populated. + if (optionsMenu != null) { + optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); + optionsMenu.findItem(R.id.fanboys_annoyance_list).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST) + " - " + + getString(R.string.fanboys_annoyance_list)); + } + }); + } - // Remove the translucent status bar overlay. - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + // The resource request was blocked. Return an empty web resource response. + return emptyWebResourceResponse; + } else if (fanboysAnnoyanceListResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)){ // The resource request matched Fanboy's Annoyance List's whitelist. + // Update the whitelist result string array tracker. + whitelistResultStringArray = new String[] {fanboysAnnoyanceListResults[0], fanboysAnnoyanceListResults[1], fanboysAnnoyanceListResults[2], fanboysAnnoyanceListResults[3], + fanboysAnnoyanceListResults[4], fanboysAnnoyanceListResults[5]}; + } + } else if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST)) { // Only check Fanboy’s Social Blocking List if Fanboy’s Annoyance List is disabled. + // Check the URL against Fanboy's Annoyance List. + String[] fanboysSocialListResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, fanboysSocialList); + + // Process the Fanboy's Social Blocking List results. + if (fanboysSocialListResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) { // The resource request matched Fanboy's Social Blocking List's blacklist. + // Add the result to the resource requests. + nestedScrollWebView.addResourceRequest(new String[] {fanboysSocialListResults[0], fanboysSocialListResults[1], fanboysSocialListResults[2], fanboysSocialListResults[3], + fanboysSocialListResults[4], fanboysSocialListResults[5]}); + + // Increment the blocked requests counters. + nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS); + nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST); + + // Update the titles of the blocklist menu items if the WebView is currently displayed. + if (webViewDisplayed) { + // Updating the UI must be run from the UI thread. + activity.runOnUiThread(() -> { + // Update the menu item titles. + navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); + + // Update the options menu if it has been populated. + if (optionsMenu != null) { + optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); + optionsMenu.findItem(R.id.fanboys_social_blocking_list).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST) + " - " + + getString(R.string.fanboys_social_blocking_list)); + } + }); + } - // Remove the translucent status bar overlay on the `Drawer Layout`, which is special and needs its own command. - drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + // The resource request was blocked. Return an empty web resource response. + return emptyWebResourceResponse; + } else if (fanboysSocialListResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) { // The resource request matched Fanboy's Social Blocking List's whitelist. + // Update the whitelist result string array tracker. + whitelistResultStringArray = new String[] {fanboysSocialListResults[0], fanboysSocialListResults[1], fanboysSocialListResults[2], fanboysSocialListResults[3], + fanboysSocialListResults[4], fanboysSocialListResults[5]}; + } + } - /* SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen. - * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen. - * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically rehides them after they are shown. - */ - rootCoordinatorLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); - } else { // Hide everything except the status and navigation bars. - // Add the translucent status flag if it is unset. - getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); - - if (translucentNavigationBarOnFullscreen) { - // Set the navigation bar to be translucent. - getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); - } else { - // Set the navigation bar to be black. - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + // Add the request to the log because it hasn't been processed by any of the previous checks. + if (whitelistResultStringArray != null) { // The request was processed by a whitelist. + nestedScrollWebView.addResourceRequest(whitelistResultStringArray); + } else { // The request didn't match any blocklist entry. Log it as a default request. + nestedScrollWebView.addResourceRequest(new String[]{BlocklistHelper.REQUEST_DEFAULT, url}); } - } - } else { // Switch to normal viewing mode. - // Reset `inFullScreenBrowsingMode` to `false`. - inFullScreenBrowsingMode = false; - // Show the `appBar` if `findOnPageLinearLayout` is not visible. - if (findOnPageLinearLayout.getVisibility() == View.GONE) { - appBar.show(); + // The resource request has not been blocked. `return null` loads the requested resource. + return null; } - // Show the `BannerAd` in the free flavor. - if (BuildConfig.FLAVOR.contentEquals("free")) { - // Reload the ad. Because the screen may have rotated, we need to use `reloadAfterRotate`. - BannerAd.reloadAfterRotate(adView, getApplicationContext(), getString(R.string.ad_id)); - - // Reinitialize the `adView` variable, as the `View` will have been removed and re-added by `BannerAd.reloadAfterRotate()`. - adView = findViewById(R.id.adview); - } + // Handle HTTP authentication requests. + @Override + public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) { + // Store the handler. + nestedScrollWebView.setHttpAuthHandler(handler); - // Remove the translucent navigation bar flag if it is set. - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + // Instantiate an HTTP authentication dialog. + DialogFragment httpAuthenticationDialogFragment = HttpAuthenticationDialog.displayDialog(host, realm, nestedScrollWebView.getWebViewFragmentId()); - // Add the translucent status flag if it is unset. This also resets `drawerLayout's` `View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN`. - getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + // Show the HTTP authentication dialog. + httpAuthenticationDialogFragment.show(getSupportFragmentManager(), getString(R.string.http_authentication)); + } - // Remove any `SYSTEM_UI` flags from `rootCoordinatorLayout`. - rootCoordinatorLayout.setSystemUiVisibility(0); + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + // Get the preferences. + boolean scrollAppBar = sharedPreferences.getBoolean("scroll_app_bar", true); - // Constrain `rootCoordinatorLayout` inside the status and navigation bars. - rootCoordinatorLayout.setFitsSystemWindows(true); - } - } + // Set the top padding of the swipe refresh layout according to the app bar scrolling preference. This can't be done in `appAppSettings()` because the app bar is not yet populated there. + if (scrollAppBar || (inFullScreenBrowsingMode && hideAppBar)) { + // No padding is needed because it will automatically be placed below the app bar layout due to the scrolling layout behavior. + swipeRefreshLayout.setPadding(0, 0, 0, 0); - // We have to use the deprecated `.getDrawable()` until the minimum API >= 21. - @SuppressWarnings("deprecation") - private void applyDomainSettings(String url) { - // Reset `navigatingHistory`. - navigatingHistory = false; + // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels. + swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10, defaultProgressViewEndOffset); + } else { + // Get the app bar layout height. This can't be done in `applyAppSettings()` because the app bar is not yet populated there. + appBarHeight = appBarLayout.getHeight(); - // Parse the URL into a URI. - Uri uri = Uri.parse(url); + // The swipe refresh layout must be manually moved below the app bar layout. + swipeRefreshLayout.setPadding(0, appBarHeight, 0, 0); - // Extract the domain from `uri`. - String hostName = uri.getHost(); + // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels. + swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10 + appBarHeight, defaultProgressViewEndOffset + appBarHeight); + } - // Initialize `loadingNewDomainName`. - boolean loadingNewDomainName; + // Reset the list of resource requests. + nestedScrollWebView.clearResourceRequests(); - // If either `hostName` or `currentDomainName` are `null`, run the options for loading a new domain name. - // The lint suggestion to simplify the `if` statement is incorrect, because `hostName.equals(currentDomainName)` can produce a `null object reference.` - //noinspection SimplifiableIfStatement - if ((hostName == null) || (currentDomainName == null)) { - loadingNewDomainName = true; - } else { // Determine if `hostName` equals `currentDomainName`. - loadingNewDomainName = !hostName.equals(currentDomainName); - } + // Reset the requests counters. + nestedScrollWebView.resetRequestsCounters(); - // Only apply the domain settings if we are loading a new domain. This allows the user to set temporary settings for JavaScript, cookies, DOM storage, etc. - if (loadingNewDomainName) { - // Set the new `hostname` as the `currentDomainName`. - currentDomainName = hostName; + // Hide the keyboard. + inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0); - // Reset `ignorePinnedSslCertificate`. - ignorePinnedSslCertificate = false; + // Get the current page position. + int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId()); - // Reset `favoriteIconBitmap` and display it in the `appbar`. - favoriteIconBitmap = favoriteIconDefaultBitmap; - favoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(favoriteIconBitmap, 64, 64, true)); + // Update the URL text bar if the page is currently selected. + if (tabLayout.getSelectedTabPosition() == currentPagePosition) { + // Clear the focus from the URL edit text. + urlEditText.clearFocus(); - // Initialize the database handler. `this` specifies the context. The two `nulls` do not specify the database name or a `CursorFactory`. - // The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`. - DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0); + // Display the formatted URL text. + urlEditText.setText(url); - // Get a full cursor from `domainsDatabaseHelper`. - Cursor domainNameCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomain(); + // Apply text highlighting to `urlTextBox`. + highlightUrlText(); + } - // Initialize `domainSettingsSet`. - Set domainSettingsSet = new HashSet<>(); + // Reset the list of host IP addresses. + nestedScrollWebView.clearCurrentIpAddresses(); - // Get the domain name column index. - int domainNameColumnIndex = domainNameCursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME); + // Get a URI for the current URL. + Uri currentUri = Uri.parse(url); - // Populate `domainSettingsSet`. - for (int i = 0; i < domainNameCursor.getCount(); i++) { - // Move `domainsCursor` to the current row. - domainNameCursor.moveToPosition(i); + // Get the IP addresses for the host. + new GetHostIpAddresses(activity, getSupportFragmentManager(), nestedScrollWebView).execute(currentUri.getHost()); - // Store the domain name in `domainSettingsSet`. - domainSettingsSet.add(domainNameCursor.getString(domainNameColumnIndex)); - } + // Replace Refresh with Stop if the options menu has been created. (The first WebView typically begins loading before the menu items are instantiated.) + if (optionsMenu != null) { + // Get a handle for the refresh menu item. + MenuItem refreshMenuItem = optionsMenu.findItem(R.id.refresh); - // Close `domainNameCursor. - domainNameCursor.close(); + // Set the title. + refreshMenuItem.setTitle(R.string.stop); - // Initialize variables to track if domain settings will be applied and, if so, under which name. - domainSettingsApplied = false; - String domainNameInDatabase = null; + // Get the app bar and theme preferences. + boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false); - // Check the hostname. - if (domainSettingsSet.contains(hostName)) { - domainSettingsApplied = true; - domainNameInDatabase = hostName; - } + // If the icon is displayed in the AppBar, set it according to the theme. + if (displayAdditionalAppBarIcons) { + // Get the current theme status. + int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; - // If `hostName` is not `null`, check all the subdomains of `hostName` against wildcard domains in `domainCursor`. - if (hostName != null) { - while (hostName.contains(".") && !domainSettingsApplied) { // Stop checking if we run out of `.` or if we already know that `domainSettingsApplied` is `true`. - if (domainSettingsSet.contains("*." + hostName)) { // Check the host name prepended by `*.`. - domainSettingsApplied = true; - domainNameInDatabase = "*." + hostName; + // Set the stop icon according to the theme. + if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { + refreshMenuItem.setIcon(R.drawable.close_day); + } else { + refreshMenuItem.setIcon(R.drawable.close_night); + } } - - // Strip out the lowest subdomain of `host`. - hostName = hostName.substring(hostName.indexOf(".") + 1); } } - // Get a handle for the shared preference. `this` references the current context. - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + @Override + public void onPageFinished(WebView view, String url) { + // Flush any cookies to persistent storage. The cookie manager has become very lazy about flushing cookies in recent versions. + if (nestedScrollWebView.getAcceptFirstPartyCookies() && Build.VERSION.SDK_INT >= 21) { + CookieManager.getInstance().flush(); + } - // Store the general preference information. - String defaultFontSizeString = sharedPreferences.getString("default_font_size", "100"); - String defaultUserAgentString = sharedPreferences.getString("user_agent", "PrivacyBrowser/1.0"); - String defaultCustomUserAgentString = sharedPreferences.getString("custom_user_agent", "PrivacyBrowser/1.0"); - nightMode = sharedPreferences.getBoolean("night_mode", false); + // Update the Refresh menu item if the options menu has been created. + if (optionsMenu != null) { + // Get a handle for the refresh menu item. + MenuItem refreshMenuItem = optionsMenu.findItem(R.id.refresh); - if (domainSettingsApplied) { // The url we are loading has custom domain settings. - // Get a cursor for the current host and move it to the first position. - Cursor currentHostDomainSettingsCursor = domainsDatabaseHelper.getCursorForDomainName(domainNameInDatabase); - currentHostDomainSettingsCursor.moveToFirst(); + // Reset the Refresh title. + refreshMenuItem.setTitle(R.string.refresh); - // Get the settings from the cursor. - domainSettingsDatabaseId = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper._ID))); - javaScriptEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)) == 1); - firstPartyCookiesEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FIRST_PARTY_COOKIES)) == 1); - thirdPartyCookiesEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_THIRD_PARTY_COOKIES)) == 1); - domStorageEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_DOM_STORAGE)) == 1); - saveFormDataEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FORM_DATA)) == 1); - String userAgentString = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.USER_AGENT)); - int fontSize = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.FONT_SIZE)); - displayWebpageImagesInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.DISPLAY_IMAGES)); - int nightModeInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.NIGHT_MODE)); - pinnedDomainSslCertificate = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)) == 1); - pinnedDomainSslIssuedToCNameString = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME)); - pinnedDomainSslIssuedToONameString = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION)); - pinnedDomainSslIssuedToUNameString = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT)); - pinnedDomainSslIssuedByCNameString = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME)); - pinnedDomainSslIssuedByONameString = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION)); - pinnedDomainSslIssuedByUNameString = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT)); - - // Set `nightMode` according to `nightModeInt`. If `nightModeInt` is `DomainsDatabaseHelper.NIGHT_MODE_SYSTEM_DEFAULT` the current setting from `sharedPreferences` will be used. - switch (nightModeInt) { - case DomainsDatabaseHelper.NIGHT_MODE_ENABLED: - nightMode = true; - break; + // Get the app bar and theme preferences. + boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false); - case DomainsDatabaseHelper.NIGHT_MODE_DISABLED: - nightMode = false; - break; - } + // If the icon is displayed in the app bar, reset it according to the theme. + if (displayAdditionalAppBarIcons) { + // Get the current theme status. + int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; - // Set `javaScriptEnabled` to be `true` if `night_mode` is `true`. - if (nightMode) { - javaScriptEnabled = true; + // Set the icon according to the theme. + if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { + refreshMenuItem.setIcon(R.drawable.refresh_enabled_day); + } else { + refreshMenuItem.setIcon(R.drawable.refresh_enabled_night); + } + } } - // Set the pinned SSL certificate start date to `null` if the saved date `long` is 0. - if (currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)) == 0) { - pinnedDomainSslStartDate = null; - } else { - pinnedDomainSslStartDate = new Date(currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE))); - } + // Clear the cache and history if Incognito Mode is enabled. + if (incognitoModeEnabled) { + // Clear the cache. `true` includes disk files. + nestedScrollWebView.clearCache(true); - // Set the pinned SSL certificate end date to `null` if the saved date `long` is 0. - if (currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)) == 0) { - pinnedDomainSslEndDate = null; - } else { - pinnedDomainSslEndDate = new Date(currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE))); - } + // Clear the back/forward history. + nestedScrollWebView.clearHistory(); - // Close `currentHostDomainSettingsCursor`. - currentHostDomainSettingsCursor.close(); + // Manually delete cache folders. + try { + // 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`. + String privateDataDirectoryString = getApplicationInfo().dataDir; - // Apply the domain settings. - mainWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled); - cookieManager.setAcceptCookie(firstPartyCookiesEnabled); - mainWebView.getSettings().setDomStorageEnabled(domStorageEnabled); - mainWebView.getSettings().setSaveFormData(saveFormDataEnabled); + // Delete the main cache directory. + Runtime.getRuntime().exec("rm -rf " + privateDataDirectoryString + "/cache"); - // Apply the font size. - if (fontSize == 0) { // Apply the default font size. - mainWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString)); - } else { // Apply the specified font size. - mainWebView.getSettings().setTextZoom(fontSize); + // Delete the secondary `Service Worker` cache directory. + // A `String[]` must be used because the directory contains a space and `Runtime.exec` will not escape the string correctly otherwise. + Runtime.getRuntime().exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"}); + } catch (IOException e) { + // Do nothing if an error is thrown. + } } - // Set third-party cookies status if API >= 21. - if (Build.VERSION.SDK_INT >= 21) { - cookieManager.setAcceptThirdPartyCookies(mainWebView, thirdPartyCookiesEnabled); - } - - // Only set the user agent if the webpage is not currently loading. Otherwise, changing the user agent on redirects can cause the original website to reload. - if (!urlIsLoading) { - switch (userAgentString) { - case "System default user agent": - // Set the user agent according to the system default. - switch (defaultUserAgentString) { - case "WebView default user agent": - // Set the user agent to `""`, which uses the default value. - mainWebView.getSettings().setUserAgentString(""); - break; - - case "Custom user agent": - // Set the custom user agent. - mainWebView.getSettings().setUserAgentString(defaultCustomUserAgentString); - break; - - default: - // Use the selected user agent. - mainWebView.getSettings().setUserAgentString(defaultUserAgentString); - } - break; - - case "WebView default user agent": - // Set the user agent to `""`, which uses the default value. - mainWebView.getSettings().setUserAgentString(""); - break; + // Get the current page position. + int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId()); - default: - // Use the selected user agent. - mainWebView.getSettings().setUserAgentString(userAgentString); - } + // Check the current website information against any pinned domain information if the current IP addresses have been loaded. + if ((nestedScrollWebView.hasPinnedSslCertificate() || nestedScrollWebView.hasPinnedIpAddresses()) && nestedScrollWebView.hasCurrentIpAddresses() && + !nestedScrollWebView.ignorePinnedDomainInformation()) { + CheckPinnedMismatchHelper.checkPinnedMismatch(getSupportFragmentManager(), nestedScrollWebView); } - // Set a green background on `urlTextBox` to indicate that custom domain settings are being used. We have to use the deprecated `.getDrawable()` until the minimum API >= 21. - if (darkTheme) { - urlAppBarRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_dark_blue)); - } else { - urlAppBarRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_light_green)); - } - } else { // The URL we are loading does not have custom domain settings. Load the defaults. - // Store the values from `sharedPreferences` in variables. - javaScriptEnabled = sharedPreferences.getBoolean("javascript_enabled", false); - firstPartyCookiesEnabled = sharedPreferences.getBoolean("first_party_cookies_enabled", false); - thirdPartyCookiesEnabled = sharedPreferences.getBoolean("third_party_cookies_enabled", false); - domStorageEnabled = sharedPreferences.getBoolean("dom_storage_enabled", false); - saveFormDataEnabled = sharedPreferences.getBoolean("save_form_data_enabled", false); - - // Set `javaScriptEnabled` to be `true` if `night_mode` is `true`. - if (nightMode) { - javaScriptEnabled = true; - } - - // Apply the default settings. - mainWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled); - cookieManager.setAcceptCookie(firstPartyCookiesEnabled); - mainWebView.getSettings().setDomStorageEnabled(domStorageEnabled); - mainWebView.getSettings().setSaveFormData(saveFormDataEnabled); - mainWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString)); - - // Reset the pinned SSL certificate information. - domainSettingsDatabaseId = -1; - pinnedDomainSslCertificate = false; - pinnedDomainSslIssuedToCNameString = ""; - pinnedDomainSslIssuedToONameString = ""; - pinnedDomainSslIssuedToUNameString = ""; - pinnedDomainSslIssuedByCNameString = ""; - pinnedDomainSslIssuedByONameString = ""; - pinnedDomainSslIssuedByUNameString = ""; - pinnedDomainSslStartDate = null; - pinnedDomainSslEndDate = null; + // Get the current URL from the nested scroll WebView. This is more accurate than using the URL passed into the method, which is sometimes not the final one. + String currentUrl = nestedScrollWebView.getUrl(); - // Set third-party cookies status if API >= 21. - if (Build.VERSION.SDK_INT >= 21) { - cookieManager.setAcceptThirdPartyCookies(mainWebView, thirdPartyCookiesEnabled); - } + // Get the current tab. + TabLayout.Tab tab = tabLayout.getTabAt(currentPagePosition); - // Only set the user agent if the webpage is not currently loading. Otherwise, changing the user agent on redirects can cause the original website to reload. - if (!urlIsLoading) { - switch (defaultUserAgentString) { - case "WebView default user agent": - // Set the user agent to `""`, which uses the default value. - mainWebView.getSettings().setUserAgentString(""); - break; + // Update the URL text bar if the page is currently selected and the user is not currently typing in the URL edit text. + // Crash records show that, in some crazy way, it is possible for the current URL to be blank at this point. + // Probably some sort of race condition when Privacy Browser is being resumed. + if ((tabLayout.getSelectedTabPosition() == currentPagePosition) && !urlEditText.hasFocus() && (currentUrl != null)) { + // Check to see if the URL is `about:blank`. + if (currentUrl.equals("about:blank")) { // The WebView is blank. + // Display the hint in the URL edit text. + urlEditText.setText(""); - case "Custom user agent": - // Set the custom user agent. - mainWebView.getSettings().setUserAgentString(defaultCustomUserAgentString); - break; + // Request focus for the URL text box. + urlEditText.requestFocus(); - default: - // Use the selected user agent. - mainWebView.getSettings().setUserAgentString(defaultUserAgentString); - } - } + // Display the keyboard. + inputMethodManager.showSoftInput(urlEditText, 0); - // Set a transparent background on `urlTextBox`. We have to use the deprecated `.getDrawable()` until the minimum API >= 21. - urlAppBarRelativeLayout.setBackgroundDrawable(getResources().getDrawable(R.color.transparent)); - } + // Apply the domain settings. This clears any settings from the previous domain. + applyDomainSettings(nestedScrollWebView, "", true, false); - // Close `domainsDatabaseHelper`. - domainsDatabaseHelper.close(); + // Only populate the title text view if the tab has been fully created. + if (tab != null) { + // Get the custom view from the tab. + View tabView = tab.getCustomView(); - // Remove the `onTheFlyDisplayImagesSet` flag and set the display webpage images mode. `true` indicates that custom domain settings are applied. - onTheFlyDisplayImagesSet = false; - setDisplayWebpageImages(); + // Remove the incorrect warning below that the current tab view might be null. + assert tabView != null; - // Update the privacy icons, but only if `mainMenu` has already been populated. - if (mainMenu != null) { - updatePrivacyIcons(true); - } - } - } + // Get the title text view from the tab. + TextView tabTitleTextView = tabView.findViewById(R.id.title_textview); - private void setDisplayWebpageImages() { - if (!onTheFlyDisplayImagesSet) { - if (domainSettingsApplied) { // Custom domain settings are applied. - switch (displayWebpageImagesInt) { - case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_SYSTEM_DEFAULT: - mainWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImagesBoolean); - break; + // Set the title as the tab text. + tabTitleTextView.setText(R.string.new_tab); + } + } else { // The WebView has loaded a webpage. + // Update the URL edit text if it is not currently being edited. + if (!urlEditText.hasFocus()) { + // Sanitize the current URL. This removes unwanted URL elements that were added by redirects, so that they won't be included if the URL is shared. + String sanitizedUrl = sanitizeUrl(currentUrl); - case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_ENABLED: - mainWebView.getSettings().setLoadsImagesAutomatically(true); - break; + // Display the final URL. Getting the URL from the WebView instead of using the one provided by `onPageFinished()` makes websites like YouTube function correctly. + urlEditText.setText(sanitizedUrl); - case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_DISABLED: - mainWebView.getSettings().setLoadsImagesAutomatically(false); - break; + // Apply text highlighting to the URL. + highlightUrlText(); + } + + // Only populate the title text view if the tab has been fully created. + if (tab != null) { + // Get the custom view from the tab. + View tabView = tab.getCustomView(); + + // Remove the incorrect warning below that the current tab view might be null. + assert tabView != null; + + // Get the title text view from the tab. + TextView tabTitleTextView = tabView.findViewById(R.id.title_textview); + + // Set the title as the tab text. Sometimes `onReceivedTitle()` is not called, especially when navigating history. + tabTitleTextView.setText(nestedScrollWebView.getTitle()); + } + } } - } else { // Default settings are applied. - mainWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImagesBoolean); } - } - } - private void updatePrivacyIcons(boolean runInvalidateOptionsMenu) { - // Get handles for the icons. - MenuItem privacyIconMenuItem = mainMenu.findItem(R.id.toggle_javascript); - MenuItem firstPartyCookiesIconMenuItem = mainMenu.findItem(R.id.toggle_first_party_cookies); - MenuItem domStorageIconMenuItem = mainMenu.findItem(R.id.toggle_dom_storage); - MenuItem formDataIconMenuItem = mainMenu.findItem(R.id.toggle_save_form_data); - - // Update `privacyIcon`. - if (javaScriptEnabled) { // JavaScript is enabled. - privacyIconMenuItem.setIcon(R.drawable.javascript_enabled); - } else if (firstPartyCookiesEnabled) { // JavaScript is disabled but cookies are enabled. - privacyIconMenuItem.setIcon(R.drawable.warning); - } else { // All the dangerous features are disabled. - privacyIconMenuItem.setIcon(R.drawable.privacy_mode); - } - - // Update `firstPartyCookiesIcon`. - if (firstPartyCookiesEnabled) { // First-party cookies are enabled. - firstPartyCookiesIconMenuItem.setIcon(R.drawable.cookies_enabled); - } else { // First-party cookies are disabled. - if (darkTheme) { - firstPartyCookiesIconMenuItem.setIcon(R.drawable.cookies_disabled_dark); - } else { - firstPartyCookiesIconMenuItem.setIcon(R.drawable.cookies_disabled_light); - } - } + // Handle SSL Certificate errors. + @Override + public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { + // Get the current website SSL certificate. + SslCertificate currentWebsiteSslCertificate = error.getCertificate(); - // Update `domStorageIcon`. - if (javaScriptEnabled && domStorageEnabled) { // Both JavaScript and DOM storage are enabled. - domStorageIconMenuItem.setIcon(R.drawable.dom_storage_enabled); - } else if (javaScriptEnabled) { // JavaScript is enabled but DOM storage is disabled. - if (darkTheme) { - domStorageIconMenuItem.setIcon(R.drawable.dom_storage_disabled_dark); - } else { - domStorageIconMenuItem.setIcon(R.drawable.dom_storage_disabled_light); - } - } else { // JavaScript is disabled, so DOM storage is ghosted. - if (darkTheme) { - domStorageIconMenuItem.setIcon(R.drawable.dom_storage_ghosted_dark); - } else { - domStorageIconMenuItem.setIcon(R.drawable.dom_storage_ghosted_light); - } - } + // Extract the individual pieces of information from the current website SSL certificate. + String currentWebsiteIssuedToCName = currentWebsiteSslCertificate.getIssuedTo().getCName(); + String currentWebsiteIssuedToOName = currentWebsiteSslCertificate.getIssuedTo().getOName(); + String currentWebsiteIssuedToUName = currentWebsiteSslCertificate.getIssuedTo().getUName(); + String currentWebsiteIssuedByCName = currentWebsiteSslCertificate.getIssuedBy().getCName(); + String currentWebsiteIssuedByOName = currentWebsiteSslCertificate.getIssuedBy().getOName(); + String currentWebsiteIssuedByUName = currentWebsiteSslCertificate.getIssuedBy().getUName(); + Date currentWebsiteSslStartDate = currentWebsiteSslCertificate.getValidNotBeforeDate(); + Date currentWebsiteSslEndDate = currentWebsiteSslCertificate.getValidNotAfterDate(); - // Update `formDataIcon`. - if (saveFormDataEnabled) { // Form data is enabled. - formDataIconMenuItem.setIcon(R.drawable.form_data_enabled); - } else { // Form data is disabled. - if (darkTheme) { - formDataIconMenuItem.setIcon(R.drawable.form_data_disabled_dark); - } else { - formDataIconMenuItem.setIcon(R.drawable.form_data_disabled_light); - } - } + // Proceed to the website if the current SSL website certificate matches the pinned domain certificate. + if (nestedScrollWebView.hasPinnedSslCertificate()) { + // Get the pinned SSL certificate. + ArrayList pinnedSslCertificateArrayList = nestedScrollWebView.getPinnedSslCertificate(); + + // Extract the arrays from the array list. + String[] pinnedSslCertificateStringArray = (String[]) pinnedSslCertificateArrayList.get(0); + Date[] pinnedSslCertificateDateArray = (Date[]) pinnedSslCertificateArrayList.get(1); + + // Check if the current SSL certificate matches the pinned certificate. + if (currentWebsiteIssuedToCName.equals(pinnedSslCertificateStringArray[0]) && currentWebsiteIssuedToOName.equals(pinnedSslCertificateStringArray[1]) && + currentWebsiteIssuedToUName.equals(pinnedSslCertificateStringArray[2]) && currentWebsiteIssuedByCName.equals(pinnedSslCertificateStringArray[3]) && + currentWebsiteIssuedByOName.equals(pinnedSslCertificateStringArray[4]) && currentWebsiteIssuedByUName.equals(pinnedSslCertificateStringArray[5]) && + currentWebsiteSslStartDate.equals(pinnedSslCertificateDateArray[0]) && currentWebsiteSslEndDate.equals(pinnedSslCertificateDateArray[1])) { + + // An SSL certificate is pinned and matches the current domain certificate. Proceed to the website without displaying an error. + handler.proceed(); + } + } else { // Either there isn't a pinned SSL certificate or it doesn't match the current website certificate. + // Store the SSL error handler. + nestedScrollWebView.setSslErrorHandler(handler); - // `invalidateOptionsMenu` calls `onPrepareOptionsMenu()` and redraws the icons in the `AppBar`. `this` references the current activity. - if (runInvalidateOptionsMenu) { - ActivityCompat.invalidateOptionsMenu(this); - } - } + // Instantiate an SSL certificate error alert dialog. + DialogFragment sslCertificateErrorDialogFragment = SslCertificateErrorDialog.displayDialog(error, nestedScrollWebView.getWebViewFragmentId()); - private void highlightUrlText() { - String urlString = urlTextBox.getText().toString(); + // Show the SSL certificate error dialog. + sslCertificateErrorDialogFragment.show(getSupportFragmentManager(), getString(R.string.ssl_certificate_error)); + } + } + }); - if (urlString.startsWith("http://")) { // Highlight connections that are not encrypted. - urlTextBox.getText().setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE); - } else if (urlString.startsWith("https://")) { // Highlight connections that are encrypted. - urlTextBox.getText().setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE); - } + // Check to see if the state is being restored. + if (restoringState) { // The state is being restored. + // Resume the nested scroll WebView JavaScript timers. + nestedScrollWebView.resumeTimers(); + } else if (pageNumber == 0) { // The first page is being loaded. + // Set this nested scroll WebView as the current WebView. + currentWebView = nestedScrollWebView; + + // Initialize the URL to load string. + String urlToLoadString; + + // Get the intent that started the app. + Intent launchingIntent = getIntent(); + + // Get the information from the intent. + String launchingIntentAction = launchingIntent.getAction(); + Uri launchingIntentUriData = launchingIntent.getData(); + + // Parse the launching intent URL. + if ((launchingIntentAction != null) && launchingIntentAction.equals(Intent.ACTION_WEB_SEARCH)) { // The intent contains a search string. + // Create an encoded URL string. + String encodedUrlString; + + // Sanitize the search input and convert it to a search. + try { + encodedUrlString = URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8"); + } catch (UnsupportedEncodingException exception) { + encodedUrlString = ""; + } - // Get the index of the `/` immediately after the domain name. - int endOfDomainName = urlString.indexOf("/", (urlString.indexOf("//") + 2)); + // Store the web search as the URL to load. + urlToLoadString = searchURL + encodedUrlString; + } else if (launchingIntentUriData != null){ // The intent contains a URL. + // Store the URL. + urlToLoadString = launchingIntentUriData.toString(); + } else if (!url.equals("")) { // The activity has been restarted. + // Load the saved URL. + urlToLoadString = url; + } else { // The is no URL in the intent. + // Store the homepage to be loaded. + urlToLoadString = sharedPreferences.getString("homepage", getString(R.string.homepage_default_value)); + } - // De-emphasize the text after the domain name. - if (endOfDomainName > 0) { - urlTextBox.getText().setSpan(finalGrayColorSpan, endOfDomainName, urlString.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + // Load the website if not waiting for the proxy. + if (waitingForProxy) { // Store the URL to be loaded in the Nested Scroll WebView. + nestedScrollWebView.setWaitingForProxyUrlString(urlToLoadString); + } else { // Load the URL. + loadUrl(nestedScrollWebView, urlToLoadString); + } + } else { // This is not the first tab. + // Load the URL. + loadUrl(nestedScrollWebView, url); + + // Set the focus and display the keyboard if the URL is blank. + if (url.equals("")) { + // Request focus for the URL text box. + urlEditText.requestFocus(); + + // Create a display keyboard handler. + Handler displayKeyboardHandler = new Handler(); + + // Create a display keyboard runnable. + Runnable displayKeyboardRunnable = () -> { + // Display the keyboard. + inputMethodManager.showSoftInput(urlEditText, 0); + }; + + // Display the keyboard after 100 milliseconds, which leaves enough time for the tab to transition. + displayKeyboardHandler.postDelayed(displayKeyboardRunnable, 100); + } } } -} +} \ No newline at end of file