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=d397a619e3c0cd9a35a32aa8ef9c7a37f2fcaef6;hp=868b6d71a7c78b033e8980858410207a0e362755;hb=c0f2dcca77fffd804c90cee5103795fc3849112b;hpb=33bd447a83bd3d763ee26bbb3a3f4adb074776ed 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 868b6d71..d397a619 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java @@ -24,7 +24,6 @@ package com.stoutner.privacybrowser.activities; import android.Manifest; import android.annotation.SuppressLint; import android.app.Activity; -import android.app.DialogFragment; import android.app.DownloadManager; import android.app.SearchManager; import android.content.ActivityNotFoundException; @@ -37,7 +36,6 @@ import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Configuration; -import android.content.res.Resources; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -47,7 +45,6 @@ import android.graphics.drawable.Drawable; import android.net.Uri; import android.net.http.SslCertificate; import android.net.http.SslError; -import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Environment; @@ -55,26 +52,6 @@ import android.os.Handler; 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.FloatingActionButton; -import android.support.design.widget.NavigationView; -import android.support.design.widget.Snackbar; -import android.support.v4.app.ActivityCompat; -import android.support.v4.app.FragmentManager; -import android.support.v4.content.ContextCompat; -// `ShortcutInfoCompat`, `ShortcutManagerCompat`, and `IconCompat` can be switched to the non-compat version once API >= 26. -import android.support.v4.content.pm.ShortcutInfoCompat; -import android.support.v4.content.pm.ShortcutManagerCompat; -import android.support.v4.graphics.drawable.IconCompat; -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; @@ -94,7 +71,6 @@ import android.webkit.CookieManager; 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; @@ -114,40 +90,62 @@ import android.widget.RadioButton; import android.widget.RelativeLayout; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.ActionBarDrawerToggle; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.core.view.GravityCompat; +import androidx.drawerlayout.widget.DrawerLayout; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.FragmentManager; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import androidx.viewpager.widget.ViewPager; + +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.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.DownloadFileDialog; import com.stoutner.privacybrowser.dialogs.DownloadImageDialog; import com.stoutner.privacybrowser.dialogs.DownloadLocationPermissionDialog; import com.stoutner.privacybrowser.dialogs.EditBookmarkDialog; import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDialog; import com.stoutner.privacybrowser.dialogs.HttpAuthenticationDialog; -import com.stoutner.privacybrowser.dialogs.PinnedMismatchDialog; +import com.stoutner.privacybrowser.dialogs.SslCertificateErrorDialog; import com.stoutner.privacybrowser.dialogs.UrlHistoryDialog; import com.stoutner.privacybrowser.dialogs.ViewSslCertificateDialog; +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.views.NestedScrollWebView; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodingException; -import java.lang.ref.WeakReference; -import java.net.InetAddress; import java.net.MalformedURLException; import java.net.URL; import java.net.URLDecoder; import java.net.URLEncoder; -import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; @@ -158,131 +156,26 @@ import java.util.Set; // AppCompatActivity from android.support.v7.app.AppCompatActivity must be used to have access to the SupportActionBar until the minimum API is >= 21. public class MainWebViewActivity extends AppCompatActivity implements CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener, - CreateHomeScreenShortcutDialog.CreateHomeScreenShortcutListener, DownloadFileDialog.DownloadFileListener, DownloadImageDialog.DownloadImageListener, - DownloadLocationPermissionDialog.DownloadLocationPermissionDialogListener, EditBookmarkDialog.EditBookmarkListener, EditBookmarkFolderDialog.EditBookmarkFolderListener, - HttpAuthenticationDialog.HttpAuthenticationListener, NavigationView.OnNavigationItemSelectedListener, PinnedMismatchDialog.PinnedMismatchListener, - SslCertificateErrorDialog.SslCertificateErrorListener, UrlHistoryDialog.UrlHistoryListener { - - // `darkTheme` is public static so it can be accessed from everywhere. - public static boolean darkTheme; - - // `allowScreenshots` is public static so it can be accessed from everywhere. It is also used in `onCreate()`. - public static boolean allowScreenshots; - - // `favoriteIconBitmap` is public static so it can be accessed from `CreateHomeScreenShortcutDialog`, `BookmarksActivity`, `BookmarksDatabaseViewActivity`, `CreateBookmarkDialog`, - // `CreateBookmarkFolderDialog`, `EditBookmarkDialog`, `EditBookmarkFolderDialog`, `EditBookmarkDatabaseViewDialog`, and `ViewSslCertificateDialog`. It is also used in `onCreate()`, - // `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onCreateHomeScreenShortcutCreate()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `applyDomainSettings()`. - public static Bitmap favoriteIconBitmap; - - // `favoriteIconDefaultBitmap` public static so it can be accessed from `PinnedMismatchDialog`. It is also used in `onCreate()` and `applyDomainSettings`. - public static Bitmap favoriteIconDefaultBitmap; - - // `formattedUrlString` is public static so it can be accessed from `AddDomainDialog`, `BookmarksActivity`, `DomainSettingsFragment`, `CreateBookmarkDialog`, and `PinnedMismatchDialog`. - // It is also used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onCreateHomeScreenShortcutCreate()`, `loadUrlFromTextBox()`, and `applyProxyThroughOrbot()`. - public static String formattedUrlString; - - // `sslCertificate` is public static so it can be accessed from `DomainsActivity`, `DomainsListFragment`, `DomainSettingsFragment`, `PinnedMismatchDialog`, and `ViewSslCertificateDialog`. - // It is also used in `onCreate()` and `checkPinnedMismatch()`. - public static SslCertificate sslCertificate; - - // `currentHostIpAddresses` is public static so it can be accessed from `DomainSettingsFragment` and `ViewSslCertificateDialog`. - // It is also used in `onCreate()` and `GetHostIpAddresses()`. - public static String currentHostIpAddresses; + DownloadFileDialog.DownloadFileListener, DownloadImageDialog.DownloadImageListener, DownloadLocationPermissionDialog.DownloadLocationPermissionDialogListener, EditBookmarkDialog.EditBookmarkListener, + EditBookmarkFolderDialog.EditBookmarkFolderListener, NavigationView.OnNavigationItemSelectedListener, WebViewTabFragment.NewTabListener { // `orbotStatus` is public static so it can be accessed from `OrbotProxyHelper`. It is also used in `onCreate()`, `onResume()`, and `applyProxyThroughOrbot()`. public static String orbotStatus; - // `webViewTitle` is public static so it can be accessed from `CreateBookmarkDialog` and `CreateHomeScreenShortcutDialog`. It is also used in `onCreate()`. - public static String webViewTitle; + // The WebView pager adapter is accessed from `HttpAuthenticationDialog`, `PinnedMismatchDialog`, and `SslCertificateErrorDialog`. It is also used in `onCreate()`, `onResume()`, and `addTab()`. + public static WebViewPagerAdapter webViewPagerAdapter; - // `appliedUserAgentString` is public static so it can be accessed from `ViewSourceActivity`. It is also used in `applyDomainSettings()`. - public static String appliedUserAgentString; - - // `reloadOnRestart` is public static so it can be accessed from `SettingsFragment`. It is also used in `onRestart()` - public static boolean reloadOnRestart; - - // `reloadUrlOnRestart` is public static so it can be accessed from `SettingsFragment` and `BookmarksActivity`. It is also used in `onRestart()`. + // 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; - // The block list versions are public static so they can be accessed from `AboutTabFragment`. They are also used in `onCreate()`. - public static String easyListVersion; - public static String easyPrivacyVersion; - public static String fanboysAnnoyanceVersion; - public static String fanboysSocialVersion; - public static String ultraPrivacyVersion; - - // The request items are public static so they can be accessed by `BlockListHelper`, `RequestsArrayAdapter`, and `ViewRequestsDialog`. They are also used in `onCreate()` and `onPrepareOptionsMenu()`. - public static List resourceRequests; - public static String[] whiteListResultStringArray; - private int blockedRequests; - private int easyListBlockedRequests; - private int easyPrivacyBlockedRequests; - private int fanboysAnnoyanceListBlockedRequests; - private int fanboysSocialBlockingListBlockedRequests; - private int ultraPrivacyBlockedRequests; - private int thirdPartyBlockedRequests; - - public final static int REQUEST_DISPOSITION = 0; - public final static int REQUEST_URL = 1; - public final static int REQUEST_BLOCKLIST = 2; - public final static int REQUEST_SUBLIST = 3; - public final static int REQUEST_BLOCKLIST_ENTRIES = 4; - public final static int REQUEST_BLOCKLIST_ORIGINAL_ENTRY = 5; - - public final static int REQUEST_DEFAULT = 0; - public final static int REQUEST_ALLOWED = 1; - public final static int REQUEST_THIRD_PARTY = 2; - public final static int REQUEST_BLOCKED = 3; - - public final static int MAIN_WHITELIST = 1; - public final static int FINAL_WHITELIST = 2; - public final static int DOMAIN_WHITELIST = 3; - public final static int DOMAIN_INITIAL_WHITELIST = 4; - public final static int DOMAIN_FINAL_WHITELIST = 5; - public final static int THIRD_PARTY_WHITELIST = 6; - public final static int THIRD_PARTY_DOMAIN_WHITELIST = 7; - public final static int THIRD_PARTY_DOMAIN_INITIAL_WHITELIST = 8; - - public final static int MAIN_BLACKLIST = 9; - public final static int INITIAL_BLACKLIST = 10; - public final static int FINAL_BLACKLIST = 11; - public final static int DOMAIN_BLACKLIST = 12; - public final static int DOMAIN_INITIAL_BLACKLIST = 13; - public final static int DOMAIN_FINAL_BLACKLIST = 14; - public final static int DOMAIN_REGULAR_EXPRESSION_BLACKLIST = 15; - public final static int THIRD_PARTY_BLACKLIST = 16; - public final static int THIRD_PARTY_INITIAL_BLACKLIST = 17; - public final static int THIRD_PARTY_DOMAIN_BLACKLIST = 18; - public final static int THIRD_PARTY_DOMAIN_INITIAL_BLACKLIST = 19; - public final static int THIRD_PARTY_REGULAR_EXPRESSION_BLACKLIST = 20; - public final static int THIRD_PARTY_DOMAIN_REGULAR_EXPRESSION_BLACKLIST = 21; - public final static int REGULAR_EXPRESSION_BLACKLIST = 22; - - // `blockAllThirdPartyRequests` is public static so it can be accessed from `RequestsActivity`. - // It is also used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyAppSettings()` - public static boolean blockAllThirdPartyRequests; - // `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; - // `domainSettingsDatabaseId` is public static so it can be accessed from `PinnedMismatchDialog`. It is also used in `onCreate()`, `onOptionsItemSelected()`, and `applyDomainSettings()`. - public static int domainSettingsDatabaseId; - - // The pinned variables are public static so they can be accessed from `PinnedMismatchDialog`. They are also used in `onCreate()`, `applyDomainSettings()`, and `checkPinnedMismatch()`. - public static String pinnedSslIssuedToCName; - public static String pinnedSslIssuedToOName; - public static String pinnedSslIssuedToUName; - public static String pinnedSslIssuedByCName; - public static String pinnedSslIssuedByOName; - public static String pinnedSslIssuedByUName; - public static Date pinnedSslStartDate; - public static Date pinnedSslEndDate; - public static String pinnedHostIpAddresses; - // 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; @@ -293,132 +186,47 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook - // `urlIsLoading` is used in `onCreate()`, `onCreateOptionsMenu()`, `loadUrl()`, `applyDomainSettings()`, and `GetHostIpAddresses`. - private static boolean urlIsLoading; - - // `gettingIpAddresses` is used in `onCreate() and `GetHostIpAddresses`. - private static boolean gettingIpAddresses; - - // `pinnedDomainSslCertificate` is used in `onCreate()`, `applyDomainSettings()`, and `checkPinnedMismatch()`. - private static boolean pinnedSslCertificate; - - // `pinnedIpAddress` is used in `applyDomainSettings()` and `checkPinnedMismatch()`. - private static boolean pinnedIpAddresses; - - // `ignorePinnedDomainInformation` is used in `onSslMismatchProceed()`, `applyDomainSettings()`, and `checkPinnedMismatch()`. - private static boolean ignorePinnedDomainInformation; - - // `supportFragmentManager` is used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onCreateContextMenu()`, `onRequestPermissionResult()`, `viewSslCertificate()`, - // `applyAppSettings()`, and `checkPinnedMismatch()`. - private static FragmentManager supportFragmentManager; - - - // `appBar` is used in `onCreate()`, `onOptionsItemSelected()`, `closeFindOnPage()`, `applyAppSettings()`, and `applyProxyThroughOrbot()`. - private ActionBar appBar; - - // `navigatingHistory` is used in `onCreate()`, `onNavigationItemSelected()`, `onSslMismatchBack()`, and `applyDomainSettings()`. - private boolean navigatingHistory; - - // `drawerLayout` is used in `onCreate()`, `onNewIntent()`, `onBackPressed()`, and `onRestart()`. - private DrawerLayout drawerLayout; - - // `rootCoordinatorLayout` is used in `onCreate()` and `applyAppSettings()`. - private CoordinatorLayout rootCoordinatorLayout; - - // `mainWebView` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`, - // `findNextOnPage()`, `closeFindOnPage()`, `loadUrlFromTextBox()`, `onSslMismatchBack()`, and `applyProxyThroughOrbot()`. - private WebView mainWebView; - - // `fullScreenVideoFrameLayout` is used in `onCreate()` and `onConfigurationChanged()`. - private FrameLayout fullScreenVideoFrameLayout; - - // `swipeRefreshLayout` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `onRestart()`. - private SwipeRefreshLayout swipeRefreshLayout; - - // `urlAppBarRelativeLayout` is used in `onCreate()` and `applyDomainSettings()`. - private RelativeLayout urlAppBarRelativeLayout; - - // `favoriteIconImageView` is used in `onCreate()` and `applyDomainSettings()` - private ImageView favoriteIconImageView; - - // `cookieManager` is used in `onCreate()`, `onOptionsItemSelected()`, and `onNavigationItemSelected()`, `loadUrlFromTextBox()`, `onDownloadImage()`, `onDownloadFile()`, and `onRestart()`. - private CookieManager cookieManager; + // The current WebView is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`, + // `findNextOnPage()`, `closeFindOnPage()`, `loadUrlFromTextBox()`, `onSslMismatchBack()`, `applyProxyThroughOrbot()`, and `applyDomainSettings()`. + private NestedScrollWebView currentWebView; // `customHeader` is used in `onCreate()`, `onOptionsItemSelected()`, `onCreateContextMenu()`, and `loadUrl()`. private final Map customHeaders = new HashMap<>(); - // `javaScriptEnabled` is also used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `applyDomainSettings()`, and `updatePrivacyIcons()`. - private boolean javaScriptEnabled; - - // `firstPartyCookiesEnabled` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onDownloadImage()`, `onDownloadFile()`, and `applyDomainSettings()`. - private boolean firstPartyCookiesEnabled; - - // `thirdPartyCookiesEnabled` used in `onCreate()`, `onPrepareOptionsMenu()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyDomainSettings()`. - private boolean thirdPartyCookiesEnabled; - - // `domStorageEnabled` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyDomainSettings()`. - private boolean domStorageEnabled; - - // `saveFormDataEnabled` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyDomainSettings()`. It can be removed once the minimum API >= 26. - private boolean saveFormDataEnabled; - - // `nightMode` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyDomainSettings()`. - private boolean nightMode; - - // 'homepage' is used in `onCreate()`, `onNavigationItemSelected()`, and `applyProxyThroughOrbot()`. - private String homepage; - - // `searchURL` is used in `loadURLFromTextBox()` and `applyProxyThroughOrbot()`. + // The search URL is set in `applyProxyThroughOrbot()` and used in `onCreate()`, `onNewIntent()`, `loadURLFromTextBox()`, and `initializeWebView()`. private String searchURL; - // `mainMenu` is used in `onCreateOptionsMenu()` and `updatePrivacyIcons()`. - private Menu mainMenu; + // The options menu is set in `onCreateOptionsMenu()` and used in `onOptionsItemSelected()`, `updatePrivacyIcons()`, and `initializeWebView()`. + private Menu optionsMenu; - // `refreshMenuItem` is used in `onCreate()` and `onCreateOptionsMenu()`. - private MenuItem refreshMenuItem; - - // The blocklist menu items are used in `onCreate()`, `onCreateOptionsMenu()`, and `onPrepareOptionsMenu()`. - private MenuItem blocklistsMenuItem; - private MenuItem easyListMenuItem; - private MenuItem easyPrivacyMenuItem; - private MenuItem fanboysAnnoyanceListMenuItem; - private MenuItem fanboysSocialBlockingListMenuItem; - private MenuItem ultraPrivacyMenuItem; - private MenuItem blockAllThirdPartyRequestsMenuItem; - - // The blocklist variables are used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyAppSettings()`. - private boolean easyListEnabled; - private boolean easyPrivacyEnabled; - private boolean fanboysAnnoyanceListEnabled; - private boolean fanboysSocialBlockingListEnabled; - private boolean ultraPrivacyEnabled; + // The blocklists are populated in `onCreate()` and accessed from `initializeWebView()`. + private ArrayList> easyList; + private ArrayList> easyPrivacy; + private ArrayList> fanboysAnnoyanceList; + private ArrayList> fanboysSocialList; + private ArrayList> ultraPrivacy; // `webViewDefaultUserAgent` is used in `onCreate()` and `onPrepareOptionsMenu()`. private String webViewDefaultUserAgent; - // `defaultCustomUserAgentString` is used in `onPrepareOptionsMenu()` and `applyDomainSettings()`. - private String defaultCustomUserAgentString; - - // `privacyBrowserRuntime` is used in `onCreate()`, `onOptionsItemSelected()`, and `applyAppSettings()`. - private Runtime privacyBrowserRuntime; - // `proxyThroughOrbot` is used in `onRestart()`, `onOptionsItemSelected()`, `applyAppSettings()`, and `applyProxyThroughOrbot()`. private boolean proxyThroughOrbot; - // `incognitoModeEnabled` is used in `onCreate()` and `applyAppSettings()`. + // The incognito mode is set in `applyAppSettings()` and used in `initializeWebView()`. private boolean incognitoModeEnabled; - // `fullScreenBrowsingModeEnabled` is used in `onCreate()` and `applyAppSettings()`. + // The full screen browsing mode tracker is set it `applyAppSettings()` and used in `initializeWebView()`. private boolean fullScreenBrowsingModeEnabled; // `inFullScreenBrowsingMode` is used in `onCreate()`, `onConfigurationChanged()`, and `applyAppSettings()`. private boolean inFullScreenBrowsingMode; - // `hideSystemBarsOnFullscreen` is used in `onCreate()` and `applyAppSettings()`. - private boolean hideSystemBarsOnFullscreen; + // The app bar trackers are set in `applyAppSettings()` and used in `initializeWebView()`. + private boolean hideAppBar; + private boolean scrollAppBar; - // `translucentNavigationBarOnFullscreen` is used in `onCreate()` and `applyAppSettings()`. - private boolean translucentNavigationBarOnFullscreen; + // The loading new intent tracker is set in `onNewIntent()` and used in `setCurrentWebView()`. + private boolean loadingNewIntent; // `reapplyDomainSettingsOnRestart` is used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, and `onAddDomain()`, . private boolean reapplyDomainSettingsOnRestart; @@ -429,47 +237,14 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // `displayingFullScreenVideo` is used in `onCreate()` and `onResume()`. private boolean displayingFullScreenVideo; - // `downloadWithExternalApp` is used in `onCreate()`, `onCreateContextMenu()`, and `applyDomainSettings()`. - private boolean downloadWithExternalApp; - - // `currentDomainName` is used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onAddDomain()`, and `applyDomainSettings()`. - private String currentDomainName; - // `orbotStatusBroadcastReceiver` is used in `onCreate()` and `onDestroy()`. private BroadcastReceiver orbotStatusBroadcastReceiver; // `waitingForOrbot` is used in `onCreate()`, `onResume()`, and `applyProxyThroughOrbot()`. private boolean waitingForOrbot; - // `domainSettingsApplied` is used in `prepareOptionsMenu()` and `applyDomainSettings()`. - private boolean domainSettingsApplied; - - // `domainSettingsJavaScriptEnabled` is used in `onOptionsItemSelected()` and `applyDomainSettings()`. - private Boolean domainSettingsJavaScriptEnabled; - - // `waitingForOrbotHtmlString` is used in `onCreate()` and `applyProxyThroughOrbot()`. - private String waitingForOrbotHtmlString; - - // `privateDataDirectoryString` is used in `onCreate()`, `onOptionsItemSelected()`, and `onNavigationItemSelected()`. - private String privateDataDirectoryString; - - // `findOnPageLinearLayout` is used in `onCreate()`, `onOptionsItemSelected()`, and `closeFindOnPage()`. - private LinearLayout findOnPageLinearLayout; - - // `findOnPageEditText` is used in `onCreate()`, `onOptionsItemSelected()`, and `closeFindOnPage()`. - private EditText findOnPageEditText; - - // `displayAdditionalAppBarIcons` is used in `onCreate()` and `onCreateOptionsMenu()`. - private boolean displayAdditionalAppBarIcons; - - // `drawerToggle` is used in `onCreate()`, `onPostCreate()`, `onConfigurationChanged()`, `onNewIntent()`, and `onNavigationItemSelected()`. - private ActionBarDrawerToggle drawerToggle; - - // `supportAppBar` is used in `onCreate()`, `onOptionsItemSelected()`, and `closeFindOnPage()`. - private Toolbar supportAppBar; - - // `urlTextBox` is used in `onCreate()`, `onOptionsItemSelected()`, `loadUrlFromTextBox()`, `loadUrl()`, and `highlightUrlText()`. - private EditText urlTextBox; + // The action bar drawer toggle is initialized in `onCreate()` and used in `onResume()`. + private ActionBarDrawerToggle actionBarDrawerToggle; // The color spans are used in `onCreate()` and `highlightUrlText()`. private ForegroundColorSpan redColorSpan; @@ -481,28 +256,10 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook private int drawerHeaderPaddingTop; private int drawerHeaderPaddingBottom; - // `sslErrorHandler` is used in `onCreate()`, `onSslErrorCancel()`, and `onSslErrorProceed`. - private SslErrorHandler sslErrorHandler; - - // `httpAuthHandler` is used in `onCreate()`, `onHttpAuthenticationCancel()`, and `onHttpAuthenticationProceed()`. - private static HttpAuthHandler httpAuthHandler; - - // `inputMethodManager` is used in `onOptionsItemSelected()`, `loadUrlFromTextBox()`, and `closeFindOnPage()`. - private InputMethodManager inputMethodManager; - - // `mainWebViewRelativeLayout` is used in `onCreate()` and `onNavigationItemSelected()`. - private RelativeLayout mainWebViewRelativeLayout; - // `bookmarksDatabaseHelper` is used in `onCreate()`, `onDestroy`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, // and `loadBookmarksFolder()`. private BookmarksDatabaseHelper bookmarksDatabaseHelper; - // `bookmarksListView` is used in `onCreate()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, and `loadBookmarksFolder()`. - private ListView bookmarksListView; - - // `bookmarksTitleTextView` is used in `onCreate()` and `loadBookmarksFolder()`. - private TextView bookmarksTitleTextView; - // `bookmarksCursor` is used in `onDestroy()`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`. private Cursor bookmarksCursor; @@ -515,7 +272,19 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // `fileChooserCallback` is used in `onCreate()` and `onActivityResult()`. private ValueCallback fileChooserCallback; - // The download strings are used in `onCreate()` and `onRequestPermissionResult()`. + // The default progress view offsets are set in `onCreate()` and used in `initializeWebView()`. + private int defaultProgressViewStartOffset; + private int defaultProgressViewEndOffset; + + // The swipe refresh layout top padding is used when exiting full screen browsing mode. It is used in an inner class in `initializeWebView()`. + private int swipeRefreshLayoutPaddingTop; + + // The URL sanitizers are set in `applyAppSettings()` and used in `sanitizeUrl()`. + private boolean sanitizeGoogleAnalytics; + private boolean sanitizeFacebookClickIds; + private boolean sanitizeTwitterAmpRedirects; + + // The download strings are used in `onCreate()`, `onRequestPermissionResult()` and `initializeWebView()`. private String downloadUrl; private String downloadContentDisposition; private long downloadContentLength; @@ -523,27 +292,20 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // `downloadImageUrl` is used in `onCreateContextMenu()` and `onRequestPermissionResult()`. private String downloadImageUrl; - // The user agent variables are used in `onCreate()` and `applyDomainSettings()`. - private ArrayAdapter userAgentNamesArray; - private String[] userAgentDataArray; - - // The request codes are used in `onCreate()`, `onCreateContextMenu()`, `onCloseDownloadLocationPermissionDialog()`, and `onRequestPermissionResult()`. + // The request codes are used in `onCreate()`, `onCreateContextMenu()`, `onCloseDownloadLocationPermissionDialog()`, `onRequestPermissionResult()`, and `initializeWebView()`. private final int DOWNLOAD_FILE_REQUEST_CODE = 1; private final int DOWNLOAD_IMAGE_REQUEST_CODE = 2; @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. - // Also, remove the warning about needing to override `performClick()` when using an `OnTouchListener` with `WebView`. - @SuppressLint({"SetJavaScriptEnabled", "ClickableViewAccessibility"}) - // Remove Android Studio's warning about deprecations. The deprecated `getColor()` must be used until API >= 23. - @SuppressWarnings("deprecation") + // Remove the warning about needing to override `performClick()` when using an `OnTouchListener` with `WebView`. + @SuppressLint("ClickableViewAccessibility") protected void onCreate(Bundle savedInstanceState) { // Get a handle for the shared preferences. SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); // Get the theme and screenshot preferences. - darkTheme = sharedPreferences.getBoolean("dark_theme", false); - allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false); + boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false); + boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false); // Disable screenshots if not allowed. if (!allowScreenshots) { @@ -561,45 +323,48 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook super.onCreate(savedInstanceState); // Set the content view. - setContentView(R.layout.main_drawerlayout); + setContentView(R.layout.main_framelayout); + + // Get a handle for the input method. + InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - // Get a handle for the resources and the support fragment manager. - Resources resources = getResources(); - supportFragmentManager = getSupportFragmentManager(); + // Remove the lint warning below that the input method manager might be null. + assert inputMethodManager != null; - // Get a handle for `inputMethodManager`. - inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + // Get a handle for the toolbar. + Toolbar toolbar = findViewById(R.id.toolbar); - // `SupportActionBar` from `android.support.v7.app.ActionBar` must be used until the minimum API is >= 21. - supportAppBar = findViewById(R.id.app_bar); - setSupportActionBar(supportAppBar); - appBar = getSupportActionBar(); + // Set the action bar. `SupportActionBar` must be used until the minimum API is >= 21. + setSupportActionBar(toolbar); - // This is needed to get rid of the Android Studio warning that `appBar` might be null. - assert appBar != null; + // Get a handle for the action bar. + ActionBar actionBar = getSupportActionBar(); - // 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); + // This is needed to get rid of the Android Studio warning that the action bar might be null. + assert actionBar != null; + + // Add the custom layout, which shows the URL text bar. + actionBar.setCustomView(R.layout.url_app_bar); + actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM); // Initialize the foreground color spans for highlighting the URLs. We have to use the deprecated `getColor()` until API >= 23. - redColorSpan = new ForegroundColorSpan(resources.getColor(R.color.red_a700)); - initialGrayColorSpan = new ForegroundColorSpan(resources.getColor(R.color.gray_500)); - finalGrayColorSpan = new ForegroundColorSpan(resources.getColor(R.color.gray_500)); + 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 a handle for `urlTextBox`. - urlTextBox = findViewById(R.id.url_edittext); + // Get handles for the URL views. + EditText urlEditText = findViewById(R.id.url_edittext); // Remove the formatting from `urlTextBar` when the user is editing the text. - urlTextBox.setOnFocusChangeListener((View v, boolean hasFocus) -> { + urlEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> { if (hasFocus) { // The user is editing the URL text box. // Remove the highlighting. - urlTextBox.getText().removeSpan(redColorSpan); - urlTextBox.getText().removeSpan(initialGrayColorSpan); - urlTextBox.getText().removeSpan(finalGrayColorSpan); + 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. - urlTextBox.setSelection(0); + urlEditText.setSelection(0); // Reapply the highlighting. highlightUrlText(); @@ -607,7 +372,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook }); // Set the go button on the keyboard to load the URL in `urlTextBox`. - urlTextBox.setOnKeyListener((View v, int keyCode, KeyEvent event) -> { + 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. @@ -621,11 +386,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook } }); - // Set `waitingForOrbotHTMLString`. - waitingForOrbotHtmlString = "

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

"; - - // Initialize `currentDomainName`, `orbotStatus`, and `waitingForOrbot`. - currentDomainName = ""; + // Initialize the Orbot status and the waiting for Orbot trackers. orbotStatus = "unknown"; waitingForOrbot = false; @@ -638,11 +399,43 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // If Privacy Browser is waiting on Orbot, load the website now that Orbot is connected. if (orbotStatus.equals("ON") && waitingForOrbot) { - // Reset `waitingForOrbot`. + // Reset the waiting for Orbot status. waitingForOrbot = false; - // Load `formattedUrlString - loadUrl(formattedUrlString); + // Get the intent that started the app. + Intent launchingIntent = getIntent(); + + // Get the information from the intent. + String launchingIntentAction = launchingIntent.getAction(); + Uri launchingIntentUriData = launchingIntent.getData(); + + // If the intent action is a web search, perform the search. + if ((launchingIntentAction != null) && launchingIntentAction.equals(Intent.ACTION_WEB_SEARCH)) { + // 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 = ""; + } + + // Load the completed search URL. + loadUrl(searchURL + encodedUrlString); + } else if (launchingIntentUriData != null){ // Check to see if the intent contains a new URL. + // Load the URL from the intent. + loadUrl(launchingIntentUriData.toString()); + } else { // The is no URL in the intent. + // Select the homepage based on the proxy through Orbot status. + if (proxyThroughOrbot) { + // Load the Tor homepage. + loadUrl(sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value))); + } else { + // Load the normal homepage. + loadUrl(sharedPreferences.getString("homepage", getString(R.string.homepage_default_value))); + } + } } } }; @@ -650,167 +443,173 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Register `orbotStatusBroadcastReceiver` on `this` context. this.registerReceiver(orbotStatusBroadcastReceiver, new IntentFilter("org.torproject.android.intent.action.STATUS")); - // Get handles for views that need to be accessed. - drawerLayout = findViewById(R.id.drawerlayout); - rootCoordinatorLayout = findViewById(R.id.root_coordinatorlayout); - bookmarksListView = findViewById(R.id.bookmarks_drawer_listview); - bookmarksTitleTextView = findViewById(R.id.bookmarks_title_textview); + // Instantiate the blocklist helper. + BlockListHelper blockListHelper = new BlockListHelper(); + + // Parse the block lists. + easyList = blockListHelper.parseBlockList(getAssets(), "blocklists/easylist.txt"); + easyPrivacy = blockListHelper.parseBlockList(getAssets(), "blocklists/easyprivacy.txt"); + fanboysAnnoyanceList = blockListHelper.parseBlockList(getAssets(), "blocklists/fanboy-annoyance.txt"); + fanboysSocialList = blockListHelper.parseBlockList(getAssets(), "blocklists/fanboy-social.txt"); + ultraPrivacy = blockListHelper.parseBlockList(getAssets(), "blocklists/ultraprivacy.txt"); + + // Get handles for views that need to be modified. + DrawerLayout drawerLayout = findViewById(R.id.drawerlayout); + NavigationView navigationView = findViewById(R.id.navigationview); + TabLayout tabLayout = findViewById(R.id.tablayout); + SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout); + ViewPager webViewPager = findViewById(R.id.webviewpager); + 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); - mainWebViewRelativeLayout = findViewById(R.id.main_webview_relativelayout); - mainWebView = findViewById(R.id.main_webview); - findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout); - findOnPageEditText = findViewById(R.id.find_on_page_edittext); - fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout); - urlAppBarRelativeLayout = findViewById(R.id.url_app_bar_relativelayout); - favoriteIconImageView = findViewById(R.id.favorite_icon); + EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext); - // Set the bookmarks drawer resources according to the theme. This can't be done in the layout due to compatibility issues with the `DrawerLayout` support widget. - if (darkTheme) { - launchBookmarksActivityFab.setImageDrawable(resources.getDrawable(R.drawable.bookmarks_dark)); - createBookmarkFolderFab.setImageDrawable(resources.getDrawable(R.drawable.create_folder_dark)); - createBookmarkFab.setImageDrawable(resources.getDrawable(R.drawable.create_bookmark_dark)); - bookmarksListView.setBackgroundColor(resources.getColor(R.color.gray_850)); - } else { - launchBookmarksActivityFab.setImageDrawable(resources.getDrawable(R.drawable.bookmarks_light)); - createBookmarkFolderFab.setImageDrawable(resources.getDrawable(R.drawable.create_folder_light)); - createBookmarkFab.setImageDrawable(resources.getDrawable(R.drawable.create_bookmark_light)); - bookmarksListView.setBackgroundColor(resources.getColor(R.color.white)); - } + // Listen for touches on the navigation menu. + navigationView.setNavigationItemSelectedListener(this); - // Set the launch bookmarks activity FAB to launch the bookmarks activity. - launchBookmarksActivityFab.setOnClickListener(v -> { - // Create an intent to launch the bookmarks activity. - Intent bookmarksIntent = new Intent(getApplicationContext(), BookmarksActivity.class); + // Get handles for the navigation menu and the back and forward menu items. The menu is zero-based. + Menu navigationMenu = navigationView.getMenu(); + MenuItem navigationBackMenuItem = navigationMenu.getItem(2); + MenuItem navigationForwardMenuItem = navigationMenu.getItem(3); + MenuItem navigationHistoryMenuItem = navigationMenu.getItem(4); + MenuItem navigationRequestsMenuItem = navigationMenu.getItem(5); - // Include the current folder with the `Intent`. - bookmarksIntent.putExtra("Current Folder", currentBookmarksFolder); + // Initialize the web view pager adapter. + webViewPagerAdapter = new WebViewPagerAdapter(getSupportFragmentManager()); - // Make it so. - startActivity(bookmarksIntent); - }); + // Set the pager adapter on the web view pager. + webViewPager.setAdapter(webViewPagerAdapter); - // Set the create new bookmark folder FAB to display an alert dialog. - createBookmarkFolderFab.setOnClickListener(v -> { - // Show the `CreateBookmarkFolderDialog` `AlertDialog` and name the instance `@string/create_folder`. - AppCompatDialogFragment createBookmarkFolderDialog = new CreateBookmarkFolderDialog(); - createBookmarkFolderDialog.show(supportFragmentManager, resources.getString(R.string.create_folder)); - }); + // Store up to 100 tabs in memory. + webViewPager.setOffscreenPageLimit(100); - // Set the create new bookmark FAB to display an alert dialog. - createBookmarkFab.setOnClickListener(view -> { - // Show the `CreateBookmarkDialog` `AlertDialog` and name the instance `@string/create_bookmark`. - AppCompatDialogFragment createBookmarkDialog = new CreateBookmarkDialog(); - createBookmarkDialog.show(supportFragmentManager, resources.getString(R.string.create_bookmark)); - }); + // 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. + } - // 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; + public void onPageSelected(int position) { + // Close the find on page bar if it is open. + closeFindOnPage(null); - if (inFullScreenBrowsingMode) { // Switch to full screen mode. - // Hide the `appBar`. - appBar.hide(); + // Set the current WebView. + setCurrentWebView(position); - // Hide the banner ad in 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.hideAd(findViewById(R.id.adview)); - } + // Select the corresponding tab if it does not match the currently selected page. This will happen if the page was scrolled via swiping in the view pager or by creating a new tab. + if (tabLayout.getSelectedTabPosition() != position) { + // Create a handler to select the tab. + Handler selectTabHandler = new Handler(); - // 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 re-hides 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); - - // 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. - if (translucentNavigationBarOnFullscreen) { - // 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(); + // Create a runnable select the new tab. + Runnable selectTabRunnable = () -> { + // Get a handle for the tab. + TabLayout.Tab tab = tabLayout.getTabAt(position); - // Show the `BannerAd` in the free flavor. - if (BuildConfig.FLAVOR.contentEquals("free")) { - // 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)); - } + // Assert that the tab is not null. + assert tab != null; - // Remove the translucent navigation bar flag if it is set. - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + // Select the tab. + tab.select(); + }; - // 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); + // Select the tab layout after 100 milliseconds, which leaves enough time for a new tab to be created. + selectTabHandler.postDelayed(selectTabRunnable, 100); + } + } - // Remove any `SYSTEM_UI` flags from `rootCoordinatorLayout`. - rootCoordinatorLayout.setSystemUiVisibility(0); + @Override + public void onPageScrollStateChanged(int state) { + // Do nothing. + } + }); - // Constrain `rootCoordinatorLayout` inside the status and navigation bars. - rootCoordinatorLayout.setFitsSystemWindows(true); - } + // 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()); + } - // Consume the double-tap. - return true; - } else { // Do not consume the double-tap because full screen browsing mode is disabled. - return false; - } + @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)); } }); - // Pass all touch events on `mainWebView` through `gestureDetector` to check for double-taps. - mainWebView.setOnTouchListener((View v, MotionEvent event) -> { - // Call `performClick()` on the view, which is required for accessibility. - v.performClick(); + // Add the first tab. + addTab(null); + + // Set the bookmarks drawer resources according to the theme. This can't be done in the layout due to compatibility issues with the `DrawerLayout` support widget. + // The deprecated `getResources().getDrawable()` must be used until the minimum API >= 21 and and `getResources().getColor()` must be used until the minimum API >= 23. + if (darkTheme) { + launchBookmarksActivityFab.setImageDrawable(getResources().getDrawable(R.drawable.bookmarks_dark)); + createBookmarkFolderFab.setImageDrawable(getResources().getDrawable(R.drawable.create_folder_dark)); + createBookmarkFab.setImageDrawable(getResources().getDrawable(R.drawable.create_bookmark_dark)); + bookmarksListView.setBackgroundColor(getResources().getColor(R.color.gray_850)); + } else { + launchBookmarksActivityFab.setImageDrawable(getResources().getDrawable(R.drawable.bookmarks_light)); + createBookmarkFolderFab.setImageDrawable(getResources().getDrawable(R.drawable.create_folder_light)); + createBookmarkFab.setImageDrawable(getResources().getDrawable(R.drawable.create_bookmark_light)); + bookmarksListView.setBackgroundColor(getResources().getColor(R.color.white)); + } + + // 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); - // Send the `event` to `gestureDetector`. - return gestureDetector.onTouchEvent(event); + // 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); }); - // Update `findOnPageCountTextView`. - mainWebView.setFindListener(new WebView.FindListener() { - // Get a handle for `findOnPageCountTextView`. - final TextView findOnPageCountTextView = findViewById(R.id.find_on_page_count_textview); + // 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()); - @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; + // Show the create bookmark folder dialog. + createBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.create_folder)); + }); - // Build the match string. - String matchString = activeMatch + "/" + numberOfMatches; + // 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()); - // Set `findOnPageCountTextView`. - findOnPageCountTextView.setText(matchString); - } - } + // 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`. @@ -827,16 +626,18 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook @Override public void afterTextChanged(Editable s) { - // Search for the text in `mainWebView`. - mainWebView.findAllAsync(findOnPageEditText.getText().toString()); + // 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. `0` indicates no additional flags. - inputMethodManager.hideSoftInputFromWindow(mainWebView.getWindowToken(), 0); + // Hide the soft keyboard. + inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0); // Consume the event. return true; @@ -847,32 +648,24 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook }); // Implement swipe to refresh. - swipeRefreshLayout = findViewById(R.id.swiperefreshlayout); - swipeRefreshLayout.setOnRefreshListener(() -> mainWebView.reload()); + swipeRefreshLayout.setOnRefreshListener(() -> currentWebView.reload()); + + // Store the default progress view offsets for use later in `initializeWebView()`. + defaultProgressViewStartOffset = swipeRefreshLayout.getProgressViewStartOffset(); + defaultProgressViewEndOffset = swipeRefreshLayout.getProgressViewEndOffset(); // Set the swipe to refresh color according to the theme. if (darkTheme) { - swipeRefreshLayout.setColorSchemeResources(R.color.blue_600); + swipeRefreshLayout.setColorSchemeResources(R.color.blue_800); swipeRefreshLayout.setProgressBackgroundColorSchemeResource(R.color.gray_850); } else { - swipeRefreshLayout.setColorSchemeResources(R.color.blue_700); + swipeRefreshLayout.setColorSchemeResources(R.color.blue_500); } // `DrawerTitle` identifies the `DrawerLayouts` in accessibility mode. drawerLayout.setDrawerTitle(GravityCompat.START, getString(R.string.navigation_drawer)); drawerLayout.setDrawerTitle(GravityCompat.END, getString(R.string.bookmarks)); - // Listen for touches on the navigation menu. - final NavigationView navigationView = findViewById(R.id.navigationview); - navigationView.setNavigationItemSelectedListener(this); - - // 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); - final MenuItem navigationRequestsMenuItem = navigationMenu.getItem(4); - // 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); @@ -921,12 +714,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook oldFolderNameString = bookmarksCursor.getString(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME)); // Show the edit bookmark folder `AlertDialog` and name the instance `@string/edit_folder`. - AppCompatDialogFragment editFolderDialog = EditBookmarkFolderDialog.folderDatabaseId(databaseId); - editFolderDialog.show(supportFragmentManager, resources.getString(R.string.edit_folder)); + DialogFragment editBookmarkFolderDialog = EditBookmarkFolderDialog.folderDatabaseId(databaseId, currentWebView.getFavoriteOrDefaultIcon()); + editBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.edit_folder)); } else { // Show the edit bookmark `AlertDialog` and name the instance `@string/edit_bookmark`. - AppCompatDialogFragment editBookmarkDialog = EditBookmarkDialog.bookmarkDatabaseId(databaseId); - editBookmarkDialog.show(supportFragmentManager, resources.getString(R.string.edit_bookmark)); + DialogFragment editBookmarkDialog = EditBookmarkDialog.bookmarkDatabaseId(databaseId, currentWebView.getFavoriteOrDefaultIcon()); + editBookmarkDialog.show(getSupportFragmentManager(), getString(R.string.edit_bookmark)); } // Consume the event. @@ -934,11 +727,11 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook }); // Get the status bar pixel size. - int statusBarResourceId = resources.getIdentifier("status_bar_height", "dimen", "android"); - int statusBarPixelSize = resources.getDimensionPixelSize(statusBarResourceId); + int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android"); + int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId); // Get the resource density. - float screenDensity = resources.getDisplayMetrics().density; + float screenDensity = getResources().getDisplayMetrics().density; // Calculate the drawer header padding. This is used to move the text in the drawer headers below any cutouts. drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity); @@ -976,4047 +769,4767 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook bookmarksHeaderTextView.setPadding(drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingTop, drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingBottom); } - // Update the back, forward, history, and requests menu items. - navigationBackMenuItem.setEnabled(mainWebView.canGoBack()); - navigationForwardMenuItem.setEnabled(mainWebView.canGoForward()); - navigationHistoryMenuItem.setEnabled((mainWebView.canGoBack() || mainWebView.canGoForward())); - navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests); + // Update the navigation menu items. + 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(mainWebView.getWindowToken(), 0); + 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. - urlTextBox.clearFocus(); - mainWebView.clearFocus(); + urlEditText.clearFocus(); + currentWebView.clearFocus(); } } }); - // 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); + // 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); - // Get a handle for the progress bar. - final ProgressBar progressBar = findViewById(R.id.progress_bar); + // 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", ""); - mainWebView.setWebChromeClient(new WebChromeClient() { - // Update the progress bar when a page is loading. - @Override - public void onProgressChanged(WebView view, int progress) { - // Inject the night mode CSS if night mode is enabled. - if (nightMode) { - // `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. - // `::selection {background: #0D47A1}' sets the text selection highlight color 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;} ::selection {background: #0D47A1 !important;}'; parent.appendChild(style)})()", 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 = () -> { - // Only display `mainWebView` if the progress bar is gone. This prevents the display of the `WebView` while it is still loading. - if (progressBar.getVisibility() == View.GONE) { - mainWebView.setVisibility(View.VISIBLE); - } - }; + // 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); - // Displaying of `mainWebView` after 500 milliseconds. - displayWebViewHandler.postDelayed(displayWebViewRunnable, 500); - }); - } + // 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); - // Update the progress bar. - progressBar.setProgress(progress); + // Get a handle for the WebView. + WebView bareWebView = webViewLayout.findViewById(R.id.bare_webview); - // 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); + // Store the default user agent. + webViewDefaultUserAgent = bareWebView.getSettings().getUserAgentString(); - // Display `mainWebView` if night mode is disabled. - // Because of a race condition between `applyDomainSettings` and `onPageStarted`, when night mode is set by domain settings the `WebView` may be hidden even if night mode is not - // currently enabled. - if (!nightMode) { - mainWebView.setVisibility(View.VISIBLE); - } + // Destroy the bare WebView. + bareWebView.destroy(); + } - //Stop the swipe to refresh indicator if it is running - swipeRefreshLayout.setRefreshing(false); - } - } + @Override + protected void onNewIntent(Intent intent) { + // Get the information from the intent. + String intentAction = intent.getAction(); + Uri intentUriData = intent.getData(); - // 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; + // Determine if this is a web search. + boolean isWebSearch = ((intentAction != null) && intentAction.equals(Intent.ACTION_WEB_SEARCH)); - // Place the favorite icon in the appBar. - favoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(icon, 64, 64, true)); - } - } + // 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); - // 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; - } + // Create a URL string. + String url; - // Enter full screen video. - @Override - public void onShowCustomView(View view, CustomViewCallback callback) { - // Set the full screen video flag. - displayingFullScreenVideo = true; + // If the intent action is a web search, perform the search. + if (isWebSearch) { + // Create an encoded URL string. + String encodedUrlString; - // 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)); + // Sanitize the search input and convert it to a search. + try { + encodedUrlString = URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8"); + } catch (UnsupportedEncodingException exception) { + encodedUrlString = ""; } - // 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); + // 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(); + } - /* 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 re-hides them after they are shown. - */ - rootCoordinatorLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + // 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; - // Set `rootCoordinatorLayout` to fill the entire screen. - rootCoordinatorLayout.setFitsSystemWindows(false); + // Add a new tab. + addNewTab(url); + } else { // Load the URL in the current tab. + // Make it so. + loadUrl(url); + } - // Disable the sliding drawers. - drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); + // Get a handle for the drawer layout. + DrawerLayout drawerLayout = findViewById(R.id.drawerlayout); - // Add `view` to `fullScreenVideoFrameLayout` and display it on the screen. - fullScreenVideoFrameLayout.addView(view); - fullScreenVideoFrameLayout.setVisibility(View.VISIBLE); + // Close the navigation drawer if it is open. + if (drawerLayout.isDrawerVisible(GravityCompat.START)) { + drawerLayout.closeDrawer(GravityCompat.START); } - // Exit full screen video. - @Override - public void onHideCustomView() { - // Unset the full screen video flag. - displayingFullScreenVideo = false; - - // Hide `fullScreenVideoFrameLayout`. - fullScreenVideoFrameLayout.removeAllViews(); - fullScreenVideoFrameLayout.setVisibility(View.GONE); - - // Enable the sliding drawers. - drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED); + // Close the bookmarks drawer if it is open. + if (drawerLayout.isDrawerVisible(GravityCompat.END)) { + drawerLayout.closeDrawer(GravityCompat.END); + } + } + } - // Apply the appropriate full screen mode the `SYSTEM_UI` flags. - if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode. - if (hideSystemBarsOnFullscreen) { // Hide everything. - // Remove the translucent navigation setting if it is currently flagged. - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + @Override + public void onRestart() { + // Run the default commands. + super.onRestart(); - // Remove the translucent status bar overlay. - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + // Make sure Orbot is running if Privacy Browser is proxying through Orbot. + if (proxyThroughOrbot) { + // Request Orbot to start. If Orbot is already running no hard will be caused by this request. + Intent orbotIntent = new Intent("org.torproject.android.intent.action.START"); - // 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); + // Send the intent to the Orbot package. + orbotIntent.setPackage("org.torproject.android"); - /* 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 re-hides 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. - // Remove any `SYSTEM_UI` flags from `rootCoordinatorLayout`. - rootCoordinatorLayout.setSystemUiVisibility(0); + // Make it so. + sendBroadcast(orbotIntent); + } - // Add the translucent status flag if it is unset. - getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + // Apply the app settings if returning from the Settings activity. + if (reapplyAppSettingsOnRestart) { + // Reset the reapply app settings on restart tracker. + reapplyAppSettingsOnRestart = false; - if (translucentNavigationBarOnFullscreen) { - // Set the navigation bar to be translucent. This also resets `drawerLayout's` `View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN`. - getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); - } else { - // Set the navigation bar to be black. - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); - } - } - } else { // Switch to normal viewing mode. - // Show the `appBar` if `findOnPageLinearLayout` is not visible. - if (findOnPageLinearLayout.getVisibility() == View.GONE) { - appBar.show(); - } + // Apply the app settings. + applyAppSettings(); + } - // Show the `BannerAd` in the free flavor. - if (BuildConfig.FLAVOR.contentEquals("free")) { - // Initialize the ad. The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations. - AdHelper.initializeAds(findViewById(R.id.adview), getApplicationContext(), getFragmentManager(), getString(R.string.google_app_id), getString(R.string.ad_unit_id)); - } + // Apply the domain settings if returning from the settings or domains activity. + if (reapplyDomainSettingsOnRestart) { + // Reset the reapply domain settings on restart tracker. + reapplyDomainSettingsOnRestart = false; - // Remove any `SYSTEM_UI` flags from `rootCoordinatorLayout`. - rootCoordinatorLayout.setSystemUiVisibility(0); + // 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); - // Remove the translucent navigation bar flag if it is set. - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + // Get the fragment view. + View fragmentView = webViewTabFragment.getView(); - // 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); + // 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); - // Constrain `rootCoordinatorLayout` inside the status and navigation bars. - rootCoordinatorLayout.setFitsSystemWindows(true); - } + // Reset the current domain name so the domain settings will be reapplied. + nestedScrollWebView.resetCurrentDomainName(); - // Show the ad if this is the free flavor. - if (BuildConfig.FLAVOR.contentEquals("free")) { - // 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)); + // 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); + } } } + } - // 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; - - // Create an intent to open a chooser based ont the file chooser parameters. - Intent fileChooserIntent = fileChooserParams.createIntent(); - - // Open the file chooser. Currently only one `startActivityForResult` exists in this activity, so the request code, used to differentiate them, is simply `0`. - startActivityForResult(fileChooserIntent, 0); - } - return true; - } - }); + // Load the URL on restart (used when loading a bookmark). + if (loadUrlOnRestart) { + // Load the specified URL. + loadUrl(urlToLoadOnRestart); - // Register `mainWebView` for a context menu. This is used to see link targets and download images. - registerForContextMenu(mainWebView); + // Reset the load on restart tracker. + loadUrlOnRestart = false; + } - // Allow the downloading of files. - mainWebView.setDownloadListener((String url, String userAgent, String contentDisposition, String mimetype, long contentLength) -> { - // Check if the download should be processed by an external app. - if (downloadWithExternalApp) { // Download with an external app. - openUrlWithExternalApp(url); - } else { // Download with Android's download manager. - // Check to see if the WRITE_EXTERNAL_STORAGE permission has already been granted. - if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission has not been granted. - // The WRITE_EXTERNAL_STORAGE permission needs to be requested. + // 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); - // Store the variables for future use by `onRequestPermissionsResult()`. - downloadUrl = url; - downloadContentDisposition = contentDisposition; - downloadContentLength = contentLength; + // Close the bookmarks drawer. + drawerLayout.closeDrawer(GravityCompat.END); - // Show a dialog if the user has previously denied the permission. - if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first. - // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE. - DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE); + // Reload the bookmarks drawer. + loadBookmarksFolder(); - // Show the download location permission alert dialog. The permission will be requested when the the dialog is closed. - downloadLocationPermissionDialogFragment.show(getFragmentManager(), getString(R.string.download_location)); - } else { // Show the permission request directly. - // Request the permission. The download dialog will be launched by `onRequestPermissionResult()`. - ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE); - } - } else { // The storage permission has already been granted. - // Get a handle for the download file alert dialog. - AppCompatDialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(url, contentDisposition, contentLength); + // Reset `restartFromBookmarksActivity`. + restartFromBookmarksActivity = false; + } - // Show the download file alert dialog. - downloadFileDialogFragment.show(supportFragmentManager, getString(R.string.download)); - } - } - }); + // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. This can be important if the screen was rotated. + updatePrivacyIcons(true); + } - // Allow pinch to zoom. - mainWebView.getSettings().setBuiltInZoomControls(true); + // `onResume()` runs after `onStart()`, which runs after `onCreate()` and `onRestart()`. + @Override + public void onResume() { + // Run the default commands. + super.onResume(); - // Hide zoom controls. - mainWebView.getSettings().setDisplayZoomControls(false); + for (int i = 0; i < webViewPagerAdapter.getCount(); i++) { + // Get the WebView tab fragment. + WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i); - // Don't allow mixed content (HTTP and HTTPS) on the same website. - if (Build.VERSION.SDK_INT >= 21) { - mainWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW); - } + // Get the fragment view. + View fragmentView = webViewTabFragment.getView(); - // Set the WebView to use a wide viewport. Otherwise, some web pages will be scrunched and some content will render outside the screen. - mainWebView.getSettings().setUseWideViewPort(true); + // 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); - // Set the WebView to load in overview mode (zoomed out to the maximum width). - mainWebView.getSettings().setLoadWithOverviewMode(true); + // Resume the nested scroll WebView JavaScript timers. + nestedScrollWebView.resumeTimers(); - // Explicitly disable geolocation. - mainWebView.getSettings().setGeolocationEnabled(false); + // Resume the nested scroll WebView. + nestedScrollWebView.onResume(); + } + } - // Initialize cookieManager. - cookieManager = CookieManager.getInstance(); + // Display a message to the user if waiting for Orbot. + if (waitingForOrbot && !orbotStatus.equals("ON")) { + // Disable the wide view port so that the waiting for Orbot text is displayed correctly. + currentWebView.getSettings().setUseWideViewPort(false); - // 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", ""); + // Load a waiting page. `null` specifies no encoding, which defaults to ASCII. + currentWebView.loadData("

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

", "text/html", null); + } - // 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); + if (displayingFullScreenVideo || inFullScreenBrowsingMode) { + // Get a handle for the root frame layouts. + FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout); - // Get a handle for the `Runtime`. - privacyBrowserRuntime = Runtime.getRuntime(); + // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen. + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); - // 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`. + /* 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")) { // Resume the adView for the free flavor. + // Resume the ad. + AdHelper.resumeAd(findViewById(R.id.adview)); + } + } - // Initialize `inFullScreenBrowsingMode`, which is always false at this point because Privacy Browser never starts in full screen browsing mode. - inFullScreenBrowsingMode = false; + @Override + public void onPause() { + // Run the default commands. + super.onPause(); - // Initialize the privacy settings variables. - javaScriptEnabled = false; - firstPartyCookiesEnabled = false; - thirdPartyCookiesEnabled = false; - domStorageEnabled = false; - saveFormDataEnabled = false; // Form data can be removed once the minimum API >= 26. - nightMode = false; + for (int i = 0; i < webViewPagerAdapter.getCount(); i++) { + // Get the WebView tab fragment. + WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i); - // Store the default user agent. - webViewDefaultUserAgent = mainWebView.getSettings().getUserAgentString(); + // Get the fragment view. + View fragmentView = webViewTabFragment.getView(); - // Initialize the WebView title. - webViewTitle = getString(R.string.no_title); + // 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); - // Initialize the favorite icon bitmap. `ContextCompat` must be used until API >= 21. - Drawable favoriteIconDrawable = ContextCompat.getDrawable(getApplicationContext(), R.drawable.world); - BitmapDrawable favoriteIconBitmapDrawable = (BitmapDrawable) favoriteIconDrawable; - assert favoriteIconBitmapDrawable != null; - favoriteIconDefaultBitmap = favoriteIconBitmapDrawable.getBitmap(); + // Pause the nested scroll WebView. + nestedScrollWebView.onPause(); - // If the favorite icon is null, load the default. - if (favoriteIconBitmap == null) { - favoriteIconBitmap = favoriteIconDefaultBitmap; + // Pause the nested scroll WebView JavaScript timers. + nestedScrollWebView.pauseTimers(); + } } - // Initialize the user agent array adapter and string array. - userAgentNamesArray = ArrayAdapter.createFromResource(this, R.array.user_agent_names, R.layout.spinner_item); - userAgentDataArray = resources.getStringArray(R.array.user_agent_data); + // 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)); + } + } - // Apply the app settings from the shared preferences. - applyAppSettings(); + @Override + public void onDestroy() { + // Unregister the Orbot status broadcast receiver. + this.unregisterReceiver(orbotStatusBroadcastReceiver); - // Instantiate the block list helper. - BlockListHelper blockListHelper = new BlockListHelper(); + // Close the bookmarks cursor and database. + bookmarksCursor.close(); + bookmarksDatabaseHelper.close(); - // Initialize the list of resource requests. - resourceRequests = new ArrayList<>(); + // Run the default commands. + super.onDestroy(); + } - // Parse the block lists. - final ArrayList> easyList = blockListHelper.parseBlockList(getAssets(), "blocklists/easylist.txt"); - final ArrayList> easyPrivacy = blockListHelper.parseBlockList(getAssets(), "blocklists/easyprivacy.txt"); - final ArrayList> fanboysAnnoyanceList = blockListHelper.parseBlockList(getAssets(), "blocklists/fanboy-annoyance.txt"); - final ArrayList> fanboysSocialList = blockListHelper.parseBlockList(getAssets(), "blocklists/fanboy-social.txt"); - final ArrayList> ultraPrivacy = blockListHelper.parseBlockList(getAssets(), "blocklists/ultraprivacy.txt"); - - // Store the list versions. - easyListVersion = easyList.get(0).get(0)[0]; - easyPrivacyVersion = easyPrivacy.get(0).get(0)[0]; - fanboysAnnoyanceVersion = fanboysAnnoyanceList.get(0).get(0)[0]; - fanboysSocialVersion = fanboysSocialList.get(0).get(0)[0]; - ultraPrivacyVersion = ultraPrivacy.get(0).get(0)[0]; - - // Get a handle for the activity. This is used to update the requests counter while the navigation menu is open. - Activity activity = this; + @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); - 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. - // The deprecated `shouldOverrideUrlLoading` must be used until API >= 24. - @SuppressWarnings("deprecation") - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - if (url.startsWith("http")) { // Load the URL in Privacy Browser. - // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled. - formattedUrlString = ""; + // Store a handle for the options menu so it can be used by `onOptionsItemSelected()` and `updatePrivacyIcons()`. + optionsMenu = menu; - // Apply the domain settings for the new URL. `applyDomainSettings` doesn't do anything if the domain has not changed. - boolean userAgentChanged = applyDomainSettings(url, true, false); + // Set the initial status of the privacy icons. `false` does not call `invalidateOptionsMenu` as the last step. + updatePrivacyIcons(false); - // Check if the user agent has changed. - if (userAgentChanged) { - // Manually load the URL. The changing of the user agent will cause WebView to reload the previous URL. - mainWebView.loadUrl(url, customHeaders); + // 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 adConsentMenuItem = menu.findItem(R.id.ad_consent); - // Returning true indicates that Privacy Browser is manually handling the loading of the URL. - return true; - } else { - // Returning false causes the current WebView to handle the URL and prevents it from adding redirects to the history list. - return false; - } - } 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); + // Only display third-party cookies if API >= 21 + toggleThirdPartyCookiesMenuItem.setVisible(Build.VERSION.SDK_INT >= 21); - // Parse the url and set it as the data for the intent. - emailIntent.setData(Uri.parse(url)); + // 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); - // Open the email program in a new task instead of as part of Privacy Browser. - emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // 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); - // Make it so. - startActivity(emailIntent); + // Only show Ad Consent if this is the free flavor. + adConsentMenuItem.setVisible(BuildConfig.FLAVOR.contentEquals("free")); - // 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); + // Get the shared preferences. + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - // Add the phone number to the intent. - dialIntent.setData(Uri.parse(url)); + // Get the dark theme and app bar preferences.. + boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false); + boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false); - // Open the dialer in a new task instead of as part of Privacy Browser. - dialIntent.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(dialIntent); + // 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 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); + // If the icon is displayed in the AppBar, set it according to the theme. + if (displayAdditionalAppBarIcons) { + if (darkTheme) { + refreshMenuItem.setIcon(R.drawable.close_dark); + } else { + refreshMenuItem.setIcon(R.drawable.close_light); + } + } + } - // Add the URL to the intent. - genericIntent.setData(Uri.parse(url)); + return true; + } - // List all apps that can handle the URL instead of just opening the first one. - genericIntent.addCategory(Intent.CATEGORY_BROWSABLE); + @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 ultraPrivacyMenuItem = menu.findItem(R.id.ultraprivacy); + MenuItem blockAllThirdPartyRequestsMenuItem = menu.findItem(R.id.block_all_third_party_requests); + MenuItem fontSizeMenuItem = menu.findItem(R.id.font_size); + MenuItem swipeToRefreshMenuItem = menu.findItem(R.id.swipe_to_refresh); + MenuItem displayImagesMenuItem = menu.findItem(R.id.display_images); + MenuItem nightModeMenuItem = menu.findItem(R.id.night_mode); + MenuItem proxyThroughOrbotMenuItem = menu.findItem(R.id.proxy_through_orbot); - // Open the app in a new task instead of as part of Privacy Browser. - genericIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // Get a handle for the cookie manager. + CookieManager cookieManager = CookieManager.getInstance(); - // Start the app or display a snackbar if no app is available to handle the URL. - try { - startActivity(genericIntent); - } catch (ActivityNotFoundException exception) { - Snackbar.make(mainWebView, getString(R.string.unrecognized_url) + " " + url, Snackbar.LENGTH_SHORT).show(); - } + // Initialize the current user agent string and the font size. + String currentUserAgent = getString(R.string.user_agent_privacy_browser); + int fontSize = 100; - // Returning true indicates Privacy Browser is handling the URL by creating an intent. - return true; - } + // 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); } - // Check requests against the block lists. The deprecated `shouldInterceptRequest()` must be used until minimum API >= 21. - @SuppressWarnings("deprecation") - @Override - public WebResourceResponse shouldInterceptRequest(WebView view, String url){ - // 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())); + // 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.EASY_LIST)); + easyPrivacyMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY)); + fanboysAnnoyanceListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST)); + fanboysSocialBlockingListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST)); + ultraPrivacyMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY)); + blockAllThirdPartyRequestsMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS)); + swipeToRefreshMenuItem.setChecked(currentWebView.getSwipeToRefresh()); + displayImagesMenuItem.setChecked(currentWebView.getSettings().getLoadsImagesAutomatically()); + nightModeMenuItem.setChecked(currentWebView.getNightMode()); + + // 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.EASY_LIST) + " - " + getString(R.string.easylist)); + easyPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASY_PRIVACY) + " - " + 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)); + ultraPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.ULTRA_PRIVACY) + " - " + 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()); + } - // Reset the whitelist results tracker. - whiteListResultStringArray = null; + // Enable DOM Storage if JavaScript is enabled. + domStorageMenuItem.setEnabled(currentWebView.getSettings().getJavaScriptEnabled()); + } - // Initialize the third party request tracker. - boolean isThirdPartyRequest = false; + // Set the status of the menu item checkboxes. + firstPartyCookiesMenuItem.setChecked(cookieManager.acceptCookie()); + proxyThroughOrbotMenuItem.setChecked(proxyThroughOrbot); - // Initialize the current domain string. - String currentDomain = ""; + // Enable Clear Cookies if there are any. + clearCookiesMenuItem.setEnabled(cookieManager.hasCookies()); - // Nobody is happy when comparing null strings. - if (!(formattedUrlString == null) && !(url == null)) { - // Get the domain strings to URIs. - Uri currentDomainUri = Uri.parse(formattedUrlString); - Uri requestDomainUri = Uri.parse(url); + // 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; - // Get the domain host names. - String currentBaseDomain = currentDomainUri.getHost(); - String requestBaseDomain = requestDomainUri.getHost(); + // 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; + } - // Update the current domain variable. - currentDomain = currentBaseDomain; + // 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; + } - // Only compare the current base domain and the request base domain if neither is null. - if (!(currentBaseDomain == null) && !(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); - } + // Enable Clear DOM Storage if there is any. + clearDOMStorageMenuItem.setEnabled(localStorageDirectoryNumberOfFiles > 0 || indexedDBDirectoryNumberOfFiles > 0); - // 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); - } + // 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); - // Update the third party request tracker. - isThirdPartyRequest = !currentBaseDomain.equals(requestBaseDomain); - } - } + // Enable the clear form data menu item if there is anything to clear. + clearFormDataMenuItem.setEnabled(webViewDatabase.hasFormData()); + } - // Block third-party requests if enabled. - if (isThirdPartyRequest && blockAllThirdPartyRequests) { - // Increment the blocked requests counters. - blockedRequests++; - thirdPartyBlockedRequests++; - - // Update the titles of the blocklist menu items. This must be run from the UI thread. - activity.runOnUiThread(() -> { - navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests); - blocklistsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests); - blockAllThirdPartyRequestsMenuItem.setTitle(thirdPartyBlockedRequests + " - " + getString(R.string.block_all_third_party_requests)); - }); + // Enable Clear Data if any of the submenu items are enabled. + clearDataMenuItem.setEnabled(clearCookiesMenuItem.isEnabled() || clearDOMStorageMenuItem.isEnabled() || clearFormDataMenuItem.isEnabled()); - // Add the request to the log. - resourceRequests.add(new String[]{String.valueOf(REQUEST_THIRD_PARTY), url}); + // Disable Fanboy's Social Blocking List menu item if Fanboy's Annoyance List is checked. + fanboysSocialBlockingListMenuItem.setEnabled(!fanboysAnnoyanceListMenuItem.isChecked()); - // Return an empty web resource response. - return emptyWebResourceResponse; - } + // 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. + menu.findItem(R.id.user_agent_privacy_browser).setChecked(true); + } else if (currentUserAgent.equals(webViewDefaultUserAgent)) { // WebView Default. + 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. + 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. + 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. + 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. + 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. + 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. + 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. + 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. + 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. + 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. + menu.findItem(R.id.user_agent_safari_on_macos).setChecked(true); + } else { // Custom user agent. + menu.findItem(R.id.user_agent_custom).setChecked(true); + } - // Check UltraPrivacy if it is enabled. - if (ultraPrivacyEnabled) { - if (blockListHelper.isBlocked(currentDomain, url, isThirdPartyRequest, ultraPrivacy)) { - // Increment the blocked requests counters. - blockedRequests++; - ultraPrivacyBlockedRequests++; + // Instantiate the font size title and the selected font size menu item. + String fontSizeTitle; + MenuItem selectedFontSizeMenuItem; - // Update the titles of the blocklist menu items. This must be run from the UI thread. - activity.runOnUiThread(() -> { - navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests); - blocklistsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests); - ultraPrivacyMenuItem.setTitle(ultraPrivacyBlockedRequests + " - " + getString(R.string.ultraprivacy)); - }); + // 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; - // The resource request was blocked. Return an empty web resource response. - return emptyWebResourceResponse; - } + case 50: + fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.fifty_percent); + selectedFontSizeMenuItem = menu.findItem(R.id.font_size_fifty_percent); + break; - // If the whitelist result is not null, the request has been allowed by UltraPrivacy. - if (whiteListResultStringArray != null) { - // Add a whitelist entry to the resource requests array. - resourceRequests.add(whiteListResultStringArray); + 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; - // The resource request has been allowed by UltraPrivacy. `return null` loads the requested resource. - return null; - } - } + 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; - // Check EasyList if it is enabled. - if (easyListEnabled) { - if (blockListHelper.isBlocked(currentDomain, url, isThirdPartyRequest, easyList)) { - // Increment the blocked requests counters. - blockedRequests++; - easyListBlockedRequests++; + 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; - // Update the titles of the blocklist menu items. This must be run from the UI thread. - activity.runOnUiThread(() -> { - navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests); - blocklistsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests); - easyListMenuItem.setTitle(easyListBlockedRequests + " - " + getString(R.string.easylist)); - }); + 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; - // Reset the whitelist results tracker (because otherwise it will sometimes add results to the list due to a race condition). - whiteListResultStringArray = null; + 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; - // The resource request was blocked. Return an empty web resource response. - return emptyWebResourceResponse; - } - } + 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; - // Check EasyPrivacy if it is enabled. - if (easyPrivacyEnabled) { - if (blockListHelper.isBlocked(currentDomain, url, isThirdPartyRequest, easyPrivacy)) { - // Increment the blocked requests counters. - blockedRequests++; - easyPrivacyBlockedRequests++; + default: + fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent); + selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent); + break; + } - // Update the titles of the blocklist menu items. This must be run from the UI thread. - activity.runOnUiThread(() -> { - navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests); - blocklistsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests); - easyPrivacyMenuItem.setTitle(easyPrivacyBlockedRequests + " - " + getString(R.string.easyprivacy)); - }); + // Set the font size title and select the current size menu item. + fontSizeMenuItem.setTitle(fontSizeTitle); + selectedFontSizeMenuItem.setChecked(true); - // Reset the whitelist results tracker (because otherwise it will sometimes add results to the list due to a race condition). - whiteListResultStringArray = null; + // Run all the other default commands. + super.onPrepareOptionsMenu(menu); - // The resource request was blocked. Return an empty web resource response. - return emptyWebResourceResponse; - } - } + // Display the menu. + return true; + } - // Check Fanboy’s Annoyance List if it is enabled. - if (fanboysAnnoyanceListEnabled) { - if (blockListHelper.isBlocked(currentDomain, url, isThirdPartyRequest, fanboysAnnoyanceList)) { - // Increment the blocked requests counters. - blockedRequests++; - fanboysAnnoyanceListBlockedRequests++; + @Override + // Remove Android Studio's warning about the dangers of using SetJavaScriptEnabled. + @SuppressLint("SetJavaScriptEnabled") + public boolean onOptionsItemSelected(MenuItem menuItem) { + // Reenter full screen browsing mode if it was interrupted by the options menu. + if (inFullScreenBrowsingMode) { + // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen. + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); - // Update the titles of the blocklist menu items. This must be run from the UI thread. - activity.runOnUiThread(() -> { - navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests); - blocklistsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests); - fanboysAnnoyanceListMenuItem.setTitle(fanboysAnnoyanceListBlockedRequests + " - " + getString(R.string.fanboys_annoyance_list)); - }); + 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); + } - // Reset the whitelist results tracker (because otherwise it will sometimes add results to the list due to a race condition). - whiteListResultStringArray = null; + // Get the selected menu item ID. + int menuItemId = menuItem.getItemId(); - // The resource request was blocked. Return an empty web resource response. - return emptyWebResourceResponse; - } - } else if (fanboysSocialBlockingListEnabled){ // Only check Fanboy’s Social Blocking List if Fanboy’s Annoyance List is disabled. - if (blockListHelper.isBlocked(currentDomain, url, isThirdPartyRequest, fanboysSocialList)) { - // Increment the blocked requests counters. - blockedRequests++; - fanboysSocialBlockingListBlockedRequests++; + // Get a handle for the shared preferences. + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - // Update the titles of the blocklist menu items. This must be run from the UI thread. - activity.runOnUiThread(() -> { - navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests); - blocklistsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests); - fanboysSocialBlockingListMenuItem.setTitle(fanboysSocialBlockingListBlockedRequests + " - " + getString(R.string.fanboys_social_blocking_list)); - }); + // Get a handle for the cookie manager. + CookieManager cookieManager = CookieManager.getInstance(); - // Reset the whitelist results tracker (because otherwise it will sometimes add results to the list due to a race condition). - whiteListResultStringArray = null; + // 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()); - // The resource request was blocked. Return an empty web resource response. - return emptyWebResourceResponse; - } - } + // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. + updatePrivacyIcons(true); - // 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. - resourceRequests.add(whiteListResultStringArray); - } else { // The request didn't match any blocklist entry. Log it as a default request. - resourceRequests.add(new String[]{String.valueOf(REQUEST_DEFAULT), url}); + // Display a `Snackbar`. + if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScrip is enabled. + Snackbar.make(findViewById(R.id.webviewpager), R.string.javascript_enabled, Snackbar.LENGTH_SHORT).show(); + } else if (cookieManager.acceptCookie()) { // JavaScript is disabled, but first-party cookies are enabled. + Snackbar.make(findViewById(R.id.webviewpager), R.string.javascript_disabled, Snackbar.LENGTH_SHORT).show(); + } else { // Privacy mode. + Snackbar.make(findViewById(R.id.webviewpager), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show(); } - // The resource request has not been blocked. `return null` loads the requested resource. - return null; - } + // Reload the current WebView. + currentWebView.reload(); + return true; - // 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; + 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; - // Display the HTTP authentication dialog. - AppCompatDialogFragment httpAuthenticationDialogFragment = HttpAuthenticationDialog.displayDialog(host, realm); - httpAuthenticationDialogFragment.show(supportFragmentManager, getString(R.string.http_authentication)); - } + // Create an intent to launch the domains activity. + Intent domainsIntent = new Intent(this, DomainsActivity.class); - // Update the URL in urlTextBox when the page starts to load. - @Override - public void onPageStarted(WebView view, String url, Bitmap favicon) { - // 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. - // This is also used to determine when to check for pinned mismatches. - urlIsLoading = true; + // 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); + } - // Reset the list of host IP addresses. - currentHostIpAddresses = ""; + // 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()); + } - // Reset the list of resource requests. - resourceRequests.clear(); + // Make it so. + startActivity(domainsIntent); + } else { // Add a new domain. + // Apply the new domain settings on returning to `MainWebViewActivity`. + reapplyDomainSettingsOnRestart = true; - // Initialize the counters for requests blocked by each blocklist. - blockedRequests = 0; - easyListBlockedRequests = 0; - easyPrivacyBlockedRequests = 0; - fanboysAnnoyanceListBlockedRequests = 0; - fanboysSocialBlockingListBlockedRequests = 0; - ultraPrivacyBlockedRequests = 0; - thirdPartyBlockedRequests = 0; + // Get the current domain + Uri currentUri = Uri.parse(currentWebView.getUrl()); + String currentDomain = currentUri.getHost(); - // If night mode is enabled, hide `mainWebView` until after the night mode CSS is applied. - if (nightMode) { - mainWebView.setVisibility(View.INVISIBLE); - } + // 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); - // Hide the keyboard. `0` indicates no additional flags. - inputMethodManager.hideSoftInputFromWindow(mainWebView.getWindowToken(), 0); + // Create the domain and store the database ID. + int newDomainDatabaseId = domainsDatabaseHelper.addDomain(currentDomain); - // Check to see if Privacy Browser is waiting on Orbot. - if (!waitingForOrbot) { // Process the URL. - // The formatted URL string must be updated at the beginning of the load, so that if the user toggles JavaScript during the load the new website is reloaded. - formattedUrlString = url; + // Create an intent to launch the domains activity. + Intent domainsIntent = new Intent(this, DomainsActivity.class); - // Display the formatted URL text. - urlTextBox.setText(formattedUrlString); + // 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); + } - // Apply text highlighting to `urlTextBox`. - highlightUrlText(); - - // Get a URI for the current URL. - Uri currentUri = Uri.parse(formattedUrlString); + // 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()); + } - // Get the IP addresses for the host. - new GetHostIpAddresses(activity).execute(currentUri.getHost()); + // Make it so. + startActivity(domainsIntent); + } + return true; - // Apply any custom domain settings if the URL was loaded by navigating history. - if (navigatingHistory) { - // Apply the domain settings. - boolean userAgentChanged = applyDomainSettings(url, true, false); + case R.id.toggle_first_party_cookies: + // Switch the first-party cookie status. + cookieManager.setAcceptCookie(!cookieManager.acceptCookie()); - // Reset `navigatingHistory`. - navigatingHistory = false; + // Store the first-party cookie status. + currentWebView.setAcceptFirstPartyCookies(cookieManager.acceptCookie()); - // Manually load the URL if the user agent has changed, which will have caused the previous URL to be reloaded. - if (userAgentChanged) { - loadUrl(formattedUrlString); - } - } + // Update the menu checkbox. + menuItem.setChecked(cookieManager.acceptCookie()); - // Replace Refresh with Stop if the menu item has been created. (The WebView typically begins loading before the menu items are instantiated.) - if (refreshMenuItem != null) { - // Set the title. - refreshMenuItem.setTitle(R.string.stop); + // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. + updatePrivacyIcons(true); - // If the icon is displayed in the AppBar, set it according to the theme. - if (displayAdditionalAppBarIcons) { - if (darkTheme) { - refreshMenuItem.setIcon(R.drawable.close_dark); - } else { - refreshMenuItem.setIcon(R.drawable.close_light); - } - } - } + // Display a snackbar. + if (cookieManager.acceptCookie()) { // First-party cookies are enabled. + Snackbar.make(findViewById(R.id.webviewpager), R.string.first_party_cookies_enabled, Snackbar.LENGTH_SHORT).show(); + } else if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScript is still enabled. + Snackbar.make(findViewById(R.id.webviewpager), R.string.first_party_cookies_disabled, Snackbar.LENGTH_SHORT).show(); + } else { // Privacy mode. + Snackbar.make(findViewById(R.id.webviewpager), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show(); } - } - // 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 the wide view port if it has been turned off by the waiting for Orbot message. - if (!waitingForOrbot) { - // Only use a wide view port if the URL starts with `http`, not for `file://` and `content://`. - mainWebView.getSettings().setUseWideViewPort(url.startsWith("http")); - } + // Reload the current WebView. + currentWebView.reload(); + return true; - // Flush any cookies to persistent storage. `CookieManager` has become very lazy about flushing cookies in recent versions. - if (firstPartyCookiesEnabled && Build.VERSION.SDK_INT >= 21) { - cookieManager.flush(); - } + 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 Refresh menu item if it has been created. - if (refreshMenuItem != null) { - // Reset the Refresh title. - refreshMenuItem.setTitle(R.string.refresh); + // Update the menu checkbox. + menuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView)); - // If the icon is displayed in the AppBar, reset it according to the theme. - if (displayAdditionalAppBarIcons) { - if (darkTheme) { - refreshMenuItem.setIcon(R.drawable.refresh_enabled_dark); - } else { - refreshMenuItem.setIcon(R.drawable.refresh_enabled_light); - } + // Display a snackbar. + if (cookieManager.acceptThirdPartyCookies(currentWebView)) { + Snackbar.make(findViewById(R.id.webviewpager), R.string.third_party_cookies_enabled, Snackbar.LENGTH_SHORT).show(); + } else { + Snackbar.make(findViewById(R.id.webviewpager), R.string.third_party_cookies_disabled, Snackbar.LENGTH_SHORT).show(); } - } - + // Reload the current WebView. + currentWebView.reload(); + } // Else do nothing because SDK < 21. + return true; - // Clear the cache and history if Incognito Mode is enabled. - if (incognitoModeEnabled) { - // Clear the cache. `true` includes disk files. - mainWebView.clearCache(true); + case R.id.toggle_dom_storage: + // Toggle the status of domStorageEnabled. + currentWebView.getSettings().setDomStorageEnabled(!currentWebView.getSettings().getDomStorageEnabled()); - // Clear the back/forward history. - mainWebView.clearHistory(); + // Update the menu checkbox. + menuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled()); - // Manually delete cache folders. - try { - // Delete the main cache directory. - privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/cache"); + // Update the privacy icon. `true` refreshes the app bar icons. + updatePrivacyIcons(true); - // 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. - privacyBrowserRuntime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"}); - } catch (IOException e) { - // Do nothing if an error is thrown. - } + // Display a snackbar. + if (currentWebView.getSettings().getDomStorageEnabled()) { + Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_enabled, Snackbar.LENGTH_SHORT).show(); + } else { + Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_disabled, Snackbar.LENGTH_SHORT).show(); } - // Update the URL text box 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 = ""; + // Reload the current WebView. + currentWebView.reload(); + return true; - urlTextBox.setText(formattedUrlString); + // 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()); - // Request focus for `urlTextBox`. - urlTextBox.requestFocus(); + // Update the menu checkbox. + menuItem.setChecked(currentWebView.getSettings().getSaveFormData()); - // Display the keyboard. - inputMethodManager.showSoftInput(urlTextBox, 0); + // Display a snackbar. + if (currentWebView.getSettings().getSaveFormData()) { + Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_enabled, Snackbar.LENGTH_SHORT).show(); + } else { + Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_disabled, Snackbar.LENGTH_SHORT).show(); + } - // Apply the domain settings. This clears any settings from the previous domain. - applyDomainSettings(formattedUrlString, true, false); - } else { // `WebView` has loaded a webpage. - // Set the formatted URL string. Getting the URL from the WebView instead of using the one provided by `onPageFinished` makes websites like YouTube function correctly. - formattedUrlString = mainWebView.getUrl(); + // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. + updatePrivacyIcons(true); - // Only update the URL text box if the user is not typing in it. - if (!urlTextBox.hasFocus()) { - // Display the formatted URL text. - urlTextBox.setText(formattedUrlString); + // Reload the current WebView. + currentWebView.reload(); + return true; - // Apply text highlighting to `urlTextBox`. - highlightUrlText(); - } - } + case R.id.clear_cookies: + Snackbar.make(findViewById(R.id.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(); + return true; - // Store the SSL certificate so it can be accessed from `ViewSslCertificateDialog` and `PinnedMismatchDialog`. - sslCertificate = mainWebView.getCertificate(); + case R.id.clear_dom_storage: + Snackbar.make(findViewById(R.id.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(); + + // Initialize a handler to manually delete the DOM storage files and directories. + Handler deleteDomStorageHandler = new Handler(); + + // 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. + } + }; - // Check the current website information against any pinned domain information if the current IP addresses have been loaded. - if (!gettingIpAddresses) { - checkPinnedMismatch(); - } - } + // Manually delete the DOM storage files after 200 milliseconds. + deleteDomStorageHandler.postDelayed(deleteDomStorageRunnable, 200); + } + } + }) + .show(); + return true; - // Reset `urlIsLoading`, which is used to prevent reloads on redirect if the user agent changes. It is also used to determine when to check for pinned mismatches. - urlIsLoading = false; - } + // Form data can be remove once the minimum API >= 26. + case R.id.clear_form_data: + Snackbar.make(findViewById(R.id.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(); + return true; - // Handle SSL Certificate errors. - @Override - public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { - // Get the current website SSL certificate. - SslCertificate currentWebsiteSslCertificate = error.getCertificate(); + case R.id.easylist: + // Toggle the EasyList status. + currentWebView.enableBlocklist(NestedScrollWebView.EASY_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST)); - // 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 menu checkbox. + menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST)); - // Proceed to the website if the current SSL website certificate matches the pinned domain certificate. - if (pinnedSslCertificate && - currentWebsiteIssuedToCName.equals(pinnedSslIssuedToCName) && currentWebsiteIssuedToOName.equals(pinnedSslIssuedToOName) && - currentWebsiteIssuedToUName.equals(pinnedSslIssuedToUName) && currentWebsiteIssuedByCName.equals(pinnedSslIssuedByCName) && - currentWebsiteIssuedByOName.equals(pinnedSslIssuedByOName) && currentWebsiteIssuedByUName.equals(pinnedSslIssuedByUName) && - currentWebsiteSslStartDate.equals(pinnedSslStartDate) && currentWebsiteSslEndDate.equals(pinnedSslEndDate)) { - - // 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; + // Reload the current WebView. + currentWebView.reload(); + return true; - // Display the SSL error `AlertDialog`. - AppCompatDialogFragment sslCertificateErrorDialogFragment = SslCertificateErrorDialog.displayDialog(error); - sslCertificateErrorDialogFragment.show(supportFragmentManager, getString(R.string.ssl_certificate_error)); - } - } - }); + case R.id.easyprivacy: + // Toggle the EasyPrivacy status. + currentWebView.enableBlocklist(NestedScrollWebView.EASY_PRIVACY, !currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY)); - // Get the intent that started the app. - Intent launchingIntent = getIntent(); + // Update the menu checkbox. + menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY)); - // Get the information from the intent. - String launchingIntentAction = launchingIntent.getAction(); - Uri launchingIntentUriData = launchingIntent.getData(); + // Reload the current WebView. + currentWebView.reload(); + return true; - // If the intent action is a web search, perform the search. - if ((launchingIntentAction != null) && launchingIntentAction.equals(Intent.ACTION_WEB_SEARCH)) { - // Create an encoded URL string. - String encodedUrlString; + case R.id.fanboys_annoyance_list: + // Toggle Fanboy's Annoyance List status. + currentWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST)); - // Sanitize the search input and convert it to a search. - try { - encodedUrlString = URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8"); - } catch (UnsupportedEncodingException exception) { - encodedUrlString = ""; - } + // Update the menu checkbox. + menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST)); - // Add the base search URL. - formattedUrlString = searchURL + encodedUrlString; - } else if (launchingIntentUriData != null){ // Check to see if the intent contains a new URL. - // Set the formatted URL string. - formattedUrlString = launchingIntentUriData.toString(); - } + // 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)); - // Load the website if not waiting for Orbot to connect. - if (!waitingForOrbot) { - loadUrl(formattedUrlString); - } - } + // Reload the current WebView. + currentWebView.reload(); + return true; - @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); + 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)); - // Get the information from the intent. - String intentAction = intent.getAction(); - Uri intentUriData = intent.getData(); + // Update the menu checkbox. + menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST)); - // If the intent action is a web search, perform the search. - if ((intentAction != null) && intentAction.equals(Intent.ACTION_WEB_SEARCH)) { - // Create an encoded URL string. - String encodedUrlString; + // Reload the current WebView. + currentWebView.reload(); + return true; - // Sanitize the search input and convert it to a search. - try { - encodedUrlString = URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8"); - } catch (UnsupportedEncodingException exception) { - encodedUrlString = ""; - } + case R.id.ultraprivacy: + // Toggle the UltraPrivacy status. + currentWebView.enableBlocklist(NestedScrollWebView.ULTRA_PRIVACY, !currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY)); - // Add the base search URL. - formattedUrlString = searchURL + encodedUrlString; - } else if (intentUriData != null){ // Check to see if the intent contains a new URL. - // Set the formatted URL string. - formattedUrlString = intentUriData.toString(); - } + // Update the menu checkbox. + menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY)); - // Load the URL. - loadUrl(formattedUrlString); + // Reload the current WebView. + currentWebView.reload(); + return true; - // Close the navigation drawer if it is open. - if (drawerLayout.isDrawerVisible(GravityCompat.START)) { - drawerLayout.closeDrawer(GravityCompat.START); - } + 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)); - // Close the bookmarks drawer if it is open. - if (drawerLayout.isDrawerVisible(GravityCompat.END)) { - drawerLayout.closeDrawer(GravityCompat.END); - } + // Update the menu checkbox. + menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS)); - // Clear the keyboard if displayed and remove the focus on the urlTextBar if it has it. - mainWebView.requestFocus(); - } + // Reload the current WebView. + currentWebView.reload(); + return true; - @Override - public void onRestart() { - // Run the default commands. - super.onRestart(); + case R.id.user_agent_privacy_browser: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[0]); - // Make sure Orbot is running if Privacy Browser is proxying through Orbot. - if (proxyThroughOrbot) { - // Request Orbot to start. If Orbot is already running no hard will be caused by this request. - Intent orbotIntent = new Intent("org.torproject.android.intent.action.START"); + // Reload the current WebView. + currentWebView.reload(); + return true; - // Send the intent to the Orbot package. - orbotIntent.setPackage("org.torproject.android"); + case R.id.user_agent_webview_default: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(""); - // Make it so. - sendBroadcast(orbotIntent); - } + // Reload the current WebView. + currentWebView.reload(); + return true; - // Apply the app settings if returning from the Settings activity.. - if (reapplyAppSettingsOnRestart) { - // Apply the app settings. - applyAppSettings(); + case R.id.user_agent_firefox_on_android: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[2]); - // Reload the webpage if displaying of images has been disabled in the Settings activity. - if (reloadOnRestart) { - // Reload `mainWebView`. - mainWebView.reload(); + // Reload the current WebView. + currentWebView.reload(); + return true; - // Reset `reloadOnRestartBoolean`. - reloadOnRestart = false; - } + case R.id.user_agent_chrome_on_android: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[3]); - // Reset the return from settings flag. - reapplyAppSettingsOnRestart = false; - } + // Reload the current WebView. + currentWebView.reload(); + return true; - // Apply the domain settings if returning from the Domains activity. - if (reapplyDomainSettingsOnRestart) { - // Reapply the domain settings. - applyDomainSettings(formattedUrlString, false, true); + case R.id.user_agent_safari_on_ios: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[4]); - // Reset `reapplyDomainSettingsOnRestart`. - reapplyDomainSettingsOnRestart = false; - } + // Reload the current WebView. + currentWebView.reload(); + return true; - // Load the URL on restart to apply changes to night mode. - if (loadUrlOnRestart) { - // Load the current `formattedUrlString`. - loadUrl(formattedUrlString); + case R.id.user_agent_firefox_on_linux: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[5]); - // Reset `loadUrlOnRestart. - loadUrlOnRestart = false; - } + // Reload the current WebView. + currentWebView.reload(); + return true; - // Update the bookmarks drawer if returning from the Bookmarks activity. - if (restartFromBookmarksActivity) { - // Close the bookmarks drawer. - drawerLayout.closeDrawer(GravityCompat.END); + case R.id.user_agent_chromium_on_linux: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[6]); - // Reload the bookmarks drawer. - loadBookmarksFolder(); + // Reload the current WebView. + currentWebView.reload(); + return true; - // Reset `restartFromBookmarksActivity`. - restartFromBookmarksActivity = false; - } + case R.id.user_agent_firefox_on_windows: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[7]); - // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. This can be important if the screen was rotated. - updatePrivacyIcons(true); - } + // Reload the current WebView. + currentWebView.reload(); + return true; - // `onResume()` runs after `onStart()`, which runs after `onCreate()` and `onRestart()`. - @Override - public void onResume() { - // Run the default commands. - super.onResume(); + case R.id.user_agent_chrome_on_windows: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[8]); - // Resume JavaScript (if enabled). - mainWebView.resumeTimers(); + // Reload the current WebView. + currentWebView.reload(); + return true; - // Resume `mainWebView`. - mainWebView.onResume(); + case R.id.user_agent_edge_on_windows: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[9]); - // Resume the adView for the free flavor. - if (BuildConfig.FLAVOR.contentEquals("free")) { - // Resume the ad. - AdHelper.resumeAd(findViewById(R.id.adview)); - } + // Reload the current WebView. + currentWebView.reload(); + return true; - // Display a message to the user if waiting for Orbot. - if (waitingForOrbot && !orbotStatus.equals("ON")) { - // Disable the wide view port so that the waiting for Orbot text is displayed correctly. - mainWebView.getSettings().setUseWideViewPort(false); + case R.id.user_agent_internet_explorer_on_windows: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[10]); - // Load a waiting page. `null` specifies no encoding, which defaults to ASCII. - mainWebView.loadData(waitingForOrbotHtmlString, "text/html", null); - } + // Reload the current WebView. + currentWebView.reload(); + return true; - if (displayingFullScreenVideo) { - // Remove the translucent overlays. - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + case R.id.user_agent_safari_on_macos: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[11]); - // 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); + // Reload the current WebView. + currentWebView.reload(); + 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 re-hides 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.user_agent_custom: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value))); - @Override - public void onPause() { - // Run the default commands. - super.onPause(); + // Reload the current WebView. + currentWebView.reload(); + return true; - // Pause `mainWebView`. - mainWebView.onPause(); + case R.id.font_size_twenty_five_percent: + currentWebView.getSettings().setTextZoom(25); + return true; - // Stop all JavaScript. - mainWebView.pauseTimers(); + case R.id.font_size_fifty_percent: + currentWebView.getSettings().setTextZoom(50); + return true; - // 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)); - } - } + case R.id.font_size_seventy_five_percent: + currentWebView.getSettings().setTextZoom(75); + return true; - @Override - public void onDestroy() { - // Unregister the Orbot status broadcast receiver. - this.unregisterReceiver(orbotStatusBroadcastReceiver); + case R.id.font_size_one_hundred_percent: + currentWebView.getSettings().setTextZoom(100); + return true; - // Close the bookmarks cursor and database. - bookmarksCursor.close(); - bookmarksDatabaseHelper.close(); + case R.id.font_size_one_hundred_twenty_five_percent: + currentWebView.getSettings().setTextZoom(125); + return true; - // Run the default commands. - super.onDestroy(); - } + case R.id.font_size_one_hundred_fifty_percent: + currentWebView.getSettings().setTextZoom(150); + return true; - @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); + case R.id.font_size_one_hundred_seventy_five_percent: + currentWebView.getSettings().setTextZoom(175); + return true; - // Set mainMenu so it can be used by `onOptionsItemSelected()` and `updatePrivacyIcons`. - mainMenu = menu; + case R.id.font_size_two_hundred_percent: + currentWebView.getSettings().setTextZoom(200); + return true; - // Set the initial status of the privacy icons. `false` does not call `invalidateOptionsMenu` as the last step. - updatePrivacyIcons(false); + case R.id.swipe_to_refresh: + // Toggle the stored status of swipe to refresh. + currentWebView.setSwipeToRefresh(!currentWebView.getSwipeToRefresh()); + + // Get a handle for the swipe refresh layout. + SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout); + + // Update the swipe refresh layout. + if (currentWebView.getSwipeToRefresh()) { // Swipe to refresh is enabled. + if (Build.VERSION.SDK_INT >= 23) { // For API >= 23, the status of the scroll refresh listener is continuously updated by the on scroll change listener. + // Only enable the swipe refresh layout if the WebView is scrolled to the top. + swipeRefreshLayout.setEnabled(currentWebView.getY() == 0); + } else { // For API < 23, the swipe refresh layout is always enabled. + // Enable the swipe refresh layout. + swipeRefreshLayout.setEnabled(true); + } + } else { // Swipe to refresh is disabled. + // Disable the swipe refresh layout. + swipeRefreshLayout.setEnabled(false); + } + return true; - // 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. - refreshMenuItem = menu.findItem(R.id.refresh); - blocklistsMenuItem = menu.findItem(R.id.blocklists); - easyListMenuItem = menu.findItem(R.id.easylist); - easyPrivacyMenuItem = menu.findItem(R.id.easyprivacy); - fanboysAnnoyanceListMenuItem = menu.findItem(R.id.fanboys_annoyance_list); - fanboysSocialBlockingListMenuItem = menu.findItem(R.id.fanboys_social_blocking_list); - ultraPrivacyMenuItem = menu.findItem(R.id.ultraprivacy); - blockAllThirdPartyRequestsMenuItem = menu.findItem(R.id.block_all_third_party_requests); - MenuItem adConsentMenuItem = menu.findItem(R.id.ad_consent); + case R.id.display_images: + if (currentWebView.getSettings().getLoadsImagesAutomatically()) { // Images are currently loaded automatically. + // Disable loading of images. + currentWebView.getSettings().setLoadsImagesAutomatically(false); - // Only display third-party cookies if API >= 21 - toggleThirdPartyCookiesMenuItem.setVisible(Build.VERSION.SDK_INT >= 21); + // 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); + } + return true; - // 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); + case R.id.night_mode: + // Toggle night mode. + currentWebView.setNightMode(!currentWebView.getNightMode()); - // Only show Ad Consent if this is the free flavor. - adConsentMenuItem.setVisible(BuildConfig.FLAVOR.contentEquals("free")); + // Enable or disable JavaScript according to night mode, the global preference, and any domain settings. + if (currentWebView.getNightMode()) { // Night mode is enabled, which requires JavaScript. + // Enable JavaScript. + currentWebView.getSettings().setJavaScriptEnabled(true); + } else if (currentWebView.getDomainSettingsApplied()) { // Night mode is disabled and domain settings are applied. Set JavaScript according to the domain settings. + // Apply the JavaScript preference that was stored the last time domain settings were loaded. + currentWebView.getSettings().setJavaScriptEnabled(currentWebView.getDomainSettingsJavaScriptEnabled()); + } else { // Night mode is disabled and domain settings are not applied. Set JavaScript according to the global preference. + // Apply the JavaScript preference. + currentWebView.getSettings().setJavaScriptEnabled(sharedPreferences.getBoolean("javascript", false)); + } - // Get the shared preference values. - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + // Update the privacy icons. + updatePrivacyIcons(false); - // Get the status of the additional AppBar icons. - displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false); + // Reload the website. + currentWebView.reload(); + return true; - // 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); - } + 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); - // Replace Refresh with Stop if a URL is already loading. - if (urlIsLoading) { - // Set the title. - refreshMenuItem.setTitle(R.string.stop); + // Set the minimum height of the find on page linear layout to match the toolbar. + findOnPageLinearLayout.setMinimumHeight(toolbar.getHeight()); - // If the icon is displayed in the AppBar, set it according to the theme. - if (displayAdditionalAppBarIcons) { - if (darkTheme) { - refreshMenuItem.setIcon(R.drawable.close_dark); - } else { - refreshMenuItem.setIcon(R.drawable.close_light); - } - } - } + // Hide the toolbar. + toolbar.setVisibility(View.GONE); - return true; - } + // Show the find on page linear layout. + findOnPageLinearLayout.setVisibility(View.VISIBLE); - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - // Get handles for the menu items. - MenuItem addOrEditDomain = menu.findItem(R.id.add_or_edit_domain); - 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 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 fontSizeMenuItem = menu.findItem(R.id.font_size); - MenuItem swipeToRefreshMenuItem = menu.findItem(R.id.swipe_to_refresh); - MenuItem displayImagesMenuItem = menu.findItem(R.id.display_images); - MenuItem nightModeMenuItem = menu.findItem(R.id.night_mode); - MenuItem proxyThroughOrbotMenuItem = menu.findItem(R.id.proxy_through_orbot); + // 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(); - // Set the text for the domain menu item. - if (domainSettingsApplied) { - addOrEditDomain.setTitle(R.string.edit_domain_settings); - } else { - addOrEditDomain.setTitle(R.string.add_domain_settings); - } + // Get a handle for the input method manager. + InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - // Set the status of the menu item checkboxes. - toggleFirstPartyCookiesMenuItem.setChecked(firstPartyCookiesEnabled); - toggleThirdPartyCookiesMenuItem.setChecked(thirdPartyCookiesEnabled); - toggleDomStorageMenuItem.setChecked(domStorageEnabled); - toggleSaveFormDataMenuItem.setChecked(saveFormDataEnabled); // Form data can be removed once the minimum API >= 26. - easyListMenuItem.setChecked(easyListEnabled); - easyPrivacyMenuItem.setChecked(easyPrivacyEnabled); - fanboysAnnoyanceListMenuItem.setChecked(fanboysAnnoyanceListEnabled); - fanboysSocialBlockingListMenuItem.setChecked(fanboysSocialBlockingListEnabled); - ultraPrivacyMenuItem.setChecked(ultraPrivacyEnabled); - blockAllThirdPartyRequestsMenuItem.setChecked(blockAllThirdPartyRequests); - swipeToRefreshMenuItem.setChecked(swipeRefreshLayout.isEnabled()); - displayImagesMenuItem.setChecked(mainWebView.getSettings().getLoadsImagesAutomatically()); - nightModeMenuItem.setChecked(nightMode); - proxyThroughOrbotMenuItem.setChecked(proxyThroughOrbot); + // Remove the lint warning below that the input method manager might be null. + assert inputMethodManager != null; - // Enable third-party cookies if first-party cookies are enabled. - toggleThirdPartyCookiesMenuItem.setEnabled(firstPartyCookiesEnabled); + // Display the keyboard. `0` sets no input flags. + inputMethodManager.showSoftInput(findOnPageEditText, 0); + }, 200); + return true; - // Enable DOM Storage if JavaScript is enabled. - toggleDomStorageMenuItem.setEnabled(javaScriptEnabled); + case R.id.view_source: + // Create an intent to launch the view source activity. + Intent viewSourceIntent = new Intent(this, ViewSourceActivity.class); - // Enable Clear Cookies if there are any. - clearCookiesMenuItem.setEnabled(cookieManager.hasCookies()); + // Add the variables to the intent. + viewSourceIntent.putExtra("user_agent", currentWebView.getSettings().getUserAgentString()); + viewSourceIntent.putExtra("current_url", currentWebView.getUrl()); - // 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; - } + // Make it so. + startActivity(viewSourceIntent); + 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.share_url: + // Setup the share string. + String shareString = currentWebView.getTitle() + " – " + currentWebView.getUrl(); - // Enable Clear DOM Storage if there is any. - clearDOMStorageMenuItem.setEnabled(localStorageDirectoryNumberOfFiles > 0 || indexedDBDirectoryNumberOfFiles > 0); + // Create the share intent. + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.putExtra(Intent.EXTRA_TEXT, shareString); + shareIntent.setType("text/plain"); - // Enable Clear Form Data is there is any. This can be removed once the minimum API >= 26. - if (Build.VERSION.SDK_INT < 26) { - WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(this); - clearFormDataMenuItem.setEnabled(mainWebViewDatabase.hasFormData()); - } else { - // Disable clear form data because it is not supported on current version of Android. - clearFormDataMenuItem.setEnabled(false); - } + // Make it so. + startActivity(Intent.createChooser(shareIntent, getString(R.string.share_url))); + return true; - // Enable Clear Data if any of the submenu items are enabled. - clearDataMenuItem.setEnabled(clearCookiesMenuItem.isEnabled() || clearDOMStorageMenuItem.isEnabled() || clearFormDataMenuItem.isEnabled()); + case R.id.print: + // Get a print manager instance. + PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE); - // Disable Fanboy's Social Blocking List if Fanboy's Annoyance List is checked. - fanboysSocialBlockingListMenuItem.setEnabled(!fanboysAnnoyanceListEnabled); + // Remove the lint error below that print manager might be null. + assert printManager != null; - // Initialize the display names for the blocklists with the number of blocked requests. - blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + blockedRequests); - easyListMenuItem.setTitle(easyListBlockedRequests + " - " + getString(R.string.easylist)); - easyPrivacyMenuItem.setTitle(easyPrivacyBlockedRequests + " - " + getString(R.string.easyprivacy)); - fanboysAnnoyanceListMenuItem.setTitle(fanboysAnnoyanceListBlockedRequests + " - " + getString(R.string.fanboys_annoyance_list)); - fanboysSocialBlockingListMenuItem.setTitle(fanboysSocialBlockingListBlockedRequests + " - " + getString(R.string.fanboys_social_blocking_list)); - ultraPrivacyMenuItem.setTitle(ultraPrivacyBlockedRequests + " - " + getString(R.string.ultraprivacy)); - blockAllThirdPartyRequestsMenuItem.setTitle(thirdPartyBlockedRequests + " - " + getString(R.string.block_all_third_party_requests)); + // Create a print document adapter from the current WebView. + PrintDocumentAdapter printDocumentAdapter = currentWebView.createPrintDocumentAdapter(); - // Get the current user agent. - String currentUserAgent = mainWebView.getSettings().getUserAgentString(); + // Print the document. + printManager.print(getString(R.string.privacy_browser_web_page), printDocumentAdapter, null); + return true; - // 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. - menu.findItem(R.id.user_agent_privacy_browser).setChecked(true); - } else if (currentUserAgent.equals(webViewDefaultUserAgent)) { // WebView Default. - 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. - 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. - 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. - 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. - 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. - 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. - 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. - 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. - 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. - 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. - menu.findItem(R.id.user_agent_safari_on_macos).setChecked(true); - } else { // Custom user agent. - menu.findItem(R.id.user_agent_custom).setChecked(true); - } + case R.id.open_with_app: + openWithApp(currentWebView.getUrl()); + return true; - // Initialize font size variables. - int fontSize = mainWebView.getSettings().getTextZoom(); - String fontSizeTitle; - MenuItem selectedFontSizeMenuItem; + case R.id.open_with_browser: + openWithBrowser(currentWebView.getUrl()); + return true; - // 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; + case R.id.add_to_homescreen: + // Instantiate the create home screen shortcut dialog. + DialogFragment createHomeScreenShortcutDialogFragment = CreateHomeScreenShortcutDialog.createDialog(currentWebView.getTitle(), currentWebView.getUrl(), + currentWebView.getFavoriteOrDefaultIcon()); - case 50: - fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.fifty_percent); - selectedFontSizeMenuItem = menu.findItem(R.id.font_size_fifty_percent); - break; - - 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 100: - fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent); - selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent); - break; + // Show the create home screen shortcut dialog. + createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getString(R.string.create_shortcut)); + return true; - 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; + case R.id.proxy_through_orbot: + // Toggle the proxy through Orbot variable. + proxyThroughOrbot = !proxyThroughOrbot; - 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; + // Apply the proxy through Orbot settings. + applyProxyThroughOrbot(true); + return true; - 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; + 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(); + } + return true; - 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; + case R.id.ad_consent: + // Display the ad consent dialog. + DialogFragment adConsentDialogFragment = new AdConsentDialog(); + adConsentDialogFragment.show(getSupportFragmentManager(), getString(R.string.ad_consent)); + 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; + // Don't consume the event. + return super.onOptionsItemSelected(menuItem); } - - // Set the font size title and select the current size menu item. - fontSizeMenuItem.setTitle(fontSizeTitle); - selectedFontSizeMenuItem.setChecked(true); - - // Run all the other default commands. - super.onPrepareOptionsMenu(menu); - - // Display the menu. - return true; } - @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) { - // Get the selected menu item ID. + @Override + public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) { + // Get the menu item ID. int menuItemId = menuItem.getItemId(); - // Run the commands that correlate to the selected menu item. + // 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.toggle_javascript: - // Switch the status of javaScriptEnabled. - javaScriptEnabled = !javaScriptEnabled; + case R.id.clear_and_exit: + // Clear and exit Privacy Browser. + clearAndExit(); + break; - // Apply the new JavaScript status. - mainWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled); + case R.id.home: + // Select the homepage based on the proxy through Orbot status. + if (proxyThroughOrbot) { + // Load the Tor homepage. + loadUrl(sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value))); + } else { + // Load the normal homepage. + loadUrl(sharedPreferences.getString("homepage", getString(R.string.homepage_default_value))); + } + break; - // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. - updatePrivacyIcons(true); + case R.id.back: + if (currentWebView.canGoBack()) { + // Reset the current domain name so that navigation works if third-party requests are blocked. + currentWebView.resetCurrentDomainName(); - // 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(); + // Set navigating history so that the domain settings are applied when the new URL is loaded. + currentWebView.setNavigatingHistory(true); + + // Load the previous website in the history. + currentWebView.goBack(); } + break; - // Reload the WebView. - mainWebView.reload(); - return true; + case R.id.forward: + if (currentWebView.canGoForward()) { + // Reset the current domain name so that navigation works if third-party requests are blocked. + currentWebView.resetCurrentDomainName(); - case R.id.add_or_edit_domain: - if (domainSettingsApplied) { // Edit the current domain settings. - // Reapply the domain settings on returning to `MainWebViewActivity`. - reapplyDomainSettingsOnRestart = true; - currentDomainName = ""; + // Set navigating history so that the domain settings are applied when the new URL is loaded. + currentWebView.setNavigatingHistory(true); - // Create an intent to launch the domains activity. - Intent domainsIntent = new Intent(this, DomainsActivity.class); + // Load the next website in the history. + currentWebView.goForward(); + } + break; - // Put extra information instructing the domains activity to directly load the current domain and close on back instead of returning to the domains list. - domainsIntent.putExtra("loadDomain", domainSettingsDatabaseId); - domainsIntent.putExtra("closeOnBack", true); + case R.id.history: + // Instantiate the URL history dialog. + DialogFragment urlHistoryDialogFragment = UrlHistoryDialog.loadBackForwardList(currentWebView.getWebViewFragmentId()); - // Make it so. - startActivity(domainsIntent); - } else { // Add a new domain. - // Apply the new domain settings on returning to `MainWebViewActivity`. - reapplyDomainSettingsOnRestart = true; - currentDomainName = ""; + // Show the URL history dialog. + urlHistoryDialogFragment.show(getSupportFragmentManager(), getString(R.string.history)); + break; - // Get the current domain - Uri currentUri = Uri.parse(formattedUrlString); - String currentDomain = currentUri.getHost(); + case R.id.requests: + // Populate the resource requests. + RequestsActivity.resourceRequests = currentWebView.getResourceRequests(); - // 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 an intent to launch the Requests activity. + Intent requestsIntent = new Intent(this, RequestsActivity.class); - // Create the domain and store the database ID. - int newDomainDatabaseId = domainsDatabaseHelper.addDomain(currentDomain); + // Add the block third-party requests status to the intent. + requestsIntent.putExtra("block_all_third_party_requests", currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS)); - // Create an intent to launch the domains activity. - Intent domainsIntent = new Intent(this, DomainsActivity.class); + // Make it so. + startActivity(requestsIntent); + break; - // Put extra information instructing the domains activity to directly load the new domain and close on back instead of returning to the domains list. - domainsIntent.putExtra("loadDomain", newDomainDatabaseId); - domainsIntent.putExtra("closeOnBack", true); + case R.id.downloads: + // Launch the system Download Manager. + Intent downloadManagerIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS); - // Make it so. - startActivity(domainsIntent); - } - return true; + // 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); - case R.id.toggle_first_party_cookies: - // Switch the status of firstPartyCookiesEnabled. - firstPartyCookiesEnabled = !firstPartyCookiesEnabled; + startActivity(downloadManagerIntent); + break; - // Update the menu checkbox. - menuItem.setChecked(firstPartyCookiesEnabled); + case R.id.domains: + // Set the flag to reapply the domain settings on restart when returning from Domain Settings. + reapplyDomainSettingsOnRestart = true; - // Apply the new cookie status. - cookieManager.setAcceptCookie(firstPartyCookiesEnabled); + // Launch the domains activity. + Intent domainsIntent = new Intent(this, DomainsActivity.class); - // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. - updatePrivacyIcons(true); + // 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); + } - // 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(); + // 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()); } - // Reload the WebView. - mainWebView.reload(); - return true; + // Make it so. + startActivity(domainsIntent); + break; - case R.id.toggle_third_party_cookies: - if (Build.VERSION.SDK_INT >= 21) { - // Switch the status of thirdPartyCookiesEnabled. - thirdPartyCookiesEnabled = !thirdPartyCookiesEnabled; + case R.id.settings: + // Set the flag to reapply app settings on restart when returning from Settings. + reapplyAppSettingsOnRestart = true; - // Update the menu checkbox. - menuItem.setChecked(thirdPartyCookiesEnabled); + // Set the flag to reapply the domain settings on restart when returning from Settings. + reapplyDomainSettingsOnRestart = true; - // Apply the new cookie status. - cookieManager.setAcceptThirdPartyCookies(mainWebView, thirdPartyCookiesEnabled); + // Launch the settings activity. + Intent settingsIntent = new Intent(this, SettingsActivity.class); + startActivity(settingsIntent); + break; - // 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.import_export: + // Launch the import/export activity. + Intent importExportIntent = new Intent (this, ImportExportActivity.class); + startActivity(importExportIntent); + break; - // Reload the WebView. - mainWebView.reload(); - } // Else do nothing because SDK < 21. - return true; + case R.id.logcat: + // Launch the logcat activity. + Intent logcatIntent = new Intent(this, LogcatActivity.class); + startActivity(logcatIntent); + break; - case R.id.toggle_dom_storage: - // Switch the status of domStorageEnabled. - domStorageEnabled = !domStorageEnabled; + case R.id.guide: + // Launch `GuideActivity`. + Intent guideIntent = new Intent(this, GuideActivity.class); + startActivity(guideIntent); + break; - // Update the menu checkbox. - menuItem.setChecked(domStorageEnabled); + case R.id.about: + // Create an intent to launch the about activity. + Intent aboutIntent = new Intent(this, AboutActivity.class); - // Apply the new DOM Storage status. - mainWebView.getSettings().setDomStorageEnabled(domStorageEnabled); + // 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], + ultraPrivacy.get(0).get(0)[0]}; - // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. - updatePrivacyIcons(true); + // Add the blocklist versions to the intent. + aboutIntent.putExtra("blocklist_versions", blocklistVersions); - // 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(); - } + // Make it so. + startActivity(aboutIntent); + break; + } - // Reload the WebView. - mainWebView.reload(); - return true; + // Get a handle for the drawer layout. + DrawerLayout drawerLayout = findViewById(R.id.drawerlayout); - // Form data can be removed once the minimum API >= 26. - case R.id.toggle_save_form_data: - // Switch the status of saveFormDataEnabled. - saveFormDataEnabled = !saveFormDataEnabled; + // Close the navigation drawer. + drawerLayout.closeDrawer(GravityCompat.START); + return true; + } - // Update the menu checkbox. - menuItem.setChecked(saveFormDataEnabled); + @Override + public void onPostCreate(Bundle savedInstanceState) { + // Run the default commands. + super.onPostCreate(savedInstanceState); - // Apply the new form data status. - mainWebView.getSettings().setSaveFormData(saveFormDataEnabled); + // Sync the state of the DrawerToggle after the default `onRestoreInstanceState()` has finished. This creates the navigation drawer icon. + actionBarDrawerToggle.syncState(); + } - // 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(); - } + @Override + public void onConfigurationChanged(Configuration newConfig) { + // Run the default commands. + super.onConfigurationChanged(newConfig); - // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. - updatePrivacyIcons(true); + // Get the status bar pixel size. + int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android"); + int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId); - // Reload the WebView. - mainWebView.reload(); - return true; + // Get the resource density. + float screenDensity = getResources().getDisplayMetrics().density; - case R.id.clear_cookies: - Snackbar.make(findViewById(R.id.main_webview), 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() { - @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; + // Recalculate the drawer header padding. + drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity); + drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity); + drawerHeaderPaddingBottom = (int) (8 * screenDensity); - 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, 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(); - - // Initialize a handler to manually delete the DOM storage files and directories. - Handler deleteDomStorageHandler = new Handler(); - - // Setup a runnable to manually delete the DOM storage files and directories. - Runnable deleteDomStorageRunnable = () -> { - try { - // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly. - Process deleteLocalStorageProcess = privacyBrowserRuntime.exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"}); - - // Multiple commands must be used because `Runtime.exec()` does not like `*`. - Process deleteIndexProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB"); - Process deleteQuotaManagerProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager"); - Process deleteQuotaManagerJournalProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal"); - Process deleteDatabasesProcess = privacyBrowserRuntime.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. - } - }; - - // Manually delete the DOM storage files after 200 milliseconds. - deleteDomStorageHandler.postDelayed(deleteDomStorageRunnable, 200); - } - } - }) - .show(); - return true; + // 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)); + } - // Form data can be remove once the minimum API >= 26. - 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, 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; + // `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); + } - case R.id.easylist: - // Toggle the EasyList status. - easyListEnabled = !easyListEnabled; + @Override + public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) { + // Store the hit test result. + final WebView.HitTestResult hitTestResult = currentWebView.getHitTestResult(); - // Update the menu checkbox. - menuItem.setChecked(easyListEnabled); + // Create the URL strings. + final String imageUrl; + final String linkUrl; - // Reload the main WebView. - mainWebView.reload(); - return true; + // Get handles for the system managers. + final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); + FragmentManager fragmentManager = getSupportFragmentManager(); + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - case R.id.easyprivacy: - // Toggle the EasyPrivacy status. - easyPrivacyEnabled = !easyPrivacyEnabled; + // Remove the lint errors below that the clipboard manager might be null. + assert clipboardManager != null; - // Update the menu checkbox. - menuItem.setChecked(easyPrivacyEnabled); + // 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(); - // Reload the main WebView. - mainWebView.reload(); - return true; + // Set the target URL as the title of the `ContextMenu`. + menu.setHeaderTitle(linkUrl); - case R.id.fanboys_annoyance_list: - // Toggle Fanboy's Annoyance List status. - fanboysAnnoyanceListEnabled = !fanboysAnnoyanceListEnabled; + // 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. + addNewTab(linkUrl); + return false; + }); - // Update the menu checkbox. - menuItem.setChecked(fanboysAnnoyanceListEnabled); + // Add an Open with App entry. + menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> { + openWithApp(linkUrl); + return false; + }); - // Update the staus of Fanboy's Social Blocking List. - MenuItem fanboysSocialBlockingListMenuItem = mainMenu.findItem(R.id.fanboys_social_blocking_list); - fanboysSocialBlockingListMenuItem.setEnabled(!fanboysAnnoyanceListEnabled); + // Add an Open with Browser entry. + menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> { + openWithBrowser(linkUrl); + return false; + }); - // Reload the main WebView. - mainWebView.reload(); - 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); - case R.id.fanboys_social_blocking_list: - // Toggle Fanboy's Social Blocking List status. - fanboysSocialBlockingListEnabled = !fanboysSocialBlockingListEnabled; + // Set the `ClipData` as the clipboard's primary clip. + clipboardManager.setPrimaryClip(srcAnchorTypeClipData); + return false; + }); - // Update the menu checkbox. - menuItem.setChecked(fanboysSocialBlockingListEnabled); + // Add a Download URL entry. + menu.add(R.string.download_url).setOnMenuItemClickListener((MenuItem item) -> { + // Check if the download should be processed by an external app. + if (sharedPreferences.getBoolean("download_with_external_app", false)) { // Download with an external app. + openUrlWithExternalApp(linkUrl); + } else { // Download with Android's download manager. + // Check to see if the storage permission has already been granted. + if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested. + // Store the variables for future use by `onRequestPermissionsResult()`. + downloadUrl = linkUrl; + downloadContentDisposition = "none"; + downloadContentLength = -1; - // Reload the main WebView. - mainWebView.reload(); - return true; + // Show a dialog if the user has previously denied the permission. + if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first. + // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE. + DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE); - case R.id.ultraprivacy: - // Toggle the UltraPrivacy status. - ultraPrivacyEnabled = !ultraPrivacyEnabled; + // Show the download location permission alert dialog. The permission will be requested when the the dialog is closed. + downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location)); + } else { // Show the permission request directly. + // Request the permission. The download dialog will be launched by `onRequestPermissionResult()`. + ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE); + } + } else { // The storage permission has already been granted. + // Get a handle for the download file alert dialog. + DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(linkUrl, "none", -1); - // Update the menu checkbox. - menuItem.setChecked(ultraPrivacyEnabled); + // Show the download file alert dialog. + downloadFileDialogFragment.show(fragmentManager, getString(R.string.download)); + } + } + return false; + }); - // Reload the main WebView. - mainWebView.reload(); - return true; + // Add a Cancel entry, which by default closes the context menu. + menu.add(R.string.cancel); + break; - case R.id.block_all_third_party_requests: - //Toggle the third-party requests blocker status. - blockAllThirdPartyRequests = !blockAllThirdPartyRequests; + case WebView.HitTestResult.EMAIL_TYPE: + // Get the target URL. + linkUrl = hitTestResult.getExtra(); - // Update the menu checkbox. - menuItem.setChecked(blockAllThirdPartyRequests); + // Set the target URL as the title of the `ContextMenu`. + menu.setHeaderTitle(linkUrl); - // Reload the main WebView. - mainWebView.reload(); - return true; + // 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); - case R.id.user_agent_privacy_browser: - // Update the user agent. - mainWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[0]); + // Parse the url and set it as the data for the `Intent`. + emailIntent.setData(Uri.parse("mailto:" + linkUrl)); - // Reload the WebView. - mainWebView.reload(); - return true; + // `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); - case R.id.user_agent_webview_default: - // Update the user agent. - mainWebView.getSettings().setUserAgentString(""); + // Make it so. + startActivity(emailIntent); + return false; + }); - // Reload the WebView. - mainWebView.reload(); - 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); - case R.id.user_agent_firefox_on_android: - // Update the user agent. - mainWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[2]); + // Set the `ClipData` as the clipboard's primary clip. + clipboardManager.setPrimaryClip(srcEmailTypeClipData); + return false; + }); - // Reload the WebView. - mainWebView.reload(); - return true; + // Add a `Cancel` entry, which by default closes the `ContextMenu`. + menu.add(R.string.cancel); + break; - case R.id.user_agent_chrome_on_android: - // Update the user agent. - mainWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[3]); + // `IMAGE_TYPE` is an image. `SRC_IMAGE_ANCHOR_TYPE` is an image that is also a link. Privacy Browser processes them the same. + case WebView.HitTestResult.IMAGE_TYPE: + case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE: + // Get the image URL. + imageUrl = hitTestResult.getExtra(); - // Reload the WebView. - mainWebView.reload(); - return true; + // Set the image URL as the title of the context menu. + menu.setHeaderTitle(imageUrl); - case R.id.user_agent_safari_on_ios: - // Update the user agent. - mainWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[4]); + // Add an Open in New Tab entry. + menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> { + // Load the image URL in a new tab. + addNewTab(imageUrl); + return false; + }); - // Reload the WebView. - mainWebView.reload(); - return true; + // Add a View Image entry. + menu.add(R.string.view_image).setOnMenuItemClickListener(item -> { + loadUrl(imageUrl); + return false; + }); - case R.id.user_agent_firefox_on_linux: - // Update the user agent. - mainWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[5]); + // Add a `Download Image` entry. + menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> { + // Check if the download should be processed by an external app. + if (sharedPreferences.getBoolean("download_with_external_app", false)) { // Download with an external app. + openUrlWithExternalApp(imageUrl); + } else { // Download with Android's download manager. + // Check to see if the storage permission has already been granted. + if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested. + // Store the image URL for use by `onRequestPermissionResult()`. + downloadImageUrl = imageUrl; - // Reload the WebView. - mainWebView.reload(); - return true; + // Show a dialog if the user has previously denied the permission. + if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first. + // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE. + DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE); - case R.id.user_agent_chromium_on_linux: - // Update the user agent. - mainWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[6]); + // Show the download location permission alert dialog. The permission will be requested when the dialog is closed. + downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location)); + } else { // Show the permission request directly. + // Request the permission. The download dialog will be launched by `onRequestPermissionResult(). + ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE); + } + } else { // The storage permission has already been granted. + // Get a handle for the download image alert dialog. + DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl); - // Reload the WebView. - mainWebView.reload(); - return true; + // Show the download image alert dialog. + downloadImageDialogFragment.show(fragmentManager, getString(R.string.download)); + } + } + return false; + }); - case R.id.user_agent_firefox_on_windows: - // Update the user agent. - mainWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[7]); + // Add a `Copy URL` entry. + menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> { + // Save the image URL in a `ClipData`. + ClipData srcImageAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl); - // Reload the WebView. - mainWebView.reload(); - return true; + // Set the `ClipData` as the clipboard's primary clip. + clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData); + return false; + }); - case R.id.user_agent_chrome_on_windows: - // Update the user agent. - mainWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[8]); + // Add an Open with App entry. + menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> { + openWithApp(imageUrl); + return false; + }); - // Reload the WebView. - mainWebView.reload(); - return true; + // Add an Open with Browser entry. + menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> { + openWithBrowser(imageUrl); + return false; + }); - case R.id.user_agent_edge_on_windows: - // Update the user agent. - mainWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[9]); + // Add a `Cancel` entry, which by default closes the `ContextMenu`. + menu.add(R.string.cancel); + break; + } + } - // Reload the WebView. - mainWebView.reload(); - return true; + @Override + public void onCreateBookmark(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) { + // Get a handle for the bookmarks list view. + ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview); - case R.id.user_agent_internet_explorer_on_windows: - // Update the user agent. - mainWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[10]); + // Get the views from the dialog fragment. + EditText createBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_name_edittext); + EditText createBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_url_edittext); - // Reload the WebView. - mainWebView.reload(); - return true; + // Extract the strings from the edit texts. + String bookmarkNameString = createBookmarkNameEditText.getText().toString(); + String bookmarkUrlString = createBookmarkUrlEditText.getText().toString(); - case R.id.user_agent_safari_on_macos: - // Update the user agent. - mainWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[11]); + // Create a favorite icon byte array output stream. + ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream(); - // Reload the WebView. - mainWebView.reload(); - return true; + // 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); - case R.id.user_agent_custom: - // Update the user agent. - mainWebView.getSettings().setUserAgentString(defaultCustomUserAgentString); + // Convert the favorite icon byte array stream to a byte array. + byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray(); - // Reload the WebView. - mainWebView.reload(); - return true; + // Display the new bookmark below the current items in the (0 indexed) list. + int newBookmarkDisplayOrder = bookmarksListView.getCount(); - case R.id.font_size_twenty_five_percent: - mainWebView.getSettings().setTextZoom(25); - return true; + // Create the bookmark. + bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolder, newBookmarkDisplayOrder, favoriteIconByteArray); - case R.id.font_size_fifty_percent: - mainWebView.getSettings().setTextZoom(50); - return true; + // Update the bookmarks cursor with the current contents of this folder. + bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder); - case R.id.font_size_seventy_five_percent: - mainWebView.getSettings().setTextZoom(75); - return true; + // Update the list view. + bookmarksCursorAdapter.changeCursor(bookmarksCursor); - case R.id.font_size_one_hundred_percent: - mainWebView.getSettings().setTextZoom(100); - return true; + // Scroll to the new bookmark. + bookmarksListView.setSelection(newBookmarkDisplayOrder); + } - case R.id.font_size_one_hundred_twenty_five_percent: - mainWebView.getSettings().setTextZoom(125); - return true; - - case R.id.font_size_one_hundred_fifty_percent: - mainWebView.getSettings().setTextZoom(150); - return true; + @Override + public void onCreateBookmarkFolder(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) { + // Get a handle for the bookmarks list view. + ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview); - case R.id.font_size_one_hundred_seventy_five_percent: - mainWebView.getSettings().setTextZoom(175); - return true; + // Get handles for the views in the dialog fragment. + EditText createFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.create_folder_name_edittext); + RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon_radiobutton); + ImageView folderIconImageView = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon); - case R.id.font_size_two_hundred_percent: - mainWebView.getSettings().setTextZoom(200); - return true; + // Get new folder name string. + String folderNameString = createFolderNameEditText.getText().toString(); - case R.id.swipe_to_refresh: - // Toggle swipe to refresh. - swipeRefreshLayout.setEnabled(!swipeRefreshLayout.isEnabled()); - return true; + // Create a folder icon bitmap. + Bitmap folderIconBitmap; - 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); - } - return true; + // 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(); - case R.id.night_mode: - // Toggle night mode. - nightMode = !nightMode; + // Convert the folder icon drawable to a bitmap drawable. + BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable; - // Enable or disable JavaScript according to night mode, the global preference, and any domain settings. - if (nightMode) { // Night mode is enabled. Enable JavaScript. - // Update the global variable. - javaScriptEnabled = true; - } else if (domainSettingsApplied) { // Night mode is disabled and domain settings are applied. Set JavaScript according to the domain settings. - // Get the JavaScript preference that was stored the last time domain settings were loaded. - javaScriptEnabled = domainSettingsJavaScriptEnabled; - } else { // Night mode is disabled and domain settings are not applied. Set JavaScript according to the global preference. - // Get a handle for the shared preference. - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + // 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; + } - // Get the JavaScript preference. - javaScriptEnabled = sharedPreferences.getBoolean("javascript", false); - } + // Create a folder icon byte array output stream. + ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream(); - // Apply the JavaScript setting to the WebView. - mainWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled); + // 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); - // Update the privacy icons. - updatePrivacyIcons(false); + // Convert the folder icon byte array stream to a byte array. + byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray(); - // Reload the website. - mainWebView.reload(); - return true; + // 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); + } - case R.id.find_on_page: - // Hide the URL app bar. - supportAppBar.setVisibility(View.GONE); + // Create the folder, which will be placed at the top of the `ListView`. + bookmarksDatabaseHelper.createFolder(folderNameString, currentBookmarksFolder, folderIconByteArray); - // Show the Find on Page `RelativeLayout`. - findOnPageLinearLayout.setVisibility(View.VISIBLE); + // Update the bookmarks cursor with the current contents of this folder. + bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder); - // 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(() -> { - // Set the focus on `findOnPageEditText`. - findOnPageEditText.requestFocus(); + // Update the `ListView`. + bookmarksCursorAdapter.changeCursor(bookmarksCursor); - // Display the keyboard. `0` sets no input flags. - inputMethodManager.showSoftInput(findOnPageEditText, 0); - }, 200); - return true; + // Scroll to the new folder. + bookmarksListView.setSelection(0); + } - case R.id.view_source: - // Launch the View Source activity. - Intent viewSourceIntent = new Intent(this, ViewSourceActivity.class); - startActivity(viewSourceIntent); - return true; + @Override + public void onSaveBookmark(DialogFragment dialogFragment, int selectedBookmarkDatabaseId, Bitmap favoriteIconBitmap) { + // Get handles for the views from `dialogFragment`. + EditText editBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_name_edittext); + EditText editBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_url_edittext); + RadioButton currentBookmarkIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_current_icon_radiobutton); - case R.id.share_url: - // Setup the share string. - String shareString = webViewTitle + " – " + urlTextBox.getText().toString(); + // Store the bookmark strings. + String bookmarkNameString = editBookmarkNameEditText.getText().toString(); + String bookmarkUrlString = editBookmarkUrlEditText.getText().toString(); - // Create the share intent. - Intent shareIntent = new Intent(Intent.ACTION_SEND); - shareIntent.putExtra(Intent.EXTRA_TEXT, shareString); - shareIntent.setType("text/plain"); + // Update the bookmark. + if (currentBookmarkIconRadioButton.isChecked()) { // Update the bookmark without changing the favorite icon. + bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString); + } else { // Update the bookmark using the `WebView` favorite icon. + // Create a favorite icon byte array output stream. + ByteArrayOutputStream newFavoriteIconByteArrayOutputStream = new ByteArrayOutputStream(); - // Make it so. - startActivity(Intent.createChooser(shareIntent, getString(R.string.share_url))); - return true; + // 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, newFavoriteIconByteArrayOutputStream); - case R.id.print: - // Get a `PrintManager` instance. - PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE); + // Convert the favorite icon byte array stream to a byte array. + byte[] newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray(); - // Convert `mainWebView` to `printDocumentAdapter`. - PrintDocumentAdapter printDocumentAdapter = mainWebView.createPrintDocumentAdapter(); + // Update the bookmark and the favorite icon. + bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, newFavoriteIconByteArray); + } - // Remove the lint error below that `printManager` might be `null`. - assert printManager != null; + // Update the bookmarks cursor with the current contents of this folder. + bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder); - // Print the document. The print attributes are `null`. - printManager.print(getString(R.string.privacy_browser_web_page), printDocumentAdapter, null); - return true; + // Update the list view. + bookmarksCursorAdapter.changeCursor(bookmarksCursor); + } - case R.id.open_with_app: - openWithApp(formattedUrlString); - return true; + @Override + public void onSaveBookmarkFolder(DialogFragment dialogFragment, int selectedFolderDatabaseId, Bitmap favoriteIconBitmap) { + // Get handles for the views from `dialogFragment`. + EditText editFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_folder_name_edittext); + RadioButton currentFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_current_icon_radiobutton); + RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_radiobutton); + ImageView defaultFolderIconImageView = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_imageview); - case R.id.open_with_browser: - openWithBrowser(formattedUrlString); - return true; + // Get the new folder name. + String newFolderNameString = editFolderNameEditText.getText().toString(); - case R.id.add_to_homescreen: - // Show the alert dialog. - AppCompatDialogFragment createHomeScreenShortcutDialogFragment = new CreateHomeScreenShortcutDialog(); - createHomeScreenShortcutDialogFragment.show(supportFragmentManager, getString(R.string.create_shortcut)); + // 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; - //Everything else will be handled by the alert dialog and the associated listener below. - return true; + // Populate the new folder icon bitmap. + if (defaultFolderIconRadioButton.isChecked()) { + // Get the default folder icon drawable. + Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable(); - case R.id.proxy_through_orbot: - // Toggle the proxy through Orbot variable. - proxyThroughOrbot = !proxyThroughOrbot; + // Convert the folder icon drawable to a bitmap drawable. + BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable; - // Apply the proxy through Orbot settings. - applyProxyThroughOrbot(true); - return true; + // 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; + } - case R.id.refresh: - if (menuItem.getTitle().equals(getString(R.string.refresh))) { // The refresh button was pushed. - // Reload the WebView. - mainWebView.reload(); - } else { // The stop button was pushed. - // Stop the loading of the WebView. - mainWebView.stopLoading(); - } - return true; + // Create a folder icon byte array output stream. + ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream(); - case R.id.ad_consent: - // Display the ad consent dialog. - DialogFragment adConsentDialogFragment = new AdConsentDialog(); - adConsentDialogFragment.show(getFragmentManager(), getString(R.string.ad_consent)); - return true; + // 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); - default: - // Don't consume the event. - return super.onOptionsItemSelected(menuItem); - } - } + // Convert the folder icon byte array stream to a byte array. + byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray(); - // removeAllCookies is deprecated, but it is required for API < 21. - @SuppressWarnings("deprecation") - @Override - public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) { - int menuItemId = menuItem.getItemId(); + // 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(); - switch (menuItemId) { - case R.id.home: - loadUrl(homepage); - break; + // Convert the folder icon drawable to a bitmap drawable. + BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable; - case R.id.back: - if (mainWebView.canGoBack()) { - // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled. - formattedUrlString = ""; + // 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; + } - // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded. - navigatingHistory = true; + // Create a folder icon byte array output stream. + ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream(); - // Load the previous website in the history. - mainWebView.goBack(); - } - break; + // 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); - case R.id.forward: - if (mainWebView.canGoForward()) { - // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled. - formattedUrlString = ""; + // Convert the folder icon byte array stream to a byte array. + byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray(); - // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded. - navigatingHistory = true; + // Update the folder name and icon in the database. + bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, newFolderIconByteArray); + } - // Load the next website in the history. - mainWebView.goForward(); - } - break; + // Update the bookmarks cursor with the current contents of this folder. + bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder); - case R.id.history: - // Get the `WebBackForwardList`. - WebBackForwardList webBackForwardList = mainWebView.copyBackForwardList(); + // Update the `ListView`. + bookmarksCursorAdapter.changeCursor(bookmarksCursor); + } - // Show the `UrlHistoryDialog` `AlertDialog` and name this instance `R.string.history`. `this` is the `Context`. - AppCompatDialogFragment urlHistoryDialogFragment = UrlHistoryDialog.loadBackForwardList(this, webBackForwardList); - urlHistoryDialogFragment.show(supportFragmentManager, getString(R.string.history)); + @Override + public void onCloseDownloadLocationPermissionDialog(int downloadType) { + switch (downloadType) { + case DownloadLocationPermissionDialog.DOWNLOAD_FILE: + // Request the WRITE_EXTERNAL_STORAGE permission with a file request code. + ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE); break; - case R.id.requests: - // Launch the requests activity. - Intent requestsIntent = new Intent(this, RequestsActivity.class); - startActivity(requestsIntent); + case DownloadLocationPermissionDialog.DOWNLOAD_IMAGE: + // Request the WRITE_EXTERNAL_STORAGE permission with an image request code. + ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE); break; + } + } - case R.id.downloads: - // Launch the system Download Manager. - Intent downloadManagerIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS); - - // 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); + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + // Get a handle for the fragment manager. + FragmentManager fragmentManager = getSupportFragmentManager(); - startActivity(downloadManagerIntent); - break; + switch (requestCode) { + case DOWNLOAD_FILE_REQUEST_CODE: + // Show the download file alert dialog. When the dialog closes, the correct command will be used based on the permission status. + DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(downloadUrl, downloadContentDisposition, downloadContentLength); - case R.id.domains: - // Set the flag to reapply the domain settings on restart when returning from Domain Settings. - reapplyDomainSettingsOnRestart = true; - currentDomainName = ""; + // On API 23, displaying the fragment must be delayed or the app will crash. + if (Build.VERSION.SDK_INT == 23) { + new Handler().postDelayed(() -> downloadFileDialogFragment.show(fragmentManager, getString(R.string.download)), 500); + } else { + downloadFileDialogFragment.show(fragmentManager, getString(R.string.download)); + } - // Launch the domains activity. - Intent domainsIntent = new Intent(this, DomainsActivity.class); - startActivity(domainsIntent); + // Reset the download variables. + downloadUrl = ""; + downloadContentDisposition = ""; + downloadContentLength = 0; break; - case R.id.settings: - // Set the flag to reapply app settings on restart when returning from Settings. - reapplyAppSettingsOnRestart = true; + case DOWNLOAD_IMAGE_REQUEST_CODE: + // Show the download image alert dialog. When the dialog closes, the correct command will be used based on the permission status. + DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(downloadImageUrl); - // Set the flag to reapply the domain settings on restart when returning from Settings. - reapplyDomainSettingsOnRestart = true; - currentDomainName = ""; + // On API 23, displaying the fragment must be delayed or the app will crash. + if (Build.VERSION.SDK_INT == 23) { + new Handler().postDelayed(() -> downloadImageDialogFragment.show(fragmentManager, getString(R.string.download)), 500); + } else { + downloadImageDialogFragment.show(fragmentManager, getString(R.string.download)); + } - // Launch the settings activity. - Intent settingsIntent = new Intent(this, SettingsActivity.class); - startActivity(settingsIntent); + // Reset the image URL variable. + downloadImageUrl = ""; break; + } + } - case R.id.import_export: - // Launch the import/export activity. - Intent importExportIntent = new Intent (this, ImportExportActivity.class); - startActivity(importExportIntent); - break; + @Override + public void onDownloadImage(DialogFragment 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); - case R.id.logcat: - // Launch the logcat activity. - Intent logcatIntent = new Intent(this, LogcatActivity.class); - startActivity(logcatIntent); - break; - - case R.id.guide: - // Launch `GuideActivity`. - Intent guideIntent = new Intent(this, GuideActivity.class); - startActivity(guideIntent); - break; - - case R.id.about: - // Launch `AboutActivity`. - Intent aboutIntent = new Intent(this, AboutActivity.class); - startActivity(aboutIntent); - break; - - case R.id.clear_and_exit: - // Close the bookmarks cursor and database. - bookmarksCursor.close(); - bookmarksDatabaseHelper.close(); + // Parse `imageUrl`. + DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(imageUrl)); - // Get a handle for the shared preferences. - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + // Get a handle for the cookie manager. + CookieManager cookieManager = CookieManager.getInstance(); - // Get the status of the clear everything preference. - boolean clearEverything = sharedPreferences.getBoolean("clear_everything", true); + // 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 (cookieManager.acceptCookie()) { + // Get the cookies for `imageUrl`. + String cookies = cookieManager.getCookie(imageUrl); - // 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(); - } + // Add the cookies to `downloadRequest`. In the HTTP request header, cookies are named `Cookie`. + downloadRequest.addRequestHeader("Cookie", cookies); + } - // 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 = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies"); - Process deleteCookiesJournalProcess = privacyBrowserRuntime.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. - } - } + // Get the file name from the dialog fragment. + EditText downloadImageNameEditText = dialogFragment.getDialog().findViewById(R.id.download_image_name); + String imageName = downloadImageNameEditText.getText().toString(); - // Clear DOM storage. - if (clearEverything || sharedPreferences.getBoolean("clear_dom_storage", true)) { - // Ask `WebStorage` to clear the DOM storage. - WebStorage webStorage = WebStorage.getInstance(); - webStorage.deleteAllData(); + // Specify the download location. + if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // External write permission granted. + // Download to the public download directory. + downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, imageName); + } else { // External write permission denied. + // Download to the app's external download directory. + downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, imageName); + } - // 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 = privacyBrowserRuntime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"}); - - // Multiple commands must be used because `Runtime.exec()` does not like `*`. - Process deleteIndexProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB"); - Process deleteQuotaManagerProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager"); - Process deleteQuotaManagerJournalProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal"); - Process deleteDatabaseProcess = privacyBrowserRuntime.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. - } - } + // Allow `MediaScanner` to index the download if it is a media file. + downloadRequest.allowScanningByMediaScanner(); - // 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(); + // Add the URL as the description for the download. + downloadRequest.setDescription(imageUrl); - // 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 = privacyBrowserRuntime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data"}); - Process deleteWebDataJournalProcess = privacyBrowserRuntime.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. - } - } + // Show the download notification after the download is completed. + downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); - // Clear the cache. - if (clearEverything || sharedPreferences.getBoolean("clear_cache", true)) { - // `true` includes disk files. - mainWebView.clearCache(true); + // Remove the lint warning below that `downloadManager` might be `null`. + assert downloadManager != null; - // Manually delete the cache directories. - try { - // Delete the main cache directory. - Process deleteCacheProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/cache"); + // Initiate the download. + downloadManager.enqueue(downloadRequest); + } else { // The image is not an HTTP or HTTPS URI. + Snackbar.make(currentWebView, R.string.cannot_download_image, Snackbar.LENGTH_INDEFINITE).show(); + } + } - // 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 = privacyBrowserRuntime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"}); + @Override + public void onDownloadFile(DialogFragment dialogFragment, String downloadUrl) { + // Download the file if it has an HTTP or HTTPS URI. + if (downloadUrl.startsWith("http")) { + // Get a handle for the system `DOWNLOAD_SERVICE`. + DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE); - // Wait until the processes have finished. - deleteCacheProcess.waitFor(); - deleteServiceWorkerProcess.waitFor(); - } catch (Exception exception) { - // Do nothing if an error is thrown. - } - } + // Parse `downloadUrl`. + DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(downloadUrl)); - // Clear SSL certificate preferences. - mainWebView.clearSslPreferences(); + // Get a handle for the cookie manager. + CookieManager cookieManager = CookieManager.getInstance(); - // Clear the back/forward history. - mainWebView.clearHistory(); + // 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 (cookieManager.acceptCookie()) { + // Get the cookies for `downloadUrl`. + String cookies = cookieManager.getCookie(downloadUrl); - // Clear `formattedUrlString`. - formattedUrlString = null; + // Add the cookies to `downloadRequest`. In the HTTP request header, cookies are named `Cookie`. + downloadRequest.addRequestHeader("Cookie", cookies); + } - // Clear `customHeaders`. - customHeaders.clear(); + // Get the file name from the dialog fragment. + EditText downloadFileNameEditText = dialogFragment.getDialog().findViewById(R.id.download_file_name); + String fileName = downloadFileNameEditText.getText().toString(); - // Detach all views from `mainWebViewRelativeLayout`. - mainWebViewRelativeLayout.removeAllViews(); + // Specify the download location. + if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // External write permission granted. + // Download to the public download directory. + downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName); + } else { // External write permission denied. + // Download to the app's external download directory. + downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, fileName); + } - // Destroy the internal state of `mainWebView`. - mainWebView.destroy(); + // Allow `MediaScanner` to index the download if it is a media file. + downloadRequest.allowScanningByMediaScanner(); - // 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 = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview"); + // Add the URL as the description for the download. + downloadRequest.setDescription(downloadUrl); - // Wait until the process has finished. - deleteAppWebviewProcess.waitFor(); - } catch (Exception exception) { - // Do nothing if an error is thrown. - } - } + // Show the download notification after the download is completed. + downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); - // Close Privacy Browser. `finishAndRemoveTask` also removes Privacy Browser from the recent app list. - if (Build.VERSION.SDK_INT >= 21) { - finishAndRemoveTask(); - } else { - finish(); - } + // Remove the lint warning below that `downloadManager` might be `null`. + assert downloadManager != null; - // Remove the terminated program from RAM. The status code is `0`. - System.exit(0); - break; + // Initiate the download. + downloadManager.enqueue(downloadRequest); + } else { // The download is not an HTTP or HTTPS URI. + Snackbar.make(currentWebView, R.string.cannot_download_file, Snackbar.LENGTH_INDEFINITE).show(); } - - // Close the navigation drawer. - drawerLayout.closeDrawer(GravityCompat.START); - return true; } + // Override `onBackPressed` to handle the navigation drawer and and the WebView. @Override - public void onPostCreate(Bundle savedInstanceState) { - super.onPostCreate(savedInstanceState); - - // Sync the state of the DrawerToggle after onRestoreInstanceState has finished. - drawerToggle.syncState(); - } + public void onBackPressed() { + // Get a handle for the drawer layout and the tab layout. + DrawerLayout drawerLayout = findViewById(R.id.drawerlayout); + TabLayout tabLayout = findViewById(R.id.tablayout); - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); + 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); - // Get the status bar pixel size. - int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android"); - int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId); + // Load the new folder. + loadBookmarksFolder(); + } + } else if (currentWebView.canGoBack()) { // There is at least one item in the current WebView history. + // Reset the current domain name so that navigation works if third-party requests are blocked. + currentWebView.resetCurrentDomainName(); - // Get the resource density. - float screenDensity = getResources().getDisplayMetrics().density; + // Set navigating history so that the domain settings are applied when the new URL is loaded. + currentWebView.setNavigatingHistory(true); - // Recalculate the drawer header padding. - drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity); - drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity); - drawerHeaderPaddingBottom = (int) (8 * screenDensity); + // 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. + // Run the default commands. + super.onBackPressed(); - // 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)); + // Manually kill Privacy Browser. Otherwise, it is glitchy when restarted. + System.exit(0); } - - // `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); } + // Process the results of an upload file chooser. Currently there is only one `startActivityForResult` in this activity, so the request code, used to differentiate them, is ignored. @Override - public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) { - // Store the `HitTestResult`. - final WebView.HitTestResult hitTestResult = mainWebView.getHitTestResult(); + public void onActivityResult(int requestCode, int resultCode, Intent data) { + // 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, data)); + } + } - // Create strings. - final String imageUrl; - final String linkUrl; + private void loadUrlFromTextBox() { + // Get a handle for the URL edit text. + EditText urlEditText = findViewById(R.id.url_edittext); - // Get a handle for the `ClipboardManager`. - final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); + // 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(); - // Remove the lint errors below that `clipboardManager` might be `null`. - assert clipboardManager != null; + // Initialize the formatted URL string. + String url = ""; - switch (hitTestResult.getType()) { - // `SRC_ANCHOR_TYPE` is a link. - case WebView.HitTestResult.SRC_ANCHOR_TYPE: - // Get the target URL. - linkUrl = hitTestResult.getExtra(); + // 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; + } - // Set the target URL as the title of the `ContextMenu`. - menu.setHeaderTitle(linkUrl); + // Initialize `unformattedUrl`. + URL unformattedUrl = null; - // Add a Load URL entry. - menu.add(R.string.load_url).setOnMenuItemClickListener((MenuItem item) -> { - loadUrl(linkUrl); - return false; - }); + // 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(); + } - // 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); + // 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; - // Set the `ClipData` as the clipboard's primary clip. - clipboardManager.setPrimaryClip(srcAnchorTypeClipData); - return false; - }); + // Build the URI. + Uri.Builder uri = new Uri.Builder(); + uri.scheme(scheme).authority(authority).path(path).query(query).fragment(fragment); - // Add a Download URL entry. - menu.add(R.string.download_url).setOnMenuItemClickListener((MenuItem item) -> { - // Check if the download should be processed by an external app. - if (downloadWithExternalApp) { // Download with an external app. - openUrlWithExternalApp(linkUrl); - } else { // Download with Android's download manager. - // Check to see if the storage permission has already been granted. - if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested. - // Store the variables for future use by `onRequestPermissionsResult()`. - downloadUrl = linkUrl; - downloadContentDisposition = "none"; - downloadContentLength = -1; + // 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; - // Show a dialog if the user has previously denied the permission. - if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first. - // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE. - DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE); + // Sanitize the search input. + try { + encodedUrlString = URLEncoder.encode(unformattedUrlString, "UTF-8"); + } catch (UnsupportedEncodingException exception) { + encodedUrlString = ""; + } - // Show the download location permission alert dialog. The permission will be requested when the the dialog is closed. - downloadLocationPermissionDialogFragment.show(getFragmentManager(), getString(R.string.download_location)); - } else { // Show the permission request directly. - // Request the permission. The download dialog will be launched by `onRequestPermissionResult()`. - ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE); - } - } else { // The storage permission has already been granted. - // Get a handle for the download file alert dialog. - AppCompatDialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(linkUrl, "none", -1); + // Add the base search URL. + url = searchURL + encodedUrlString; + } - // Show the download file alert dialog. - downloadFileDialogFragment.show(supportFragmentManager, getString(R.string.download)); - } - } - return false; - }); + // 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(); - // Add an Open with App entry. - menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> { - openWithApp(linkUrl); - return false; - }); + // Make it so. + loadUrl(url); + } - // Add an Open with Browser entry. - menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> { - openWithBrowser(linkUrl); - return false; - }); + private void loadUrl(String url) { + // Sanitize the URL. + url = sanitizeUrl(url); + + // Apply the domain settings. + applyDomainSettings(currentWebView, url, true, false); + + // Load the URL. + currentWebView.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(currentWebView.getWindowToken(), 0); + } + + 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); + proxyThroughOrbot = sharedPreferences.getBoolean("proxy_through_orbot", false); + fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean("full_screen_browsing_mode", false); + hideAppBar = sharedPreferences.getBoolean("hide_app_bar", true); + scrollAppBar = sharedPreferences.getBoolean("scroll_app_bar", true); + + // Get handles for the views that need to be modified. + FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout); + AppBarLayout appBarLayout = findViewById(R.id.appbar_layout); + ActionBar actionBar = 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 through Orbot settings. + applyProxyThroughOrbot(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)); + } + + // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen. + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + + /* 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); + + // Add the translucent status flag. + getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + } + } + + + // `reloadWebsite` is used if returning from the Domains activity. Otherwise JavaScript might not function correctly if it is newly enabled. + @SuppressLint("SetJavaScriptEnabled") + private boolean applyDomainSettings(NestedScrollWebView nestedScrollWebView, String url, boolean resetTab, boolean reloadWebsite) { + // Store a copy of the current user agent to track changes for the return boolean. + String initialUserAgent = nestedScrollWebView.getSettings().getUserAgentString(); + + // 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 = ""; + } + + // Only apply the domain settings if a new domain is being loaded. This allows the user to set temporary settings for JavaScript, cookies, DOM storage, etc. + if (!nestedScrollWebView.getCurrentDomainName().equals(newHostName)) { + // 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 a handle for the tab layout. + TabLayout tabLayout = findViewById(R.id.tablayout); + + // 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 displayWebpageImages = sharedPreferences.getBoolean("display_webpage_images", true); + boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false); + + // 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.setDomainSettingsJavaScriptEnabled(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.EASY_LIST, + currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYLIST)) == 1); + nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASY_PRIVACY, + 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.ULTRA_PRIVACY, + 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 nightModeInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.NIGHT_MODE)); + 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)); + + // Create 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 (currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)) == 0) { + pinnedSslStartDate = null; + } else { + pinnedSslStartDate = new Date(currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE))); + } + + // 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 (currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)) == 0) { + pinnedSslEndDate = null; + } else { + pinnedSslEndDate = new Date(currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE))); + } + + // 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); + } + + // Set night mode according to the night mode int. + switch (nightModeInt) { + case DomainsDatabaseHelper.NIGHT_MODE_SYSTEM_DEFAULT: + // Set night mode according to the current default. + nestedScrollWebView.setNightMode(sharedPreferences.getBoolean("night_mode", false)); + break; + + case DomainsDatabaseHelper.NIGHT_MODE_ENABLED: + // Enable night mode. + nestedScrollWebView.setNightMode(true); + break; + + case DomainsDatabaseHelper.NIGHT_MODE_DISABLED: + // Disable night mode. + nestedScrollWebView.setNightMode(false); + break; + } + + // Enable JavaScript if night mode is enabled. + if (nestedScrollWebView.getNightMode()) { + // Enable JavaScript. + nestedScrollWebView.getSettings().setJavaScriptEnabled(true); + } else { + // Set JavaScript according to the domain settings. + nestedScrollWebView.getSettings().setJavaScriptEnabled(nestedScrollWebView.getDomainSettingsJavaScriptEnabled()); + } + + // Close the current host domain settings cursor. + currentDomainSettingsCursor.close(); + + // Apply the 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. + if (fontSize == 0) { // Apply the default font size. + nestedScrollWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString)); + } else { // Apply the specified font size. + nestedScrollWebView.getSettings().setTextZoom(fontSize); + } + + // 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.SWIPE_TO_REFRESH_SYSTEM_DEFAULT: + // Store the swipe to refresh status in the nested scroll WebView. + nestedScrollWebView.setSwipeToRefresh(defaultSwipeToRefresh); + + // Apply swipe to refresh according to the default. This can be removed once the minimum API >= 23 because it is continuously set by an on scroll change listener. + swipeRefreshLayout.setEnabled(defaultSwipeToRefresh); + break; + + case DomainsDatabaseHelper.SWIPE_TO_REFRESH_ENABLED: + // Store the swipe to refresh status in the nested scroll WebView. + nestedScrollWebView.setSwipeToRefresh(true); + + // Enable swipe to refresh. This can be removed once the minimum API >= 23 because it is continuously set by an on scroll change listener. + swipeRefreshLayout.setEnabled(true); + break; + + case DomainsDatabaseHelper.SWIPE_TO_REFRESH_DISABLED: + // Store the swipe to refresh status in the nested scroll WebView. + nestedScrollWebView.setSwipeToRefresh(false); + + // Disable swipe to refresh. This can be removed once the minimum API >= 23 because it is continuously set by an on scroll change listener. + swipeRefreshLayout.setEnabled(false); + } + + // Set the loading of webpage images. + switch (displayWebpageImagesInt) { + case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_SYSTEM_DEFAULT: + nestedScrollWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages); + break; - // Add a Cancel entry, which by default closes the context menu. - menu.add(R.string.cancel); - break; + case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_ENABLED: + nestedScrollWebView.getSettings().setLoadsImagesAutomatically(true); + break; - case WebView.HitTestResult.EMAIL_TYPE: - // Get the target URL. - linkUrl = hitTestResult.getExtra(); + case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_DISABLED: + nestedScrollWebView.getSettings().setLoadsImagesAutomatically(false); + break; + } - // Set the target URL as the title of the `ContextMenu`. - menu.setHeaderTitle(linkUrl); + // Set a green background on the URL relative layout to indicate that custom domain settings are being used. The deprecated `.getDrawable()` must be used until the minimum API >= 21. + if (darkTheme) { + urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_dark_blue)); + } else { + urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_light_green)); + } + } else { // The new URL does not have custom domain settings. Load the defaults. + // Store the values from the shared preferences. + boolean defaultJavaScriptEnabled = 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.EASY_LIST, sharedPreferences.getBoolean("easylist", true)); + nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASY_PRIVACY, 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.ULTRA_PRIVACY, sharedPreferences.getBoolean("ultraprivacy", true)); + nestedScrollWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS, sharedPreferences.getBoolean("block_all_third_party_requests", false)); + nestedScrollWebView.setNightMode(sharedPreferences.getBoolean("night_mode", false)); - // 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); + // Enable JavaScript if night mode is enabled. + if (nestedScrollWebView.getNightMode()) { + // Enable JavaScript. + nestedScrollWebView.getSettings().setJavaScriptEnabled(true); + } else { + // Set JavaScript according to the domain settings. + nestedScrollWebView.getSettings().setJavaScriptEnabled(defaultJavaScriptEnabled); + } - // Parse the url and set it as the data for the `Intent`. - emailIntent.setData(Uri.parse("mailto:" + linkUrl)); + // Apply the default settings. + cookieManager.setAcceptCookie(nestedScrollWebView.getAcceptFirstPartyCookies()); + nestedScrollWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString)); - // `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); + // Apply the form data setting if the API < 26. + if (Build.VERSION.SDK_INT < 26) { + nestedScrollWebView.getSettings().setSaveFormData(saveFormData); + } - // Make it so. - startActivity(emailIntent); - return false; - }); + // Store the swipe to refresh status in the nested scroll WebView. + nestedScrollWebView.setSwipeToRefresh(defaultSwipeToRefresh); - // 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); + // Apply swipe to refresh according to the default. + swipeRefreshLayout.setEnabled(defaultSwipeToRefresh); - // Set the `ClipData` as the clipboard's primary clip. - clipboardManager.setPrimaryClip(srcEmailTypeClipData); - return false; - }); + // Reset the pinned variables. + nestedScrollWebView.setDomainSettingsDatabaseId(-1); - // Add a `Cancel` entry, which by default closes the `ContextMenu`. - menu.add(R.string.cancel); - break; + // Set third-party cookies status if API >= 21. + if (Build.VERSION.SDK_INT >= 21) { + cookieManager.setAcceptThirdPartyCookies(nestedScrollWebView, defaultThirdPartyCookiesEnabled); + } - // `IMAGE_TYPE` is an image. - case WebView.HitTestResult.IMAGE_TYPE: - // Get the image URL. - imageUrl = hitTestResult.getExtra(); + // Get the array position of the user agent name. + int userAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName); - // Set the image URL as the title of the `ContextMenu`. - menu.setHeaderTitle(imageUrl); + // 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; - // Add a View Image entry. - menu.add(R.string.view_image).setOnMenuItemClickListener(item -> { - loadUrl(imageUrl); - return false; - }); + 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]); + } + + // Set the loading of webpage images. + nestedScrollWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages); + + // Set a transparent background on URL edit text. The deprecated `getResources().getDrawable()` must be used until the minimum API >= 21. + urlRelativeLayout.setBackground(getResources().getDrawable(R.color.transparent)); + } + + // 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(); + } + + // Return the user agent changed status. + return !nestedScrollWebView.getSettings().getUserAgentString().equals(initialUserAgent); + } + + private void applyProxyThroughOrbot(boolean reloadWebsite) { + // Get a handle for the shared preferences. + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + + // Get the search and theme preferences. + String torSearchString = sharedPreferences.getString("tor_search", getString(R.string.tor_search_default_value)); + String torSearchCustomUrlString = sharedPreferences.getString("tor_search_custom_url", getString(R.string.tor_search_custom_url_default_value)); + String searchString = sharedPreferences.getString("search", getString(R.string.search_default_value)); + String searchCustomUrlString = sharedPreferences.getString("search_custom_url", getString(R.string.search_custom_url_default_value)); + boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false); + + // Get a handle for the app bar layout. + AppBarLayout appBarLayout = findViewById(R.id.appbar_layout); + + // Set the homepage, search, and proxy options. + if (proxyThroughOrbot) { // Set the Tor options. + // 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; + } + + // Set the proxy. `this` refers to the current activity where an `AlertDialog` might be displayed. + OrbotProxyHelper.setProxy(getApplicationContext(), this, "localhost", "8118"); + + // Set the app bar background to indicate proxying through Orbot is enabled. + if (darkTheme) { + appBarLayout.setBackgroundResource(R.color.dark_blue_30); + } else { + appBarLayout.setBackgroundResource(R.color.blue_50); + } + + // Check to see if Orbot is ready. + if (!orbotStatus.equals("ON")) { // Orbot is not ready. + // Set `waitingForOrbot`. + waitingForOrbot = true; + + // Disable the wide view port so that the waiting for Orbot text is displayed correctly. + currentWebView.getSettings().setUseWideViewPort(false); + + // Load a waiting page. `null` specifies no encoding, which defaults to ASCII. + currentWebView.loadData("

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

", "text/html", null); + } else if (reloadWebsite) { // Orbot is ready and the website should be reloaded. + // Reload the website. + currentWebView.reload(); + } + } else { // Set the non-Tor options. + // 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; + } + + // Reset the proxy to default. The host is `""` and the port is `"0"`. + OrbotProxyHelper.setProxy(getApplicationContext(), this, "", "0"); + + // Set the default app bar layout background. + if (darkTheme) { + appBarLayout.setBackgroundResource(R.color.gray_900); + } else { + appBarLayout.setBackgroundResource(R.color.gray_100); + } + + // Reset `waitingForOrbot. + waitingForOrbot = false; + + // Reload the WebViews if requested. + if (reloadWebsite) { + // 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 a handle for the shared preferences. + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + + // Get the theme and screenshot preferences. + boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false); + + // 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); + } + + // 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 (darkTheme) { + firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_dark); + } else { + firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_light); + } + } + + // 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 (darkTheme) { + domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_dark); + } else { + domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_light); + } + } else { // JavaScript is disabled, so DOM storage is ghosted. + if (darkTheme) { + domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_dark); + } else { + domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_light); + } + } + + // Update the refresh icon. + if (darkTheme) { + refreshMenuItem.setIcon(R.drawable.refresh_enabled_dark); + } else { + refreshMenuItem.setIcon(R.drawable.refresh_enabled_light); + } + + // `invalidateOptionsMenu` calls `onPrepareOptionsMenu()` and redraws the icons in the `AppBar`. + if (runInvalidateOptionsMenu) { + invalidateOptionsMenu(); + } + } + } + + private void openUrlWithExternalApp(String url) { + // Create a download intent. Not specifying the action type will display the maximum number of options. + Intent downloadIntent = new Intent(); + + // Set the URI and the MIME type. Specifying `text/html` displays a good number of options. + downloadIntent.setDataAndType(Uri.parse(url), "text/html"); + + // Flag the intent to open in a new task. + downloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + // Show the chooser. + startActivity(Intent.createChooser(downloadIntent, getString(R.string.open_with))); + } + + 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://")) { // This is a file URL. + // De-emphasize only the protocol. + urlEditText.getText().setSpan(initialGrayColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE); + } else if (urlString.startsWith("content://")) { + // De-emphasize only the protocol. + urlEditText.getText().setSpan(initialGrayColorSpan, 0, 10, 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); - // Add a Download Image entry. - menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> { - // Check if the download should be processed by an external app. - if (downloadWithExternalApp) { // Download with an external app. - openUrlWithExternalApp(imageUrl); - } else { // Download with Android's download manager. - // Check to see if the storage permission has already been granted. - if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested. - // Store the image URL for use by `onRequestPermissionResult()`. - downloadImageUrl = imageUrl; + // Get the favorite icon byte array from the cursor. + byte[] favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON)); - // Show a dialog if the user has previously denied the permission. - if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first. - // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE. - DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE); + // 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); - // Show the download location permission alert dialog. The permission will be requested when the dialog is closed. - downloadLocationPermissionDialogFragment.show(getFragmentManager(), getString(R.string.download_location)); - } else { // Show the permission request directly. - // Request the permission. The download dialog will be launched by `onRequestPermissionResult(). - ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE); - } - } else { // The storage permission has already been granted. - // Get a handle for the download image alert dialog. - AppCompatDialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl); + // Display the bitmap in `bookmarkFavoriteIcon`. + bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap); - // Show the download image alert dialog. - downloadImageDialogFragment.show(supportFragmentManager, getString(R.string.download)); - } - } - return false; - }); + // Get the bookmark name from the cursor and display it in `bookmarkNameTextView`. + String bookmarkNameString = cursor.getString(cursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME)); + bookmarkNameTextView.setText(bookmarkNameString); - // Add a Copy URL entry. - menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> { - // Save the image URL in a `ClipData`. - ClipData srcImageTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl); + // 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); + } + } + }; - // Set the `ClipData` as the clipboard's primary clip. - clipboardManager.setPrimaryClip(srcImageTypeClipData); - return false; - }); + // Get a handle for the bookmarks list view. + ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview); - // Add an Open with App entry. - menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> { - openWithApp(imageUrl); - return false; - }); + // Populate the list view with the adapter. + bookmarksListView.setAdapter(bookmarksCursorAdapter); - // Add an Open with Browser entry. - menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> { - openWithBrowser(imageUrl); - return false; - }); + // Get a handle for the bookmarks title text view. + TextView bookmarksTitleTextView = findViewById(R.id.bookmarks_title_textview); - // Add a `Cancel` entry, which by default closes the `ContextMenu`. - menu.add(R.string.cancel); - break; + // Set the bookmarks drawer title. + if (currentBookmarksFolder.isEmpty()) { + bookmarksTitleTextView.setText(R.string.bookmarks); + } else { + bookmarksTitleTextView.setText(currentBookmarksFolder); + } + } + private void openWithApp(String url) { + // Create the open with intent with `ACTION_VIEW`. + Intent openWithAppIntent = new Intent(Intent.ACTION_VIEW); - // `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(); + // Set the URI but not the MIME type. This should open all available apps. + openWithAppIntent.setData(Uri.parse(url)); - // Set the image URL as the title of the `ContextMenu`. - menu.setHeaderTitle(imageUrl); + // Flag the intent to open in a new task. + openWithAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - // Add a `View Image` entry. - menu.add(R.string.view_image).setOnMenuItemClickListener(item -> { - loadUrl(imageUrl); - return false; - }); + // Show the chooser. + startActivity(openWithAppIntent); + } - // Add a `Download Image` entry. - menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> { - // Check if the download should be processed by an external app. - if (downloadWithExternalApp) { // Download with an external app. - openUrlWithExternalApp(imageUrl); - } else { // Download with Android's download manager. - // Check to see if the storage permission has already been granted. - if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested. - // Store the image URL for use by `onRequestPermissionResult()`. - downloadImageUrl = imageUrl; + private void openWithBrowser(String url) { + // Create the open with intent with `ACTION_VIEW`. + Intent openWithBrowserIntent = new Intent(Intent.ACTION_VIEW); - // Show a dialog if the user has previously denied the permission. - if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first. - // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE. - DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE); + // Set the URI and the MIME type. `"text/html"` should load browser options. + openWithBrowserIntent.setDataAndType(Uri.parse(url), "text/html"); - // Show the download location permission alert dialog. The permission will be requested when the dialog is closed. - downloadLocationPermissionDialogFragment.show(getFragmentManager(), getString(R.string.download_location)); - } else { // Show the permission request directly. - // Request the permission. The download dialog will be launched by `onRequestPermissionResult(). - ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE); - } - } else { // The storage permission has already been granted. - // Get a handle for the download image alert dialog. - AppCompatDialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl); + // Flag the intent to open in a new task. + openWithBrowserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - // Show the download image alert dialog. - downloadImageDialogFragment.show(supportFragmentManager, getString(R.string.download)); - } - } - return false; - }); + // Show the chooser. + startActivity(openWithBrowserIntent); + } - // Add a `Copy URL` entry. - menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> { - // Save the image URL in a `ClipData`. - ClipData srcImageAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl); + private String sanitizeUrl(String url) { + // Sanitize Google Analytics. + if (sanitizeGoogleAnalytics) { + // Remove `?utm_`. + if (url.contains("?utm_")) { + url = url.substring(0, url.indexOf("?utm_")); + } - // Set the `ClipData` as the clipboard's primary clip. - clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData); - return false; - }); + // Remove `&utm_`. + if (url.contains("&utm_")) { + url = url.substring(0, url.indexOf("&utm_")); + } + } - // Add an Open with App entry. - menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> { - openWithApp(imageUrl); - return false; - }); + // Sanitize Facebook Click IDs. + if (sanitizeFacebookClickIds) { + // Remove `?fbclid=`. + if (url.contains("?fbclid=")) { + url = url.substring(0, url.indexOf("?fbclid=")); + } - // Add an Open with Browser entry. - menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> { - openWithBrowser(imageUrl); - return false; - }); + // Remove `&fbclid=`. + if (url.contains("&fbclid=")) { + url = url.substring(0, url.indexOf("&fbclid=")); + } + } - // Add a `Cancel` entry, which by default closes the `ContextMenu`. - menu.add(R.string.cancel); - break; + // 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; } - @Override - public void onCreateBookmark(AppCompatDialogFragment dialogFragment) { - // Get the `EditTexts` from the `dialogFragment`. - EditText createBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_name_edittext); - EditText createBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_url_edittext); + public void addTab(View view) { + // Add a new tab with a blank URL. + addNewTab(""); + } - // Extract the strings from the `EditTexts`. - String bookmarkNameString = createBookmarkNameEditText.getText().toString(); - String bookmarkUrlString = createBookmarkUrlEditText.getText().toString(); + private void addNewTab(String url) { + // Sanitize the URL. + url = sanitizeUrl(url); - // Convert the favoriteIcon Bitmap to a byte array. `0` is for lossless compression (the only option for a PNG). - ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream(); - favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream); - byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray(); + // Get a handle for the tab layout and the view pager. + TabLayout tabLayout = findViewById(R.id.tablayout); + ViewPager webViewPager = findViewById(R.id.webviewpager); - // Display the new bookmark below the current items in the (0 indexed) list. - int newBookmarkDisplayOrder = bookmarksListView.getCount(); + // 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(); - // Create the bookmark. - bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolder, newBookmarkDisplayOrder, favoriteIconByteArray); + // Add a new tab. + tabLayout.addTab(tabLayout.newTab()); - // Update the bookmarks cursor with the current contents of this folder. - bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder); + // Get the new tab. + TabLayout.Tab newTab = tabLayout.getTabAt(newTabNumber); - // Update the `ListView`. - bookmarksCursorAdapter.changeCursor(bookmarksCursor); + // Remove the lint warning below that the current tab might be null. + assert newTab != null; - // Scroll to the new bookmark. - bookmarksListView.setSelection(newBookmarkDisplayOrder); - } + // Set a custom view on the new tab. + newTab.setCustomView(R.layout.tab_custom_view); - @Override - public void onCreateBookmarkFolder(AppCompatDialogFragment dialogFragment) { - // Get handles for the views in `dialogFragment`. - EditText createFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.create_folder_name_edittext); - RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon_radiobutton); - ImageView folderIconImageView = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon); + // Add the new WebView page. + webViewPagerAdapter.addPage(newTabNumber, webViewPager, url); + } - // Get new folder name string. - String folderNameString = createFolderNameEditText.getText().toString(); + public void closeTab(View view) { + // Get a handle for the tab layout. + TabLayout tabLayout = findViewById(R.id.tablayout); - // Get the new folder icon `Bitmap`. - Bitmap folderIconBitmap; - if (defaultFolderIconRadioButton.isChecked()) { // Use the default folder icon. - // Get the default folder icon and convert it to a `Bitmap`. - Drawable folderIconDrawable = folderIconImageView.getDrawable(); - BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable; - folderIconBitmap = folderIconBitmapDrawable.getBitmap(); - } else { // Use the `WebView` favorite icon. - folderIconBitmap = favoriteIconBitmap; + // 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(); } + } - // Convert `folderIconBitmap` to a byte array. `0` is for lossless compression (the only option for a PNG). - ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream(); - folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream); - 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); - } + private void closeCurrentTab() { + // Get handles for the views. + AppBarLayout appBarLayout = findViewById(R.id.appbar_layout); + TabLayout tabLayout = findViewById(R.id.tablayout); + ViewPager webViewPager = findViewById(R.id.webviewpager); - // Create the folder, which will be placed at the top of the `ListView`. - bookmarksDatabaseHelper.createFolder(folderNameString, currentBookmarksFolder, folderIconByteArray); + // Get the current tab number. + int currentTabNumber = tabLayout.getSelectedTabPosition(); - // Update the bookmarks cursor with the current contents of this folder. - bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder); + // Delete the current tab. + tabLayout.removeTabAt(currentTabNumber); - // Update the `ListView`. - bookmarksCursorAdapter.changeCursor(bookmarksCursor); + // 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); + } - // Scroll to the new folder. - bookmarksListView.setSelection(0); + // Expand the app bar if it is currently collapsed. + appBarLayout.setExpanded(true); } - @Override - public void onCreateHomeScreenShortcut(AppCompatDialogFragment dialogFragment) { - // Get the shortcut name. - EditText shortcutNameEditText = dialogFragment.getDialog().findViewById(R.id.shortcut_name_edittext); - String shortcutNameString = shortcutNameEditText.getText().toString(); + private void clearAndExit() { + // Get a handle for the shared preferences. + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - // Convert the favorite icon bitmap to an `Icon`. `IconCompat` is required until API >= 26. - IconCompat favoriteIcon = IconCompat.createWithBitmap(favoriteIconBitmap); + // Close the bookmarks cursor and database. + bookmarksCursor.close(); + bookmarksDatabaseHelper.close(); - // Setup the shortcut intent. - Intent shortcutIntent = new Intent(Intent.ACTION_VIEW); - shortcutIntent.setData(Uri.parse(formattedUrlString)); + // Get the status of the clear everything preference. + boolean clearEverything = sharedPreferences.getBoolean("clear_everything", true); - // Create a shortcut info builder. The shortcut name becomes the shortcut ID. - ShortcutInfoCompat.Builder shortcutInfoBuilder = new ShortcutInfoCompat.Builder(this, shortcutNameString); + // Get a handle for the runtime. + Runtime runtime = Runtime.getRuntime(); - // Add the required fields to the shortcut info builder. - shortcutInfoBuilder.setIcon(favoriteIcon); - shortcutInfoBuilder.setIntent(shortcutIntent); - shortcutInfoBuilder.setShortLabel(shortcutNameString); + // 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; - // Request the pin. `ShortcutManagerCompat` can be switched to `ShortcutManager` once API >= 26. - ShortcutManagerCompat.requestPinShortcut(this, shortcutInfoBuilder.build(), null); - } + // 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(); + } - @Override - public void onCloseDownloadLocationPermissionDialog(int downloadType) { - switch (downloadType) { - case DownloadLocationPermissionDialog.DOWNLOAD_FILE: - // Request the WRITE_EXTERNAL_STORAGE permission with a file request code. - ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE); - break; + // 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. + } + } - case DownloadLocationPermissionDialog.DOWNLOAD_IMAGE: - // Request the WRITE_EXTERNAL_STORAGE permission with an image request code. - ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE); - break; + // 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. + } } - } - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - switch (requestCode) { - case DOWNLOAD_FILE_REQUEST_CODE: - // Show the download file alert dialog. When the dialog closes, the correct command will be used based on the permission status. - AppCompatDialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(downloadUrl, downloadContentDisposition, downloadContentLength); + // 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(); - // On API 23, displaying the fragment must be delayed or the app will crash. - if (Build.VERSION.SDK_INT == 23) { - new Handler().postDelayed(() -> downloadFileDialogFragment.show(supportFragmentManager, getString(R.string.download)), 500); - } else { - downloadFileDialogFragment.show(supportFragmentManager, getString(R.string.download)); - } + // 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. + } + } - // Reset the download variables. - downloadUrl = ""; - downloadContentDisposition = ""; - downloadContentLength = 0; - break; + // 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); - case DOWNLOAD_IMAGE_REQUEST_CODE: - // Show the download image alert dialog. When the dialog closes, the correct command will be used based on the permission status. - AppCompatDialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(downloadImageUrl); + // Get the fragment view. + View fragmentView = webViewTabFragment.getView(); - // On API 23, displaying the fragment must be delayed or the app will crash. - if (Build.VERSION.SDK_INT == 23) { - new Handler().postDelayed(() -> downloadImageDialogFragment.show(supportFragmentManager, getString(R.string.download)), 500); - } else { - downloadImageDialogFragment.show(supportFragmentManager, getString(R.string.download)); + // 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); } + } - // Reset the image URL variable. - downloadImageUrl = ""; - break; + // 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. + } } - } - @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); + // Wipe out each WebView. + for (int i = 0; i < webViewPagerAdapter.getCount(); i++) { + // Get the WebView tab fragment. + WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i); - // Parse `imageUrl`. - DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(imageUrl)); + // Get the fragment view. + View fragmentView = webViewTabFragment.getView(); - // 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); + // 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); - // Add the cookies to `downloadRequest`. In the HTTP request header, cookies are named `Cookie`. - downloadRequest.addRequestHeader("Cookie", cookies); - } + // Clear SSL certificate preferences for this WebView. + nestedScrollWebView.clearSslPreferences(); - // Get the file name from the dialog fragment. - EditText downloadImageNameEditText = dialogFragment.getDialog().findViewById(R.id.download_image_name); - String imageName = downloadImageNameEditText.getText().toString(); + // Clear the back/forward history for this WebView. + nestedScrollWebView.clearHistory(); - // Specify the download location. - if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // External write permission granted. - // Download to the public download directory. - downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, imageName); - } else { // External write permission denied. - // Download to the app's external download directory. - downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, imageName); + // Destroy the internal state of `mainWebView`. + nestedScrollWebView.destroy(); } + } - // Allow `MediaScanner` to index the download if it is a media file. - downloadRequest.allowScanningByMediaScanner(); - - // Add the URL as the description for the download. - downloadRequest.setDescription(imageUrl); + // Clear the custom headers. + customHeaders.clear(); - // Show the download notification after the download is completed. - downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); + // 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"); - // Remove the lint warning below that `downloadManager` might be `null`. - assert downloadManager != null; + // Wait until the process has finished. + deleteAppWebviewProcess.waitFor(); + } catch (Exception exception) { + // Do nothing if an error is thrown. + } + } - // 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(); + // 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); } - @Override - public void onDownloadFile(AppCompatDialogFragment dialogFragment, String downloadUrl) { - // Download the file if it has an HTTP or HTTPS URI. - if (downloadUrl.startsWith("http")) { - // Get a handle for the system `DOWNLOAD_SERVICE`. - DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE); + private void setCurrentWebView(int pageNumber) { + // Get a handle for the shared preferences. + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - // Parse `downloadUrl`. - DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(downloadUrl)); + // Get the theme preference. + boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false); - // 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); + // 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); - // Add the cookies to `downloadRequest`. In the HTTP request header, cookies are named `Cookie`. - downloadRequest.addRequestHeader("Cookie", cookies); - } + //Stop the swipe to refresh indicator if it is running + swipeRefreshLayout.setRefreshing(false); - // Get the file name from the dialog fragment. - EditText downloadFileNameEditText = dialogFragment.getDialog().findViewById(R.id.download_file_name); - String fileName = downloadFileNameEditText.getText().toString(); + // Get the WebView tab fragment. + WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(pageNumber); - // Specify the download location. - if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // External write permission granted. - // Download to the public download directory. - downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName); - } else { // External write permission denied. - // Download to the app's external download directory. - downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, fileName); + // Get the fragment view. + View fragmentView = webViewTabFragment.getView(); + + // Set the current WebView if the fragment view is not null. + if (fragmentView != null) { + // 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. + if (Build.VERSION.SDK_INT >= 23) { // For API >= 23, swipe refresh layout is continuously updated with an on scroll change listener and only enabled if the WebView is scrolled to the top. + // Enable the swipe refresh layout if the WebView is scrolled all the way to the top. + swipeRefreshLayout.setEnabled(currentWebView.getY() == 0); + } else { + // Enable the swipe refresh layout. + swipeRefreshLayout.setEnabled(true); + } + } else { // Swipe to refresh is disabled. + // Disable the swipe refresh layout. + swipeRefreshLayout.setEnabled(false); } - // Allow `MediaScanner` to index the download if it is a media file. - downloadRequest.allowScanningByMediaScanner(); + // Get a handle for the cookie manager. + CookieManager cookieManager = CookieManager.getInstance(); - // Add the URL as the description for the download. - downloadRequest.setDescription(downloadUrl); + // Set the first-party cookie status. + cookieManager.setAcceptCookie(currentWebView.getAcceptFirstPartyCookies()); - // Show the download notification after the download is completed. - downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); + // Update the privacy icons. `true` redraws the icons in the app bar. + updatePrivacyIcons(true); - // Remove the lint warning below that `downloadManager` might be `null`. - assert downloadManager != null; + // Get a handle for the input method manager. + InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - // 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(); - } - } + // Remove the lint warning below that the input method manager might be null. + assert inputMethodManager != null; - @Override - public void onSaveBookmark(AppCompatDialogFragment dialogFragment, int selectedBookmarkDatabaseId) { - // Get handles for the views from `dialogFragment`. - EditText editBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_name_edittext); - EditText editBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_url_edittext); - RadioButton currentBookmarkIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_current_icon_radiobutton); + // Get the current URL. + String url = currentWebView.getUrl(); - // Store the bookmark strings. - String bookmarkNameString = editBookmarkNameEditText.getText().toString(); - String bookmarkUrlString = editBookmarkUrlEditText.getText().toString(); + // 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(""); - // Update the bookmark. - if (currentBookmarkIconRadioButton.isChecked()) { // Update the bookmark without changing the favorite icon. - bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString); - } else { // Update the bookmark using the `WebView` favorite icon. - // Convert the favorite icon to a byte array. `0` is for lossless compression (the only option for a PNG). - ByteArrayOutputStream newFavoriteIconByteArrayOutputStream = new ByteArrayOutputStream(); - favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFavoriteIconByteArrayOutputStream); - byte[] newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray(); + // Request focus for the URL text box. + urlEditText.requestFocus(); - // Update the bookmark and the favorite icon. - bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, newFavoriteIconByteArray); - } + // Display the keyboard. + inputMethodManager.showSoftInput(urlEditText, 0); + } else { // The WebView has a loaded URL. + // Clear the focus from the URL text box. + urlEditText.clearFocus(); - // Update the bookmarks cursor with the current contents of this folder. - bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder); + // Hide the soft keyboard. + inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0); - // Update the `ListView`. - bookmarksCursorAdapter.changeCursor(bookmarksCursor); + // 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; + } + + // Set the background to indicate the domain settings status. + if (currentWebView.getDomainSettingsApplied()) { + // Set a green background on the URL relative layout to indicate that custom domain settings are being used. The deprecated `.getDrawable()` must be used until the minimum API >= 21. + if (darkTheme) { + urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_dark_blue)); + } else { + urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_light_green)); + } + } else { + urlRelativeLayout.setBackground(getResources().getDrawable(R.color.transparent)); + } + } } @Override - public void onSaveBookmarkFolder(AppCompatDialogFragment dialogFragment, int selectedFolderDatabaseId) { - // Get handles for the views from `dialogFragment`. - EditText editFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_folder_name_edittext); - RadioButton currentFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_current_icon_radiobutton); - RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_radiobutton); - ImageView folderIconImageView = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_imageview); + public void initializeWebView(NestedScrollWebView nestedScrollWebView, int pageNumber, ProgressBar progressBar, String url) { + // 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 = getSupportActionBar(); + LinearLayout tabsLinearLayout = findViewById(R.id.tabs_linearlayout); + EditText urlEditText = findViewById(R.id.url_edittext); + TabLayout tabLayout = findViewById(R.id.tablayout); + SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout); + + // Remove the incorrect lint warning below that the action bar might be null. + assert actionBar != null; + + // Get a handle for the activity + Activity activity = this; - // Get the new folder name. - String newFolderNameString = editFolderNameEditText.getText().toString(); + // Get a handle for the input method manager. + InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - // 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. - // Get the new folder icon `Bitmap`. - Bitmap folderIconBitmap; - if (defaultFolderIconRadioButton.isChecked()) { - // Get the default folder icon and convert it to a `Bitmap`. - Drawable folderIconDrawable = folderIconImageView.getDrawable(); - BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable; - folderIconBitmap = folderIconBitmapDrawable.getBitmap(); - } else { // Use the `WebView` favorite icon. - folderIconBitmap = favoriteIconBitmap; - } + // Instantiate the blocklist helper. + BlockListHelper blockListHelper = new BlockListHelper(); - // Convert the folder `Bitmap` to a byte array. `0` is for lossless compression (the only option for a PNG). - ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream(); - folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream); - byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray(); + // Remove the lint warning below that the input method manager might be null. + assert inputMethodManager != null; - // Update the folder icon in the database. - bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, folderIconByteArray); - } 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 and convert it to a `Bitmap`. - Drawable folderIconDrawable = folderIconImageView.getDrawable(); - BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable; - folderIconBitmap = folderIconBitmapDrawable.getBitmap(); - } else { // Use the `WebView` favorite icon. - folderIconBitmap = MainWebViewActivity.favoriteIconBitmap; - } + // Get a handle for the shared preferences. + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - // Convert the folder `Bitmap` to a byte array. `0` is for lossless compression (the only option for a PNG). - ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream(); - folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream); - byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray(); + // Get the relevant preferences. + boolean downloadWithExternalApp = sharedPreferences.getBoolean("download_with_external_app", false); - // Update the folder name and icon in the database. - bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, folderIconByteArray); + // Initialize the favorite icon. + nestedScrollWebView.initializeFavoriteIcon(); + + // Set the app bar scrolling. + nestedScrollWebView.setNestedScrollingEnabled(sharedPreferences.getBoolean("scroll_app_bar", true)); + + // Allow pinch to zoom. + nestedScrollWebView.getSettings().setBuiltInZoomControls(true); + + // Hide zoom controls. + nestedScrollWebView.getSettings().setDisplayZoomControls(false); + + // 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); } - // Update the bookmarks cursor with the current contents of this folder. - bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder); + // Set the WebView to use a wide viewport. Otherwise, some web pages will be scrunched and some content will render outside the screen. + nestedScrollWebView.getSettings().setUseWideViewPort(true); - // Update the `ListView`. - bookmarksCursorAdapter.changeCursor(bookmarksCursor); - } + // Set the WebView to load in overview mode (zoomed out to the maximum width). + nestedScrollWebView.getSettings().setLoadWithOverviewMode(true); - @Override - public void onHttpAuthenticationCancel() { - // Cancel the `HttpAuthHandler`. - httpAuthHandler.cancel(); - } + // Explicitly disable geolocation. + nestedScrollWebView.getSettings().setGeolocationEnabled(false); - @Override - public void onHttpAuthenticationProceed(AppCompatDialogFragment dialogFragment) { - // Get handles for the `EditTexts`. - EditText usernameEditText = dialogFragment.getDialog().findViewById(R.id.http_authentication_username); - EditText passwordEditText = dialogFragment.getDialog().findViewById(R.id.http_authentication_password); + // 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; - // Proceed with the HTTP authentication. - httpAuthHandler.proceed(usernameEditText.getText().toString(), passwordEditText.getText().toString()); - } + // Toggle the full screen browsing mode. + if (inFullScreenBrowsingMode) { // Switch to full screen mode. + // Store the swipe refresh layout top padding. + swipeRefreshLayoutPaddingTop = swipeRefreshLayout.getPaddingTop(); - 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)); - } + // Hide the app bar if specified. + if (hideAppBar) { + // Close the find on page bar if it is visible. + closeFindOnPage(null); - @Override - public void onSslErrorCancel() { - sslErrorHandler.cancel(); - } + // Hide the tab linear layout. + tabsLinearLayout.setVisibility(View.GONE); - @Override - public void onSslErrorProceed() { - sslErrorHandler.proceed(); - } + // Hide the action bar. + actionBar.hide(); - @Override - public void onPinnedMismatchBack() { - if (mainWebView.canGoBack()) { // There is a back page in the history. - // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled. - formattedUrlString = ""; + // Check to see if app bar scrolling is disabled. + if (!scrollAppBar) { + // Remove the padding from the top of the swipe refresh layout. + swipeRefreshLayout.setPadding(0, 0, 0, 0); + } + } - // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded. - navigatingHistory = true; + // Hide the banner ad in the free flavor. + if (BuildConfig.FLAVOR.contentEquals("free")) { + AdHelper.hideAd(findViewById(R.id.adview)); + } - // Go back. - mainWebView.goBack(); - } else { // There are no pages to go back to. - // Load a blank page - loadUrl(""); - } - } + // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen. + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); - @Override - public void onPinnedMismatchProceed() { - // Do not check the pinned information for this domain again until the domain changes. - ignorePinnedDomainInformation = true; - } + /* 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 tab linear layout. + tabsLinearLayout.setVisibility(View.VISIBLE); - @Override - public void onUrlHistoryEntrySelected(int moveBackOrForwardSteps) { - // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled. - formattedUrlString = ""; + // Show the action bar. + actionBar.show(); - // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded. - navigatingHistory = true; + // Check to see if app bar scrolling is disabled. + if (!scrollAppBar) { + // Add the padding from the top of the swipe refresh layout. + swipeRefreshLayout.setPadding(0, swipeRefreshLayoutPaddingTop, 0, 0); + } - // Load the history entry. - mainWebView.goBackOrForward(moveBackOrForwardSteps); - } + // 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)); + } - @Override - public void onClearHistory() { - // Clear the history. - mainWebView.clearHistory(); - } + // Remove the `SYSTEM_UI` flags from the root frame layout. + rootFrameLayout.setSystemUiVisibility(0); - // Override `onBackPressed` to handle the navigation drawer and `mainWebView`. - @Override - public void onBackPressed() { - 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); + // Add the translucent status flag. + getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + } - // Load the new folder. - loadBookmarksFolder(); + // Consume the double-tap. + return true; + } else { // Do not consume the double-tap because full screen browsing mode is disabled. + return false; + } } + }); - } else if (mainWebView.canGoBack()) { // There is at least one item in the `WebView` history. - // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled. - formattedUrlString = ""; + // 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 `navigatingHistory` so that the domain settings are applied when the new URL is loaded. - navigatingHistory = true; + // Send the event to the gesture detector. + return doubleTapGestureDetector.onTouchEvent(event); + }); - // Go back. - mainWebView.goBack(); - } else { // There isn't anything to do in Privacy Browser. - // Pass `onBackPressed()` to the system. - super.onBackPressed(); - } - } + // Register the WebView for a context menu. This is used to see link targets and download images. + registerForContextMenu(nestedScrollWebView); - // Process the results of an upload file chooser. Currently there is only one `startActivityForResult` in this activity, so the request code, used to differentiate them, is ignored. - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - // 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, data)); - } - } + // Allow the downloading of files. + nestedScrollWebView.setDownloadListener((String downloadUrl, String userAgent, String contentDisposition, String mimetype, long contentLength) -> { + // Check if the download should be processed by an external app. + if (downloadWithExternalApp) { // Download with an external app. + // Create a download intent. Not specifying the action type will display the maximum number of options. + Intent downloadIntent = new Intent(); - private void loadUrlFromTextBox() { - // 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(); + // Set the URI and the MIME type. Specifying `text/html` displays a good number of options. + downloadIntent.setDataAndType(Uri.parse(downloadUrl), "text/html"); - // Check to see if `unformattedUrlString` is a valid URL. Otherwise, convert it into a search. - if (unformattedUrlString.startsWith("content://")) { - // Load the entire content URL. - formattedUrlString = unformattedUrlString; - } else if (Patterns.WEB_URL.matcher(unformattedUrlString).matches() || unformattedUrlString.startsWith("http://") || unformattedUrlString.startsWith("https://") - || unformattedUrlString.startsWith("file://")) { - // 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; - } + // Flag the intent to open in a new task. + downloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - // Initialize `unformattedUrl`. - URL unformattedUrl = null; + // Show the chooser. + startActivity(Intent.createChooser(downloadIntent, getString(R.string.open_with))); + } else { // Download with Android's download manager. + // Check to see if the WRITE_EXTERNAL_STORAGE permission has already been granted. + if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission has not been granted. + // The WRITE_EXTERNAL_STORAGE permission needs to be requested. - // 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(); + // Store the variables for future use by `onRequestPermissionsResult()`. + this.downloadUrl = downloadUrl; + downloadContentDisposition = contentDisposition; + downloadContentLength = contentLength; + + // Show a dialog if the user has previously denied the permission. + if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first. + // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE. + DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE); + + // Show the download location permission alert dialog. The permission will be requested when the the dialog is closed. + downloadLocationPermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.download_location)); + } else { // Show the permission request directly. + // Request the permission. The download dialog will be launched by `onRequestPermissionResult()`. + ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE); + } + } else { // The storage permission has already been granted. + // Get a handle for the download file alert dialog. + DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(downloadUrl, contentDisposition, contentLength); + + // Show the download file alert dialog. + downloadFileDialogFragment.show(getSupportFragmentManager(), getString(R.string.download)); + } } + }); - // 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; + // 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); - // Build the URI. - Uri.Builder formattedUri = new Uri.Builder(); - formattedUri.scheme(scheme).authority(authority).path(path).query(query).fragment(fragment); + @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; - // Decode `formattedUri` as a `String` in `UTF-8`. - try { - formattedUrlString = URLDecoder.decode(formattedUri.build().toString(), "UTF-8"); - } catch (UnsupportedEncodingException exception) { - // Load a blank string. - formattedUrlString = ""; + // Build the match string. + String matchString = activeMatch + "/" + numberOfMatches; + + // Set `findOnPageCountTextView`. + findOnPageCountTextView.setText(matchString); + } } - } else if (unformattedUrlString.isEmpty()){ // Load a blank web site. - // Load a blank string. - formattedUrlString = ""; - } else { // Search for the contents of the URL box. - // Create an encoded URL String. - String encodedUrlString; + }); - // Sanitize the search input. - try { - encodedUrlString = URLEncoder.encode(unformattedUrlString, "UTF-8"); - } catch (UnsupportedEncodingException exception) { - encodedUrlString = ""; + // Update the status of swipe to refresh based on the scroll position of the nested scroll WebView. + // Once the minimum API >= 23 this can be replaced with `nestedScrollWebView.setOnScrollChangeListener()`. + nestedScrollWebView.getViewTreeObserver().addOnScrollChangedListener(() -> { + if (nestedScrollWebView.getSwipeToRefresh()) { + // Only enable swipe to refresh if the WebView is scrolled to the top. + swipeRefreshLayout.setEnabled(nestedScrollWebView.getScrollY() == 0); } + }); - // Add the base search URL. - formattedUrlString = searchURL + encodedUrlString; - } + // 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) { + // Inject the night mode CSS if night mode is enabled. + if (nestedScrollWebView.getNightMode()) { // 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. + // `::selection {background: #0D47A1}' sets the text selection highlight color to be a dark blue. `!important` takes precedent over any existing sub-settings. + nestedScrollWebView.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;} ::selection {background: #0D47A1 !important;}'; parent.appendChild(style)})()", 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 = () -> { + // Only display `mainWebView` if the progress bar is gone. This prevents the display of the `WebView` while it is still loading. + if (progressBar.getVisibility() == View.GONE) { + nestedScrollWebView.setVisibility(View.VISIBLE); + } + }; - // Clear the focus from the URL text box. Otherwise, proximate typing in the box will retain the colorized formatting instead of being reset during refocus. - urlTextBox.clearFocus(); + // Display the WebView after 500 milliseconds. + displayWebViewHandler.postDelayed(displayWebViewRunnable, 500); + }); + } else { // Night mode is disabled. + // Display the nested scroll WebView if night mode is disabled. + // Because of a race condition between `applyDomainSettings` and `onPageStarted`, + // when night mode is set by domain settings the WebView may be hidden even if night mode is not currently enabled. + nestedScrollWebView.setVisibility(View.VISIBLE); + } - // Make it so. - loadUrl(formattedUrlString); - } + // Update the progress bar. + progressBar.setProgress(progress); - private void loadUrl(String url) {// Apply any custom domain settings. - // Set the URL as the formatted URL string so that checking third-party requests works correctly. - formattedUrlString = url; + // 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); - // Apply the domain settings. - applyDomainSettings(url, true, false); + //Stop the swipe to refresh indicator if it is running + swipeRefreshLayout.setRefreshing(false); + } + } - // If loading a website, set `urlIsLoading` to prevent changes in the user agent on websites with redirects from reloading the current website. - urlIsLoading = !url.equals(""); + // 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); - // Load the URL. - mainWebView.loadUrl(url, customHeaders); - } + // Get the current page position. + int currentPosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId()); - public void findPreviousOnPage(View view) { - // Go to the previous highlighted phrase on the page. `false` goes backwards instead of forwards. - mainWebView.findNext(false); - } + // Get the current tab. + TabLayout.Tab tab = tabLayout.getTabAt(currentPosition); - public void findNextOnPage(View view) { - // Go to the next highlighted phrase on the page. `true` goes forwards instead of backwards. - mainWebView.findNext(true); - } + // Check to see if the tab has been populated. + if (tab != null) { + // Get the custom view from the tab. + View tabView = tab.getCustomView(); - public void closeFindOnPage(View view) { - // Delete the contents of `find_on_page_edittext`. - findOnPageEditText.setText(null); + // 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); - // Clear the highlighted phrases. - mainWebView.clearMatches(); + // Display the favorite icon in the tab. + tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(icon, 64, 64, true)); + } + } + } + } - // Hide the Find on Page `RelativeLayout`. - findOnPageLinearLayout.setVisibility(View.GONE); + // 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()); - // Show the URL app bar. - supportAppBar.setVisibility(View.VISIBLE); + // Get the current tab. + TabLayout.Tab tab = tabLayout.getTabAt(currentPosition); - // Hide the keyboard so we can see the webpage. `0` indicates no additional flags. - inputMethodManager.hideSoftInputFromWindow(mainWebView.getWindowToken(), 0); - } + // 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(); - private void applyAppSettings() { - // Get a handle for the shared preferences. - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + // Remove the incorrect warning below that the current tab view might be null. + assert tabView != null; - // Store the values from the shared preferences in variables. - incognitoModeEnabled = sharedPreferences.getBoolean("incognito_mode", false); - boolean doNotTrackEnabled = sharedPreferences.getBoolean("do_not_track", false); - 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); - downloadWithExternalApp = sharedPreferences.getBoolean("download_with_external_app", false); + // Get the title text view from the tab. + TextView tabTitleTextView = tabView.findViewById(R.id.title_textview); - // Apply the proxy through Orbot settings. - applyProxyThroughOrbot(false); + // Set the title as the tab text. + tabTitleTextView.setText(title); + } + } - // Set Do Not Track status. - if (doNotTrackEnabled) { - customHeaders.put("DNT", "1"); - } else { - customHeaders.remove("DNT"); - } + // 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); - // Apply the appropriate full screen mode the `SYSTEM_UI` flags. - if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode. - if (hideSystemBarsOnFullscreen) { // Hide everything. - // Remove the translucent navigation setting if it is currently flagged. - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + // Set the full screen video flag. + displayingFullScreenVideo = true; - // Remove the translucent status bar overlay. - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + // 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)); + } + + // Hide the keyboard. + inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0); + + // Hide the main content relative layout. + mainContentRelativeLayout.setVisibility(View.GONE); // 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. + // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen. + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + + /* 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. */ - 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. - // Remove any `SYSTEM_UI` flags from `rootCoordinatorLayout`. - rootCoordinatorLayout.setSystemUiVisibility(0); + 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 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); - } - } - } 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; + // Disable the sliding drawers. + drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); - // Show the `appBar` if `findOnPageLinearLayout` is not visible. - if (findOnPageLinearLayout.getVisibility() == View.GONE) { - appBar.show(); - } + // Add the video view to the full screen video frame layout. + fullScreenVideoFrameLayout.addView(video); - // Show the `BannerAd` in the free flavor. - if (BuildConfig.FLAVOR.contentEquals("free")) { - // Initialize the ad. The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations. - AdHelper.initializeAds(findViewById(R.id.adview), getApplicationContext(), getFragmentManager(), getString(R.string.google_app_id), getString(R.string.ad_unit_id)); + // Show the full screen video frame layout. + fullScreenVideoFrameLayout.setVisibility(View.VISIBLE); } - // Remove any `SYSTEM_UI` flags from `rootCoordinatorLayout`. - rootCoordinatorLayout.setSystemUiVisibility(0); + // 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); - // Remove the translucent navigation bar flag if it is set. - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + // Unset the full screen video flag. + displayingFullScreenVideo = false; - // 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); + // Remove all the views from the full screen video frame layout. + fullScreenVideoFrameLayout.removeAllViews(); - // Constrain `rootCoordinatorLayout` inside the status and navigation bars. - rootCoordinatorLayout.setFitsSystemWindows(true); - } - } + // Hide the full screen video frame layout. + fullScreenVideoFrameLayout.setVisibility(View.GONE); - // `reloadWebsite` is used if returning from the Domains activity. Otherwise JavaScript might not function correctly if it is newly enabled. - // The deprecated `.getDrawable()` must be used until the minimum API >= 21. - @SuppressWarnings("deprecation") - private boolean applyDomainSettings(String url, boolean resetFavoriteIcon, boolean reloadWebsite) { - // Get the current user agent. - String initialUserAgent = mainWebView.getSettings().getUserAgentString(); + // Enable the sliding drawers. + drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED); - // Initialize a variable to track if the user agent changes. - boolean userAgentChanged = false; + // Show the main content relative layout. + mainContentRelativeLayout.setVisibility(View.VISIBLE); - // Parse the URL into a URI. - Uri uri = Uri.parse(url); + // 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); - // Extract the domain from `uri`. - String hostName = uri.getHost(); - - // Initialize `loadingNewDomainName`. - boolean loadingNewDomainName; - - // 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); - } + // Hide the action bar. + actionBar.hide(); + } - // Strings don't like to be null. - if (hostName == null) { - hostName = ""; - } + // Hide the banner ad in the free flavor. + if (BuildConfig.FLAVOR.contentEquals("free")) { + AdHelper.hideAd(findViewById(R.id.adview)); + } - // Only apply the domain settings if a new domain is being loaded. This allows the user to set temporary settings for JavaScript, cookies, DOM storage, etc. - if (loadingNewDomainName) { - // Set the new `hostname` as the `currentDomainName`. - currentDomainName = hostName; + // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen. + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + + /* 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); - // Reset the ignoring of pinned domain information. - ignorePinnedDomainInformation = false; + // Add the translucent status flag. + getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + } - // Reset the favorite icon if specified. - if (resetFavoriteIcon) { - favoriteIconBitmap = favoriteIconDefaultBitmap; - favoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(favoriteIconBitmap, 64, 64, true)); + // 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)); + } } - // 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); + // 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; - // Populate `domainSettingsSet`. - for (int i = 0; i < domainNameCursor.getCount(); i++) { - // Move `domainsCursor` to the current row. - domainNameCursor.moveToPosition(i); + // Create an intent to open a chooser based ont the file chooser parameters. + Intent fileChooserIntent = fileChooserParams.createIntent(); - // Store the domain name in `domainSettingsSet`. - domainSettingsSet.add(domainNameCursor.getString(domainNameColumnIndex)); + // Open the file chooser. Currently only one `startActivityForResult` exists in this activity, so the request code, used to differentiate them, is simply `0`. + startActivityForResult(fileChooserIntent, 0); + } + return true; } + }); - // Close `domainNameCursor. - domainNameCursor.close(); + 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); - // Initialize variables to track if domain settings will be applied and, if so, under which name. - domainSettingsApplied = false; - String domainNameInDatabase = null; + 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. + boolean userAgentChanged = applyDomainSettings(nestedScrollWebView, url, true, false); - // Check the hostname. - if (domainSettingsSet.contains(hostName)) { - domainSettingsApplied = true; - domainNameInDatabase = hostName; - } + // Check if the user agent has changed. + if (userAgentChanged) { + // Manually load the URL. The changing of the user agent will cause WebView to reload the previous URL. + nestedScrollWebView.loadUrl(url, customHeaders); + + // Returning true indicates that Privacy Browser is manually handling the loading of the URL. + return true; + } else { + // Returning false causes the current WebView to handle the URL and prevents it from adding redirects to the history list. + return false; + } + } 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); - // Check all the subdomains of the host name against wildcard domains in the domain cursor. - while (!domainSettingsApplied && hostName.contains(".")) { // Stop checking if domain settings are already applied or there are no more `.` in the host name. - if (domainSettingsSet.contains("*." + hostName)) { // Check the host name prepended by `*.`. - // Apply the domain settings. - domainSettingsApplied = true; + // Parse the url and set it as the data for the intent. + emailIntent.setData(Uri.parse(url)); - // Store the applied domain names as it appears in the database. - domainNameInDatabase = "*." + hostName; - } + // Open the email program in a new task instead of as part of Privacy Browser. + emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - // Strip out the lowest subdomain of of the host name. - hostName = hostName.substring(hostName.indexOf(".") + 1); - } + // Make it so. + startActivity(emailIntent); + // 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); - // Get a handle for the shared preference. - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + // Add the phone number to the intent. + dialIntent.setData(Uri.parse(url)); - // 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)); - defaultCustomUserAgentString = sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)); - boolean defaultSwipeToRefresh = sharedPreferences.getBoolean("swipe_to_refresh", true); - nightMode = sharedPreferences.getBoolean("night_mode", false); - boolean displayWebpageImages = sharedPreferences.getBoolean("display_webpage_images", true); + // Open the dialer in a new task instead of as part of Privacy Browser. + dialIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - if (domainSettingsApplied) { // The url 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(); + // Make it so. + startActivity(dialIntent); - // 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); - // Form data can be removed once the minimum API >= 26. - saveFormDataEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FORM_DATA)) == 1); - easyListEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYLIST)) == 1); - easyPrivacyEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYPRIVACY)) == 1); - fanboysAnnoyanceListEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST)) == 1); - fanboysSocialBlockingListEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)) == 1); - ultraPrivacyEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY)) == 1); - blockAllThirdPartyRequests = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS)) == 1); - String userAgentName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.USER_AGENT)); - int fontSize = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.FONT_SIZE)); - int swipeToRefreshInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SWIPE_TO_REFRESH)); - int nightModeInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.NIGHT_MODE)); - int displayWebpageImagesInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.DISPLAY_IMAGES)); - pinnedSslCertificate = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)) == 1); - pinnedSslIssuedToCName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME)); - pinnedSslIssuedToOName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION)); - pinnedSslIssuedToUName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT)); - pinnedSslIssuedByCName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME)); - pinnedSslIssuedByOName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION)); - pinnedSslIssuedByUName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT)); - pinnedIpAddresses = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_IP_ADDRESSES)) == 1); - pinnedHostIpAddresses = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.IP_ADDRESSES)); - - // 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; + // 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); - case DomainsDatabaseHelper.NIGHT_MODE_DISABLED: - nightMode = false; - break; - } + // Add the URL to the intent. + genericIntent.setData(Uri.parse(url)); - // Store the domain JavaScript status. This is used by the options menu night mode toggle. - domainSettingsJavaScriptEnabled = javaScriptEnabled; + // List all apps that can handle the URL instead of just opening the first one. + genericIntent.addCategory(Intent.CATEGORY_BROWSABLE); - // Enable JavaScript if night mode is enabled. - if (nightMode) { - javaScriptEnabled = true; - } + // Open the app in a new task instead of as part of Privacy Browser. + genericIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - // 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) { - pinnedSslStartDate = null; - } else { - pinnedSslStartDate = new Date(currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE))); - } + // 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(); + } - // 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) { - pinnedSslEndDate = null; - } else { - pinnedSslEndDate = new Date(currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE))); + // Returning true indicates Privacy Browser is handling the URL by creating an intent. + return true; } + } - // Close `currentHostDomainSettingsCursor`. - currentHostDomainSettingsCursor.close(); + // Check requests against the block lists. The deprecated `shouldInterceptRequest()` must be used until minimum API >= 21. + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, String url) { + // Sanitize the URL. + url = sanitizeUrl(url); - // Apply the domain settings. - mainWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled); - cookieManager.setAcceptCookie(firstPartyCookiesEnabled); - mainWebView.getSettings().setDomStorageEnabled(domStorageEnabled); + // Get a handle for the navigation view. + NavigationView navigationView = findViewById(R.id.navigationview); - // Apply the form data setting if the API < 26. - if (Build.VERSION.SDK_INT < 26) { - mainWebView.getSettings().setSaveFormData(saveFormDataEnabled); - } + // Get a handle for the navigation menu. + Menu navigationMenu = navigationView.getMenu(); - // 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); - } + // Get a handle for the navigation requests menu item. The menu is 0 based. + MenuItem navigationRequestsMenuItem = navigationMenu.getItem(5); - // Set third-party cookies status if API >= 21. - if (Build.VERSION.SDK_INT >= 21) { - cookieManager.setAcceptThirdPartyCookies(mainWebView, thirdPartyCookiesEnabled); - } + // 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())); - // 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) { - // 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. - mainWebView.getSettings().setUserAgentString(defaultUserAgentName); - break; - - case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT: - // Set the user agent to `""`, which uses the default value. - mainWebView.getSettings().setUserAgentString(""); - break; - - case SETTINGS_CUSTOM_USER_AGENT: - // Set the custom user agent. - mainWebView.getSettings().setUserAgentString(defaultCustomUserAgentString); - break; - - default: - // Get the user agent string from the user agent data array - mainWebView.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. - mainWebView.getSettings().setUserAgentString(userAgentName); - break; - - case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT: - // Set the user agent to `""`, which uses the default value. - mainWebView.getSettings().setUserAgentString(""); - break; - - default: - // Get the user agent string from the user agent data array. - mainWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]); - } - } + // Reset the whitelist results tracker. + String[] whitelistResultStringArray = null; - // Store the applied user agent string, which is used in the View Source activity. - appliedUserAgentString = mainWebView.getSettings().getUserAgentString(); + // Initialize the third party request tracker. + boolean isThirdPartyRequest = false; - // Update the user agent change tracker. - userAgentChanged = !appliedUserAgentString.equals(initialUserAgent); - } + // Get the current URL. `.getUrl()` throws an error because operations on the WebView cannot be made from this thread. + String currentBaseDomain = nestedScrollWebView.getCurrentDomainName(); - // Set swipe to refresh. - switch (swipeToRefreshInt) { - case DomainsDatabaseHelper.SWIPE_TO_REFRESH_SYSTEM_DEFAULT: - // Set swipe to refresh according to the default. - swipeRefreshLayout.setEnabled(defaultSwipeToRefresh); - break; + // Store a copy of the current domain for use in later requests. + String currentDomain = currentBaseDomain; - case DomainsDatabaseHelper.SWIPE_TO_REFRESH_ENABLED: - // Enable swipe to refresh. - swipeRefreshLayout.setEnabled(true); - break; + // Nobody is happy when comparing null strings. + if ((currentBaseDomain != null) && (url != null)) { + // Convert the request URL to a URI. + Uri requestUri = Uri.parse(url); - case DomainsDatabaseHelper.SWIPE_TO_REFRESH_DISABLED: - // Disable swipe to refresh. - swipeRefreshLayout.setEnabled(false); - } + // Get the request host name. + String requestBaseDomain = requestUri.getHost(); - // Set the loading of webpage images. - switch (displayWebpageImagesInt) { - case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_SYSTEM_DEFAULT: - mainWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages); - break; + // 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); + } - case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_ENABLED: - mainWebView.getSettings().setLoadsImagesAutomatically(true); - break; + // 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); + } - case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_DISABLED: - mainWebView.getSettings().setLoadsImagesAutomatically(false); - break; + // Update the third party request tracker. + isThirdPartyRequest = !currentBaseDomain.equals(requestBaseDomain); + } } - // 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 new URL does not have custom domain settings. Load the defaults. - // Store the values from `sharedPreferences` in variables. - javaScriptEnabled = sharedPreferences.getBoolean("javascript", false); - firstPartyCookiesEnabled = sharedPreferences.getBoolean("first_party_cookies", false); - thirdPartyCookiesEnabled = sharedPreferences.getBoolean("third_party_cookies", false); - domStorageEnabled = sharedPreferences.getBoolean("dom_storage", false); - saveFormDataEnabled = sharedPreferences.getBoolean("save_form_data", false); // Form data can be removed once the minimum API >= 26. - easyListEnabled = sharedPreferences.getBoolean("easylist", true); - easyPrivacyEnabled = sharedPreferences.getBoolean("easyprivacy", true); - fanboysAnnoyanceListEnabled = sharedPreferences.getBoolean("fanboys_annoyance_list", true); - fanboysSocialBlockingListEnabled = sharedPreferences.getBoolean("fanboys_social_blocking_list", true); - ultraPrivacyEnabled = sharedPreferences.getBoolean("ultraprivacy", true); - blockAllThirdPartyRequests = sharedPreferences.getBoolean("block_all_third_party_requests", false); - - // Set `javaScriptEnabled` to be `true` if `night_mode` is `true`. - if (nightMode) { - javaScriptEnabled = true; - } + // Get the current WebView page position. + int webViewPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId()); - // Apply the default settings. - mainWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled); - cookieManager.setAcceptCookie(firstPartyCookiesEnabled); - mainWebView.getSettings().setDomStorageEnabled(domStorageEnabled); - mainWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString)); - swipeRefreshLayout.setEnabled(defaultSwipeToRefresh); + // Determine if the WebView is currently displayed. + boolean webViewDisplayed = (webViewPagePosition == tabLayout.getSelectedTabPosition()); - // Apply the form data setting if the API < 26. - if (Build.VERSION.SDK_INT < 26) { - mainWebView.getSettings().setSaveFormData(saveFormDataEnabled); - } + // 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}); - // Reset the pinned variables. - domainSettingsDatabaseId = -1; - pinnedSslCertificate = false; - pinnedSslIssuedToCName = ""; - pinnedSslIssuedToOName = ""; - pinnedSslIssuedToUName = ""; - pinnedSslIssuedByCName = ""; - pinnedSslIssuedByOName = ""; - pinnedSslIssuedByUName = ""; - pinnedSslStartDate = null; - pinnedSslEndDate = null; - pinnedIpAddresses = false; - pinnedHostIpAddresses = ""; + // Increment the blocked requests counters. + nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS); + nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS); - // Set third-party cookies status if API >= 21. - if (Build.VERSION.SDK_INT >= 21) { - cookieManager.setAcceptThirdPartyCookies(mainWebView, thirdPartyCookiesEnabled); + // 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)); + } + }); + } + + // Return an empty web resource response. + return emptyWebResourceResponse; } - // 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) { - // Get the array position of the user agent name. - int userAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName); + // Check UltraPrivacy if it is enabled. + if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY)) { + // Check the URL against UltraPrivacy. + String[] ultraPrivacyResults = blockListHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, ultraPrivacy); - // 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. - mainWebView.getSettings().setUserAgentString(defaultUserAgentName); - break; + // 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]}); - case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT: - // Set the user agent to `""`, which uses the default value. - mainWebView.getSettings().setUserAgentString(""); - break; + // Increment the blocked requests counters. + nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS); + nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.ULTRA_PRIVACY); + + // 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.ULTRA_PRIVACY) + " - " + getString(R.string.ultraprivacy)); + } + }); + } - case SETTINGS_CUSTOM_USER_AGENT: - // Set the custom user agent. - mainWebView.getSettings().setUserAgentString(defaultCustomUserAgentString); - break; + // 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]}); - default: - // Get the user agent string from the user agent data array - mainWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]); + // The resource request has been allowed by UltraPrivacy. `return null` loads the requested resource. + return null; } - - // Store the applied user agent string, which is used in the View Source activity. - appliedUserAgentString = mainWebView.getSettings().getUserAgentString(); - - // Update the user agent change tracker. - userAgentChanged = !appliedUserAgentString.equals(initialUserAgent); } - // Set the loading of webpage images. - mainWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages); - - // Set a transparent background on `urlTextBox`. The deprecated `.getDrawable()` must be used until the minimum API >= 21. - urlAppBarRelativeLayout.setBackgroundDrawable(getResources().getDrawable(R.color.transparent)); - } + // Check EasyList if it is enabled. + if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST)) { + // Check the URL against EasyList. + String[] easyListResults = blockListHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, easyList); - // Close the domains database helper. - domainsDatabaseHelper.close(); + // 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]}); - // Update the privacy icons, but only if `mainMenu` has already been populated. - if (mainMenu != null) { - updatePrivacyIcons(true); - } - } + // Increment the blocked requests counters. + nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS); + nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.EASY_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.easylist).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.EASY_LIST) + " - " + getString(R.string.easylist)); + } + }); + } - // Reload the website if returning from the Domains activity. - if (reloadWebsite) { - mainWebView.reload(); - } + // 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]}; + } + } - // Return the user agent changed status. - return userAgentChanged; - } + // Check EasyPrivacy if it is enabled. + if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY)) { + // Check the URL against EasyPrivacy. + String[] easyPrivacyResults = blockListHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, easyPrivacy); - private void applyProxyThroughOrbot(boolean reloadWebsite) { - // Get a handle for the shared preferences. - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + // 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]}); - // Get the search preferences. - String homepageString = sharedPreferences.getString("homepage", getString(R.string.homepage_default_value)); - String torHomepageString = sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value)); - String torSearchString = sharedPreferences.getString("tor_search", getString(R.string.tor_search_default_value)); - String torSearchCustomUrlString = sharedPreferences.getString("tor_search_custom_url", getString(R.string.tor_search_custom_url_default_value)); - String searchString = sharedPreferences.getString("search", getString(R.string.search_default_value)); - String searchCustomUrlString = sharedPreferences.getString("search_custom_url", getString(R.string.search_custom_url_default_value)); + // Increment the blocked requests counters. + nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS); + nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.EASY_PRIVACY); + + // 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.EASY_PRIVACY) + " - " + getString(R.string.easyprivacy)); + } + }); + } - // Set the homepage, search, and proxy options. - if (proxyThroughOrbot) { // Set the Tor options. - // Set `torHomepageString` as `homepage`. - homepage = torHomepageString; + // 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]}; + } + } - // If formattedUrlString is null assign the homepage to it. - if (formattedUrlString == null) { - formattedUrlString = homepage; - } + // 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); - // 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; - } + // 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]}); - // Set the proxy. `this` refers to the current activity where an `AlertDialog` might be displayed. - OrbotProxyHelper.setProxy(getApplicationContext(), this, "localhost", "8118"); + // 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)); + } + }); + } - // 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)); - } + // 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); - // Check to see if Orbot is ready. - if (!orbotStatus.equals("ON")) { // Orbot is not ready. - // Set `waitingForOrbot`. - waitingForOrbot = true; + // 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]}); - // Disable the wide view port so that the waiting for Orbot text is displayed correctly. - mainWebView.getSettings().setUseWideViewPort(false); + // 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)); + } + }); + } - // Load a waiting page. `null` specifies no encoding, which defaults to ASCII. - mainWebView.loadData(waitingForOrbotHtmlString, "text/html", null); - } else if (reloadWebsite) { // Orbot is ready and the website should be reloaded. - // Reload the website. - mainWebView.reload(); - } - } else { // Set the non-Tor options. - // Set `homepageString` as `homepage`. - homepage = homepageString; + // 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]}; + } + } - // If formattedUrlString is null assign the homepage to it. - if (formattedUrlString == null) { - formattedUrlString = homepage; - } + // 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}); + } - // 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 has not been blocked. `return null` loads the requested resource. + return null; } - // Reset the proxy to default. The host is `""` and the port is `"0"`. - OrbotProxyHelper.setProxy(getApplicationContext(), this, "", "0"); + // Handle HTTP authentication requests. + @Override + public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) { + // Store the handler. + nestedScrollWebView.setHttpAuthHandler(handler); - // 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)); + // Instantiate an HTTP authentication dialog. + DialogFragment httpAuthenticationDialogFragment = HttpAuthenticationDialog.displayDialog(host, realm, nestedScrollWebView.getWebViewFragmentId()); + + // Show the HTTP authentication dialog. + httpAuthenticationDialogFragment.show(getSupportFragmentManager(), getString(R.string.http_authentication)); } - // Reset `waitingForOrbot. - waitingForOrbot = false; + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + // Get the preferences. + boolean scrollAppBar = sharedPreferences.getBoolean("scroll_app_bar", true); + boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false); - // Reload the website if requested. - if (reloadWebsite) { - mainWebView.reload(); - } - } - } + // Get a handler for the app bar layout. + AppBarLayout appBarLayout = findViewById(R.id.appbar_layout); - private void updatePrivacyIcons(boolean runInvalidateOptionsMenu) { - // Get handles for the menu items. - MenuItem privacyMenuItem = mainMenu.findItem(R.id.toggle_javascript); - MenuItem firstPartyCookiesMenuItem = mainMenu.findItem(R.id.toggle_first_party_cookies); - MenuItem domStorageMenuItem = mainMenu.findItem(R.id.toggle_dom_storage); - MenuItem refreshMenuItem = mainMenu.findItem(R.id.refresh); - - // Update the privacy icon. - if (javaScriptEnabled) { // JavaScript is enabled. - privacyMenuItem.setIcon(R.drawable.javascript_enabled); - } else if (firstPartyCookiesEnabled) { // 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); - } + // Set the top padding of the swipe refresh layout according to the app bar scrolling preference. + if (scrollAppBar) { + // 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); - // Update the first-party cookies icon. - if (firstPartyCookiesEnabled) { // First-party cookies are enabled. - firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_enabled); - } else { // First-party cookies are disabled. - if (darkTheme) { - firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_dark); - } else { - firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_light); - } - } + // 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. + int appBarHeight = appBarLayout.getHeight(); - // Update the DOM storage icon. - if (javaScriptEnabled && domStorageEnabled) { // Both JavaScript and DOM storage are enabled. - domStorageMenuItem.setIcon(R.drawable.dom_storage_enabled); - } else if (javaScriptEnabled) { // JavaScript is enabled but DOM storage is disabled. - if (darkTheme) { - domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_dark); - } else { - domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_light); - } - } else { // JavaScript is disabled, so DOM storage is ghosted. - if (darkTheme) { - domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_dark); - } else { - domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_light); - } - } + // The swipe refresh layout must be manually moved below the app bar layout. + swipeRefreshLayout.setPadding(0, appBarHeight, 0, 0); - // Update the refresh icon. - if (darkTheme) { - refreshMenuItem.setIcon(R.drawable.refresh_enabled_dark); - } else { - refreshMenuItem.setIcon(R.drawable.refresh_enabled_light); - } + // 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); + } - // `invalidateOptionsMenu` calls `onPrepareOptionsMenu()` and redraws the icons in the `AppBar`. - if (runInvalidateOptionsMenu) { - invalidateOptionsMenu(); - } - } + // Reset the list of resource requests. + nestedScrollWebView.clearResourceRequests(); - private void openUrlWithExternalApp(String url) { - // Create a download intent. Not specifying the action type will display the maximum number of options. - Intent downloadIntent = new Intent(); + // Reset the requests counters. + nestedScrollWebView.resetRequestsCounters(); - // Set the URI and the MIME type. Specifying `text/html` displays a good number of options. - downloadIntent.setDataAndType(Uri.parse(url), "text/html"); + // If night mode is enabled, hide `mainWebView` until after the night mode CSS is applied. + if (nestedScrollWebView.getNightMode()) { + nestedScrollWebView.setVisibility(View.INVISIBLE); + } else { + nestedScrollWebView.setVisibility(View.VISIBLE); + } - // Flag the intent to open in a new task. - downloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // Hide the keyboard. + inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0); - // Show the chooser. - startActivity(Intent.createChooser(downloadIntent, getString(R.string.open_with))); - } + // Check to see if Privacy Browser is waiting on Orbot. + if (!waitingForOrbot) { // Process the URL. + // Get the current page position. + int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId()); - private void highlightUrlText() { - // Only highlight the URL text if the box is not currently selected. - if (!urlTextBox.hasFocus()) { - // Get the URL string. - String urlString = urlTextBox.getText().toString(); + // 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(); - // Highlight the URL according to the protocol. - if (urlString.startsWith("file://")) { // This is a file URL. - // De-emphasize only the protocol. - urlTextBox.getText().setSpan(initialGrayColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE); - } else if (urlString.startsWith("content://")) { - // De-emphasize only the protocol. - urlTextBox.getText().setSpan(initialGrayColorSpan, 0, 10, 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)); + // Display the formatted URL text. + urlEditText.setText(url); - // Create a base URL string. - String baseUrl; + // Apply text highlighting to `urlTextBox`. + highlightUrlText(); + } - // 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; - } + // Reset the list of host IP addresses. + nestedScrollWebView.clearCurrentIpAddresses(); - // Get the index of the last `.` in the domain. - int lastDotIndex = baseUrl.lastIndexOf("."); + // Get a URI for the current URL. + Uri currentUri = Uri.parse(url); - // Get the index of the penultimate `.` in the domain. - int penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1); + // Get the IP addresses for the host. + new GetHostIpAddresses(activity, getSupportFragmentManager(), nestedScrollWebView).execute(currentUri.getHost()); - // Markup the beginning of the URL. - if (urlString.startsWith("http://")) { // Highlight the protocol of connections that are not encrypted. - urlTextBox.getText().setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE); + // Apply any custom domain settings if the URL was loaded by navigating history. + if (nestedScrollWebView.getNavigatingHistory()) { + // Reset navigating history. + nestedScrollWebView.setNavigatingHistory(false); - // De-emphasize subdomains. - if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name. - urlTextBox.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. - urlTextBox.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. - urlTextBox.getText().setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE); + // Apply the domain settings. + boolean userAgentChanged = applyDomainSettings(nestedScrollWebView, url, true, false); + + // Manually load the URL if the user agent has changed, which will have caused the previous URL to be reloaded. + if (userAgentChanged) { + loadUrl(url); + } } - } - // De-emphasize the text after the domain name. - if (endOfDomainName > 0) { - urlTextBox.getText().setSpan(finalGrayColorSpan, endOfDomainName, urlString.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); - } - } - } - } + // Replace Refresh with Stop if the options menu has been created. (The 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); - private void loadBookmarksFolder() { - // Update the bookmarks cursor with the contents of the bookmarks database for the current folder. - bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder); + // Set the title. + refreshMenuItem.setTitle(R.string.stop); - // 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); + // Get the app bar and theme preferences. + boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false); + + // If the icon is displayed in the AppBar, set it according to the theme. + if (displayAdditionalAppBarIcons) { + if (darkTheme) { + refreshMenuItem.setIcon(R.drawable.close_dark); + } else { + refreshMenuItem.setIcon(R.drawable.close_light); + } + } + } + } } @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); + public void onPageFinished(WebView view, String url) { + // Reset the wide view port if it has been turned off by the waiting for Orbot message. + if (!waitingForOrbot) { + // Only use a wide view port if the URL starts with `http`, not for `file://` and `content://`. + nestedScrollWebView.getSettings().setUseWideViewPort(url.startsWith("http")); + } - // Get the favorite icon byte array from the cursor. - byte[] favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON)); + // 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(); + } - // 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); + // 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); - // Display the bitmap in `bookmarkFavoriteIcon`. - bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap); + // Reset the Refresh title. + refreshMenuItem.setTitle(R.string.refresh); - // Get the bookmark name from the cursor and display it in `bookmarkNameTextView`. - String bookmarkNameString = cursor.getString(cursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME)); - bookmarkNameTextView.setText(bookmarkNameString); + // Get the app bar and theme preferences. + boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false); + boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false); - // 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); + // If the icon is displayed in the AppBar, reset it according to the theme. + if (displayAdditionalAppBarIcons) { + if (darkTheme) { + refreshMenuItem.setIcon(R.drawable.refresh_enabled_dark); + } else { + refreshMenuItem.setIcon(R.drawable.refresh_enabled_light); + } + } } - } - }; - // Populate the `ListView` with the adapter. - bookmarksListView.setAdapter(bookmarksCursorAdapter); + // Clear the cache and history if Incognito Mode is enabled. + if (incognitoModeEnabled) { + // Clear the cache. `true` includes disk files. + nestedScrollWebView.clearCache(true); - // Set the bookmarks drawer title. - if (currentBookmarksFolder.isEmpty()) { - bookmarksTitleTextView.setText(R.string.bookmarks); - } else { - bookmarksTitleTextView.setText(currentBookmarksFolder); - } - } + // Clear the back/forward history. + nestedScrollWebView.clearHistory(); - private void openWithApp(String url) { - // Create the open with intent with `ACTION_VIEW`. - Intent openWithAppIntent = new Intent(Intent.ACTION_VIEW); + // 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; - // Set the URI but not the MIME type. This should open all available apps. - openWithAppIntent.setData(Uri.parse(url)); + // Delete the main cache directory. + Runtime.getRuntime().exec("rm -rf " + privateDataDirectoryString + "/cache"); - // Flag the intent to open in a new task. - openWithAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // 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. + } + } - // Show the chooser. - startActivity(openWithAppIntent); - } + // Update the URL text box and apply domain settings if not waiting on Orbot. + if (!waitingForOrbot) { + // Get the current page position. + int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId()); - private void openWithBrowser(String url) { - // Create the open with intent with `ACTION_VIEW`. - Intent openWithBrowserIntent = new Intent(Intent.ACTION_VIEW); + // 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 the URI and the MIME type. `"text/html"` should load browser options. - openWithBrowserIntent.setDataAndType(Uri.parse(url), "text/html"); + // 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(); - // Flag the intent to open in a new task. - openWithBrowserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // 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(""); - // Show the chooser. - startActivity(openWithBrowserIntent); - } + // Request focus for the URL text box. + urlEditText.requestFocus(); - private static void checkPinnedMismatch() { - if ((pinnedSslCertificate || pinnedIpAddresses) && !ignorePinnedDomainInformation) { - // 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(); - } + // Display the keyboard. + inputMethodManager.showSoftInput(urlEditText, 0); - // 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 pinnedSslStartDateString = ""; - String pinnedSslEndDateString = ""; + // Hide the WebView, which causes the default background color to be displayed according to the theme. + nestedScrollWebView.setVisibility(View.INVISIBLE); - // Convert the `Dates` to `Strings` if they are not `null`. - if (currentWebsiteSslStartDate != null) { - currentWebsiteSslStartDateString = currentWebsiteSslStartDate.toString(); - } + // Apply the domain settings. This clears any settings from the previous domain. + applyDomainSettings(nestedScrollWebView, "", true, false); + } else { // The WebView has loaded a webpage. + // 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(currentUrl); - if (currentWebsiteSslEndDate != null) { - currentWebsiteSslEndDateString = currentWebsiteSslEndDate.toString(); - } + // Apply text highlighting to the URL. + highlightUrlText(); + } + } - if (pinnedSslStartDate != null) { - pinnedSslStartDateString = pinnedSslStartDate.toString(); - } + // Get the current tab. + TabLayout.Tab tab = tabLayout.getTabAt(currentPagePosition); - if (pinnedSslEndDate != null) { - pinnedSslEndDateString = pinnedSslEndDate.toString(); - } + // 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(); - // Check to see if the pinned information matches the current information. - if ((pinnedIpAddresses && !currentHostIpAddresses.equals(pinnedHostIpAddresses)) || (pinnedSslCertificate && (!currentWebsiteIssuedToCName.equals(pinnedSslIssuedToCName) || - !currentWebsiteIssuedToOName.equals(pinnedSslIssuedToOName) || !currentWebsiteIssuedToUName.equals(pinnedSslIssuedToUName) || - !currentWebsiteIssuedByCName.equals(pinnedSslIssuedByCName) || !currentWebsiteIssuedByOName.equals(pinnedSslIssuedByOName) || - !currentWebsiteIssuedByUName.equals(pinnedSslIssuedByUName) || !currentWebsiteSslStartDateString.equals(pinnedSslStartDateString) || - !currentWebsiteSslEndDateString.equals(pinnedSslEndDateString)))) { + // Remove the incorrect warning below that the current tab view might be null. + assert tabView != null; - // Get a handle for the pinned mismatch alert dialog. - AppCompatDialogFragment pinnedMismatchDialogFragment = PinnedMismatchDialog.displayDialog(pinnedSslCertificate, pinnedIpAddresses); + // Get the title text view from the tab. + TextView tabTitleTextView = tabView.findViewById(R.id.title_textview); - // Show the pinned mismatch alert dialog. - pinnedMismatchDialogFragment.show(supportFragmentManager, "Pinned Mismatch"); + // Set the title as the tab text. Sometimes `onReceivedTitle()` is not called, especially when navigating history. + tabTitleTextView.setText(nestedScrollWebView.getTitle()); + } + } } - } - } - // This must run asynchronously because it involves a network request. `String` declares the parameters. `Void` does not declare progress units. `String` contains the results. - private static class GetHostIpAddresses extends AsyncTask { - // The weak references are used to determine if the activity have disappeared while the AsyncTask is running. - private final WeakReference activityWeakReference; + // Handle SSL Certificate errors. + @Override + public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { + // Get the current website SSL certificate. + SslCertificate currentWebsiteSslCertificate = error.getCertificate(); + + // 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(); - GetHostIpAddresses(Activity activity) { - // Populate the weak references. - activityWeakReference = new WeakReference<>(activity); - } + // 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); - // `onPreExecute()` operates on the UI thread. - @Override - protected void onPreExecute() { - // Get a handle for the activity. - Activity activity = activityWeakReference.get(); + // Instantiate an SSL certificate error alert dialog. + DialogFragment sslCertificateErrorDialogFragment = SslCertificateErrorDialog.displayDialog(error, nestedScrollWebView.getWebViewFragmentId()); - // Abort if the activity is gone. - if ((activity == null) || activity.isFinishing()) { - return; + // Show the SSL certificate error dialog. + sslCertificateErrorDialogFragment.show(getSupportFragmentManager(), getString(R.string.ssl_certificate_error)); + } } + }); - // Set the getting IP addresses tracker. - gettingIpAddresses = true; - } + // Check to see if this is the first page. + if (pageNumber == 0) { + // Set this nested scroll WebView as the current WebView. + currentWebView = nestedScrollWebView; + // Apply the app settings from the shared preferences. + applyAppSettings(); - @Override - protected String doInBackground(String... domainName) { - // Get a handle for the activity. - Activity activity = activityWeakReference.get(); + // Load the website if not waiting for Orbot to connect. + if (!waitingForOrbot) { + // Get the intent that started the app. + Intent launchingIntent = getIntent(); - // Abort if the activity is gone. - if ((activity == null) || activity.isFinishing()) { - // Return an empty spannable string builder. - return ""; - } + // Get the information from the intent. + String launchingIntentAction = launchingIntent.getAction(); + Uri launchingIntentUriData = launchingIntent.getData(); - // Initialize an IP address string builder. - StringBuilder ipAddresses = new StringBuilder(); + // If the intent action is a web search, perform the search. + if ((launchingIntentAction != null) && launchingIntentAction.equals(Intent.ACTION_WEB_SEARCH)) { + // Create an encoded URL string. + String encodedUrlString; - // Get an array with the IP addresses for the host. - try { - // Get an array with all the IP addresses for the domain. - InetAddress[] inetAddressesArray = InetAddress.getAllByName(domainName[0]); - - // Add each IP address to the string builder. - for (InetAddress inetAddress : inetAddressesArray) { - if (ipAddresses.length() == 0) { // This is the first IP address. - // Add the IP address to the string builder. - ipAddresses.append(inetAddress.getHostAddress()); - } else { // This is not the first IP address. - // Add a line break to the string builder first. - ipAddresses.append("\n"); - - // Add the IP address to the string builder. - ipAddresses.append(inetAddress.getHostAddress()); + // Sanitize the search input and convert it to a search. + try { + encodedUrlString = URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8"); + } catch (UnsupportedEncodingException exception) { + encodedUrlString = ""; } - } - } catch (UnknownHostException exception) { - // Do nothing. - } - - // Return the string. - return ipAddresses.toString(); - } - - // `onPostExecute()` operates on the UI thread. - @Override - protected void onPostExecute(String ipAddresses) { - // Get a handle for the activity. - Activity activity = activityWeakReference.get(); - // Abort if the activity is gone. - if ((activity == null) || activity.isFinishing()) { - return; + // Load the completed search URL. + loadUrl(searchURL + encodedUrlString); + } else if (launchingIntentUriData != null){ // Check to see if the intent contains a new URL. + // Load the URL from the intent. + loadUrl(launchingIntentUriData.toString()); + } else { // The is no URL in the intent. + // Select the homepage based on the proxy through Orbot status. + if (proxyThroughOrbot) { + // Load the Tor homepage. + loadUrl(sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value))); + } else { + // Load the normal homepage. + loadUrl(sharedPreferences.getString("homepage", getString(R.string.homepage_default_value))); + } + } } + } else { // This is not the first tab. + // Apply the domain settings. + applyDomainSettings(nestedScrollWebView, url, false, false); - // Store the IP addresses. - currentHostIpAddresses = ipAddresses; + // Load the URL. + nestedScrollWebView.loadUrl(url, customHeaders); - if (!urlIsLoading) { - checkPinnedMismatch(); + // Display the keyboard if the URL is blank. + if (url.equals("")) { + inputMethodManager.showSoftInput(urlEditText, 0); } - - // Reset the getting IP addresses tracker. - gettingIpAddresses = false; } } } \ No newline at end of file