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=076f4145b83cc2d4ab84a5d3ce0e795374f9c8ce;hp=9cb3a71a4ce61628206eef57e3b0e81a1b438adf;hb=68e889e6e7f692b09240c03b565dbb56251b328a;hpb=65753c63057ebb382a4ff2b14c1cc27460dc15e6 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 9cb3a71a..076f4145 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java @@ -21,9 +21,12 @@ 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.content.ActivityNotFoundException; import android.content.BroadcastReceiver; import android.content.ClipData; import android.content.ClipboardManager; @@ -31,6 +34,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.content.res.Configuration; import android.database.Cursor; import android.graphics.Bitmap; @@ -43,6 +47,7 @@ import android.net.http.SslCertificate; import android.net.http.SslError; import android.os.Build; import android.os.Bundle; +import android.os.Environment; import android.os.Handler; import android.preference.PreferenceManager; import android.print.PrintDocumentAdapter; @@ -70,7 +75,6 @@ import android.text.Editable; import android.text.Spanned; import android.text.TextWatcher; import android.text.style.ForegroundColorSpan; -import android.util.Log; import android.util.Patterns; import android.view.ContextMenu; import android.view.GestureDetector; @@ -85,6 +89,7 @@ import android.view.inputmethod.InputMethodManager; 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; @@ -92,6 +97,7 @@ import android.webkit.WebStorage; import android.webkit.WebView; import android.webkit.WebViewClient; import android.webkit.WebViewDatabase; +import android.widget.ArrayAdapter; import android.widget.CursorAdapter; import android.widget.EditText; import android.widget.FrameLayout; @@ -103,66 +109,72 @@ import android.widget.RadioButton; import android.widget.RelativeLayout; import android.widget.TextView; -import com.stoutner.privacybrowser.BannerAd; import com.stoutner.privacybrowser.BuildConfig; import com.stoutner.privacybrowser.R; -import com.stoutner.privacybrowser.dialogs.AddDomainDialog; +import com.stoutner.privacybrowser.dialogs.AdConsentDialog; import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog; import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog; import com.stoutner.privacybrowser.dialogs.CreateHomeScreenShortcutDialog; import com.stoutner.privacybrowser.dialogs.DownloadImageDialog; +import com.stoutner.privacybrowser.dialogs.DownloadLocationPermissionDialog; import com.stoutner.privacybrowser.dialogs.EditBookmarkDialog; import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDialog; import com.stoutner.privacybrowser.dialogs.HttpAuthenticationDialog; import com.stoutner.privacybrowser.dialogs.PinnedSslCertificateMismatchDialog; import com.stoutner.privacybrowser.dialogs.UrlHistoryDialog; import com.stoutner.privacybrowser.dialogs.ViewSslCertificateDialog; +import com.stoutner.privacybrowser.helpers.AdHelper; +import com.stoutner.privacybrowser.helpers.BlockListHelper; import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper; 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 java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; -import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLDecoder; import java.net.URLEncoder; +import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; -// We need to use AppCompatActivity from android.support.v7.app.AppCompatActivity to have access to the SupportActionBar until the minimum API is >= 21. -public class MainWebViewActivity extends AppCompatActivity implements AddDomainDialog.AddDomainListener, CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener, CreateHomeScreenShortcutDialog.CreateHomeScreenSchortcutListener, - DownloadFileDialog.DownloadFileListener, DownloadImageDialog.DownloadImageListener, EditBookmarkDialog.EditBookmarkListener, EditBookmarkFolderDialog.EditBookmarkFolderListener, HttpAuthenticationDialog.HttpAuthenticationListener, - NavigationView.OnNavigationItemSelectedListener, PinnedSslCertificateMismatchDialog.PinnedSslCertificateMismatchListener, SslCertificateErrorDialog.SslCertificateErrorListener, UrlHistoryDialog.UrlHistoryListener { +// 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, PinnedSslCertificateMismatchDialog.PinnedSslCertificateMismatchListener, + SslCertificateErrorDialog.SslCertificateErrorListener, UrlHistoryDialog.UrlHistoryListener { - // `darkTheme` is public static so it can be accessed from `AboutActivity`, `GuideActivity`, `AddDomainDialog`, `SettingsActivity`, `DomainsActivity`, `DomainsListFragment`, `BookmarksActivity`, `BookmarksDatabaseViewActivity`, - // `CreateBookmarkDialog`, `CreateBookmarkFolderDialog`, `DownloadFileDialog`, `DownloadImageDialog`, `EditBookmarkDialog`, `EditBookmarkFolderDialog`, `EditBookmarkDatabaseViewDialog`, `HttpAuthenticationDialog`, `MoveToFolderDialog`, - // `SslCertificateErrorDialog`, `UrlHistoryDialog`, `ViewSslCertificateDialog`, `CreateHomeScreenShortcutDialog`, and `OrbotProxyHelper`. It is also used in `onCreate()`, `applyAppSettings()`, `applyDomainSettings()`, and `updatePrivacyIcons()`. + // `darkTheme` is public static so it can be accessed from everywhere. public static boolean darkTheme; - // `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()`. + // `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; // `formattedUrlString` is public static so it can be accessed from `BookmarksActivity`, `CreateBookmarkDialog`, and `AddDomainDialog`. // It is also used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onCreateHomeScreenShortcutCreate()`, and `loadUrlFromTextBox()`. public static String formattedUrlString; - // `sslCertificate` is public static so it can be accessed from `DomainsActivity`, `DomainsListFragment`, `DomainSettingsFragment`, `PinnedSslCertificateMismatchDialog`, and `ViewSslCertificateDialog`. It is also used in `onCreate()`. + // `sslCertificate` is public static so it can be accessed from `DomainsActivity`, `DomainsListFragment`, `DomainSettingsFragment`, `PinnedSslCertificateMismatchDialog`, + // and `ViewSslCertificateDialog`. It is also used in `onCreate()`. public static SslCertificate sslCertificate; - // `orbotStatus` is public static so it can be accessed from `OrbotProxyHelper`. It is also used in `onCreate()`. + // `orbotStatus` is public static so it can be accessed from `OrbotProxyHelper`. It is also used in `onCreate()` and `onResume()`. public static String orbotStatus; // `webViewTitle` is public static so it can be accessed from `CreateBookmarkDialog` and `CreateHomeScreenShortcutDialog`. It is also used in `onCreate()`. @@ -171,23 +183,75 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD // `appliedUserAgentString` is public static so it can be accessed from `ViewSourceActivity`. It is also used in `applyDomainSettings()`. public static String appliedUserAgentString; - // `displayWebpageImagesBoolean` is public static so it can be accessed from `DomainSettingsFragment`. It is also used in `applyAppSettings()` and `applyDomainSettings()`. - public static boolean displayWebpageImagesBoolean; - // `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`. It is also used in `onRestart()`. + // `reloadUrlOnRestart` is public static so it can be accessed from `SettingsFragment` and `BookmarksActivity`. It is also used in `onRestart()`. public static boolean loadUrlOnRestart; // `restartFromBookmarksActivity` is public static so it can be accessed from `BookmarksActivity`. It is also used in `onRestart()`. public static boolean restartFromBookmarksActivity; - // `easyListVersion` is public static so it can be accessed from `AboutTabFragment`. It is also used in `onCreate()`. + // The block list versions are public static so they can be accessed from `AboutTabFragment`. They are also used in `onCreate()`. public static String easyListVersion; - - // `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 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; + int blockedRequests; + int easyListBlockedRequests; + int easyPrivacyBlockedRequests; + int fanboysAnnoyanceListBlockedRequests; + int fanboysSocialBlockingListBlockedRequests; + int ultraPrivacyBlockedRequests; + 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 `PinnedSslCertificateMismatchDialog`. It is also used in `onCreate()`, `onOptionsItemSelected()`, and `applyDomainSettings()`. @@ -203,6 +267,14 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD public static Date pinnedDomainSslStartDate; public static Date pinnedDomainSslEndDate; + // The user agent constants are public static so they can be accessed from `SettingsFragment`, `DomainsActivity`, and `DomainSettingsFragment`. + public final static int UNRECOGNIZED_USER_AGENT = -1; + public final static int SETTINGS_WEBVIEW_DEFAULT_USER_AGENT = 1; + public final static int SETTINGS_CUSTOM_USER_AGENT = 12; + public final static int DOMAINS_SYSTEM_DEFAULT_USER_AGENT = 0; + public final static int DOMAINS_WEBVIEW_DEFAULT_USER_AGENT = 2; + public final static int DOMAINS_CUSTOM_USER_AGENT = 13; + // `appBar` is used in `onCreate()`, `onOptionsItemSelected()`, `closeFindOnPage()`, and `applyAppSettings()`. private ActionBar appBar; @@ -219,14 +291,14 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD // `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 `setDisplayWebpageImages()`. + // `mainWebView` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`, + // `findNextOnPage()`, `closeFindOnPage()`, `loadUrlFromTextBox()`, `onSslMismatchBack()`, and `setDisplayWebpageImages()`. private WebView mainWebView; // `fullScreenVideoFrameLayout` is used in `onCreate()` and `onConfigurationChanged()`. private FrameLayout fullScreenVideoFrameLayout; - // `swipeRefreshLayout` is used in `onCreate()`, `onPrepareOptionsMenu`, and `onRestart()`. + // `swipeRefreshLayout` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `onRestart()`. private SwipeRefreshLayout swipeRefreshLayout; // `urlAppBarRelativeLayout` is used in `onCreate()` and `applyDomainSettings()`. @@ -253,14 +325,14 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD // `domStorageEnabled` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyDomainSettings()`. private boolean domStorageEnabled; - // `saveFormDataEnabled` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyDomainSettings()`. + // `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()` and `applyDomainSettings()`. private boolean nightMode; - // `swipeToRefreshEnabled` is used in `onPrepareOptionsMenu()` and `applyAppSettings()`. - private boolean swipeToRefreshEnabled; + // `displayWebpageImagesBoolean` is used in `applyAppSettings()` and `applyDomainSettings()`. + private boolean displayWebpageImagesBoolean; // 'homepage' is used in `onCreate()`, `onNavigationItemSelected()`, and `applyAppSettings()`. private String homepage; @@ -268,12 +340,34 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD // `searchURL` is used in `loadURLFromTextBox()` and `applyAppSettings()`. private String searchURL; - // `adBlockerEnabled` is used in `onCreate()` and `applyAppSettings()`. - private boolean adBlockerEnabled; + // `mainMenu` is used in `onCreateOptionsMenu()` and `updatePrivacyIcons()`. + private Menu mainMenu; + + // `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 blockAllThirdParyRequestsMenuItem; + + // 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; // `privacyBrowserRuntime` is used in `onCreate()`, `onOptionsItemSelected()`, and `applyAppSettings()`. private Runtime privacyBrowserRuntime; + // `proxyThroughOrbot` is used in `onRestart()` and `applyAppSettings()`. + private boolean proxyThroughOrbot; + // `incognitoModeEnabled` is used in `onCreate()` and `applyAppSettings()`. private boolean incognitoModeEnabled; @@ -292,13 +386,22 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD // `reapplyDomainSettingsOnRestart` is used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, and `onAddDomain()`, . private boolean reapplyDomainSettingsOnRestart; + // `reapplyAppSettingsOnRestart` is used in `onNavigationItemSelected()` and `onRestart()`. + private boolean reapplyAppSettingsOnRestart; + + // `displayingFullScreenVideo` is used in `onCreate()` and `onResume()`. + private boolean displayingFullScreenVideo; + // `currentDomainName` is used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onAddDomain()`, and `applyDomainSettings()`. private String currentDomainName; // `ignorePinnedSslCertificateForDomain` is used in `onCreate()`, `onSslMismatchProceed()`, and `applyDomainSettings()`. private boolean ignorePinnedSslCertificate; - // `waitingForOrbot` is used in `onCreate()` and `applyAppSettings()`. + // `orbotStatusBroadcastReciever` is used in `onCreate()` and `onDestroy()`. + private BroadcastReceiver orbotStatusBroadcastReceiver; + + // `waitingForOrbot` is used in `onCreate()`, `onResume()`, and `applyAppSettings()`. private boolean waitingForOrbot; // `domainSettingsApplied` is used in `prepareOptionsMenu()`, `applyDomainSettings()`, and `setDisplayWebpageImages()`. @@ -322,8 +425,8 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD // `findOnPageEditText` is used in `onCreate()`, `onOptionsItemSelected()`, and `closeFindOnPage()`. private EditText findOnPageEditText; - // `mainMenu` is used in `onCreateOptionsMenu()` and `updatePrivacyIcons()`. - private Menu mainMenu; + // `displayAdditionalAppBarIcons` is used in `onCreate()` and `onCreateOptionsMenu()`. + private boolean displayAdditionalAppBarIcons; // `drawerToggle` is used in `onCreate()`, `onPostCreate()`, `onConfigurationChanged()`, `onNewIntent()`, and `onNavigationItemSelected()`. private ActionBarDrawerToggle drawerToggle; @@ -339,9 +442,6 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD private ForegroundColorSpan initialGrayColorSpan; private ForegroundColorSpan finalGrayColorSpan; - // `adView` is used in `onCreate()` and `onConfigurationChanged()`. - private View adView; - // `sslErrorHandler` is used in `onCreate()`, `onSslErrorCancel()`, and `onSslErrorProceed`. private SslErrorHandler sslErrorHandler; @@ -354,7 +454,7 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD // `mainWebViewRelativeLayout` is used in `onCreate()` and `onNavigationItemSelected()`. private RelativeLayout mainWebViewRelativeLayout; - // `urlIsLoading` is used in `onCreate()`, `loadUrl()`, and `applyDomainSettings()`. + // `urlIsLoading` is used in `onCreate()`, `onCreateOptionsMenu()`, `loadUrl()`, and `applyDomainSettings()`. private boolean urlIsLoading; // `pinnedDomainSslCertificate` is used in `onCreate()` and `applyDomainSettings()`. @@ -378,17 +478,43 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD // `oldFolderNameString` is used in `onCreate()` and `onSaveEditBookmarkFolder()`. private String oldFolderNameString; + // `fileChooserCallback` is used in `onCreate()` and `onActivityResult()`. + private ValueCallback fileChooserCallback; + + // The download strings are used in `onCreate()` and `onRequestPermissionResult()`. + private String downloadUrl; + private String downloadContentDisposition; + private long downloadContentLength; + + // `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()`. + 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. - @SuppressLint({"SetJavaScriptEnabled"}) + // 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. We have to use the deprecated `getColor()` until API >= 23. @SuppressWarnings("deprecation") protected void onCreate(Bundle savedInstanceState) { - // Get a handle for `sharedPreferences`. `this` references the current context. + // Get a handle for the shared preferences. SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - // Get the theme preference. + // Get the theme and screenshot preferences. darkTheme = sharedPreferences.getBoolean("dark_theme", false); + allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false); + + // Disable screenshots if not allowed. + if (!allowScreenshots) { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE); + } // Set the activity theme. if (darkTheme) { @@ -400,52 +526,6 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD // Run the default commands. super.onCreate(savedInstanceState); - // **DEBUG** Log the beginning of the loading of the ad blocker. - Log.i("AdBlocker", "Begin loading ad blocker"); - - // Initialize `adServerSet`. - final Set adServersSet = new HashSet<>(); - - // Load the list of ad servers into memory. - try { - // Load `easylist.txt` into a `BufferedReader`. - BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(getAssets().open("easylist.txt"))); - - // Create a string for storing each ad server. - String adBlockerEntry; - - // Populate `adServersSet`. - while ((adBlockerEntry = bufferedReader.readLine()) != null) { - //noinspection StatementWithEmptyBody - if (adBlockerEntry.contains("##") || adBlockerEntry.contains("#?#") || adBlockerEntry.contains("#@#") || adBlockerEntry.startsWith("[")) { - // Entries that contain `##`, `#?#`, and `#@#` are for hiding elements in the main page's HTML. Entries that start with `[` describe the AdBlock compatibility level. - - // Do nothing. Privacy Browser does not currently use these entries. - - // **DEBUG** Log the entries that are not added. - // Log.i("AdBlocker", "Not added: " + adBlockerEntry); - } else if (adBlockerEntry.startsWith("!")){ // Entries that begin with `!` are comments. - if (adBlockerEntry.startsWith("! Version:")) { - // Store the EasyList version number. - easyListVersion = adBlockerEntry.substring(11); - } - - // **DEBUG** Log the entries that are not added. - // Log.i("AdBlocker", "Not added: " + adBlockerEntry); - } else { - adServersSet.add(adBlockerEntry); - } - } - - // Close `bufferedReader`. - bufferedReader.close(); - } catch (IOException e) { - // The asset exists, so the `IOException` will never be thrown. - } - - // **DEBUG** Log the finishing of the loading of the ad blocker. - Log.i("AdBlocker", "Finish loading ad blocker"); - // Set the content view. setContentView(R.layout.main_drawerlayout); @@ -512,7 +592,7 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD waitingForOrbot = false; // Create an Orbot status `BroadcastReceiver`. - BroadcastReceiver orbotStatusBroadcastReceiver = new BroadcastReceiver() { + orbotStatusBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { // Store the content of the status message in `orbotStatus`. @@ -573,14 +653,14 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD startActivity(bookmarksIntent); }); - // Set the create new bookmark folder FAB to display the `AlertDialog`. + // 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(getSupportFragmentManager(), getResources().getString(R.string.create_folder)); }); - // Set the create new bookmark FAB to display the `AlertDialog`. + // 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(); @@ -600,9 +680,10 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD // Hide the `appBar`. appBar.hide(); - // Hide the `BannerAd` in the free flavor. + // Hide the banner ad in the free flavor. if (BuildConfig.FLAVOR.contentEquals("free")) { - BannerAd.hideAd(adView); + // 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)); } // Modify the system bars. @@ -615,7 +696,7 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD /* SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen. * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen. - * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically rehides them after they are shown. + * 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); @@ -625,7 +706,8 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD // Set `rootCoordinatorLayout` to fit under the status and navigation bars. rootCoordinatorLayout.setFitsSystemWindows(false); - if (translucentNavigationBarOnFullscreen) { // There is an Android Support Library bug that causes a scrim to print on the right side of the `Drawer Layout` when the navigation bar is displayed on the right of the screen. + // 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); } @@ -636,11 +718,8 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD // Show the `BannerAd` in the free flavor. if (BuildConfig.FLAVOR.contentEquals("free")) { - // Reload the ad. Because the screen may have rotated, we need to use `reloadAfterRotate`. - BannerAd.reloadAfterRotate(adView, getApplicationContext(), getString(R.string.ad_id)); - - // Reinitialize the `adView` variable, as the `View` will have been removed and re-added by `BannerAd.reloadAfterRotate()`. - adView = findViewById(R.id.adview); + // 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_id)); } // Remove the translucent navigation bar flag if it is set. @@ -747,6 +826,7 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD 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. `this` specifies the context. The two `nulls` do not specify the database name or a `CursorFactory`. // The `0` specifies a database version, but that is ignored and set instead using a constant in `BookmarksDatabaseHelper`. @@ -762,7 +842,7 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD // Convert the id from long to int to match the format of the bookmarks database. int databaseID = (int) id; - // Get the bookmark `Cursor` for this ID and move it to the first row. + // Get the bookmark cursor for this ID and move it to the first row. Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmarkCursor(databaseID); bookmarkCursor.moveToFirst(); @@ -809,32 +889,33 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD return true; }); - // The `DrawerListener` allows us to update the Navigation Menu. + // The drawer listener is used to update the navigation menu. drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() { @Override - public void onDrawerSlide(View drawerView, float slideOffset) { + public void onDrawerSlide(@NonNull View drawerView, float slideOffset) { } @Override - public void onDrawerOpened(View drawerView) { + public void onDrawerOpened(@NonNull View drawerView) { } @Override - public void onDrawerClosed(View drawerView) { + public void onDrawerClosed(@NonNull View drawerView) { } @Override public void onDrawerStateChanged(int newState) { - if ((newState == DrawerLayout.STATE_SETTLING) || (newState == DrawerLayout.STATE_DRAGGING)) { // The drawer is opening or closing. - // Update the `Back`, `Forward`, and `History` menu items. + if ((newState == DrawerLayout.STATE_SETTLING) || (newState == DrawerLayout.STATE_DRAGGING)) { // A drawer is opening or closing. + // 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); - // Hide the keyboard (if displayed) so we can see the navigation menu. `0` indicates no additional flags. + // Hide the keyboard (if displayed). inputMethodManager.hideSoftInputFromWindow(mainWebView.getWindowToken(), 0); - // Clear the focus from `urlTextBox` if it has it. + // Clear the focus from from the URL text box. urlTextBox.clearFocus(); } } @@ -843,86 +924,602 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD // drawerToggle creates the hamburger icon at the start of the AppBar. drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, supportAppBar, R.string.open_navigation_drawer, R.string.close_navigation_drawer); + // Get a handle for the progress bar. + final ProgressBar progressBar = findViewById(R.id.progress_bar); + + 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. `!important` takes precedent over any existing sub-settings. + mainWebView.evaluateJavascript("(function() {var parent = document.getElementsByTagName('head').item(0); var style = document.createElement('style'); style.type = 'text/css'; " + + "style.innerHTML = '* {background-color: #212121 !important; color: #BDBDBD !important; box-shadow: none !important; text-decoration: none !important;" + + "text-shadow: none !important; border: none !important;} a {color: #1565C0 !important;}'; parent.appendChild(style)})()", 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 one. This prevents the display of the `WebView` while it is still loading. + if (progressBar.getVisibility() == View.GONE) { + mainWebView.setVisibility(View.VISIBLE); + } + }; + + // Use `displayWebViewHandler` to delay the displaying of `mainWebView` for 500 milliseconds. + displayWebViewHandler.postDelayed(displayWebViewRunnable, 500); + }); + } + + // Update the progress bar. + progressBar.setProgress(progress); + + // Set the visibility of the progress bar. + if (progress < 100) { + // Show the progress bar. + progressBar.setVisibility(View.VISIBLE); + } else { + // Hide the progress bar. + progressBar.setVisibility(View.GONE); + + // 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); + } + + //Stop the swipe to refresh indicator if it is running + swipeRefreshLayout.setRefreshing(false); + } + } + + // Set the favorite icon when it changes. + @Override + public void onReceivedIcon(WebView view, Bitmap icon) { + // Only update the favorite icon if the website has finished loading. + if (progressBar.getVisibility() == View.GONE) { + // Save a copy of the favorite icon. + favoriteIconBitmap = icon; + + // Place the favorite icon in the appBar. + favoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(icon, 64, 64, true)); + } + } + + // 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; + } + + // Enter full screen video. + @Override + public void onShowCustomView(View view, CustomViewCallback callback) { + // Set the full screen video flag. + displayingFullScreenVideo = true; + + // Pause the ad if this is the free flavor. + if (BuildConfig.FLAVOR.contentEquals("free")) { + // The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations. + AdHelper.pauseAd(findViewById(R.id.adview)); + } + + // 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 entire screen. + rootCoordinatorLayout.setFitsSystemWindows(false); + + // Disable the sliding drawers. + drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); + + // Add `view` to `fullScreenVideoFrameLayout` and display it on the screen. + fullScreenVideoFrameLayout.addView(view); + fullScreenVideoFrameLayout.setVisibility(View.VISIBLE); + } + + // 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); + + // 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); + + // Remove the translucent status bar overlay. + 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); + } else { // Hide everything except the status and navigation bars. + // Remove any `SYSTEM_UI` flags from `rootCoordinatorLayout`. + rootCoordinatorLayout.setSystemUiVisibility(0); + + // 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. 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(); + } + + // 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.ad_id)); + } + + // Remove any `SYSTEM_UI` flags from `rootCoordinatorLayout`. + rootCoordinatorLayout.setSystemUiVisibility(0); + + // Remove the translucent navigation bar flag if it is set. + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + + // 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); + + // Constrain `rootCoordinatorLayout` inside the status and navigation bars. + rootCoordinatorLayout.setFitsSystemWindows(true); + } + + // 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_id)); + } + } + + // 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; + } + }); + + // Register `mainWebView` for a context menu. This is used to see link targets and download images. + registerForContextMenu(mainWebView); + + // Allow the downloading of files. + mainWebView.setDownloadListener((String url, String userAgent, String contentDisposition, String mimetype, long contentLength) -> { + // 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 WRITE_EXTERNAL_STORAGE permission needs to be requested. + + // Store the variables for future use by `onRequestPermissionsResult()`. + downloadUrl = url; + 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. + // Get a handle for 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(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 WRITE_EXTERNAL_STORAGE permission has already been granted. + // Get a handle for the download file alert dialog. + AppCompatDialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(url, contentDisposition, contentLength); + + // Show the download file alert dialog. + downloadFileDialogFragment.show(getSupportFragmentManager(), getString(R.string.download)); + } + }); + + // Allow pinch to zoom. + mainWebView.getSettings().setBuiltInZoomControls(true); + + // Hide zoom controls. + mainWebView.getSettings().setDisplayZoomControls(false); + + // Set `mainWebView` to use a wide viewport. Otherwise, some web pages will be scrunched and some content will render outside the screen. + mainWebView.getSettings().setUseWideViewPort(true); + + // Set `mainWebView` to load in overview mode (zoomed out to the maximum width). + mainWebView.getSettings().setLoadWithOverviewMode(true); + + // Explicitly disable geolocation. + mainWebView.getSettings().setGeolocationEnabled(false); + + // Initialize cookieManager. + cookieManager = CookieManager.getInstance(); + + // 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", ""); + + // 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); + + // Get the intent that started the app. + final Intent launchingIntent = getIntent(); + + // Extract the launching intent data as `launchingIntentUriData`. + final Uri launchingIntentUriData = launchingIntent.getData(); + + // Convert the launching intent URI data (if it exists) to a string and store it in `formattedUrlString`. + if (launchingIntentUriData != null) { + formattedUrlString = launchingIntentUriData.toString(); + } + + // Get a handle for the `Runtime`. + privacyBrowserRuntime = Runtime.getRuntime(); + + // 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`. + + // Initialize `inFullScreenBrowsingMode`, which is always false at this point because Privacy Browser never starts in full screen browsing mode. + inFullScreenBrowsingMode = false; + + // 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; + + // Initialize the WebView title. + webViewTitle = getString(R.string.no_title); + + // 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(); + + // If the favorite icon is null, load the default. + if (favoriteIconBitmap == null) { + favoriteIconBitmap = favoriteIconDefaultBitmap; + } + + // Initialize the user agent array adapter and string array. + userAgentNamesArray = ArrayAdapter.createFromResource(this, R.array.user_agent_names, R.layout.domain_settings_spinner_item); + userAgentDataArray = getResources().getStringArray(R.array.user_agent_data); + + // Apply the app settings from the shared preferences. + applyAppSettings(); + + // Instantiate the block list helper. + BlockListHelper blockListHelper = new BlockListHelper(); + + // Initialize the list of resource requests. + resourceRequests = new ArrayList<>(); + + // 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; + mainWebView.setWebViewClient(new WebViewClient() { // `shouldOverrideUrlLoading` makes this `WebView` the default handler for URLs inside the app, so that links are not kicked out to other apps. - // We have to use the deprecated `shouldOverrideUrlLoading` until API >= 24. + // The deprecated `shouldOverrideUrlLoading` must be used until API >= 24. @SuppressWarnings("deprecation") @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { - if (url.startsWith("mailto:")) { // Load the email address in an external email program. + 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 = ""; + + // Apply the domain settings for the new URL. `applyDomainSettings` doesn't do anything if the domain has not changed. + applyDomainSettings(url, true, false); + + // 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); - // Parse the url and set it as the data for the `Intent`. + // Parse the url and set it as the data for the intent. emailIntent.setData(Uri.parse(url)); - // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser. + // Open the email program in a new task instead of as part of Privacy Browser. emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // Make it so. startActivity(emailIntent); - // Returning `true` indicates the application is handling the URL. + // 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. - // `ACTION_DIAL` open the dialer and loads the phone number, but waits for the user to place the call. + // Open the dialer and load the phone number, but wait for the user to place the call. Intent dialIntent = new Intent(Intent.ACTION_DIAL); // Add the phone number to the intent. dialIntent.setData(Uri.parse(url)); - // `FLAG_ACTIVITY_NEW_TASK` opens the dialer in a new task instead as part of Privacy Browser. + // Open the dialer in a new task instead of as part of Privacy Browser. dialIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // Make it so. startActivity(dialIntent); - // Returning `true` indicates the application is handling the URL. + // Returning true indicates Privacy Browser is handling the URL by creating an intent. return true; - } else { // Load the URL in Privacy Browser. - // Apply the domain settings for the new URL. - applyDomainSettings(url); + } 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); - // Returning `false` causes the current `WebView` to handle the URL and prevents it from adding redirects to the history list. - return false; + // Add the URL to the intent. + genericIntent.setData(Uri.parse(url)); + + // List all apps that can handle the URL instead of just opening the first one. + genericIntent.addCategory(Intent.CATEGORY_BROWSABLE); + + // Open the app in a new task instead of as part of Privacy Browser. + genericIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + // 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(); + } + + // Returning true indicates Privacy Browser is handling the URL by creating an intent. + return true; } } - // Block ads. We have to use the deprecated `shouldInterceptRequest` until minimum API >= 21. + // 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){ - if (adBlockerEnabled) { // Block ads. - // Extract the host from `url`. - Uri requestUri = Uri.parse(url); - String requestHost = requestUri.getHost(); - - // Initialize a variable to track if this is an ad server. - boolean requestHostIsAdServer = false; - - // Check all the subdomains of `requestHost` if it is not `null` against the ad server database. - if (requestHost != null) { - while (requestHost.contains(".") && !requestHostIsAdServer) { // Stop checking if we run out of `.` or if we already know that `requestHostIsAdServer` is `true`. - if (adServersSet.contains(requestHost)) { - requestHostIsAdServer = true; - } + // 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())); - // Strip out the lowest subdomain of `requestHost`. - requestHost = requestHost.substring(requestHost.indexOf(".") + 1); + // Reset the whitelist results tracker. + whiteListResultStringArray = null; + + // Initialize the third party request tracker. + boolean isThirdPartyRequest = false; + + // Initialize the current domain string. + String currentDomain = ""; + + // 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 domain host names. + String currentBaseDomain = currentDomainUri.getHost(); + String requestBaseDomain = requestDomainUri.getHost(); + + // Update the current domain variable. + currentDomain = currentBaseDomain; + + // 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); + } + + // 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); } + + // Update the third party request tracker. + isThirdPartyRequest = !currentBaseDomain.equals(requestBaseDomain); } + } + + // 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); + blockAllThirdParyRequestsMenuItem.setTitle(thirdPartyBlockedRequests + " - " + getString(R.string.block_all_third_party_requests)); + }); - if (requestHostIsAdServer) { // It is an ad server. - // Return an empty `WebResourceResponse`. - return new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes())); - } else { // It is not an ad server. - // `return null` loads the requested resource. + // Add the request to the log. + resourceRequests.add(new String[]{String.valueOf(REQUEST_THIRD_PARTY), url}); + + // Return an empty web resource response. + return emptyWebResourceResponse; + } + + // Check UltraPrivacy if it is enabled. + if (ultraPrivacyEnabled) { + if (blockListHelper.isBlocked(currentDomain, url, isThirdPartyRequest, ultraPrivacy)) { + // Increment the blocked requests counters. + blockedRequests++; + ultraPrivacyBlockedRequests++; + + // 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)); + }); + + // The resource request was blocked. Return an empty web resource response. + return emptyWebResourceResponse; + } + + // 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); + + // The resource request has been allowed by UltraPrivacy. `return null` loads the requested resource. return null; } - } else { // Ad blocking is disabled. - // `return null` loads the requested resource. - return null; } + + // Check EasyList if it is enabled. + if (easyListEnabled) { + if (blockListHelper.isBlocked(currentDomain, url, isThirdPartyRequest, easyList)) { + // Increment the blocked requests counters. + blockedRequests++; + easyListBlockedRequests++; + + // 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)); + }); + + // Reset the whitelist results tracker (because otherwise it will sometimes add results to the list due to a race condition). + whiteListResultStringArray = null; + + // The resource request was blocked. Return an empty web resource response. + return emptyWebResourceResponse; + } + } + + // Check EasyPrivacy if it is enabled. + if (easyPrivacyEnabled) { + if (blockListHelper.isBlocked(currentDomain, url, isThirdPartyRequest, easyPrivacy)) { + // Increment the blocked requests counters. + blockedRequests++; + easyPrivacyBlockedRequests++; + + // 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)); + }); + + // Reset the whitelist results tracker (because otherwise it will sometimes add results to the list due to a race condition). + whiteListResultStringArray = null; + + // The resource request was blocked. Return an empty web resource response. + return emptyWebResourceResponse; + } + } + + // 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++; + + // 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)); + }); + + // Reset the whitelist results tracker (because otherwise it will sometimes add results to the list due to a race condition). + whiteListResultStringArray = null; + + // 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++; + + // 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)); + }); + + // Reset the whitelist results tracker (because otherwise it will sometimes add results to the list due to a race condition). + whiteListResultStringArray = null; + + // The resource request was blocked. Return an empty web resource response. + return emptyWebResourceResponse; + } + } + + // 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 defult request. + resourceRequests.add(new String[]{String.valueOf(REQUEST_DEFAULT), url}); + } + + // The resource request has not been blocked. `return null` loads the requested resource. + return null; } // Handle HTTP authentication requests. @@ -939,6 +1536,18 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD // Update the URL in urlTextBox when the page starts to load. @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { + // Reset the list of resource requests. + resourceRequests.clear(); + + // Initialize the counters for requests blocked by each blocklist. + blockedRequests = 0; + easyListBlockedRequests = 0; + easyPrivacyBlockedRequests = 0; + fanboysAnnoyanceListBlockedRequests = 0; + fanboysSocialBlockingListBlockedRequests = 0; + ultraPrivacyBlockedRequests = 0; + thirdPartyBlockedRequests = 0; + // If night mode is enabled, hide `mainWebView` until after the night mode CSS is applied. if (nightMode) { mainWebView.setVisibility(View.INVISIBLE); @@ -947,7 +1556,7 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD // Hide the keyboard. `0` indicates no additional flags. inputMethodManager.hideSoftInputFromWindow(mainWebView.getWindowToken(), 0); - // Check to see if we are waiting on Orbot. + // Check to see if Privacy Browser is waiting on Orbot. if (!waitingForOrbot) { // We are not waiting on Orbot, so we need to process the URL. // We need to update `formattedUrlString` at the beginning of the load, so that if the user toggles JavaScript during the load the new website is reloaded. formattedUrlString = url; @@ -960,17 +1569,56 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD // Apply any custom domain settings if the URL was loaded by navigating history. if (navigatingHistory) { - applyDomainSettings(url); + // Reset `navigatingHistory`. + navigatingHistory = false; + + // Apply the domain settings. + applyDomainSettings(url, true, false); } // Set `urlIsLoading` to `true`, so that redirects while loading do not trigger changes in the user agent, which forces another reload of the existing page. urlIsLoading = true; + + // 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); + + // 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); + } + } + } } } // 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) { + // 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(); + } + + // Update the Refresh menu item if it has been created. + if (refreshMenuItem != null) { + // Reset the Refresh title. + refreshMenuItem.setTitle(R.string.refresh); + + // 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); + } + } + } + // Reset `urlIsLoading`, which is used to prevent reloads on redirect if the user agent changes. urlIsLoading = false; @@ -984,11 +1632,12 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD // Manually delete cache folders. try { - // Delete the main `cache` folder. + // Delete the main cache directory. privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/cache"); - // Delete the `app_webview` folder, which contains an additional `WebView` cache. See `https://code.google.com/p/android/issues/detail?id=233826&thanks=233826&ts=1486670530`. - privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview"); + // 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. } @@ -1010,7 +1659,7 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD inputMethodManager.showSoftInput(urlTextBox, 0); // Apply the domain settings. This clears any settings from the previous domain. - applyDomainSettings(formattedUrlString); + applyDomainSettings(formattedUrlString, true, false); } else { // `WebView` has loaded a webpage. // Set `formattedUrlString`. formattedUrlString = url; @@ -1077,9 +1726,11 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD } // Check to see if the pinned SSL certificate matches the current website certificate. - if (!currentWebsiteIssuedToCName.equals(pinnedDomainSslIssuedToCNameString) || !currentWebsiteIssuedToOName.equals(pinnedDomainSslIssuedToONameString) || !currentWebsiteIssuedToUName.equals(pinnedDomainSslIssuedToUNameString) || - !currentWebsiteIssuedByCName.equals(pinnedDomainSslIssuedByCNameString) || !currentWebsiteIssuedByOName.equals(pinnedDomainSslIssuedByONameString) || !currentWebsiteIssuedByUName.equals(pinnedDomainSslIssuedByUNameString) || - !currentWebsiteSslStartDateString.equals(pinnedDomainSslStartDateString) || !currentWebsiteSslEndDateString.equals(pinnedDomainSslEndDateString)) { // The pinned SSL certificate doesn't match the current domain certificate. + if (!currentWebsiteIssuedToCName.equals(pinnedDomainSslIssuedToCNameString) || !currentWebsiteIssuedToOName.equals(pinnedDomainSslIssuedToONameString) || + !currentWebsiteIssuedToUName.equals(pinnedDomainSslIssuedToUNameString) || !currentWebsiteIssuedByCName.equals(pinnedDomainSslIssuedByCNameString) || + !currentWebsiteIssuedByOName.equals(pinnedDomainSslIssuedByONameString) || !currentWebsiteIssuedByUName.equals(pinnedDomainSslIssuedByUNameString) || + !currentWebsiteSslStartDateString.equals(pinnedDomainSslStartDateString) || !currentWebsiteSslEndDateString.equals(pinnedDomainSslEndDateString)) { + // The pinned SSL certificate doesn't match the current domain certificate. //Display the pinned SSL certificate mismatch `AlertDialog`. AppCompatDialogFragment pinnedSslCertificateMismatchDialogFragment = new PinnedSslCertificateMismatchDialog(); pinnedSslCertificateMismatchDialogFragment.show(getSupportFragmentManager(), getString(R.string.ssl_certificate_mismatch)); @@ -1106,9 +1757,11 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD // Proceed to the website if the current SSL website certificate matches the pinned domain certificate. if (pinnedDomainSslCertificate && - currentWebsiteIssuedToCName.equals(pinnedDomainSslIssuedToCNameString) && currentWebsiteIssuedToOName.equals(pinnedDomainSslIssuedToONameString) && currentWebsiteIssuedToUName.equals(pinnedDomainSslIssuedToUNameString) && - currentWebsiteIssuedByCName.equals(pinnedDomainSslIssuedByCNameString) && currentWebsiteIssuedByOName.equals(pinnedDomainSslIssuedByONameString) && currentWebsiteIssuedByUName.equals(pinnedDomainSslIssuedByUNameString) && - currentWebsiteSslStartDate.equals(pinnedDomainSslStartDate) && currentWebsiteSslEndDate.equals(pinnedDomainSslEndDate)) { // An SSL certificate is pinned and matches the current domain certificate. + currentWebsiteIssuedToCName.equals(pinnedDomainSslIssuedToCNameString) && currentWebsiteIssuedToOName.equals(pinnedDomainSslIssuedToONameString) && + currentWebsiteIssuedToUName.equals(pinnedDomainSslIssuedToUNameString) && currentWebsiteIssuedByCName.equals(pinnedDomainSslIssuedByCNameString) && + currentWebsiteIssuedByOName.equals(pinnedDomainSslIssuedByONameString) && currentWebsiteIssuedByUName.equals(pinnedDomainSslIssuedByUNameString) && + currentWebsiteSslStartDate.equals(pinnedDomainSslStartDate) && currentWebsiteSslEndDate.equals(pinnedDomainSslEndDate)) { + // An SSL certificate is pinned and matches the current domain certificate. // Proceed to the website without displaying an error. handler.proceed(); } else { // Either there isn't a pinned SSL certificate or it doesn't match the current website certificate. @@ -1122,213 +1775,7 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD } }); - // Get a handle for the progress bar. - final ProgressBar progressBar = findViewById(R.id.progress_bar); - - 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. `!important` takes precedent over any existing sub-settings. - mainWebView.evaluateJavascript("(function() {var parent = document.getElementsByTagName('head').item(0); var style = document.createElement('style'); style.type = 'text/css'; style.innerHTML = '" + - "* {background-color: #212121 !important; color: #BDBDBD !important; box-shadow: none !important; text-decoration: none !important; text-shadow: none !important; border: none !important;}" + - "a {color: #1565C0 !important;}" + - "'; parent.appendChild(style)})()", 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 one. This prevents the display of the `WebView` while it is still loading. - if (progressBar.getVisibility() == View.GONE) { - mainWebView.setVisibility(View.VISIBLE); - } - }; - - // Use `displayWebViewHandler` to delay the displaying of `mainWebView` for 500 milliseconds. - displayWebViewHandler.postDelayed(displayWebViewRunnable, 500); - }); - } - - // Update the progress bar. - progressBar.setProgress(progress); - - // Set the visibility of the progress bar. - if (progress < 100) { - // Show the progress bar. - progressBar.setVisibility(View.VISIBLE); - } else { - // Hide the progress bar. - progressBar.setVisibility(View.GONE); - - // 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); - } - - //Stop the `SwipeToRefresh` indicator if it is running - swipeRefreshLayout.setRefreshing(false); - } - } - - // Set the favorite icon when it changes. - @Override - public void onReceivedIcon(WebView view, Bitmap icon) { - // Only update the favorite icon if the website has finished loading. - if (progressBar.getVisibility() == View.GONE) { - // Save a copy of the favorite icon. - favoriteIconBitmap = icon; - - // Place the favorite icon in the appBar. - favoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(icon, 64, 64, true)); - } - } - - // 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; - } - - // Enter full screen video - @Override - public void onShowCustomView(View view, CustomViewCallback callback) { - // Pause the ad if this is the free flavor. - if (BuildConfig.FLAVOR.contentEquals("free")) { - BannerAd.pauseAd(adView); - } - - // Remove the translucent overlays. - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); - - // Remove the translucent status bar overlay on the `Drawer Layout`, which is special and needs its own command. - drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); - - /* SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen. - * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen. - * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically rehides them after they are shown. - */ - rootCoordinatorLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); - - // Set `rootCoordinatorLayout` to fill the entire screen. - rootCoordinatorLayout.setFitsSystemWindows(false); - - // Add `view` to `fullScreenVideoFrameLayout` and display it on the screen. - fullScreenVideoFrameLayout.addView(view); - fullScreenVideoFrameLayout.setVisibility(View.VISIBLE); - } - - // Exit full screen video - public void onHideCustomView() { - // Hide `fullScreenVideoFrameLayout`. - fullScreenVideoFrameLayout.removeAllViews(); - fullScreenVideoFrameLayout.setVisibility(View.GONE); - - // Add the translucent status flag. This also resets `drawerLayout's` `View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN`. - getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); - - // Set `rootCoordinatorLayout` to fit inside the status and navigation bars. This also clears the `SYSTEM_UI` flags. - rootCoordinatorLayout.setFitsSystemWindows(true); - - // Show the ad if this is the free flavor. - if (BuildConfig.FLAVOR.contentEquals("free")) { - // Reload the ad. Because the screen may have rotated, we need to use `reloadAfterRotate`. - BannerAd.reloadAfterRotate(adView, getApplicationContext(), getString(R.string.ad_id)); - - // Reinitialize the `adView` variable, as the `View` will have been removed and re-added by `BannerAd.reloadAfterRotate()`. - adView = findViewById(R.id.adview); - } - } - }); - - // Register `mainWebView` for a context menu. This is used to see link targets and download images. - registerForContextMenu(mainWebView); - - // Allow the downloading of files. - mainWebView.setDownloadListener((String url, String userAgent, String contentDisposition, String mimetype, long contentLength) -> { - // Show the `DownloadFileDialog` `AlertDialog` and name this instance `@string/download`. - AppCompatDialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(url, contentDisposition, contentLength); - downloadFileDialogFragment.show(getSupportFragmentManager(), getString(R.string.download)); - }); - - // Allow pinch to zoom. - mainWebView.getSettings().setBuiltInZoomControls(true); - - // Hide zoom controls. - mainWebView.getSettings().setDisplayZoomControls(false); - - // Set `mainWebView` to use a wide viewport. Otherwise, some web pages will be scrunched and some content will render outside the screen. - mainWebView.getSettings().setUseWideViewPort(true); - - // Set `mainWebView` to load in overview mode (zoomed out to the maximum width). - mainWebView.getSettings().setLoadWithOverviewMode(true); - - // Explicitly disable geolocation. - mainWebView.getSettings().setGeolocationEnabled(false); - - // Initialize cookieManager. - cookieManager = CookieManager.getInstance(); - - // 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", ""); - - // Initialize the default preference values the first time the program is run. `this` is the context. `false` keeps this command from resetting any current preferences back to default. - PreferenceManager.setDefaultValues(this, R.xml.preferences, false); - - // Get the intent that started the app. - final Intent launchingIntent = getIntent(); - - // Extract the launching intent data as `launchingIntentUriData`. - final Uri launchingIntentUriData = launchingIntent.getData(); - - // Convert the launching intent URI data (if it exists) to a string and store it in `formattedUrlString`. - if (launchingIntentUriData != null) { - formattedUrlString = launchingIntentUriData.toString(); - } - - // Get a handle for the `Runtime`. - privacyBrowserRuntime = Runtime.getRuntime(); - - // 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`. - - // Initialize `inFullScreenBrowsingMode`, which is always false at this point because Privacy Browser never starts in full screen browsing mode. - inFullScreenBrowsingMode = false; - - // Initialize AdView for the free flavor. - adView = findViewById(R.id.adview); - - // Initialize the privacy settings variables. - javaScriptEnabled = false; - firstPartyCookiesEnabled = false; - thirdPartyCookiesEnabled = false; - domStorageEnabled = false; - saveFormDataEnabled = false; - nightMode = false; - - // Initialize `webViewTitle`. - webViewTitle = getString(R.string.no_title); - - // Initialize `favoriteIconBitmap`. We have to use `ContextCompat` until API >= 21. - Drawable favoriteIconDrawable = ContextCompat.getDrawable(getApplicationContext(), R.drawable.world); - BitmapDrawable favoriteIconBitmapDrawable = (BitmapDrawable) favoriteIconDrawable; - favoriteIconDefaultBitmap = favoriteIconBitmapDrawable.getBitmap(); - - // If the favorite icon is null, load the default. - if (favoriteIconBitmap == null) { - favoriteIconBitmap = favoriteIconDefaultBitmap; - } - - // Apply the app settings from the shared preferences. - applyAppSettings(); - - // Load `formattedUrlString` if we are not waiting for Orbot to connect. + // Load the website if not waiting for Orbot to connect. if (!waitingForOrbot) { loadUrl(formattedUrlString); } @@ -1339,22 +1786,27 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD // 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); + // Check to see if the intent contains a new URL. if (intent.getData() != null) { - // Get the intent data and convert it to a string. + // Get the intent data. final Uri intentUriData = intent.getData(); - formattedUrlString = intentUriData.toString(); - } - // Close the navigation drawer if it is open. - if (drawerLayout.isDrawerVisible(GravityCompat.START)) { - drawerLayout.closeDrawer(GravityCompat.START); - } + // Load the website. + loadUrl(intentUriData.toString()); - // Load the website. - loadUrl(formattedUrlString); + // Close the navigation drawer if it is open. + if (drawerLayout.isDrawerVisible(GravityCompat.START)) { + drawerLayout.closeDrawer(GravityCompat.START); + } - // Clear the keyboard if displayed and remove the focus on the urlTextBar if it has it. - mainWebView.requestFocus(); + // Close the bookmarks drawer if it is open. + if (drawerLayout.isDrawerVisible(GravityCompat.END)) { + drawerLayout.closeDrawer(GravityCompat.END); + } + + // Clear the keyboard if displayed and remove the focus on the urlTextBar if it has it. + mainWebView.requestFocus(); + } } @Override @@ -1362,31 +1814,43 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD // Run the default commands. super.onRestart(); - // Apply the app settings, which may have been changed in `SettingsActivity`. - applyAppSettings(); + // 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"); - // Apply the domain settings if returning from the Domains Activity. - if (reapplyDomainSettingsOnRestart) { - // Reset `reapplyDomainSettingsOnRestart`. - reapplyDomainSettingsOnRestart = false; + // Send the intent to the Orbot package. + orbotIntent.setPackage("org.torproject.android"); - // Reapply the domain settings. - applyDomainSettings(formattedUrlString); + // Make it so. + sendBroadcast(orbotIntent); } - // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. - updatePrivacyIcons(true); + // Apply the app settings if returning from the Settings activity.. + if (reapplyAppSettingsOnRestart) { + // Apply the app settings. + applyAppSettings(); + + // Reload the webpage if displaying of images has been disabled in the Settings activity. + if (reloadOnRestart) { + // Reload `mainWebView`. + mainWebView.reload(); - // Set the display webpage images mode. - setDisplayWebpageImages(); + // Reset `reloadOnRestartBoolean`. + reloadOnRestart = false; + } - // Reload the webpage if displaying of images has been disabled in `SettingsFragment`. - if (reloadOnRestart) { - // Reload `mainWebView`. - mainWebView.reload(); + // Reset the return from settings flag. + reapplyAppSettingsOnRestart = false; + } - // Reset `reloadOnRestartBoolean`. - reloadOnRestart = false; + // Apply the domain settings if returning from the Domains activity. + if (reapplyDomainSettingsOnRestart) { + // Reapply the domain settings. + applyDomainSettings(formattedUrlString, false, true); + + // Reset `reapplyDomainSettingsOnRestart`. + reapplyDomainSettingsOnRestart = false; } // Load the URL on restart to apply changes to night mode. @@ -1398,7 +1862,7 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD loadUrlOnRestart = false; } - // + // Update the bookmarks drawer if returning from the Bookmarks activity. if (restartFromBookmarksActivity) { // Close the bookmarks drawer. drawerLayout.closeDrawer(GravityCompat.END); @@ -1409,6 +1873,9 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD // Reset `restartFromBookmarksActivity`. restartFromBookmarksActivity = false; } + + // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. This can be important if the screen was rotated. + updatePrivacyIcons(true); } // `onResume()` runs after `onStart()`, which runs after `onCreate()` and `onRestart()`. @@ -1425,24 +1892,56 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD // Resume the adView for the free flavor. if (BuildConfig.FLAVOR.contentEquals("free")) { - BannerAd.resumeAd(adView); + // Resume the ad. + AdHelper.resumeAd(findViewById(R.id.adview)); + } + + // Display a message to the user if waiting for Orbot. + if (waitingForOrbot && !orbotStatus.equals("ON")) { + // Load a waiting page. `null` specifies no encoding, which defaults to ASCII. + mainWebView.loadData(waitingForOrbotHTMLString, "text/html", null); + } + + if (displayingFullScreenVideo) { + // 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); } } @Override public void onPause() { + // Run the default commands. + super.onPause(); + // Pause `mainWebView`. mainWebView.onPause(); // Stop all JavaScript. mainWebView.pauseTimers(); - // Pause the adView or it will continue to consume resources in the background on the free flavor. + // Pause the ad or it will continue to consume resources in the background on the free flavor. if (BuildConfig.FLAVOR.contentEquals("free")) { - BannerAd.pauseAd(adView); + // Pause the ad. + AdHelper.pauseAd(findViewById(R.id.adview)); } + } - super.onPause(); + @Override + public void onDestroy() { + // Unregister the Orbot status broadcast receiver. + this.unregisterReceiver(orbotStatusBroadcastReceiver); + + // Run the default commands. + super.onDestroy(); } @Override @@ -1460,23 +1959,58 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD 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); - - // Only display third-party cookies if SDK >= 21 + 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); + blockAllThirdParyRequestsMenuItem = menu.findItem(R.id.block_all_third_party_requests); + MenuItem adConsentMenuItem = menu.findItem(R.id.ad_consent); + + // Only display third-party cookies if API >= 21 toggleThirdPartyCookiesMenuItem.setVisible(Build.VERSION.SDK_INT >= 21); - // Get the shared preference values. `this` references the current context. + // 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); + + // Only show Ad Consent if this is the free flavor. + adConsentMenuItem.setVisible(BuildConfig.FLAVOR.contentEquals("free")); + + // Get the shared preference values. SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - // Set the status of the additional app bar icons. The default is `false`. - if (sharedPreferences.getBoolean("display_additional_app_bar_icons", false)) { + // Get the status of the additional AppBar icons. + displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false); + + // 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); - toggleSaveFormDataMenuItem.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); - toggleSaveFormDataMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + refreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + } + + // Replace Refresh with Stop if a URL is already loading. + if (urlIsLoading) { + // Set the title. + refreshMenuItem.setTitle(R.string.stop); + + // 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); + } + } } return true; @@ -1489,14 +2023,14 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD MenuItem toggleFirstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies); MenuItem toggleThirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies); MenuItem toggleDomStorageMenuItem = menu.findItem(R.id.toggle_dom_storage); - MenuItem toggleSaveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data); + MenuItem 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); + 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 refreshMenuItem = menu.findItem(R.id.refresh); // Set the text for the domain menu item. if (domainSettingsApplied) { @@ -1509,42 +2043,66 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD toggleFirstPartyCookiesMenuItem.setChecked(firstPartyCookiesEnabled); toggleThirdPartyCookiesMenuItem.setChecked(thirdPartyCookiesEnabled); toggleDomStorageMenuItem.setChecked(domStorageEnabled); - toggleSaveFormDataMenuItem.setChecked(saveFormDataEnabled); + 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); + blockAllThirdParyRequestsMenuItem.setChecked(blockAllThirdPartyRequests); + swipeToRefreshMenuItem.setChecked(swipeRefreshLayout.isEnabled()); displayImagesMenuItem.setChecked(mainWebView.getSettings().getLoadsImagesAutomatically()); // Enable third-party cookies if first-party cookies are enabled. toggleThirdPartyCookiesMenuItem.setEnabled(firstPartyCookiesEnabled); - // Enable `DOM Storage` if JavaScript is enabled. + // Enable DOM Storage if JavaScript is enabled. toggleDomStorageMenuItem.setEnabled(javaScriptEnabled); - // Enable `Clear Cookies` if there are any. + // Enable Clear Cookies if there are any. clearCookiesMenuItem.setEnabled(cookieManager.hasCookies()); - // Get a count of the number of files in the `Local Storage` directory. + // 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; } - // Get a count of the number of files in the `IndexedDB` directory. + // 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; } - // Enable `Clear DOM Storage` if there is any. + // Enable Clear DOM Storage if there is any. clearDOMStorageMenuItem.setEnabled(localStorageDirectoryNumberOfFiles > 0 || indexedDBDirectoryNumberOfFiles > 0); - // Enable `Clear Form Data` is there is any. - WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(this); - clearFormDataMenuItem.setEnabled(mainWebViewDatabase.hasFormData()); + // 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); + } - // Enable `Clear Data` if any of the submenu items are enabled. + // Enable Clear Data if any of the submenu items are enabled. clearDataMenuItem.setEnabled(clearCookiesMenuItem.isEnabled() || clearDOMStorageMenuItem.isEnabled() || clearFormDataMenuItem.isEnabled()); + // Disable Fanboy's Social Blocking List if Fanboy's Annoyance List is checked. + fanboysSocialBlockingListMenuItem.setEnabled(!fanboysAnnoyanceListEnabled); + + // 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)); + blockAllThirdParyRequestsMenuItem.setTitle(thirdPartyBlockedRequests + " - " + getString(R.string.block_all_third_party_requests)); + // Initialize font size variables. int fontSize = mainWebView.getSettings().getTextZoom(); String fontSizeTitle; @@ -1602,9 +2160,6 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD fontSizeMenuItem.setTitle(fontSizeTitle); selectedFontSizeMenuItem.setChecked(true); - // Only show `Refresh` if `swipeToRefresh` is disabled. - refreshMenuItem.setVisible(!swipeToRefreshEnabled); - // Run all the other default commands. super.onPrepareOptionsMenu(menu); @@ -1623,27 +2178,6 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD // Set the commands that relate to the menu entries. switch (menuItemId) { - 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 = ""; - - // Create an intent to launch the domains activity. - Intent domainsIntent = new Intent(this, DomainsActivity.class); - - // Put extra information instructing the domains activity to directly load the current domain. - domainsIntent.putExtra("LoadDomain", domainSettingsDatabaseId); - - // Make it so. - startActivity(domainsIntent); - } else { // Add a new domain. - // Show the add domain `AlertDialog`. - AppCompatDialogFragment addDomainDialog = new AddDomainDialog(); - addDomainDialog.show(getSupportFragmentManager(), getResources().getString(R.string.add_domain)); - } - return true; - case R.id.toggle_javascript: // Switch the status of javaScriptEnabled. javaScriptEnabled = !javaScriptEnabled; @@ -1667,6 +2201,48 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD mainWebView.reload(); return true; + 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 = ""; + + // Create an intent to launch the domains activity. + Intent domainsIntent = new Intent(this, DomainsActivity.class); + + // 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); + + // Make it so. + startActivity(domainsIntent); + } else { // Add a new domain. + // Apply the new domain settings on returning to `MainWebViewActivity`. + reapplyDomainSettingsOnRestart = true; + currentDomainName = ""; + + // Get the current domain + Uri currentUri = Uri.parse(formattedUrlString); + String currentDomain = currentUri.getHost(); + + // Initialize the database handler. The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`. + DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0); + + // Create the domain and store the database ID. + int newDomainDatabaseId = domainsDatabaseHelper.addDomain(currentDomain); + + // Create an intent to launch the domains activity. + Intent domainsIntent = new Intent(this, DomainsActivity.class); + + // 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); + + // Make it so. + startActivity(domainsIntent); + } + return true; + case R.id.toggle_first_party_cookies: // Switch the status of firstPartyCookiesEnabled. firstPartyCookiesEnabled = !firstPartyCookiesEnabled; @@ -1683,7 +2259,7 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD // 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. + } 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(); @@ -1740,6 +2316,7 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD mainWebView.reload(); return true; + // Form data can be removed once the minimum API >= 26. case R.id.toggle_save_form_data: // Switch the status of saveFormDataEnabled. saveFormDataEnabled = !saveFormDataEnabled; @@ -1832,6 +2409,7 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD .show(); return true; + // 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 -> { @@ -1889,6 +2467,11 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD mainWebView.getSettings().setTextZoom(200); return true; + case R.id.swipe_to_refresh: + // Toggle swipe to refresh. + swipeRefreshLayout.setEnabled(!swipeRefreshLayout.isEnabled()); + return true; + case R.id.display_images: if (mainWebView.getSettings().getLoadsImagesAutomatically()) { // Images are currently loaded automatically. mainWebView.getSettings().setLoadsImagesAutomatically(false); @@ -1901,6 +2484,82 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD onTheFlyDisplayImagesSet = true; return true; + case R.id.view_source: + // Launch the View Source activity. + Intent viewSourceIntent = new Intent(this, ViewSourceActivity.class); + startActivity(viewSourceIntent); + return true; + + case R.id.easylist: + // Toggle the EasyList status. + easyListEnabled = !easyListEnabled; + + // Update the menu checkbox. + menuItem.setChecked(easyListEnabled); + + // Reload the main WebView. + mainWebView.reload(); + return true; + + case R.id.easyprivacy: + // Toggle the EasyPrivacy status. + easyPrivacyEnabled = !easyPrivacyEnabled; + + // Update the menu checkbox. + menuItem.setChecked(easyPrivacyEnabled); + + // Reload the main WebView. + mainWebView.reload(); + return true; + + case R.id.fanboys_annoyance_list: + // Toggle Fanboy's Annoyance List status. + fanboysAnnoyanceListEnabled = !fanboysAnnoyanceListEnabled; + + // Update the menu checkbox. + menuItem.setChecked(fanboysAnnoyanceListEnabled); + + // Update the staus of Fanboy's Social Blocking List. + MenuItem fanboysSocialBlockingListMenuItem = mainMenu.findItem(R.id.fanboys_social_blocking_list); + fanboysSocialBlockingListMenuItem.setEnabled(!fanboysAnnoyanceListEnabled); + + // Reload the main WebView. + mainWebView.reload(); + return true; + + case R.id.fanboys_social_blocking_list: + // Toggle Fanboy's Social Blocking List status. + fanboysSocialBlockingListEnabled = !fanboysSocialBlockingListEnabled; + + // Update the menu checkbox. + menuItem.setChecked(fanboysSocialBlockingListEnabled); + + // Reload the main WebView. + mainWebView.reload(); + return true; + + case R.id.ultraprivacy: + // Toggle the UltraPrivacy status. + ultraPrivacyEnabled = !ultraPrivacyEnabled; + + // Update the menu checkbox. + menuItem.setChecked(ultraPrivacyEnabled); + + // Reload the main WebView. + mainWebView.reload(); + return true; + + case R.id.block_all_third_party_requests: + //Toggle the third-party requests blocker status. + blockAllThirdPartyRequests = !blockAllThirdPartyRequests; + + // Update the menu checkbox. + menuItem.setChecked(blockAllThirdPartyRequests); + + // Reload the main WebView. + mainWebView.reload(); + return true; + case R.id.share: // Setup the share string. String shareString = webViewTitle + " – " + urlTextBox.getText().toString(); @@ -1922,7 +2581,8 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD // Show the Find on Page `RelativeLayout`. findOnPageLinearLayout.setVisibility(View.VISIBLE); - // 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 + // 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(); @@ -1946,12 +2606,6 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD printManager.print(getString(R.string.privacy_browser_web_page), printDocumentAdapter, null); return true; - case R.id.view_source: - // Launch the Vew Source activity. - Intent viewSourceIntent = new Intent(this, ViewSourceActivity.class); - startActivity(viewSourceIntent); - return true; - case R.id.add_to_homescreen: // Show the `CreateHomeScreenShortcutDialog` `AlertDialog` and name this instance `R.string.create_shortcut`. AppCompatDialogFragment createHomeScreenShortcutDialogFragment = new CreateHomeScreenShortcutDialog(); @@ -1961,7 +2615,19 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD return true; case R.id.refresh: - mainWebView.reload(); + 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; + + case R.id.ad_consent: + // Display the ad consent dialog. + DialogFragment adConsentDialogFragment = new AdConsentDialog(); + adConsentDialogFragment.show(getFragmentManager(), getString(R.string.ad_consent)); return true; default: @@ -1983,6 +2649,9 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD 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 = ""; + // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded. navigatingHistory = true; @@ -1993,6 +2662,9 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD 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 = ""; + // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded. navigatingHistory = true; @@ -2010,6 +2682,12 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD urlHistoryDialogFragment.show(getSupportFragmentManager(), getString(R.string.history)); break; + case R.id.requests: + // Launch the requests activity. + Intent requestsIntent = new Intent(this, RequestsActivity.class); + startActivity(requestsIntent); + break; + case R.id.downloads: // Launch the system Download Manager. Intent downloadManagerIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS); @@ -2021,21 +2699,24 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD break; case R.id.domains: - // Reapply the domain settings on returning to `MainWebViewActivity`. + // Set the flag to reapply the domain settings on restart when returning from Domain Settings. reapplyDomainSettingsOnRestart = true; currentDomainName = ""; - // Launch `DomainsActivity`. + // Launch the domains activity. Intent domainsIntent = new Intent(this, DomainsActivity.class); startActivity(domainsIntent); break; case R.id.settings: - // Reapply the domain settings on returning to `MainWebViewActivity`. + // Set the flag to reapply app settings on restart when returning from Settings. + reapplyAppSettingsOnRestart = true; + + // Set the flag to reapply the domain settings on restart when returning from Settings. reapplyDomainSettingsOnRestart = true; currentDomainName = ""; - // Launch `SettingsActivity`. + // Launch the settings activity. Intent settingsIntent = new Intent(this, SettingsActivity.class); startActivity(settingsIntent); break; @@ -2098,8 +2779,8 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD } } - // Clear form data. - if (clearEverything || sharedPreferences.getBoolean("clear_form_data", true)) { + // 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(); @@ -2123,7 +2804,8 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD // Delete the main cache directory. privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/cache"); - // Delete the secondary `Service Worker` cache directory. We have to use a `String[]` because the directory contains a space and `Runtime.exec` will not escape the string correctly otherwise. + // 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. @@ -2187,16 +2869,14 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); - // Reload the ad for the free flavor if we are not in full screen mode. + // Reload the ad for the free flavor if we not in full screen mode. if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) { - // Reload the ad. - BannerAd.reloadAfterRotate(adView, getApplicationContext(), getString(R.string.ad_id)); - - // Reinitialize the `adView` variable, as the `View` will have been removed and re-added by `BannerAd.reloadAfterRotate()`. - adView = findViewById(R.id.adview); + // 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_id)); } - // `invalidateOptionsMenu` should recalculate the number of action buttons from the menu to display on the app bar, but it doesn't because of the this bug: https://code.google.com/p/android/issues/detail?id=20493#c8 + // `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); } @@ -2224,14 +2904,14 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD // Set the target URL as the title of the `ContextMenu`. menu.setHeaderTitle(linkUrl); - // Add a `Load URL` entry. - menu.add(R.string.load_url).setOnMenuItemClickListener(item -> { + // Add a Load URL entry. + menu.add(R.string.load_url).setOnMenuItemClickListener((MenuItem item) -> { loadUrl(linkUrl); return false; }); - // Add a `Copy URL` entry. - menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> { + // 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); @@ -2240,6 +2920,38 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD return false; }); + // Add a Download URL entry. + menu.add(R.string.download_url).setOnMenuItemClickListener((MenuItem item) -> { + // 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 WRITE_EXTERNAL_STORAGE permission needs to be requested. + + // Store the variables for future use by `onRequestPermissionsResult()`. + downloadUrl = linkUrl; + downloadContentDisposition = "none"; + downloadContentLength = -1; + + // 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. + // Get a handle for 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(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 WRITE_EXTERNAL_STORAGE permission has already been granted. + // Get a handle for the download file alert dialog. + AppCompatDialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(linkUrl, "none", -1); + + // Show the download file alert dialog. + downloadFileDialogFragment.show(getSupportFragmentManager(), getString(R.string.download)); + } + return false; + }); + // Add a `Cancel` entry, which by default closes the `ContextMenu`. menu.add(R.string.cancel); break; @@ -2297,9 +3009,31 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD // Add a `Download Image` entry. menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> { - // Show the `DownloadImageDialog` `AlertDialog` and name this instance `@string/download`. - AppCompatDialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl); - downloadImageDialogFragment.show(getSupportFragmentManager(), getString(R.string.download)); + // 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 WRITE_EXTERNAL_STORAGE permission needs to be requested. + + // Store the image URL for use by `onRequestPermissionResult()`. + downloadImageUrl = imageUrl; + + // 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. + // Get a handle for the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE. + DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE); + + // 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 WRITE_EXTERNAL_STORAGE permission has already been granted. + // Get a handle for the download image alert dialog. + AppCompatDialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl); + + // Show the download image alert dialog. + downloadImageDialogFragment.show(getSupportFragmentManager(), getString(R.string.download)); + } return false; }); @@ -2334,9 +3068,31 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD // Add a `Download Image` entry. menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> { - // Show the `DownloadImageDialog` `AlertDialog` and name this instance `@string/download`. - AppCompatDialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl); - downloadImageDialogFragment.show(getSupportFragmentManager(), getString(R.string.download)); + // 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 WRITE_EXTERNAL_STORAGE permission needs to be requested. + + // Store the image URL for use by `onRequestPermissionResult()`. + downloadImageUrl = imageUrl; + + // 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. + // Get a handle for the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE. + DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE); + + // 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 WRITE_EXTERNAL_STORAGE permission has already been granted. + // Get a handle for the download image alert dialog. + AppCompatDialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl); + + // Show the download image alert dialog. + downloadImageDialogFragment.show(getSupportFragmentManager(), getString(R.string.download)); + } return false; }); @@ -2356,33 +3112,6 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD } } - @Override - public void onAddDomain(AppCompatDialogFragment dialogFragment) { - // Reapply the domain settings on returning to `MainWebViewActivity`. - reapplyDomainSettingsOnRestart = true; - currentDomainName = ""; - - // Get the new domain name `String` from `dialogFragment`. - EditText domainNameEditText = dialogFragment.getDialog().findViewById(R.id.domain_name_edittext); - String domainNameString = domainNameEditText.getText().toString(); - - // Initialize the database handler. `this` specifies the context. The two `nulls` do not specify the database name or a `CursorFactory`. - // The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`. - DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0); - - // Create the domain and store the database ID in `currentDomainDatabaseId`. - int newDomainDatabaseId = domainsDatabaseHelper.addDomain(domainNameString); - - // Create an intent to launch the domains activity. - Intent domainsIntent = new Intent(this, DomainsActivity.class); - - // Put extra information instructing the domains activity to directly load the current domain. - domainsIntent.putExtra("LoadDomain", newDomainDatabaseId); - - // Make it so. - startActivity(domainsIntent); - } - @Override public void onCreateBookmark(AppCompatDialogFragment dialogFragment) { // Get the `EditTexts` from the `dialogFragment`. @@ -2485,6 +3214,58 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD ShortcutManagerCompat.requestPinShortcut(this, shortcutInfoBuilder.build(), null); } + @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 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; + } + } + + @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); + + // 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(getSupportFragmentManager(), getString(R.string.download)), 500); + } else { + downloadFileDialogFragment.show(getSupportFragmentManager(), getString(R.string.download)); + } + + // Reset the download variables. + downloadUrl = ""; + downloadContentDisposition = ""; + downloadContentLength = 0; + break; + + 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); + + // 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(getSupportFragmentManager(), getString(R.string.download)), 500); + } else { + downloadImageDialogFragment.show(getSupportFragmentManager(), getString(R.string.download)); + } + + // Reset the image URL variable. + downloadImageUrl = ""; + break; + } + } + @Override public void onDownloadImage(AppCompatDialogFragment dialogFragment, String imageUrl) { // Download the image if it has an HTTP or HTTPS URI. @@ -2505,15 +3286,17 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD downloadRequest.addRequestHeader("Cookie", cookies); } - // Get the file name from `dialogFragment`. + // Get the file name from the dialog fragment. EditText downloadImageNameEditText = dialogFragment.getDialog().findViewById(R.id.download_image_name); String imageName = downloadImageNameEditText.getText().toString(); - // Once we have `WRITE_EXTERNAL_STORAGE` permissions we can use `setDestinationInExternalPublicDir`. - if (Build.VERSION.SDK_INT >= 23) { // If API >= 23, set the download save in the the `DIRECTORY_DOWNLOADS` using `imageName`. - downloadRequest.setDestinationInExternalFilesDir(this, "/", imageName); - } else { // Only set the title using `imageName`. - downloadRequest.setTitle(imageName); + // 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); } // Allow `MediaScanner` to index the download if it is a media file. @@ -2539,7 +3322,6 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD 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); @@ -2556,15 +3338,17 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD downloadRequest.addRequestHeader("Cookie", cookies); } - // Get the file name from `dialogFragment`. + // Get the file name from the dialog fragment. EditText downloadFileNameEditText = dialogFragment.getDialog().findViewById(R.id.download_file_name); String fileName = downloadFileNameEditText.getText().toString(); - // Once we have `WRITE_EXTERNAL_STORAGE` permissions we can use `setDestinationInExternalPublicDir`. - if (Build.VERSION.SDK_INT >= 23) { // If API >= 23, set the download location to `/sdcard/Android/data/com.stoutner.privacybrowser.standard/files` named `fileName`. - downloadRequest.setDestinationInExternalFilesDir(this, "/", fileName); - } else { // Only set the title using `fileName`. - downloadRequest.setTitle(fileName); + // 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); } // Allow `MediaScanner` to index the download if it is a media file. @@ -2714,6 +3498,9 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD @Override public void onSslMismatchBack() { 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 = ""; + // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded. navigatingHistory = true; @@ -2733,6 +3520,9 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD @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 = ""; + // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded. navigatingHistory = true; @@ -2765,6 +3555,9 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD } } 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 = ""; + // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded. navigatingHistory = true; @@ -2776,6 +3569,16 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD } } + // 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)); + } + } + private void loadUrlFromTextBox() throws UnsupportedEncodingException { // Get the text from urlTextBox and convert it to a string. trim() removes white spaces from the beginning and end of the string. String unformattedUrlString = urlTextBox.getText().toString().trim(); @@ -2810,7 +3613,10 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD // Decode `formattedUri` as a `String` in `UTF-8`. formattedUrlString = URLDecoder.decode(formattedUri.build().toString(), "UTF-8"); - } else { + } else if (unformattedUrlString.isEmpty()){ // Load a blank web site. + // Load a blank string. + formattedUrlString = ""; + } else { // Search for the contents of the URL box. // Sanitize the search input and convert it to a search. final String encodedUrlString = URLEncoder.encode(unformattedUrlString, "UTF-8"); @@ -2818,19 +3624,25 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD formattedUrlString = searchURL + encodedUrlString; } + // 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(); + + // Make it so. loadUrl(formattedUrlString); } + 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; - private void loadUrl(String url) { - // Apply any custom domain settings. - applyDomainSettings(url); + // Apply the domain settings. + applyDomainSettings(url, true, 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(""); // Load the URL. mainWebView.loadUrl(url, customHeaders); - - // Set `urlIsLoading` to prevent changes in the user agent on websites with redirects from reloading the current website. - urlIsLoading = true; } public void findPreviousOnPage(View view) { @@ -2865,20 +3677,18 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); // Store the values from the shared preferences in variables. - String homepageString = sharedPreferences.getString("homepage", "https://start.duckduckgo.com"); - String torHomepageString = sharedPreferences.getString("tor_homepage", "https://3g2upl4pq6kufc4m.onion"); - String torSearchString = sharedPreferences.getString("tor_search", "https://3g2upl4pq6kufc4m.onion/html/?q="); + String homepageString = sharedPreferences.getString("homepage", "https://searx.me/"); + String torHomepageString = sharedPreferences.getString("tor_homepage", "http://ulrn6sryqaifefld.onion/"); + String torSearchString = sharedPreferences.getString("tor_search", "http://ulrn6sryqaifefld.onion/?q="); String torSearchCustomURLString = sharedPreferences.getString("tor_search_custom_url", ""); - String searchString = sharedPreferences.getString("search", "https://duckduckgo.com/html/?q="); + String searchString = sharedPreferences.getString("search", "https://searx.me/?q="); String searchCustomURLString = sharedPreferences.getString("search_custom_url", ""); - adBlockerEnabled = sharedPreferences.getBoolean("block_ads", true); incognitoModeEnabled = sharedPreferences.getBoolean("incognito_mode", false); boolean doNotTrackEnabled = sharedPreferences.getBoolean("do_not_track", false); - boolean proxyThroughOrbot = sharedPreferences.getBoolean("proxy_through_orbot", false); + proxyThroughOrbot = sharedPreferences.getBoolean("proxy_through_orbot", false); fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean("full_screen_browsing_mode", false); hideSystemBarsOnFullscreen = sharedPreferences.getBoolean("hide_system_bars", false); translucentNavigationBarOnFullscreen = sharedPreferences.getBoolean("translucent_navigation_bar", true); - swipeToRefreshEnabled = sharedPreferences.getBoolean("swipe_to_refresh", false); displayWebpageImagesBoolean = sharedPreferences.getBoolean("display_webpage_images", true); // Set the homepage, search, and proxy options. @@ -2908,7 +3718,7 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD appBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.blue_50)); } - // Display a message to the user if we are waiting on Orbot. + // Display a message to the user if waiting for Orbot. if (!orbotStatus.equals("ON")) { // Set `waitingForOrbot`. waitingForOrbot = true; @@ -2946,9 +3756,6 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD waitingForOrbot = false; } - // Set swipe to refresh. - swipeRefreshLayout.setEnabled(swipeToRefreshEnabled); - // Set Do Not Track status. if (doNotTrackEnabled) { customHeaders.put("DNT", "1"); @@ -2957,7 +3764,7 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD } // Apply the appropriate full screen mode the `SYSTEM_UI` flags. - if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { + 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); @@ -2970,10 +3777,13 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD /* SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen. * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen. - * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically rehides them after they are shown. + * 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); + // Add the translucent status flag if it is unset. getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); @@ -2985,8 +3795,8 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); } } - } else { // Switch to normal viewing mode. - // Reset `inFullScreenBrowsingMode` to `false`. + } 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 `appBar` if `findOnPageLinearLayout` is not visible. @@ -2996,33 +3806,28 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD // Show the `BannerAd` in the free flavor. if (BuildConfig.FLAVOR.contentEquals("free")) { - // Reload the ad. Because the screen may have rotated, we need to use `reloadAfterRotate`. - BannerAd.reloadAfterRotate(adView, getApplicationContext(), getString(R.string.ad_id)); - - // Reinitialize the `adView` variable, as the `View` will have been removed and re-added by `BannerAd.reloadAfterRotate()`. - adView = findViewById(R.id.adview); + // 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.ad_id)); } + // Remove any `SYSTEM_UI` flags from `rootCoordinatorLayout`. + rootCoordinatorLayout.setSystemUiVisibility(0); + // Remove the translucent navigation bar flag if it is set. getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); // 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 any `SYSTEM_UI` flags from `rootCoordinatorLayout`. - rootCoordinatorLayout.setSystemUiVisibility(0); - // Constrain `rootCoordinatorLayout` inside the status and navigation bars. rootCoordinatorLayout.setFitsSystemWindows(true); } } - // We have to use the deprecated `.getDrawable()` until the minimum API >= 21. + // `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 void applyDomainSettings(String url) { - // Reset `navigatingHistory`. - navigatingHistory = false; - + private void applyDomainSettings(String url, boolean resetFavoriteIcon, boolean reloadWebsite) { // Parse the URL into a URI. Uri uri = Uri.parse(url); @@ -3041,7 +3846,12 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD loadingNewDomainName = !hostName.equals(currentDomainName); } - // Only apply the domain settings if we are loading a new domain. This allows the user to set temporary settings for JavaScript, cookies, DOM storage, etc. + // Strings don't like to be null. + if (hostName == null) { + hostName = ""; + } + + // 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; @@ -3049,9 +3859,11 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD // Reset `ignorePinnedSslCertificate`. ignorePinnedSslCertificate = false; - // Reset `favoriteIconBitmap` and display it in the `appbar`. - favoriteIconBitmap = favoriteIconDefaultBitmap; - favoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(favoriteIconBitmap, 64, 64, true)); + // Reset the favorite icon if specified. + if (resetFavoriteIcon) { + favoriteIconBitmap = favoriteIconDefaultBitmap; + favoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(favoriteIconBitmap, 64, 64, true)); + } // Initialize the database handler. `this` specifies the context. The two `nulls` do not specify the database name or a `CursorFactory`. // The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`. @@ -3088,26 +3900,29 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD domainNameInDatabase = hostName; } - // If `hostName` is not `null`, check all the subdomains of `hostName` against wildcard domains in `domainCursor`. - if (hostName != null) { - while (hostName.contains(".") && !domainSettingsApplied) { // Stop checking if we run out of `.` or if we already know that `domainSettingsApplied` is `true`. - if (domainSettingsSet.contains("*." + hostName)) { // Check the host name prepended by `*.`. - domainSettingsApplied = true; - domainNameInDatabase = "*." + hostName; - } + // 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; - // Strip out the lowest subdomain of `host`. - hostName = hostName.substring(hostName.indexOf(".") + 1); + // Store the applied domain names as it appears in the database. + domainNameInDatabase = "*." + hostName; } + + // Strip out the lowest subdomain of of the host name. + hostName = hostName.substring(hostName.indexOf(".") + 1); } - // Get a handle for the shared preference. `this` references the current context. + + // Get a handle for the shared preference. SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); // Store the general preference information. String defaultFontSizeString = sharedPreferences.getString("default_font_size", "100"); - String defaultUserAgentString = sharedPreferences.getString("user_agent", "PrivacyBrowser/1.0"); + String defaultUserAgentName = sharedPreferences.getString("user_agent", "Privacy Browser"); String defaultCustomUserAgentString = sharedPreferences.getString("custom_user_agent", "PrivacyBrowser/1.0"); + boolean defaultSwipeToRefresh = sharedPreferences.getBoolean("swipe_to_refresh", true); nightMode = sharedPreferences.getBoolean("night_mode", false); if (domainSettingsApplied) { // The url we are loading has custom domain settings. @@ -3121,11 +3936,19 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD 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); - String userAgentString = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.USER_AGENT)); + 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)); - displayWebpageImagesInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.DISPLAY_IMAGES)); + int swipeToRefreshInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SWIPE_TO_REFRESH)); int nightModeInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.NIGHT_MODE)); + displayWebpageImagesInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.DISPLAY_IMAGES)); pinnedDomainSslCertificate = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)) == 1); pinnedDomainSslIssuedToCNameString = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME)); pinnedDomainSslIssuedToONameString = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION)); @@ -3171,7 +3994,11 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD mainWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled); cookieManager.setAcceptCookie(firstPartyCookiesEnabled); mainWebView.getSettings().setDomStorageEnabled(domStorageEnabled); - mainWebView.getSettings().setSaveFormData(saveFormDataEnabled); + + // Apply the form data setting if the API < 26. + if (Build.VERSION.SDK_INT < 26) { + mainWebView.getSettings().setSaveFormData(saveFormDataEnabled); + } // Apply the font size. if (fontSize == 0) { // Apply the default font size. @@ -3185,39 +4012,73 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD cookieManager.setAcceptThirdPartyCookies(mainWebView, thirdPartyCookiesEnabled); } - // Only set the user agent if the webpage is not currently loading. Otherwise, changing the user agent on redirects can cause the original website to reload. + // Only set the user agent if the webpage is not currently loading. Otherwise, changing the user agent on redirects can cause the original website to reload. + // if (!urlIsLoading) { - switch (userAgentString) { - case "System default user agent": - // Set the user agent according to the system default. - switch (defaultUserAgentString) { - case "WebView default user agent": - // Set the user agent to `""`, which uses the default value. - mainWebView.getSettings().setUserAgentString(""); - break; - - case "Custom user agent": - // Set the custom user agent. - mainWebView.getSettings().setUserAgentString(defaultCustomUserAgentString); - break; - - default: - // Use the selected user agent. - mainWebView.getSettings().setUserAgentString(defaultUserAgentString); - } + // 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]); + } + } + + // 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; - case "WebView default user agent": - // Set the user agent to `""`, which uses the default value. - mainWebView.getSettings().setUserAgentString(""); + case DomainsDatabaseHelper.SWIPE_TO_REFRESH_ENABLED: + // Enable swipe to refresh. + swipeRefreshLayout.setEnabled(true); break; - default: - // Use the selected user agent. - mainWebView.getSettings().setUserAgentString(userAgentString); + case DomainsDatabaseHelper.SWIPE_TO_REFRESH_DISABLED: + // Disable swipe to refresh. + swipeRefreshLayout.setEnabled(false); } - // Store the applied user agent string. + // Store the applied user agent string, which is used in the View Source activity. appliedUserAgentString = mainWebView.getSettings().getUserAgentString(); } @@ -3227,13 +4088,19 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD } else { urlAppBarRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_light_green)); } - } else { // The URL we are loading does not have custom domain settings. Load the defaults. + } else { // The new URL does not have custom domain settings. Load the defaults. // Store the values from `sharedPreferences` in variables. javaScriptEnabled = sharedPreferences.getBoolean("javascript_enabled", false); firstPartyCookiesEnabled = sharedPreferences.getBoolean("first_party_cookies_enabled", false); thirdPartyCookiesEnabled = sharedPreferences.getBoolean("third_party_cookies_enabled", false); domStorageEnabled = sharedPreferences.getBoolean("dom_storage_enabled", false); - saveFormDataEnabled = sharedPreferences.getBoolean("save_form_data_enabled", false); + saveFormDataEnabled = sharedPreferences.getBoolean("save_form_data_enabled", false); // Form data can be removed once the minimum API >= 26. + easyListEnabled = sharedPreferences.getBoolean("easylist", true); + easyPrivacyEnabled = sharedPreferences.getBoolean("easyprivacy", true); + fanboysAnnoyanceListEnabled = sharedPreferences.getBoolean("fanboy_annoyance_list", true); + fanboysSocialBlockingListEnabled = sharedPreferences.getBoolean("fanboy_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) { @@ -3244,8 +4111,13 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD mainWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled); cookieManager.setAcceptCookie(firstPartyCookiesEnabled); mainWebView.getSettings().setDomStorageEnabled(domStorageEnabled); - mainWebView.getSettings().setSaveFormData(saveFormDataEnabled); mainWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString)); + swipeRefreshLayout.setEnabled(defaultSwipeToRefresh); + + // Apply the form data setting if the API < 26. + if (Build.VERSION.SDK_INT < 26) { + mainWebView.getSettings().setSaveFormData(saveFormDataEnabled); + } // Reset the pinned SSL certificate information. domainSettingsDatabaseId = -1; @@ -3264,25 +4136,35 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD cookieManager.setAcceptThirdPartyCookies(mainWebView, thirdPartyCookiesEnabled); } - // Only set the user agent if the webpage is not currently loading. Otherwise, changing the user agent on redirects can cause the original website to reload. + // Only set the user agent if the webpage is not currently loading. Otherwise, changing the user agent on redirects can cause the original website to reload. + // if (!urlIsLoading) { - switch (defaultUserAgentString) { - case "WebView default user agent": + // Get the array position of the user agent name. + int userAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName); + + // Set the user agent. + switch (userAgentArrayPosition) { + case UNRECOGNIZED_USER_AGENT: // The default user agent name is not on the canonical list. + // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names. + 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 "Custom user agent": + case SETTINGS_CUSTOM_USER_AGENT: // Set the custom user agent. mainWebView.getSettings().setUserAgentString(defaultCustomUserAgentString); break; default: - // Use the selected user agent. - mainWebView.getSettings().setUserAgentString(defaultUserAgentString); + // Get the user agent string from the user agent data array + mainWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]); } - // Store the applied user agent string. + // Store the applied user agent string, which is used in the View Source activity. appliedUserAgentString = mainWebView.getSettings().getUserAgentString(); } @@ -3302,6 +4184,11 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD updatePrivacyIcons(true); } } + + // Reload the website if returning from the Domains activity. + if (reloadWebsite) { + mainWebView.reload(); + } } private void setDisplayWebpageImages() { @@ -3327,63 +4214,59 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD } private void updatePrivacyIcons(boolean runInvalidateOptionsMenu) { - // Get handles for the icons. - MenuItem privacyIconMenuItem = mainMenu.findItem(R.id.toggle_javascript); - MenuItem firstPartyCookiesIconMenuItem = mainMenu.findItem(R.id.toggle_first_party_cookies); - MenuItem domStorageIconMenuItem = mainMenu.findItem(R.id.toggle_dom_storage); - MenuItem formDataIconMenuItem = mainMenu.findItem(R.id.toggle_save_form_data); + // 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 `privacyIcon`. + // Update the privacy icon. if (javaScriptEnabled) { // JavaScript is enabled. - privacyIconMenuItem.setIcon(R.drawable.javascript_enabled); + privacyMenuItem.setIcon(R.drawable.javascript_enabled); } else if (firstPartyCookiesEnabled) { // JavaScript is disabled but cookies are enabled. - privacyIconMenuItem.setIcon(R.drawable.warning); + privacyMenuItem.setIcon(R.drawable.warning); } else { // All the dangerous features are disabled. - privacyIconMenuItem.setIcon(R.drawable.privacy_mode); + privacyMenuItem.setIcon(R.drawable.privacy_mode); } - // Update `firstPartyCookiesIcon`. + // Update the first-party cookies icon. if (firstPartyCookiesEnabled) { // First-party cookies are enabled. - firstPartyCookiesIconMenuItem.setIcon(R.drawable.cookies_enabled); + firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_enabled); } else { // First-party cookies are disabled. if (darkTheme) { - firstPartyCookiesIconMenuItem.setIcon(R.drawable.cookies_disabled_dark); + firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_dark); } else { - firstPartyCookiesIconMenuItem.setIcon(R.drawable.cookies_disabled_light); + firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_light); } } - // Update `domStorageIcon`. + // Update the DOM storage icon. if (javaScriptEnabled && domStorageEnabled) { // Both JavaScript and DOM storage are enabled. - domStorageIconMenuItem.setIcon(R.drawable.dom_storage_enabled); + domStorageMenuItem.setIcon(R.drawable.dom_storage_enabled); } else if (javaScriptEnabled) { // JavaScript is enabled but DOM storage is disabled. if (darkTheme) { - domStorageIconMenuItem.setIcon(R.drawable.dom_storage_disabled_dark); + domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_dark); } else { - domStorageIconMenuItem.setIcon(R.drawable.dom_storage_disabled_light); + domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_light); } } else { // JavaScript is disabled, so DOM storage is ghosted. if (darkTheme) { - domStorageIconMenuItem.setIcon(R.drawable.dom_storage_ghosted_dark); + domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_dark); } else { - domStorageIconMenuItem.setIcon(R.drawable.dom_storage_ghosted_light); + domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_light); } } - // Update `formDataIcon`. - if (saveFormDataEnabled) { // Form data is enabled. - formDataIconMenuItem.setIcon(R.drawable.form_data_enabled); - } else { // Form data is disabled. - if (darkTheme) { - formDataIconMenuItem.setIcon(R.drawable.form_data_disabled_dark); - } else { - formDataIconMenuItem.setIcon(R.drawable.form_data_disabled_light); - } + // 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`. `this` references the current activity. + // `invalidateOptionsMenu` calls `onPrepareOptionsMenu()` and redraws the icons in the `AppBar`. if (runInvalidateOptionsMenu) { - ActivityCompat.invalidateOptionsMenu(this); + invalidateOptionsMenu(); } } @@ -3406,7 +4289,7 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD } private void loadBookmarksFolder() { - // Update `bookmarksCursor` with the contents of the bookmarks database for the current folder. + // Update the bookmarks cursor with the contents of the bookmarks database for the current folder. bookmarksCursor = bookmarksDatabaseHelper.getAllBookmarksCursorByDisplayOrder(currentBookmarksFolder); // Populate the bookmarks cursor adapter. `this` specifies the `Context`. `false` disables `autoRequery`. @@ -3423,7 +4306,7 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD ImageView bookmarkFavoriteIcon = view.findViewById(R.id.bookmark_favorite_icon); TextView bookmarkNameTextView = view.findViewById(R.id.bookmark_name); - // Get the favorite icon byte array from the `Cursor`. + // Get the favorite icon byte array from the cursor. byte[] favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON)); // Convert the byte array to a `Bitmap` beginning at the first byte and ending at the last. @@ -3455,4 +4338,4 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD bookmarksTitleTextView.setText(currentBookmarksFolder); } } -} +} \ No newline at end of file