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=c4715ec5a0fb4ae144dcf1c0c6a5f24e86956f32;hp=f21cae48e51812b8f606a520d45bd2f0049ccb2b;hb=6849b42dea845a29d5cfbc709b25b134846754e9;hpb=08a9cd90dcf5977eb64c26e1bbdbc2bfc65be9b5 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 f21cae48..c4715ec5 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java @@ -1,5 +1,5 @@ /* - * Copyright © 2015-2017 Soren Stoutner . + * Copyright © 2015-2018 Soren Stoutner . * * Download cookie code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner . * @@ -34,6 +34,8 @@ import android.content.SharedPreferences; import android.content.res.Configuration; import android.database.Cursor; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Typeface; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; @@ -41,15 +43,20 @@ import android.net.http.SslCertificate; import android.net.http.SslError; import android.os.Build; import android.os.Bundle; +import android.os.Handler; import android.preference.PreferenceManager; import android.print.PrintDocumentAdapter; import android.print.PrintManager; import android.support.annotation.NonNull; import android.support.design.widget.CoordinatorLayout; +import android.support.design.widget.FloatingActionButton; import android.support.design.widget.NavigationView; import android.support.design.widget.Snackbar; -import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; +// `ShortcutInfoCompat`, `ShortcutManagerCompat`, and `IconCompat` can be switched to the non-compat version once API >= 26. +import android.support.v4.content.pm.ShortcutInfoCompat; +import android.support.v4.content.pm.ShortcutManagerCompat; +import android.support.v4.graphics.drawable.IconCompat; import android.support.v4.view.GravityCompat; import android.support.v4.widget.DrawerLayout; import android.support.v4.widget.SwipeRefreshLayout; @@ -59,7 +66,9 @@ import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatDialogFragment; import android.support.v7.widget.Toolbar; import android.text.Editable; +import android.text.Spanned; import android.text.TextWatcher; +import android.text.style.ForegroundColorSpan; import android.util.Patterns; import android.view.ContextMenu; import android.view.GestureDetector; @@ -68,10 +77,11 @@ import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; +import android.view.ViewGroup; import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; import android.webkit.CookieManager; -import android.webkit.DownloadListener; +import android.webkit.HttpAuthHandler; import android.webkit.SslErrorHandler; import android.webkit.WebBackForwardList; import android.webkit.WebChromeClient; @@ -80,56 +90,79 @@ import android.webkit.WebStorage; import android.webkit.WebView; import android.webkit.WebViewClient; import android.webkit.WebViewDatabase; +import android.widget.CursorAdapter; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; +import android.widget.ListView; import android.widget.ProgressBar; +import android.widget.RadioButton; import android.widget.RelativeLayout; import android.widget.TextView; import com.stoutner.privacybrowser.BannerAd; import com.stoutner.privacybrowser.BuildConfig; import com.stoutner.privacybrowser.R; +import com.stoutner.privacybrowser.dialogs.AddDomainDialog; +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.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.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 NavigationView.OnNavigationItemSelectedListener, CreateHomeScreenShortcutDialog.CreateHomeScreenSchortcutListener, - SslCertificateErrorDialog.SslCertificateErrorListener, DownloadFileDialog.DownloadFileListener, DownloadImageDialog.DownloadImageListener, UrlHistoryDialog.UrlHistoryListener { - - // `appBar` is public static so it can be accessed from `OrbotProxyHelper`. It is also used in `onCreate()`, `onOptionsItemSelected()`, `closeFindOnPage()`, and `applyAppSettings()`. - public static ActionBar appBar; - - // `favoriteIconBitmap` is public static so it can be accessed from `CreateHomeScreenShortcutDialog`, `BookmarksActivity`, `CreateBookmarkDialog`, `CreateBookmarkFolderDialog`, `EditBookmarkDialog`, `EditBookmarkFolderDialog`, `ViewSslCertificateDialog`. - // It is also used in `onCreate()`, `onCreateHomeScreenShortcutCreate()`, and `applyDomainSettings`. +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 { + + // `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()`. + 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()`. 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 `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()`. @@ -138,24 +171,59 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // `webViewTitle` is public static so it can be accessed from `CreateBookmarkDialog` and `CreateHomeScreenShortcutDialog`. It is also used in `onCreate()`. public static String webViewTitle; - // `displayWebpageImagesBoolean` is public static so it can be accessed from `DomainSettingsFragment`. It is also used in `applyAppSettings` and `applyDomainSettings()`. - public static boolean displayWebpageImagesBoolean; + // `appliedUserAgentString` is public static so it can be accessed from `ViewSourceActivity`. It is also used in `applyDomainSettings()`. + public static String appliedUserAgentString; + + // `reloadOnRestart` is public static so it can be accessed from `SettingsFragment`. It is also used in `onRestart()` + public static boolean reloadOnRestart; + + // `reloadUrlOnRestart` is public static so it can be accessed from `SettingsFragment`. 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; + + // The block list versions are public static so they can be accessed from `AboutTabFragment`. They are also used in `onCreate()`. + public static String easyListVersion; + public static String easyPrivacyVersion; + public static String fanboyAnnoyanceVersion; + public static String fanboySocialVersion; + + // `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()`. + public static int domainSettingsDatabaseId; - // `navigatingHistory` is used in `onCreate()`, `onNavigationItemSelected()`, and `applyDomainSettings()`. + // The pinned domain SSL Certificate variables are public static so they can be accessed from `PinnedSslCertificateMismatchDialog`. They are also used in `onCreate()` and `applyDomainSettings()`. + public static String pinnedDomainSslIssuedToCNameString; + public static String pinnedDomainSslIssuedToONameString; + public static String pinnedDomainSslIssuedToUNameString; + public static String pinnedDomainSslIssuedByCNameString; + public static String pinnedDomainSslIssuedByONameString; + public static String pinnedDomainSslIssuedByUNameString; + public static Date pinnedDomainSslStartDate; + public static Date pinnedDomainSslEndDate; + + + // `appBar` is used in `onCreate()`, `onOptionsItemSelected()`, `closeFindOnPage()`, and `applyAppSettings()`. + private ActionBar appBar; + + // `navigatingHistory` is used in `onCreate()`, `onNavigationItemSelected()`, `onSslMismatchBack()`, and `applyDomainSettings()`. private boolean navigatingHistory; // `favoriteIconDefaultBitmap` is used in `onCreate()` and `applyDomainSettings`. private Bitmap favoriteIconDefaultBitmap; - // `drawerLayout` is used in `onCreate()`, `onNewIntent()`, and `onBackPressed()`. + // `drawerLayout` is used in `onCreate()`, `onNewIntent()`, `onBackPressed()`, and `onRestart()`. private DrawerLayout drawerLayout; // `rootCoordinatorLayout` is used in `onCreate()` and `applyAppSettings()`. private CoordinatorLayout rootCoordinatorLayout; - // `mainWebView` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`, `findNextOnPage()`, `closeFindOnPage()`, `loadUrlFromTextBox()` - // 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()`. @@ -176,35 +244,43 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // `customHeader` is used in `onCreate()`, `onOptionsItemSelected()`, `onCreateContextMenu()`, and `loadUrl()`. private final Map customHeaders = new HashMap<>(); - // `javaScriptEnabled` is also used in `onCreate()`, `onCreateOptionsMenu()`, `onOptionsItemSelected()`, `loadUrlFromTextBox()`, and `applyAppSettings()`. - // It is `Boolean` instead of `boolean` because `applyAppSettings()` needs to know if it is `null`. - private Boolean javaScriptEnabled; + // `javaScriptEnabled` is also used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `applyDomainSettings()`, and `updatePrivacyIcons()`. + private boolean javaScriptEnabled; - // `firstPartyCookiesEnabled` is used in `onCreate()`, `onCreateOptionsMenu()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onDownloadImage()`, `onDownloadFile()`, and `applyAppSettings()`. + // `firstPartyCookiesEnabled` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onDownloadImage()`, `onDownloadFile()`, and `applyDomainSettings()`. private boolean firstPartyCookiesEnabled; - // `thirdPartyCookiesEnabled` used in `onCreate()`, `onCreateOptionsMenu()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyAppSettings()`. + // `thirdPartyCookiesEnabled` used in `onCreate()`, `onPrepareOptionsMenu()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyDomainSettings()`. private boolean thirdPartyCookiesEnabled; - // `domStorageEnabled` is used in `onCreate()`, `onCreateOptionsMenu()`, `onOptionsItemSelected()`, and `applyAppSettings()`. + // `domStorageEnabled` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyDomainSettings()`. private boolean domStorageEnabled; - // `saveFormDataEnabled` is used in `onCreate()`, `onCreateOptionsMenu()`, `onOptionsItemSelected()`, and `applyAppSettings()`. + // `saveFormDataEnabled` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyDomainSettings()`. 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; // `searchURL` is used in `loadURLFromTextBox()` and `applyAppSettings()`. private String searchURL; - // `adBlockerEnabled` is used in `onCreate()` and `applyAppSettings()`. - private boolean adBlockerEnabled; + // The block list variables are used in `onCreate()` and `applyAppSettings()`. + private boolean easyListEnabled; + private boolean easyPrivacyEnabled; + private boolean fanboyAnnoyanceListEnabled; + private boolean fanboySocialBlockingListEnabled; - // `privacyBrowserRuntime` is used in `onCreate()` and `applyAppSettings()`. + // `privacyBrowserRuntime` is used in `onCreate()`, `onOptionsItemSelected()`, and `applyAppSettings()`. private Runtime privacyBrowserRuntime; // `incognitoModeEnabled` is used in `onCreate()` and `applyAppSettings()`. @@ -222,13 +298,22 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // `translucentNavigationBarOnFullscreen` is used in `onCreate()` and `applyAppSettings()`. private boolean translucentNavigationBarOnFullscreen; - // `currentDomainName` is used in `onCreate()`, `onNavigationItemSelected()`, and `applyDomainSettings()`. + // `reapplyDomainSettingsOnRestart` is used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, and `onAddDomain()`, . + private boolean reapplyDomainSettingsOnRestart; + + // `reapplyAppSettingsOnRestart` is used in `onNavigationItemSelected()` and `onRestart()`. + private boolean reapplyAppSettingsOnRestert; + + // `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()`. private boolean waitingForOrbot; - // `domainSettingsApplied` is used in `applyDomainSettings()` and `setDisplayWebpageImages()`. + // `domainSettingsApplied` is used in `prepareOptionsMenu()`, `applyDomainSettings()`, and `setDisplayWebpageImages()`. private boolean domainSettingsApplied; // `displayWebpageImagesInt` is used in `applyDomainSettings()` and `setDisplayWebpageImages()`. @@ -237,13 +322,10 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // `onTheFlyDisplayImagesSet` is used in `applyDomainSettings()` and `setDisplayWebpageImages()`. private boolean onTheFlyDisplayImagesSet; - // `loadingNewIntentBoolean` is used in `onNewIntent()` and `onRestart()`. - private boolean loadingNewIntentBoolean; - // `waitingForOrbotData` is used in `onCreate()` and `applyAppSettings()`. private String waitingForOrbotHTMLString; - // `privateDataDirectoryString` is used in `onCreate()` and `onNavigationItemSelected()`. + // `privateDataDirectoryString` is used in `onCreate()`, `onOptionsItemSelected()`, and `onNavigationItemSelected()`. private String privateDataDirectoryString; // `findOnPageLinearLayout` is used in `onCreate()`, `onOptionsItemSelected()`, and `closeFindOnPage()`. @@ -261,33 +343,83 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // `supportAppBar` is used in `onCreate()`, `onOptionsItemSelected()`, and `closeFindOnPage()`. private Toolbar supportAppBar; - // `urlTextBox` is used in `onCreate()`, `onOptionsItemSelected()`, `loadUrlFromTextBox()`, and `loadUrl()`. + // `urlTextBox` is used in `onCreate()`, `onOptionsItemSelected()`, `loadUrlFromTextBox()`, `loadUrl()`, and `highlightUrlText()`. private EditText urlTextBox; + // The color spans are used in `onCreate()` and `highlightUrlText()`. + private ForegroundColorSpan redColorSpan; + 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; + // `httpAuthHandler` is used in `onCreate()`, `onHttpAuthenticationCancel()`, and `onHttpAuthenticationProceed()`. + private static HttpAuthHandler httpAuthHandler; + // `inputMethodManager` is used in `onOptionsItemSelected()`, `loadUrlFromTextBox()`, and `closeFindOnPage()`. private InputMethodManager inputMethodManager; // `mainWebViewRelativeLayout` is used in `onCreate()` and `onNavigationItemSelected()`. private RelativeLayout mainWebViewRelativeLayout; + // `urlIsLoading` is used in `onCreate()`, `loadUrl()`, and `applyDomainSettings()`. + private boolean urlIsLoading; + + // `pinnedDomainSslCertificate` is used in `onCreate()` and `applyDomainSettings()`. + private boolean pinnedDomainSslCertificate; + + // `bookmarksDatabaseHelper` is used in `onCreate()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`. + private BookmarksDatabaseHelper bookmarksDatabaseHelper; + + // `bookmarksListView` is used in `onCreate()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, and `loadBookmarksFolder()`. + private ListView bookmarksListView; + + // `bookmarksTitleTextView` is used in `onCreate()` and `loadBookmarksFolder()`. + private TextView bookmarksTitleTextView; + + // `bookmarksCursor` is used in `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`. + private Cursor bookmarksCursor; + + // `bookmarksCursorAdapter` is used in `onCreateBookmark()`, `onCreateBookmarkFolder()` `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`. + private CursorAdapter bookmarksCursorAdapter; + + // `oldFolderNameString` is used in `onCreate()` and `onSaveEditBookmarkFolder()`. + private String oldFolderNameString; + @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") + @SuppressLint({"SetJavaScriptEnabled"}) + // Remove Android Studio's warning about deprecations. We have to use the deprecated `getColor()` until API >= 23. + @SuppressWarnings("deprecation") protected void onCreate(Bundle savedInstanceState) { + // Get a handle for `sharedPreferences`. `this` references the current context. + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + + // Get the theme preference. + darkTheme = sharedPreferences.getBoolean("dark_theme", false); + + // Set the activity theme. + if (darkTheme) { + setTheme(R.style.PrivacyBrowserDark); + } else { + setTheme(R.style.PrivacyBrowserLight); + } + + // Run the default commands. super.onCreate(savedInstanceState); + + // Set the content view. setContentView(R.layout.main_drawerlayout); // Get a handle for `inputMethodManager`. inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - // We need to use the `SupportActionBar` from `android.support.v7.app.ActionBar` until the minimum API is >= 21. - supportAppBar = (Toolbar) findViewById(R.id.app_bar); + // `SupportActionBar` from `android.support.v7.app.ActionBar` must be used until the minimum API is >= 21. + supportAppBar = findViewById(R.id.app_bar); setSupportActionBar(supportAppBar); appBar = getSupportActionBar(); @@ -298,25 +430,42 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation appBar.setCustomView(R.layout.url_app_bar); appBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM); - // Set the "go" button on the keyboard to load the URL in urlTextBox. - urlTextBox = (EditText) appBar.getCustomView().findViewById(R.id.url_edittext); - urlTextBox.setOnKeyListener(new View.OnKeyListener() { - @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { - // If the event is a key-down event on the `enter` button, load the URL. - if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { - // Load the URL into the mainWebView and consume the event. - try { - loadUrlFromTextBox(); - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } - // If the enter key was pressed, consume the event. - return true; - } else { - // If any other key was pressed, do not consume the event. - return false; + // Initialize the foreground color spans for highlighting the URLs. We have to use the deprecated `getColor()` until API >= 23. + redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_a700)); + initialGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500)); + finalGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500)); + + // Get a handle for `urlTextBox`. + urlTextBox = findViewById(R.id.url_edittext); + + // Remove the formatting from `urlTextBar` when the user is editing the text. + urlTextBox.setOnFocusChangeListener((View v, boolean hasFocus) -> { + if (hasFocus) { // The user is editing `urlTextBox`. + // Remove the highlighting. + urlTextBox.getText().removeSpan(redColorSpan); + urlTextBox.getText().removeSpan(initialGrayColorSpan); + urlTextBox.getText().removeSpan(finalGrayColorSpan); + } else { // The user has stopped editing `urlTextBox`. + // Reapply the highlighting. + highlightUrlText(); + } + }); + + // Set the go button on the keyboard to load the URL in `urlTextBox`. + urlTextBox.setOnKeyListener((View v, int keyCode, KeyEvent event) -> { + // If the event is a key-down event on the `enter` button, load the URL. + if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { + // Load the URL into the mainWebView and consume the event. + try { + loadUrlFromTextBox(); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); } + // If the enter key was pressed, consume the event. + return true; + } else { + // If any other key was pressed, do not consume the event. + return false; } }); @@ -350,15 +499,59 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation this.registerReceiver(orbotStatusBroadcastReceiver, new IntentFilter("org.torproject.android.intent.action.STATUS")); // Get handles for views that need to be accessed. - drawerLayout = (DrawerLayout) findViewById(R.id.drawerlayout); - rootCoordinatorLayout = (CoordinatorLayout) findViewById(R.id.root_coordinatorlayout); - mainWebViewRelativeLayout = (RelativeLayout) findViewById(R.id.main_webview_relativelayout); - mainWebView = (WebView) findViewById(R.id.main_webview); - findOnPageLinearLayout = (LinearLayout) findViewById(R.id.find_on_page_linearlayout); - findOnPageEditText = (EditText) findViewById(R.id.find_on_page_edittext); - fullScreenVideoFrameLayout = (FrameLayout) findViewById(R.id.full_screen_video_framelayout); - urlAppBarRelativeLayout = (RelativeLayout) findViewById(R.id.url_app_bar_relativelayout); - favoriteIconImageView = (ImageView) findViewById(R.id.favorite_icon); + drawerLayout = findViewById(R.id.drawerlayout); + rootCoordinatorLayout = findViewById(R.id.root_coordinatorlayout); + bookmarksListView = findViewById(R.id.bookmarks_drawer_listview); + bookmarksTitleTextView = findViewById(R.id.bookmarks_title_textview); + FloatingActionButton launchBookmarksActivityFab = findViewById(R.id.launch_bookmarks_activity_fab); + FloatingActionButton createBookmarkFolderFab = findViewById(R.id.create_bookmark_folder_fab); + FloatingActionButton createBookmarkFab = findViewById(R.id.create_bookmark_fab); + mainWebViewRelativeLayout = findViewById(R.id.main_webview_relativelayout); + mainWebView = findViewById(R.id.main_webview); + findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout); + findOnPageEditText = findViewById(R.id.find_on_page_edittext); + fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout); + urlAppBarRelativeLayout = findViewById(R.id.url_app_bar_relativelayout); + favoriteIconImageView = findViewById(R.id.favorite_icon); + + // Set the bookmarks drawer resources according to the theme. This can't be done in the layout due to compatibility issues with the `DrawerLayout` support widget. + if (darkTheme) { + launchBookmarksActivityFab.setImageDrawable(getResources().getDrawable(R.drawable.bookmarks_dark)); + createBookmarkFolderFab.setImageDrawable(getResources().getDrawable(R.drawable.create_folder_dark)); + createBookmarkFab.setImageDrawable(getResources().getDrawable(R.drawable.create_bookmark_dark)); + bookmarksListView.setBackgroundColor(getResources().getColor(R.color.gray_850)); + } else { + launchBookmarksActivityFab.setImageDrawable(getResources().getDrawable(R.drawable.bookmarks_light)); + createBookmarkFolderFab.setImageDrawable(getResources().getDrawable(R.drawable.create_folder_light)); + createBookmarkFab.setImageDrawable(getResources().getDrawable(R.drawable.create_bookmark_light)); + bookmarksListView.setBackgroundColor(getResources().getColor(R.color.white)); + } + + // Set the launch bookmarks activity FAB to launch the bookmarks activity. + launchBookmarksActivityFab.setOnClickListener(v -> { + // Create an intent to launch the bookmarks activity. + Intent bookmarksIntent = new Intent(getApplicationContext(), BookmarksActivity.class); + + // Include the current folder with the `Intent`. + bookmarksIntent.putExtra("Current Folder", currentBookmarksFolder); + + // Make it so. + startActivity(bookmarksIntent); + }); + + // Set the create new bookmark folder FAB to display the `AlertDialog`. + 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`. + createBookmarkFab.setOnClickListener(view -> { + // Show the `CreateBookmarkDialog` `AlertDialog` and name the instance `@string/create_bookmark`. + AppCompatDialogFragment createBookmarkDialog = new CreateBookmarkDialog(); + createBookmarkDialog.show(getSupportFragmentManager(), getResources().getString(R.string.create_bookmark)); + }); // Create a double-tap listener to toggle full-screen mode. final GestureDetector gestureDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() { @@ -388,7 +581,7 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation /* 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); @@ -398,7 +591,8 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // 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); } @@ -438,12 +632,12 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation }); // Pass all touch events on `mainWebView` through `gestureDetector` to check for double-taps. - mainWebView.setOnTouchListener(new View.OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent event) { - // Send the `event` to `gestureDetector`. - return gestureDetector.onTouchEvent(event); - } + mainWebView.setOnTouchListener((View v, MotionEvent event) -> { + // Call `performClick()` on the view, which is required for accessibility. + v.performClick(); + + // Send the `event` to `gestureDetector`. + return gestureDetector.onTouchEvent(event); }); // Update `findOnPageCountTextView`. @@ -460,8 +654,11 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // `activeMatchOrdinal` is zero-based. int activeMatch = activeMatchOrdinal + 1; + // Build the match string. + String matchString = activeMatch + "/" + numberOfMatches; + // Set `findOnPageCountTextView`. - findOnPageCountTextView.setText(activeMatch + "/" + numberOfMatches); + findOnPageCountTextView.setText(matchString); } } }); @@ -486,37 +683,30 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation }); // Set the `check mark` button for the `findOnPageEditText` keyboard to close the soft keyboard. - findOnPageEditText.setOnKeyListener(new View.OnKeyListener() { - @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { - if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { // The `enter` key was pressed. - // Hide the soft keyboard. `0` indicates no additional flags. - inputMethodManager.hideSoftInputFromWindow(mainWebView.getWindowToken(), 0); + findOnPageEditText.setOnKeyListener((v, keyCode, event) -> { + if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { // The `enter` key was pressed. + // Hide the soft keyboard. `0` indicates no additional flags. + inputMethodManager.hideSoftInputFromWindow(mainWebView.getWindowToken(), 0); - // Consume the event. - return true; - } else { // A different key was pressed. - // Do not consume the event. - return false; - } + // Consume the event. + return true; + } else { // A different key was pressed. + // Do not consume the event. + return false; } }); // Implement swipe to refresh - swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipe_refreshlayout); + swipeRefreshLayout = findViewById(R.id.swipe_refreshlayout); swipeRefreshLayout.setColorSchemeResources(R.color.blue_700); - swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { - @Override - public void onRefresh() { - mainWebView.reload(); - } - }); + swipeRefreshLayout.setOnRefreshListener(() -> mainWebView.reload()); - // `DrawerTitle` identifies the `DrawerLayout` in accessibility mode. + // `DrawerTitle` identifies the `DrawerLayouts` in accessibility mode. drawerLayout.setDrawerTitle(GravityCompat.START, getString(R.string.navigation_drawer)); + drawerLayout.setDrawerTitle(GravityCompat.END, getString(R.string.bookmarks)); // Listen for touches on the navigation menu. - final NavigationView navigationView = (NavigationView) findViewById(R.id.navigationview); + final NavigationView navigationView = findViewById(R.id.navigationview); navigationView.setNavigationItemSelectedListener(this); // Get handles for `navigationMenu` and the back and forward menu items. The menu is zero-based, so items 1, 2, and 3 are the second, third, and fourth entries in the menu. @@ -525,228 +715,151 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation final MenuItem navigationForwardMenuItem = navigationMenu.getItem(2); final MenuItem navigationHistoryMenuItem = navigationMenu.getItem(3); - // The `DrawerListener` allows us to update the Navigation Menu. - drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() { - @Override - public void onDrawerSlide(View drawerView, float slideOffset) { - } - - @Override - public void onDrawerOpened(View drawerView) { - } - - @Override - public void onDrawerClosed(View drawerView) { - } + // 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`. + bookmarksDatabaseHelper = new BookmarksDatabaseHelper(this, null, null, 0); - @Override - public void onDrawerStateChanged(int newState) { - if ((newState == DrawerLayout.STATE_SETTLING) || (newState == DrawerLayout.STATE_DRAGGING)) { // The drawer is opening or closing. - // Update the `Back`, `Forward`, and `History` menu items. - navigationBackMenuItem.setEnabled(mainWebView.canGoBack()); - navigationForwardMenuItem.setEnabled(mainWebView.canGoForward()); - navigationHistoryMenuItem.setEnabled((mainWebView.canGoBack() || mainWebView.canGoForward())); + // Initialize `currentBookmarksFolder`. `""` is the home folder in the database. + currentBookmarksFolder = ""; - // Hide the keyboard so we can see the navigation menu. `0` indicates no additional flags. - inputMethodManager.hideSoftInputFromWindow(mainWebView.getWindowToken(), 0); - } - } - }); + // Load the home folder, which is `""` in the database. + loadBookmarksFolder(); - // 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); + bookmarksListView.setOnItemClickListener((parent, view, position, id) -> { + // Convert the id from long to int to match the format of the bookmarks database. + int databaseID = (int) id; - // Initialize `adServerSet`. - final Set adServersSet = new HashSet<>(); + // Get the bookmark `Cursor` for this ID and move it to the first row. + Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmarkCursor(databaseID); + bookmarkCursor.moveToFirst(); - // Load the list of ad servers into memory. - try { - // Load `pgl.yoyo.org_adservers.txt` into a `BufferedReader`. - BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(getAssets().open("pgl.yoyo.org_adservers.txt"))); + // Act upon the bookmark according to the type. + if (bookmarkCursor.getInt(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) { // The selected bookmark is a folder. + // Store the new folder name in `currentBookmarksFolder`. + currentBookmarksFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME)); - // Create a string for storing each ad server. - String adServer; + // Load the new folder. + loadBookmarksFolder(); + } else { // The selected bookmark is not a folder. + // Load the bookmark URL. + loadUrl(bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_URL))); - // Populate `adServersSet`. - while ((adServer = bufferedReader.readLine()) != null) { - adServersSet.add(adServer); + // Close the bookmarks drawer. + drawerLayout.closeDrawer(GravityCompat.END); } - // Close `bufferedReader`. - bufferedReader.close(); - } catch (IOException ioException) { - // We're pretty sure the asset exists, so we don't need to worry about the `IOException` ever being thrown. - } + // Close the `Cursor`. + bookmarkCursor.close(); + }); - mainWebView.setWebViewClient(new WebViewClient() { - // `shouldOverrideUrlLoading` makes this `WebView` the default handler for URLs inside the app, so that links are not kicked out to other apps. - // We have to use the deprecated `shouldOverrideUrlLoading` until API >= 24. - @SuppressWarnings("deprecation") - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - if (url.startsWith("mailto:")) { // Load the URL in an external email program because it begins with `mailto:`. - // We use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched. - Intent emailIntent = new Intent(Intent.ACTION_SENDTO); + bookmarksListView.setOnItemLongClickListener((parent, view, position, id) -> { + // Convert the database ID from `long` to `int`. + int databaseId = (int) id; - // Parse the url and set it as the data for the `Intent`. - emailIntent.setData(Uri.parse(url)); + // Find out if the selected bookmark is a folder. + boolean isFolder = bookmarksDatabaseHelper.isFolder(databaseId); - // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser. - emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + if (isFolder) { + // Save the current folder name, which is used in `onSaveEditBookmarkFolder()`. + oldFolderNameString = bookmarksCursor.getString(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME)); - // Make it so. - startActivity(emailIntent); + // Show the edit bookmark folder `AlertDialog` and name the instance `@string/edit_folder`. + AppCompatDialogFragment editFolderDialog = EditBookmarkFolderDialog.folderDatabaseId(databaseId); + editFolderDialog.show(getSupportFragmentManager(), getResources().getString(R.string.edit_folder)); + } else { + // Show the edit bookmark `AlertDialog` and name the instance `@string/edit_bookmark`. + AppCompatDialogFragment editBookmarkDialog = EditBookmarkDialog.bookmarkDatabaseId(databaseId); + editBookmarkDialog.show(getSupportFragmentManager(), getResources().getString(R.string.edit_bookmark)); + } - // Returning `true` indicates the application is handling the URL. - return true; - } else { // Load the URL in Privacy Browser. - // Apply the domain settings for the new URL. - applyDomainSettings(url); + // Consume the event. + return true; + }); - // Returning `false` causes the current `WebView` to handle the URL and prevents it from adding redirects to the history list. - return false; - } + // The `DrawerListener` allows us to update the Navigation Menu. + drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() { + @Override + public void onDrawerSlide(@NonNull View drawerView, float slideOffset) { } - // Block ads. We have to use the deprecated `shouldInterceptRequest` until minimum API >= 21. - @SuppressWarnings("deprecation") @Override - public WebResourceResponse shouldInterceptRequest(WebView view, String url){ - if (adBlockerEnabled) { // Block ads. - // Extract the host from `url`. - Uri requestUri = Uri.parse(url); - String requestHost = requestUri.getHost(); - - // 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; - } - - // Strip out the lowest subdomain of `requestHost`. - requestHost = requestHost.substring(requestHost.indexOf(".") + 1); - } - } - - if (requestHostIsAdServer) { // It is an ad server. - // Return an empty `WebResourceResponse`. - return new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes())); - } else { // It is not an ad server. - // `return null` loads the requested resource. - return null; - } - } else { // Ad blocking is disabled. - // `return null` loads the requested resource. - return null; - } + public void onDrawerOpened(@NonNull View drawerView) { } - // Update the URL in urlTextBox when the page starts to load. @Override - public void onPageStarted(WebView view, String url, Bitmap favicon) { - // Reset `webViewTitle` - webViewTitle = getString(R.string.no_title); - - // Check to see if we are waiting on Orbot. - if (!waitingForOrbot) { // We are not waiting on Orbot, so we need to process the URL. - // We need to update `formattedUrlString` at the beginning of the load, so that if the user toggles JavaScript during the load the new website is reloaded. - formattedUrlString = url; - - // Display the loading URL is the URL text box. - urlTextBox.setText(url); - - // Apply any custom domain settings if the URL was loaded by navigating history. - if (navigatingHistory) { - applyDomainSettings(url); - } - } + public void onDrawerClosed(@NonNull View drawerView) { } - // Update formattedUrlString and urlTextBox. It is necessary to do this after the page finishes loading because the final URL can change during load. @Override - public void onPageFinished(WebView view, String url) { - // Clear the cache and history if Incognito Mode is enabled. - if (incognitoModeEnabled) { - // Clear the cache. `true` includes disk files. - mainWebView.clearCache(true); - - // Clear the back/forward history. - mainWebView.clearHistory(); - - // Manually delete cache folders. - try { - // Delete the main `cache` folder. - 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"); - } catch (IOException e) { - // Do nothing if an error is thrown. - } - } - - // Update `urlTextBox` and apply domain settings if not waiting on Orbot. - if (!waitingForOrbot) { - // Check to see if `WebView` has set `url` to be `about:blank`. - if (url.equals("about:blank")) { // `WebView` is blank, so `formattedUrlString` should be `""` and `urlTextBox` should display a hint. - // Set `formattedUrlString` to `""`. - formattedUrlString = ""; - - // Update `urlTextBox`. - urlTextBox.setText(formattedUrlString); - - // Request focus for `urlTextBox`. - urlTextBox.requestFocus(); - - // Display the keyboard. - inputMethodManager.showSoftInput(urlTextBox, 0); - - // Apply the domain settings. - applyDomainSettings(formattedUrlString); - } else { // `WebView` has loaded a webpage. - // Set `formattedUrlString`. - formattedUrlString = url; + public void onDrawerStateChanged(int newState) { + if ((newState == DrawerLayout.STATE_SETTLING) || (newState == DrawerLayout.STATE_DRAGGING)) { // The drawer is opening or closing. + // Update the `Back`, `Forward`, and `History` menu items. + navigationBackMenuItem.setEnabled(mainWebView.canGoBack()); + navigationForwardMenuItem.setEnabled(mainWebView.canGoForward()); + navigationHistoryMenuItem.setEnabled((mainWebView.canGoBack() || mainWebView.canGoForward())); - // Only update `urlTextBox` if the user is not typing in it. - if (!urlTextBox.hasFocus()) { - urlTextBox.setText(formattedUrlString); - } - } + // Hide the keyboard (if displayed) so we can see the navigation menu. `0` indicates no additional flags. + inputMethodManager.hideSoftInputFromWindow(mainWebView.getWindowToken(), 0); - // Store the SSL certificate so it can be accessed from `ViewSslCertificateDialog`. - sslCertificate = mainWebView.getCertificate(); + // Clear the focus from `urlTextBox` if it has it. + urlTextBox.clearFocus(); } } - - // Handle SSL Certificate errors. - @Override - public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { - // Store `handler` so it can be accesses from `onSslErrorCancel()` and `onSslErrorProceed()`. - sslErrorHandler = handler; - - // Display the SSL error `AlertDialog`. - AppCompatDialogFragment sslCertificateErrorDialogFragment = SslCertificateErrorDialog.displayDialog(error); - sslCertificateErrorDialogFragment.show(getSupportFragmentManager(), getResources().getString(R.string.ssl_certificate_error)); - } }); + // 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 = (ProgressBar) findViewById(R.id.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); } @@ -788,7 +901,7 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation /* 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); @@ -827,13 +940,10 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation registerForContextMenu(mainWebView); // Allow the downloading of files. - mainWebView.setDownloadListener(new DownloadListener() { - @Override - public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) { - // Show the `DownloadFileDialog` `AlertDialog` and name this instance `@string/download`. - AppCompatDialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(url, contentDisposition, contentLength); - downloadFileDialogFragment.show(getSupportFragmentManager(), getResources().getString(R.string.download)); - } + 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. @@ -848,6 +958,9 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // 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(); @@ -872,7 +985,8 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation 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`. + 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; @@ -886,13 +1000,15 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation 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. + // Initialize `favoriteIconBitmap`. `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. @@ -903,7 +1019,312 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // Apply the app settings from the shared preferences. applyAppSettings(); - // Load `formattedUrlString` if we are not waiting for Orbot to connect. + // Instantiate the block list helper. + BlockListHelper blockListHelper = new BlockListHelper(); + + // Parse the block lists. + final ArrayList> easyList = blockListHelper.parseBlockList(getAssets(), "blocklists/easylist.txt"); + final ArrayList> easyPrivacy = blockListHelper.parseBlockList(getAssets(), "blocklists/easyprivacy.txt"); + final ArrayList> fanboyAnnoyance = blockListHelper.parseBlockList(getAssets(), "blocklists/fanboy-annoyance.txt"); + final ArrayList> fanboySocial = blockListHelper.parseBlockList(getAssets(), "blocklists/fanboy-social.txt"); + + // Store the list versions. + easyListVersion = easyList.get(0).get(0)[0]; + easyPrivacyVersion = easyPrivacy.get(0).get(0)[0]; + fanboyAnnoyanceVersion = fanboyAnnoyance.get(0).get(0)[0]; + fanboySocialVersion = fanboySocial.get(0).get(0)[0]; + + mainWebView.setWebViewClient(new WebViewClient() { + // `shouldOverrideUrlLoading` makes this `WebView` the default handler for URLs inside the app, so that links are not kicked out to other apps. + // We have to use the deprecated `shouldOverrideUrlLoading` until API >= 24. + @SuppressWarnings("deprecation") + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + if (url.startsWith("mailto:")) { // Load the 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`. + emailIntent.setData(Uri.parse(url)); + + // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser. + emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + // Make it so. + startActivity(emailIntent); + + // Returning `true` indicates the application is handling the URL. + 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. + 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. + dialIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + // Make it so. + startActivity(dialIntent); + + // Returning `true` indicates the application is handling the URL. + return true; + } else { // Load the URL in Privacy Browser. + // Apply the domain settings for the new URL. + applyDomainSettings(url, true); + + // Returning `false` causes the current `WebView` to handle the URL and prevents it from adding redirects to the history list. + return false; + } + } + + // Check requests against the block lists. The deprecated `shouldInterceptRequest` must be used until minimum API >= 21. + @SuppressWarnings("deprecation") + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, String url){ + // Create an empty web resource response to be used if the resource request is blocked. + WebResourceResponse emptyWebResourceResponse = new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes())); + + // Check EasyList if it is enabled. + if (easyListEnabled) { + if (blockListHelper.isBlocked(formattedUrlString, url, easyList)) { + // The resource request was blocked. Return an empty web resource response. + return emptyWebResourceResponse; + } + } + + // Check EasyPrivacy if it is enabled. + if (easyPrivacyEnabled) { + if (blockListHelper.isBlocked(formattedUrlString, url, easyPrivacy)) { + // The resource request was blocked. Return an empty web resource response. + return emptyWebResourceResponse; + } + } + + // Check Fanboy’s Annoyance List if it is enabled. + if (fanboyAnnoyanceListEnabled) { + if (blockListHelper.isBlocked(formattedUrlString, url, fanboyAnnoyance)) { + // The resource request was blocked. Return an empty web resource response. + return emptyWebResourceResponse; + } + } else if (fanboySocialBlockingListEnabled){ // Only check Fanboy’s Social Blocking List if Fanboy’s Annoyance List is disabled. + if (blockListHelper.isBlocked(formattedUrlString, url, fanboySocial)) { + // The resource request was blocked. Return an empty web resource response. + return emptyWebResourceResponse; + } + } + + // The resource request has not been blocked. `return null` loads the requested resource. + return null; + } + + // Handle HTTP authentication requests. + @Override + public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) { + // Store `handler` so it can be accessed from `onHttpAuthenticationCancel()` and `onHttpAuthenticationProceed()`. + httpAuthHandler = handler; + + // Display the HTTP authentication dialog. + AppCompatDialogFragment httpAuthenticationDialogFragment = HttpAuthenticationDialog.displayDialog(host, realm); + httpAuthenticationDialogFragment.show(getSupportFragmentManager(), getString(R.string.http_authentication)); + } + + // Update the URL in urlTextBox when the page starts to load. + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) {// If night mode is enabled, hide `mainWebView` until after the night mode CSS is applied. + if (nightMode) { + mainWebView.setVisibility(View.INVISIBLE); + } + + // Hide the keyboard. `0` indicates no additional flags. + inputMethodManager.hideSoftInputFromWindow(mainWebView.getWindowToken(), 0); + + // Check to see if we are waiting on Orbot. + if (!waitingForOrbot) { // We are not waiting on Orbot, so we need to process the URL. + // We need to update `formattedUrlString` at the beginning of the load, so that if the user toggles JavaScript during the load the new website is reloaded. + formattedUrlString = url; + + // Display the formatted URL text. + urlTextBox.setText(formattedUrlString); + + // Apply text highlighting to `urlTextBox`. + highlightUrlText(); + + // Apply any custom domain settings if the URL was loaded by navigating history. + if (navigatingHistory) { + applyDomainSettings(url, true); + } + + // 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; + } + } + + // 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(); + } + + // Reset `urlIsLoading`, which is used to prevent reloads on redirect if the user agent changes. + urlIsLoading = false; + + // Clear the cache and history if Incognito Mode is enabled. + if (incognitoModeEnabled) { + // Clear the cache. `true` includes disk files. + mainWebView.clearCache(true); + + // Clear the back/forward history. + mainWebView.clearHistory(); + + // Manually delete cache folders. + try { + // Delete the main `cache` folder. + 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"); + } catch (IOException e) { + // Do nothing if an error is thrown. + } + } + + // Update `urlTextBox` and apply domain settings if not waiting on Orbot. + if (!waitingForOrbot) { + // Check to see if `WebView` has set `url` to be `about:blank`. + if (url.equals("about:blank")) { // `WebView` is blank, so `formattedUrlString` should be `""` and `urlTextBox` should display a hint. + // Set `formattedUrlString` to `""`. + formattedUrlString = ""; + + urlTextBox.setText(formattedUrlString); + + // Request focus for `urlTextBox`. + urlTextBox.requestFocus(); + + // Display the keyboard. + inputMethodManager.showSoftInput(urlTextBox, 0); + + // Apply the domain settings. This clears any settings from the previous domain. + applyDomainSettings(formattedUrlString, true); + } else { // `WebView` has loaded a webpage. + // Set `formattedUrlString`. + formattedUrlString = url; + + // Only update `urlTextBox` if the user is not typing in it. + if (!urlTextBox.hasFocus()) { + // Display the formatted URL text. + urlTextBox.setText(formattedUrlString); + + // Apply text highlighting to `urlTextBox`. + highlightUrlText(); + } + } + + // Store the SSL certificate so it can be accessed from `ViewSslCertificateDialog` and `PinnedSslCertificateMismatchDialog`. + sslCertificate = mainWebView.getCertificate(); + + // Check the current website SSL certificate against the pinned SSL certificate if there is a pinned SSL certificate the user has not chosen to ignore it for this session. + if (pinnedDomainSslCertificate && !ignorePinnedSslCertificate) { + // Initialize the current SSL certificate variables. + String currentWebsiteIssuedToCName = ""; + String currentWebsiteIssuedToOName = ""; + String currentWebsiteIssuedToUName = ""; + String currentWebsiteIssuedByCName = ""; + String currentWebsiteIssuedByOName = ""; + String currentWebsiteIssuedByUName = ""; + Date currentWebsiteSslStartDate = null; + Date currentWebsiteSslEndDate = null; + + + // Extract the individual pieces of information from the current website SSL certificate if it is not null. + if (sslCertificate != null) { + currentWebsiteIssuedToCName = sslCertificate.getIssuedTo().getCName(); + currentWebsiteIssuedToOName = sslCertificate.getIssuedTo().getOName(); + currentWebsiteIssuedToUName = sslCertificate.getIssuedTo().getUName(); + currentWebsiteIssuedByCName = sslCertificate.getIssuedBy().getCName(); + currentWebsiteIssuedByOName = sslCertificate.getIssuedBy().getOName(); + currentWebsiteIssuedByUName = sslCertificate.getIssuedBy().getUName(); + currentWebsiteSslStartDate = sslCertificate.getValidNotBeforeDate(); + currentWebsiteSslEndDate = sslCertificate.getValidNotAfterDate(); + } + + // Initialize `String` variables to store the SSL certificate dates. `Strings` are needed to compare the values below, which doesn't work with `Dates` if they are `null`. + String currentWebsiteSslStartDateString = ""; + String currentWebsiteSslEndDateString = ""; + String pinnedDomainSslStartDateString = ""; + String pinnedDomainSslEndDateString = ""; + + // Convert the `Dates` to `Strings` if they are not `null`. + if (currentWebsiteSslStartDate != null) { + currentWebsiteSslStartDateString = currentWebsiteSslStartDate.toString(); + } + + if (currentWebsiteSslEndDate != null) { + currentWebsiteSslEndDateString = currentWebsiteSslEndDate.toString(); + } + + if (pinnedDomainSslStartDate != null) { + pinnedDomainSslStartDateString = pinnedDomainSslStartDate.toString(); + } + + if (pinnedDomainSslEndDate != null) { + pinnedDomainSslEndDateString = pinnedDomainSslEndDate.toString(); + } + + // Check to see if the pinned SSL certificate matches the current website certificate. + if (!currentWebsiteIssuedToCName.equals(pinnedDomainSslIssuedToCNameString) || !currentWebsiteIssuedToOName.equals(pinnedDomainSslIssuedToONameString) || + !currentWebsiteIssuedToUName.equals(pinnedDomainSslIssuedToUNameString) || !currentWebsiteIssuedByCName.equals(pinnedDomainSslIssuedByCNameString) || + !currentWebsiteIssuedByOName.equals(pinnedDomainSslIssuedByONameString) || !currentWebsiteIssuedByUName.equals(pinnedDomainSslIssuedByUNameString) || + !currentWebsiteSslStartDateString.equals(pinnedDomainSslStartDateString) || !currentWebsiteSslEndDateString.equals(pinnedDomainSslEndDateString)) { + // The pinned SSL certificate doesn't match the current domain certificate. + //Display the pinned SSL certificate mismatch `AlertDialog`. + AppCompatDialogFragment pinnedSslCertificateMismatchDialogFragment = new PinnedSslCertificateMismatchDialog(); + pinnedSslCertificateMismatchDialogFragment.show(getSupportFragmentManager(), getString(R.string.ssl_certificate_mismatch)); + } + } + } + } + + // Handle SSL Certificate errors. + @Override + public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { + // Get the current website SSL certificate. + SslCertificate currentWebsiteSslCertificate = error.getCertificate(); + + // Extract the individual pieces of information from the current website SSL certificate. + String currentWebsiteIssuedToCName = currentWebsiteSslCertificate.getIssuedTo().getCName(); + String currentWebsiteIssuedToOName = currentWebsiteSslCertificate.getIssuedTo().getOName(); + String currentWebsiteIssuedToUName = currentWebsiteSslCertificate.getIssuedTo().getUName(); + String currentWebsiteIssuedByCName = currentWebsiteSslCertificate.getIssuedBy().getCName(); + String currentWebsiteIssuedByOName = currentWebsiteSslCertificate.getIssuedBy().getOName(); + String currentWebsiteIssuedByUName = currentWebsiteSslCertificate.getIssuedBy().getUName(); + Date currentWebsiteSslStartDate = currentWebsiteSslCertificate.getValidNotBeforeDate(); + Date currentWebsiteSslEndDate = currentWebsiteSslCertificate.getValidNotAfterDate(); + + // Proceed to the website if the current SSL website certificate matches the pinned domain certificate. + if (pinnedDomainSslCertificate && + currentWebsiteIssuedToCName.equals(pinnedDomainSslIssuedToCNameString) && currentWebsiteIssuedToOName.equals(pinnedDomainSslIssuedToONameString) && + currentWebsiteIssuedToUName.equals(pinnedDomainSslIssuedToUNameString) && currentWebsiteIssuedByCName.equals(pinnedDomainSslIssuedByCNameString) && + currentWebsiteIssuedByOName.equals(pinnedDomainSslIssuedByONameString) && currentWebsiteIssuedByUName.equals(pinnedDomainSslIssuedByUNameString) && + currentWebsiteSslStartDate.equals(pinnedDomainSslStartDate) && currentWebsiteSslEndDate.equals(pinnedDomainSslEndDate)) { + // An SSL certificate is pinned and matches the current domain certificate. + // Proceed to the website without displaying an error. + handler.proceed(); + } else { // Either there isn't a pinned SSL certificate or it doesn't match the current website certificate. + // Store `handler` so it can be accesses from `onSslErrorCancel()` and `onSslErrorProceed()`. + sslErrorHandler = handler; + + // Display the SSL error `AlertDialog`. + AppCompatDialogFragment sslCertificateErrorDialogFragment = SslCertificateErrorDialog.displayDialog(error); + sslCertificateErrorDialogFragment.show(getSupportFragmentManager(), getString(R.string.ssl_certificate_error)); + } + } + }); + + // Load the website if not waiting for Orbot to connect. if (!waitingForOrbot) { loadUrl(formattedUrlString); } @@ -911,55 +1332,89 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation @Override protected void onNewIntent(Intent intent) { - // Set `loadingNewIntentBoolean`. - loadingNewIntentBoolean = true; - // 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. 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(formattedUrlString); - // 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(); + // Clear the keyboard if displayed and remove the focus on the urlTextBar if it has it. + mainWebView.requestFocus(); + } } @Override public void onRestart() { + // Run the default commands. super.onRestart(); - // Apply the app settings, which may have been changed in `SettingsActivity`. - applyAppSettings(); + // Apply the app settings if returning from the Settings activity.. + if (reapplyAppSettingsOnRestert) { + // Apply the app settings. + applyAppSettings(); - // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. - updatePrivacyIcons(true); + // Reload the webpage if displaying of images has been disabled in the Settings activity. + if (reloadOnRestart) { + // Reload `mainWebView`. + mainWebView.reload(); + + // Reset `reloadOnRestartBoolean`. + reloadOnRestart = false; + } - // Set the display webpage images mode. - setDisplayWebpageImages(); + // Reset the return from settings flag. + reapplyAppSettingsOnRestert = false; + } + + // Apply the domain settings if returning from the Domains activity. + if (reapplyDomainSettingsOnRestart) { + // Reapply the domain settings. + applyDomainSettings(formattedUrlString, false); - // Only reload `mainWebView` if not loading a new intent and not waiting for Orbot. - if (!loadingNewIntentBoolean && !waitingForOrbot) { - // Reload the webpage to remove images if `setDisplayWebpageImages` has turned them off. - mainWebView.reload(); - } else if (loadingNewIntentBoolean) { // Reset `loadingNewIntentBoolean` if this run comes from a new intent. - loadingNewIntentBoolean = false; + // Reset `reapplyDomainSettingsOnRestart`. + reapplyDomainSettingsOnRestart = false; } + + // Load the URL on restart to apply changes to night mode. + if (loadUrlOnRestart) { + // Load the current `formattedUrlString`. + loadUrl(formattedUrlString); + + // Reset `loadUrlOnRestart. + loadUrlOnRestart = false; + } + + // Update the bookmarks drawer if returning from the Bookmarks activity. + if (restartFromBookmarksActivity) { + // Close the bookmarks drawer. + drawerLayout.closeDrawer(GravityCompat.END); + + // Reload the bookmarks drawer. + loadBookmarksFolder(); + + // 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()`. @Override public void onResume() { + // Run the default commands. super.onResume(); // Resume JavaScript (if enabled). @@ -1030,16 +1485,26 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation @Override public boolean onPrepareOptionsMenu(Menu menu) { // Get handles for the menu items. + MenuItem addOrEditDomain = menu.findItem(R.id.add_or_edit_domain); MenuItem toggleFirstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies); MenuItem toggleThirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies); MenuItem toggleDomStorageMenuItem = menu.findItem(R.id.toggle_dom_storage); MenuItem toggleSaveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data); + 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 fontSizeMenuItem = menu.findItem(R.id.font_size); 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) { + addOrEditDomain.setTitle(R.string.edit_domain_settings); + } else { + addOrEditDomain.setTitle(R.string.add_domain_settings); + } + // Set the status of the menu item checkboxes. toggleFirstPartyCookiesMenuItem.setChecked(firstPartyCookiesEnabled); toggleThirdPartyCookiesMenuItem.setChecked(thirdPartyCookiesEnabled); @@ -1050,16 +1515,36 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // 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()); - // Enable Clear Form Data is there is any. + // 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. + 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. + 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 Data` if any of the submenu items are enabled. + clearDataMenuItem.setEnabled(clearCookiesMenuItem.isEnabled() || clearDOMStorageMenuItem.isEnabled() || clearFormDataMenuItem.isEnabled()); + // Initialize font size variables. int fontSize = mainWebView.getSettings().getTextZoom(); String fontSizeTitle; @@ -1068,47 +1553,47 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // Prepare the font size title and current size menu item. switch (fontSize) { case 25: - fontSizeTitle = getResources().getString(R.string.font_size) + " - " + getResources().getString(R.string.twenty_five_percent); + fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.twenty_five_percent); selectedFontSizeMenuItem = menu.findItem(R.id.font_size_twenty_five_percent); break; case 50: - fontSizeTitle = getResources().getString(R.string.font_size) + " - " + getResources().getString(R.string.fifty_percent); + fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.fifty_percent); selectedFontSizeMenuItem = menu.findItem(R.id.font_size_fifty_percent); break; case 75: - fontSizeTitle = getResources().getString(R.string.font_size) + " - " + getResources().getString(R.string.seventy_five_percent); + fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.seventy_five_percent); selectedFontSizeMenuItem = menu.findItem(R.id.font_size_seventy_five_percent); break; case 100: - fontSizeTitle = getResources().getString(R.string.font_size) + " - " + getResources().getString(R.string.one_hundred_percent); + fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent); selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent); break; case 125: - fontSizeTitle = getResources().getString(R.string.font_size) + " - " + getResources().getString(R.string.one_hundred_twenty_five_percent); + fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_twenty_five_percent); selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_twenty_five_percent); break; case 150: - fontSizeTitle = getResources().getString(R.string.font_size) + " - " + getResources().getString(R.string.one_hundred_fifty_percent); + fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_fifty_percent); selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_fifty_percent); break; case 175: - fontSizeTitle = getResources().getString(R.string.font_size) + " - " + getResources().getString(R.string.one_hundred_seventy_five_percent); + fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_seventy_five_percent); selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_seventy_five_percent); break; case 200: - fontSizeTitle = getResources().getString(R.string.font_size) + " - " + getResources().getString(R.string.two_hundred_percent); + fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.two_hundred_percent); selectedFontSizeMenuItem = menu.findItem(R.id.font_size_two_hundred_percent); break; default: - fontSizeTitle = getResources().getString(R.string.font_size) + " - " + getResources().getString(R.string.one_hundred_percent); + fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent); selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent); break; } @@ -1123,7 +1608,7 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // Run all the other default commands. super.onPrepareOptionsMenu(menu); - // `return true` displays the menu. + // Display the menu. return true; } @@ -1133,10 +1618,32 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // removeAllCookies is deprecated, but it is required for API < 21. @SuppressWarnings("deprecation") public boolean onOptionsItemSelected(MenuItem menuItem) { + // Get the selected menu item ID. int menuItemId = menuItem.getItemId(); // 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; @@ -1259,11 +1766,8 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation case R.id.clear_cookies: Snackbar.make(findViewById(R.id.main_webview), R.string.cookies_deleted, Snackbar.LENGTH_LONG) - .setAction(R.string.undo, new View.OnClickListener() { - @Override - public void onClick(View v) { - // Do nothing because everything will be handled by `onDismissed()` below. - } + .setAction(R.string.undo, v -> { + // Do nothing because everything will be handled by `onDismissed()` below. }) .addCallback(new Snackbar.Callback() { @Override @@ -1291,11 +1795,8 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation case R.id.clear_dom_storage: Snackbar.make(findViewById(R.id.main_webview), R.string.dom_storage_deleted, Snackbar.LENGTH_LONG) - .setAction(R.string.undo, new View.OnClickListener() { - @Override - public void onClick(View v) { - // Do nothing because everything will be handled by `onDismissed()` below. - } + .setAction(R.string.undo, v -> { + // Do nothing because everything will be handled by `onDismissed()` below. }) .addCallback(new Snackbar.Callback() { @Override @@ -1311,6 +1812,20 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // Delete the DOM Storage. WebStorage webStorage = WebStorage.getInstance(); webStorage.deleteAllData(); + + // Manually delete the DOM storage files and directories. + try { + // A `String[]` must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly. + privacyBrowserRuntime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"}); + + // Multiple commands must be used because `Runtime.exec()` does not like `*`. + privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB"); + privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager"); + privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal"); + privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases"); + } catch (IOException e) { + // Do nothing if an error is thrown. + } } } }) @@ -1319,11 +1834,8 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation case R.id.clear_form_data: Snackbar.make(findViewById(R.id.main_webview), R.string.form_data_deleted, Snackbar.LENGTH_LONG) - .setAction(R.string.undo, new View.OnClickListener() { - @Override - public void onClick(View v) { - // Do nothing because everything will be handled by `onDismissed()` below. - } + .setAction(R.string.undo, v -> { + // Do nothing because everything will be handled by `onDismissed()` below. }) .addCallback(new Snackbar.Callback() { @Override @@ -1390,13 +1902,8 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation return true; case R.id.share: - // Setup the share string. - String shareString; - if (webViewTitle != null) { - shareString = webViewTitle + " – " + urlTextBox.getText().toString(); - } else { - shareString = urlTextBox.getText().toString(); - } + // Setup the share string. + String shareString = webViewTitle + " – " + urlTextBox.getText().toString(); // Create the share intent. Intent shareIntent = new Intent(); @@ -1417,17 +1924,12 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // Display the keyboard. We have to wait 200 ms before running the command to work around a bug in Android. // http://stackoverflow.com/questions/5520085/android-show-softkeyboard-with-showsoftinput-is-not-working - findOnPageEditText.postDelayed(new Runnable() - { - @Override - public void run() - { - // Set the focus on `findOnPageEditText`. - findOnPageEditText.requestFocus(); + findOnPageEditText.postDelayed(() -> { + // Set the focus on `findOnPageEditText`. + findOnPageEditText.requestFocus(); - // Display the keyboard. - inputMethodManager.showSoftInput(findOnPageEditText, 0); - } + // Display the keyboard. `0` sets no input flags. + inputMethodManager.showSoftInput(findOnPageEditText, 0); }, 200); return true; @@ -1438,14 +1940,23 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // Convert `mainWebView` to `printDocumentAdapter`. PrintDocumentAdapter printDocumentAdapter = mainWebView.createPrintDocumentAdapter(); + // Remove the lint error below that `printManager` might be `null`. + assert printManager != null; + // Print the document. The print attributes are `null`. - printManager.print(getResources().getString(R.string.privacy_browser_web_page), printDocumentAdapter, null); + 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(); - createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getResources().getString(R.string.create_shortcut)); + createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getString(R.string.create_shortcut)); //Everything else will be handled by `CreateHomeScreenShortcutDialog` and the associated listener below. return true; @@ -1497,13 +2008,7 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // Show the `UrlHistoryDialog` `AlertDialog` and name this instance `R.string.history`. `this` is the `Context`. AppCompatDialogFragment urlHistoryDialogFragment = UrlHistoryDialog.loadBackForwardList(this, webBackForwardList); - urlHistoryDialogFragment.show(getSupportFragmentManager(), getResources().getString(R.string.history)); - break; - - case R.id.bookmarks: - // Launch BookmarksActivity. - Intent bookmarksIntent = new Intent(this, BookmarksActivity.class); - startActivity(bookmarksIntent); + urlHistoryDialogFragment.show(getSupportFragmentManager(), getString(R.string.history)); break; case R.id.downloads: @@ -1516,17 +2021,9 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation startActivity(downloadManagerIntent); break; - case R.id.settings: - // Reset `currentDomainName` so that domain settings are reapplied after returning to `MainWebViewActivity`. - currentDomainName = ""; - - // Launch `SettingsActivity`. - Intent settingsIntent = new Intent(this, SettingsActivity.class); - startActivity(settingsIntent); - break; - case R.id.domains: - // Reset `currentDomainName` so that domain settings are reapplied after returning to `MainWebViewActivity`. + // Set the flag to reapply the domain settings on restart when returning from Domain Settings. + reapplyDomainSettingsOnRestart = true; currentDomainName = ""; // Launch `DomainsActivity`. @@ -1534,6 +2031,19 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation startActivity(domainsIntent); break; + case R.id.settings: + // Set the flag to reapply app settings on restart when returning from Settings. + reapplyAppSettingsOnRestert = true; + + // Set the flag to reapply the domain settings on restart when returning from Settings. + reapplyDomainSettingsOnRestart = true; + currentDomainName = ""; + + // Launch `SettingsActivity`. + Intent settingsIntent = new Intent(this, SettingsActivity.class); + startActivity(settingsIntent); + break; + case R.id.guide: // Launch `GuideActivity`. Intent guideIntent = new Intent(this, GuideActivity.class); @@ -1547,30 +2057,89 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation break; case R.id.clearAndExit: - // Clear cookies. The commands changed slightly in API 21. - if (Build.VERSION.SDK_INT >= 21) { - cookieManager.removeAllCookies(null); - } else { - cookieManager.removeAllCookie(); + // Get a handle for `sharedPreferences`. `this` references the current context. + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + + boolean clearEverything = sharedPreferences.getBoolean("clear_everything", true); + + // Clear cookies. + if (clearEverything || sharedPreferences.getBoolean("clear_cookies", true)) { + // The command to remove cookies changed slightly in API 21. + if (Build.VERSION.SDK_INT >= 21) { + cookieManager.removeAllCookies(null); + } else { + cookieManager.removeAllCookie(); + } + + // Manually delete the cookies database, as `CookieManager` sometimes will not flush its changes to disk before `System.exit(0)` is run. + try { + // We have to use two commands because `Runtime.exec()` does not like `*`. + privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies"); + privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies-journal"); + } catch (IOException e) { + // Do nothing if an error is thrown. + } } // Clear DOM storage. - WebStorage domStorage = WebStorage.getInstance(); - domStorage.deleteAllData(); + if (clearEverything || sharedPreferences.getBoolean("clear_dom_storage", true)) { + // Ask `WebStorage` to clear the DOM storage. + WebStorage webStorage = WebStorage.getInstance(); + webStorage.deleteAllData(); + + // Manually delete the DOM storage files and directories, as `WebStorage` sometimes will not flush its changes to disk before `System.exit(0)` is run. + try { + // A `String[]` must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly. + privacyBrowserRuntime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"}); + + // Multiple commands must be used because `Runtime.exec()` does not like `*`. + privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB"); + privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager"); + privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal"); + privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases"); + } catch (IOException e) { + // Do nothing if an error is thrown. + } + } // Clear form data. - WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this); - webViewDatabase.clearFormData(); + if (clearEverything || sharedPreferences.getBoolean("clear_form_data", true)) { + WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this); + webViewDatabase.clearFormData(); - // Clear the cache. `true` includes disk files. - mainWebView.clearCache(true); + // Manually delete the form data database, as `WebViewDatabase` sometimes will not flush its changes to disk before `System.exit(0)` is run. + try { + // We have to use a `String[]` because the database contains a space and `Runtime.exec` will not escape the string correctly otherwise. + privacyBrowserRuntime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data"}); + privacyBrowserRuntime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data-journal"}); + } catch (IOException e) { + // Do nothing if an error is thrown. + } + } - // Clear the back/forward history. - mainWebView.clearHistory(); + // Clear the cache. + if (clearEverything || sharedPreferences.getBoolean("clear_cache", true)) { + // `true` includes disk files. + mainWebView.clearCache(true); + + // Manually delete the cache directories. + try { + // 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. + privacyBrowserRuntime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"}); + } catch (IOException e) { + // Do nothing if an error is thrown. + } + } - // Clear any SSL certificate preferences. + // Clear SSL certificate preferences. mainWebView.clearSslPreferences(); + // Clear the back/forward history. + mainWebView.clearHistory(); + // Clear `formattedUrlString`. formattedUrlString = null; @@ -1583,15 +2152,14 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // Destroy the internal state of `mainWebView`. mainWebView.destroy(); - // Manually delete cache folders. - try { - // Delete the main `cache` folder. - 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"); - } catch (IOException e) { - // Do nothing if an error is thrown. + // Manually delete the `app_webview` folder, which contains the cookies, DOM storage, form data, and `Service Worker` cache. + // See `https://code.google.com/p/android/issues/detail?id=233826&thanks=233826&ts=1486670530`. + if (clearEverything) { + try { + privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview"); + } catch (IOException e) { + // Do nothing if an error is thrown. + } } // Close Privacy Browser. `finishAndRemoveTask` also removes Privacy Browser from the recent app list. @@ -1604,9 +2172,6 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // Remove the terminated program from RAM. The status code is `0`. System.exit(0); break; - - default: - break; } // Close the navigation drawer. @@ -1635,7 +2200,8 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation adView = findViewById(R.id.adview); } - // `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); } @@ -1651,6 +2217,9 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // Get a handle for the `ClipboardManager`. final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); + // Remove the lint errors below that `clipboardManager` might be `null`. + assert clipboardManager != null; + switch (hitTestResult.getType()) { // `SRC_ANCHOR_TYPE` is a link. case WebView.HitTestResult.SRC_ANCHOR_TYPE: @@ -1661,25 +2230,19 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation menu.setHeaderTitle(linkUrl); // Add a `Load URL` entry. - menu.add(R.string.load_url).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - loadUrl(linkUrl); - return false; - } + menu.add(R.string.load_url).setOnMenuItemClickListener(item -> { + loadUrl(linkUrl); + return false; }); // Add a `Copy URL` entry. - menu.add(R.string.copy_url).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - // Save the link URL in a `ClipData`. - ClipData srcAnchorTypeClipData = ClipData.newPlainText(getResources().getString(R.string.url), linkUrl); - - // Set the `ClipData` as the clipboard's primary clip. - clipboardManager.setPrimaryClip(srcAnchorTypeClipData); - return false; - } + menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> { + // Save the link URL in a `ClipData`. + ClipData srcAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl); + + // Set the `ClipData` as the clipboard's primary clip. + clipboardManager.setPrimaryClip(srcAnchorTypeClipData); + return false; }); // Add a `Cancel` entry, which by default closes the `ContextMenu`. @@ -1694,35 +2257,29 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation menu.setHeaderTitle(linkUrl); // Add a `Write Email` entry. - menu.add(R.string.write_email).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - // We use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched. - Intent emailIntent = new Intent(Intent.ACTION_SENDTO); + menu.add(R.string.write_email).setOnMenuItemClickListener(item -> { + // We use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched. + Intent emailIntent = new Intent(Intent.ACTION_SENDTO); - // Parse the url and set it as the data for the `Intent`. - emailIntent.setData(Uri.parse("mailto:" + linkUrl)); + // Parse the url and set it as the data for the `Intent`. + emailIntent.setData(Uri.parse("mailto:" + linkUrl)); - // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser. - emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser. + emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - // Make it so. - startActivity(emailIntent); - return false; - } + // Make it so. + startActivity(emailIntent); + return false; }); // Add a `Copy Email Address` entry. - menu.add(R.string.copy_email_address).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - // Save the email address in a `ClipData`. - ClipData srcEmailTypeClipData = ClipData.newPlainText(getResources().getString(R.string.email_address), linkUrl); - - // Set the `ClipData` as the clipboard's primary clip. - clipboardManager.setPrimaryClip(srcEmailTypeClipData); - return false; - } + menu.add(R.string.copy_email_address).setOnMenuItemClickListener(item -> { + // Save the email address in a `ClipData`. + ClipData srcEmailTypeClipData = ClipData.newPlainText(getString(R.string.email_address), linkUrl); + + // Set the `ClipData` as the clipboard's primary clip. + clipboardManager.setPrimaryClip(srcEmailTypeClipData); + return false; }); // Add a `Cancel` entry, which by default closes the `ContextMenu`. @@ -1738,36 +2295,27 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation menu.setHeaderTitle(imageUrl); // Add a `View Image` entry. - menu.add(R.string.view_image).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - loadUrl(imageUrl); - return false; - } + menu.add(R.string.view_image).setOnMenuItemClickListener(item -> { + loadUrl(imageUrl); + return false; }); // Add a `Download Image` entry. - menu.add(R.string.download_image).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - // Show the `DownloadImageDialog` `AlertDialog` and name this instance `@string/download`. - AppCompatDialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl); - downloadImageDialogFragment.show(getSupportFragmentManager(), getResources().getString(R.string.download)); - return false; - } + 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)); + return false; }); // Add a `Copy URL` entry. - menu.add(R.string.copy_url).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - // Save the image URL in a `ClipData`. - ClipData srcImageTypeClipData = ClipData.newPlainText(getResources().getString(R.string.url), imageUrl); - - // Set the `ClipData` as the clipboard's primary clip. - clipboardManager.setPrimaryClip(srcImageTypeClipData); - return false; - } + menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> { + // Save the image URL in a `ClipData`. + ClipData srcImageTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl); + + // Set the `ClipData` as the clipboard's primary clip. + clipboardManager.setPrimaryClip(srcImageTypeClipData); + return false; }); // Add a `Cancel` entry, which by default closes the `ContextMenu`. @@ -1784,36 +2332,27 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation menu.setHeaderTitle(imageUrl); // Add a `View Image` entry. - menu.add(R.string.view_image).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - loadUrl(imageUrl); - return false; - } + menu.add(R.string.view_image).setOnMenuItemClickListener(item -> { + loadUrl(imageUrl); + return false; }); // Add a `Download Image` entry. - menu.add(R.string.download_image).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - // Show the `DownloadImageDialog` `AlertDialog` and name this instance `@string/download`. - AppCompatDialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl); - downloadImageDialogFragment.show(getSupportFragmentManager(), getResources().getString(R.string.download)); - return false; - } + 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)); + return false; }); // Add a `Copy URL` entry. - menu.add(R.string.copy_url).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - // Save the image URL in a `ClipData`. - ClipData srcImageAnchorTypeClipData = ClipData.newPlainText(getResources().getString(R.string.url), imageUrl); - - // Set the `ClipData` as the clipboard's primary clip. - clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData); - return false; - } + menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> { + // Save the image URL in a `ClipData`. + ClipData srcImageAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl); + + // Set the `ClipData` as the clipboard's primary clip. + clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData); + return false; }); // Add a `Cancel` entry, which by default closes the `ContextMenu`. @@ -1822,23 +2361,133 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation } } + @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`. + EditText createBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_name_edittext); + EditText createBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_url_edittext); + + // Extract the strings from the `EditTexts`. + String bookmarkNameString = createBookmarkNameEditText.getText().toString(); + String bookmarkUrlString = createBookmarkUrlEditText.getText().toString(); + + // Convert the favoriteIcon Bitmap to a byte array. `0` is for lossless compression (the only option for a PNG). + ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream(); + favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream); + byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray(); + + // Display the new bookmark below the current items in the (0 indexed) list. + int newBookmarkDisplayOrder = bookmarksListView.getCount(); + + // Create the bookmark. + bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolder, newBookmarkDisplayOrder, favoriteIconByteArray); + + // Update `bookmarksCursor` with the current contents of this folder. + bookmarksCursor = bookmarksDatabaseHelper.getAllBookmarksCursorByDisplayOrder(currentBookmarksFolder); + + // Update the `ListView`. + bookmarksCursorAdapter.changeCursor(bookmarksCursor); + + // Scroll to the new bookmark. + bookmarksListView.setSelection(newBookmarkDisplayOrder); + } + + @Override + public void onCreateBookmarkFolder(AppCompatDialogFragment dialogFragment) { + // Get handles for the views in `dialogFragment`. + EditText createFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.create_folder_name_edittext); + RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon_radiobutton); + ImageView folderIconImageView = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon); + + // Get new folder name string. + String folderNameString = createFolderNameEditText.getText().toString(); + + // Get the new folder icon `Bitmap`. + Bitmap folderIconBitmap; + if (defaultFolderIconRadioButton.isChecked()) { // Use the default folder icon. + // Get the default folder icon and convert it to a `Bitmap`. + Drawable folderIconDrawable = folderIconImageView.getDrawable(); + BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable; + folderIconBitmap = folderIconBitmapDrawable.getBitmap(); + } else { // Use the `WebView` favorite icon. + folderIconBitmap = favoriteIconBitmap; + } + + // Convert `folderIconBitmap` to a byte array. `0` is for lossless compression (the only option for a PNG). + ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream(); + folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream); + byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray(); + + // Move all the bookmarks down one in the display order. + for (int i = 0; i < bookmarksListView.getCount(); i++) { + int databaseId = (int) bookmarksListView.getItemIdAtPosition(i); + bookmarksDatabaseHelper.updateDisplayOrder(databaseId, i + 1); + } + + // Create the folder, which will be placed at the top of the `ListView`. + bookmarksDatabaseHelper.createFolder(folderNameString, currentBookmarksFolder, folderIconByteArray); + + // Update `bookmarksCursor` with the current contents of this folder. + bookmarksCursor = bookmarksDatabaseHelper.getAllBookmarksCursorByDisplayOrder(currentBookmarksFolder); + + // Update the `ListView`. + bookmarksCursorAdapter.changeCursor(bookmarksCursor); + + // Scroll to the new folder. + bookmarksListView.setSelection(0); + } + @Override public void onCreateHomeScreenShortcut(AppCompatDialogFragment dialogFragment) { - // Get shortcutNameEditText from the alert dialog. - EditText shortcutNameEditText = (EditText) dialogFragment.getDialog().findViewById(R.id.shortcut_name_edittext); - - // Create the bookmark shortcut based on formattedUrlString. - Intent bookmarkShortcut = new Intent(); - bookmarkShortcut.setAction(Intent.ACTION_VIEW); - bookmarkShortcut.setData(Uri.parse(formattedUrlString)); - - // Place the bookmark shortcut on the home screen. - Intent placeBookmarkShortcut = new Intent(); - placeBookmarkShortcut.putExtra("android.intent.extra.shortcut.INTENT", bookmarkShortcut); - placeBookmarkShortcut.putExtra("android.intent.extra.shortcut.NAME", shortcutNameEditText.getText().toString()); - placeBookmarkShortcut.putExtra("android.intent.extra.shortcut.ICON", favoriteIconBitmap); - placeBookmarkShortcut.setAction("com.android.launcher.action.INSTALL_SHORTCUT"); - sendBroadcast(placeBookmarkShortcut); + // Get the shortcut name. + EditText shortcutNameEditText = dialogFragment.getDialog().findViewById(R.id.shortcut_name_edittext); + String shortcutNameString = shortcutNameEditText.getText().toString(); + + // Convert the favorite icon bitmap to an `Icon`. `IconCompat` is required until API >= 26. + IconCompat favoriteIcon = IconCompat.createWithBitmap(favoriteIconBitmap); + + // Setup the shortcut intent. + Intent shortcutIntent = new Intent(); + shortcutIntent.setAction(Intent.ACTION_VIEW); + shortcutIntent.setData(Uri.parse(formattedUrlString)); + + // Create a shortcut info builder. The shortcut name becomes the shortcut ID. + ShortcutInfoCompat.Builder shortcutInfoBuilder = new ShortcutInfoCompat.Builder(this, shortcutNameString); + + // Add the required fields to the shortcut info builder. + shortcutInfoBuilder.setIcon(favoriteIcon); + shortcutInfoBuilder.setIntent(shortcutIntent); + shortcutInfoBuilder.setShortLabel(shortcutNameString); + + // Request the pin. `ShortcutManagerCompat` can be switched to `ShortcutManager` once API >= 26. + ShortcutManagerCompat.requestPinShortcut(this, shortcutInfoBuilder.build(), null); } @Override @@ -1862,7 +2511,7 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation } // Get the file name from `dialogFragment`. - EditText downloadImageNameEditText = (EditText) dialogFragment.getDialog().findViewById(R.id.download_image_name); + 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`. @@ -1881,6 +2530,9 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // Show the download notification after the download is completed. downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); + // Remove the lint warning below that `downloadManager` might be `null`. + assert downloadManager != null; + // Initiate the download. downloadManager.enqueue(downloadRequest); } else { // The image is not an HTTP or HTTPS URI. @@ -1910,7 +2562,7 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation } // Get the file name from `dialogFragment`. - EditText downloadFileNameEditText = (EditText) dialogFragment.getDialog().findViewById(R.id.download_file_name); + 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`. @@ -1929,6 +2581,9 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // Show the download notification after the download is completed. downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); + // Remove the lint warning below that `downloadManager` might be `null`. + assert downloadManager != null; + // Initiate the download. downloadManager.enqueue(downloadRequest); } else { // The download is not an HTTP or HTTPS URI. @@ -1936,10 +2591,119 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation } } + @Override + public void onSaveBookmark(AppCompatDialogFragment dialogFragment, int selectedBookmarkDatabaseId) { + // Get handles for the views from `dialogFragment`. + EditText editBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_name_edittext); + EditText editBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_url_edittext); + RadioButton currentBookmarkIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_current_icon_radiobutton); + + // Store the bookmark strings. + String bookmarkNameString = editBookmarkNameEditText.getText().toString(); + String bookmarkUrlString = editBookmarkUrlEditText.getText().toString(); + + // Update the bookmark. + if (currentBookmarkIconRadioButton.isChecked()) { // Update the bookmark without changing the favorite icon. + bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString); + } else { // Update the bookmark using the `WebView` favorite icon. + // Convert the favorite icon to a byte array. `0` is for lossless compression (the only option for a PNG). + ByteArrayOutputStream newFavoriteIconByteArrayOutputStream = new ByteArrayOutputStream(); + favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFavoriteIconByteArrayOutputStream); + byte[] newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray(); + + // Update the bookmark and the favorite icon. + bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, newFavoriteIconByteArray); + } + + // Update `bookmarksCursor` with the current contents of this folder. + bookmarksCursor = bookmarksDatabaseHelper.getAllBookmarksCursorByDisplayOrder(currentBookmarksFolder); + + // Update the `ListView`. + bookmarksCursorAdapter.changeCursor(bookmarksCursor); + } + + @Override + public void onSaveBookmarkFolder(AppCompatDialogFragment dialogFragment, int selectedFolderDatabaseId) { + // Get handles for the views from `dialogFragment`. + EditText editFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_folder_name_edittext); + RadioButton currentFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_current_icon_radiobutton); + RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_radiobutton); + ImageView folderIconImageView = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_imageview); + + // Get the new folder name. + String newFolderNameString = editFolderNameEditText.getText().toString(); + + // Check if the favorite icon has changed. + if (currentFolderIconRadioButton.isChecked()) { // Only the name has changed. + // Update the name in the database. + bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString); + } else if (!currentFolderIconRadioButton.isChecked() && newFolderNameString.equals(oldFolderNameString)) { // Only the icon has changed. + // Get the new folder icon `Bitmap`. + Bitmap folderIconBitmap; + if (defaultFolderIconRadioButton.isChecked()) { + // Get the default folder icon and convert it to a `Bitmap`. + Drawable folderIconDrawable = folderIconImageView.getDrawable(); + BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable; + folderIconBitmap = folderIconBitmapDrawable.getBitmap(); + } else { // Use the `WebView` favorite icon. + folderIconBitmap = favoriteIconBitmap; + } + + // Convert the folder `Bitmap` to a byte array. `0` is for lossless compression (the only option for a PNG). + ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream(); + folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream); + byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray(); + + // Update the folder icon in the database. + bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, folderIconByteArray); + } else { // The folder icon and the name have changed. + // Get the new folder icon `Bitmap`. + Bitmap folderIconBitmap; + if (defaultFolderIconRadioButton.isChecked()) { + // Get the default folder icon and convert it to a `Bitmap`. + Drawable folderIconDrawable = folderIconImageView.getDrawable(); + BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable; + folderIconBitmap = folderIconBitmapDrawable.getBitmap(); + } else { // Use the `WebView` favorite icon. + folderIconBitmap = MainWebViewActivity.favoriteIconBitmap; + } + + // Convert the folder `Bitmap` to a byte array. `0` is for lossless compression (the only option for a PNG). + ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream(); + folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream); + byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray(); + + // Update the folder name and icon in the database. + bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, folderIconByteArray); + } + + // Update `bookmarksCursor` with the current contents of this folder. + bookmarksCursor = bookmarksDatabaseHelper.getAllBookmarksCursorByDisplayOrder(currentBookmarksFolder); + + // Update the `ListView`. + bookmarksCursorAdapter.changeCursor(bookmarksCursor); + } + + @Override + public void onHttpAuthenticationCancel() { + // Cancel the `HttpAuthHandler`. + httpAuthHandler.cancel(); + } + + @Override + public void onHttpAuthenticationProceed(AppCompatDialogFragment dialogFragment) { + // Get handles for the `EditTexts`. + EditText usernameEditText = dialogFragment.getDialog().findViewById(R.id.http_authentication_username); + EditText passwordEditText = dialogFragment.getDialog().findViewById(R.id.http_authentication_password); + + // Proceed with the HTTP authentication. + httpAuthHandler.proceed(usernameEditText.getText().toString(), passwordEditText.getText().toString()); + } + public void viewSslCertificate(View view) { // Show the `ViewSslCertificateDialog` `AlertDialog` and name this instance `@string/view_ssl_certificate`. DialogFragment viewSslCertificateDialogFragment = new ViewSslCertificateDialog(); - viewSslCertificateDialogFragment.show(getFragmentManager(), getResources().getString(R.string.view_ssl_certificate)); + viewSslCertificateDialogFragment.show(getFragmentManager(), getString(R.string.view_ssl_certificate)); } @Override @@ -1952,6 +2716,26 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation sslErrorHandler.proceed(); } + @Override + public void onSslMismatchBack() { + if (mainWebView.canGoBack()) { // There is a back page in the history. + // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded. + navigatingHistory = true; + + // Go back. + mainWebView.goBack(); + } else { // There are no pages to go back to. + // Load a blank page + loadUrl(""); + } + } + + @Override + public void onSslMismatchProceed() { + // Do not check the pinned SSL certificate for this domain again until the domain changes. + ignorePinnedSslCertificate = true; + } + @Override public void onUrlHistoryEntrySelected(int moveBackOrForwardSteps) { // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded. @@ -1970,21 +2754,30 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // Override `onBackPressed` to handle the navigation drawer and `mainWebView`. @Override public void onBackPressed() { - // Close the navigation drawer if it is available. GravityCompat.START is the drawer on the left on Left-to-Right layout text. - if (drawerLayout.isDrawerVisible(GravityCompat.START)) { + if (drawerLayout.isDrawerVisible(GravityCompat.START)) { // The navigation drawer is open. + // Close the navigation drawer. drawerLayout.closeDrawer(GravityCompat.START); - } else { - // Load the previous URL if available. - if (mainWebView.canGoBack()) { - // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded. - navigatingHistory = true; - - // Go back. - mainWebView.goBack(); - } else { - // Pass `onBackPressed()` to the system. - super.onBackPressed(); + } else if (drawerLayout.isDrawerVisible(GravityCompat.END)){ // The bookmarks drawer is open. + if (currentBookmarksFolder.isEmpty()) { // The home folder is displayed. + // close the bookmarks drawer. + drawerLayout.closeDrawer(GravityCompat.END); + } else { // A subfolder is displayed. + // Place the former parent folder in `currentFolder`. + currentBookmarksFolder = bookmarksDatabaseHelper.getParentFolder(currentBookmarksFolder); + + // Load the new folder. + loadBookmarksFolder(); } + + } else if (mainWebView.canGoBack()) { // There is at least one item in the `WebView` history. + // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded. + navigatingHistory = true; + + // Go back. + mainWebView.goBack(); + } else { // There isn't anything to do in Privacy Browser. + // Pass `onBackPressed()` to the system. + super.onBackPressed(); } } @@ -2030,19 +2823,22 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation formattedUrlString = searchURL + encodedUrlString; } - loadUrl(formattedUrlString); + // 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(); - // Hide the keyboard so we can see the webpage. `0` indicates no additional flags. - inputMethodManager.hideSoftInputFromWindow(mainWebView.getWindowToken(), 0); + loadUrl(formattedUrlString); } private void loadUrl(String url) { // Apply any custom domain settings. - applyDomainSettings(url); + applyDomainSettings(url, true); // 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) { @@ -2073,17 +2869,20 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation } private void applyAppSettings() { - // Get the shared preference values. `this` references the current context. + // Get a handle for the shared preferences. SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - // Store the values from `sharedPreferences` in variables. - String homepageString = sharedPreferences.getString("homepage", "https://duckduckgo.com"); + // 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 torSearchCustomURLString = sharedPreferences.getString("tor_search_custom_url", ""); String searchString = sharedPreferences.getString("search", "https://duckduckgo.com/html/?q="); String searchCustomURLString = sharedPreferences.getString("search_custom_url", ""); - adBlockerEnabled = sharedPreferences.getBoolean("block_ads", true); + easyListEnabled = sharedPreferences.getBoolean("easylist", true); + easyPrivacyEnabled = sharedPreferences.getBoolean("easyprivacy", true); + fanboyAnnoyanceListEnabled = sharedPreferences.getBoolean("fanboy_annoyance_list", true); + fanboySocialBlockingListEnabled = sharedPreferences.getBoolean("fanboy_social_blocking_list", true); incognitoModeEnabled = sharedPreferences.getBoolean("incognito_mode", false); boolean doNotTrackEnabled = sharedPreferences.getBoolean("do_not_track", false); boolean proxyThroughOrbot = sharedPreferences.getBoolean("proxy_through_orbot", false); @@ -2113,6 +2912,13 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // Set the proxy. `this` refers to the current activity where an `AlertDialog` might be displayed. OrbotProxyHelper.setProxy(getApplicationContext(), this, "localhost", "8118"); + // Set the `appBar` background to indicate proxying through Orbot is enabled. `this` refers to the context. + if (darkTheme) { + appBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.dark_blue_30)); + } else { + appBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.blue_50)); + } + // Display a message to the user if we are waiting on Orbot. if (!orbotStatus.equals("ON")) { // Set `waitingForOrbot`. @@ -2140,6 +2946,13 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // Reset the proxy to default. The host is `""` and the port is `"0"`. OrbotProxyHelper.setProxy(getApplicationContext(), this, "", "0"); + // Set the default `appBar` background. `this` refers to the context. + if (darkTheme) { + appBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.gray_900)); + } else { + appBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.gray_100)); + } + // Reset `waitingForOrbot. waitingForOrbot = false; } @@ -2168,7 +2981,7 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation /* 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. @@ -2215,9 +3028,10 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation } } - // We have to use the deprecated `.getDrawable()` until the minimum API >= 21. + // + // The deprecated `.getDrawable()` must be used until the minimum API >= 21. @SuppressWarnings("deprecation") - private void applyDomainSettings(String url) { + private void applyDomainSettings(String url, boolean resetFavoriteIcon) { // Reset `navigatingHistory`. navigatingHistory = false; @@ -2239,14 +3053,19 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation 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. + // 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; - // Reset `favoriteIconBitmap` and display it in the `appbar`. - favoriteIconBitmap = favoriteIconDefaultBitmap; - favoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(favoriteIconBitmap, 64, 64, true)); + // Reset `ignorePinnedSslCertificate`. + ignorePinnedSslCertificate = false; + + // 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`. @@ -2299,12 +3118,19 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // Get a handle for the shared preference. `this` references the current context. 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 defaultCustomUserAgentString = sharedPreferences.getString("custom_user_agent", "PrivacyBrowser/1.0"); + nightMode = sharedPreferences.getBoolean("night_mode", false); + if (domainSettingsApplied) { // The url we are loading has custom domain settings. // Get a cursor for the current host and move it to the first position. Cursor currentHostDomainSettingsCursor = domainsDatabaseHelper.getCursorForDomainName(domainNameInDatabase); currentHostDomainSettingsCursor.moveToFirst(); // Get the settings from the cursor. + domainSettingsDatabaseId = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper._ID))); javaScriptEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)) == 1); firstPartyCookiesEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FIRST_PARTY_COOKIES)) == 1); thirdPartyCookiesEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_THIRD_PARTY_COOKIES)) == 1); @@ -2313,6 +3139,44 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation String userAgentString = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.USER_AGENT)); int fontSize = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.FONT_SIZE)); displayWebpageImagesInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.DISPLAY_IMAGES)); + int nightModeInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.NIGHT_MODE)); + pinnedDomainSslCertificate = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)) == 1); + pinnedDomainSslIssuedToCNameString = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME)); + pinnedDomainSslIssuedToONameString = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION)); + pinnedDomainSslIssuedToUNameString = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT)); + pinnedDomainSslIssuedByCNameString = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME)); + pinnedDomainSslIssuedByONameString = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION)); + pinnedDomainSslIssuedByUNameString = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT)); + + // Set `nightMode` according to `nightModeInt`. If `nightModeInt` is `DomainsDatabaseHelper.NIGHT_MODE_SYSTEM_DEFAULT` the current setting from `sharedPreferences` will be used. + switch (nightModeInt) { + case DomainsDatabaseHelper.NIGHT_MODE_ENABLED: + nightMode = true; + break; + + case DomainsDatabaseHelper.NIGHT_MODE_DISABLED: + nightMode = false; + break; + } + + // Set `javaScriptEnabled` to be `true` if `night_mode` is `true`. + if (nightMode) { + javaScriptEnabled = true; + } + + // Set the pinned SSL certificate start date to `null` if the saved date `long` is 0. + if (currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)) == 0) { + pinnedDomainSslStartDate = null; + } else { + pinnedDomainSslStartDate = new Date(currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE))); + } + + // Set the pinned SSL certificate end date to `null` if the saved date `long` is 0. + if (currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)) == 0) { + pinnedDomainSslEndDate = null; + } else { + pinnedDomainSslEndDate = new Date(currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE))); + } // Close `currentHostDomainSettingsCursor`. currentHostDomainSettingsCursor.close(); @@ -2322,24 +3186,62 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation cookieManager.setAcceptCookie(firstPartyCookiesEnabled); mainWebView.getSettings().setDomStorageEnabled(domStorageEnabled); mainWebView.getSettings().setSaveFormData(saveFormDataEnabled); - mainWebView.getSettings().setTextZoom(fontSize); + + // Apply the font size. + if (fontSize == 0) { // Apply the default font size. + mainWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString)); + } else { // Apply the specified font size. + mainWebView.getSettings().setTextZoom(fontSize); + } // Set third-party cookies status if API >= 21. if (Build.VERSION.SDK_INT >= 21) { cookieManager.setAcceptThirdPartyCookies(mainWebView, thirdPartyCookiesEnabled); } - // Set the user agent. - if (userAgentString.equals("WebView default user agent")) { - // Set the user agent to `""`, which uses the default value. - mainWebView.getSettings().setUserAgentString(""); - } else { - // Use the selected user agent. - mainWebView.getSettings().setUserAgentString(userAgentString); + // Only set the user agent if the webpage is not currently loading. Otherwise, changing the user agent on redirects can cause the original website to reload. + // + if (!urlIsLoading) { + switch (userAgentString) { + case "System default user agent": + // Set the user agent according to the system default. + switch (defaultUserAgentString) { + case "WebView default user agent": + // Set the user agent to `""`, which uses the default value. + mainWebView.getSettings().setUserAgentString(""); + break; + + case "Custom user agent": + // Set the custom user agent. + mainWebView.getSettings().setUserAgentString(defaultCustomUserAgentString); + break; + + default: + // Use the selected user agent. + mainWebView.getSettings().setUserAgentString(defaultUserAgentString); + } + break; + + case "WebView default user agent": + // Set the user agent to `""`, which uses the default value. + mainWebView.getSettings().setUserAgentString(""); + break; + + default: + // Use the selected user agent. + mainWebView.getSettings().setUserAgentString(userAgentString); + } + + // Store the applied user agent string. + appliedUserAgentString = mainWebView.getSettings().getUserAgentString(); } // Set a green background on `urlTextBox` to indicate that custom domain settings are being used. We have to use the deprecated `.getDrawable()` until the minimum API >= 21. - urlAppBarRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_green)); + if (darkTheme) { + urlAppBarRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_dark_blue)); + } else { + urlAppBarRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_light_green)); + } } else { // The URL we are loading does not have custom domain settings. Load the defaults. // Store the values from `sharedPreferences` in variables. javaScriptEnabled = sharedPreferences.getBoolean("javascript_enabled", false); @@ -2347,9 +3249,11 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation thirdPartyCookiesEnabled = sharedPreferences.getBoolean("third_party_cookies_enabled", false); domStorageEnabled = sharedPreferences.getBoolean("dom_storage_enabled", false); saveFormDataEnabled = sharedPreferences.getBoolean("save_form_data_enabled", false); - String userAgentString = sharedPreferences.getString("user_agent", "PrivacyBrowser/1.0"); - String customUserAgentString = sharedPreferences.getString("custom_user_agent", "PrivacyBrowser/1.0"); - String defaultFontSizeString = sharedPreferences.getString("default_font_size", "100"); + + // Set `javaScriptEnabled` to be `true` if `night_mode` is `true`. + if (nightMode) { + javaScriptEnabled = true; + } // Apply the default settings. mainWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled); @@ -2358,31 +3262,48 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation mainWebView.getSettings().setSaveFormData(saveFormDataEnabled); mainWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString)); + // Reset the pinned SSL certificate information. + domainSettingsDatabaseId = -1; + pinnedDomainSslCertificate = false; + pinnedDomainSslIssuedToCNameString = ""; + pinnedDomainSslIssuedToONameString = ""; + pinnedDomainSslIssuedToUNameString = ""; + pinnedDomainSslIssuedByCNameString = ""; + pinnedDomainSslIssuedByONameString = ""; + pinnedDomainSslIssuedByUNameString = ""; + pinnedDomainSslStartDate = null; + pinnedDomainSslEndDate = null; + // Set third-party cookies status if API >= 21. if (Build.VERSION.SDK_INT >= 21) { cookieManager.setAcceptThirdPartyCookies(mainWebView, thirdPartyCookiesEnabled); } - // Set the default user agent. - switch (userAgentString) { - 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(customUserAgentString); - break; + // Only set the user agent if the webpage is not currently loading. Otherwise, changing the user agent on redirects can cause the original website to reload. + // + if (!urlIsLoading) { + switch (defaultUserAgentString) { + case "WebView default user agent": + // Set the user agent to `""`, which uses the default value. + mainWebView.getSettings().setUserAgentString(""); + break; + + case "Custom user agent": + // Set the custom user agent. + mainWebView.getSettings().setUserAgentString(defaultCustomUserAgentString); + break; + + default: + // Use the selected user agent. + mainWebView.getSettings().setUserAgentString(defaultUserAgentString); + } - default: - // Use the selected user agent. - mainWebView.getSettings().setUserAgentString(userAgentString); - break; + // Store the applied user agent string. + appliedUserAgentString = mainWebView.getSettings().getUserAgentString(); } // Set a transparent background on `urlTextBox`. We have to use the deprecated `.getDrawable()` until the minimum API >= 21. - urlAppBarRelativeLayout.setBackgroundDrawable(getResources().getDrawable(R.drawable.url_bar_background_transparent)); + urlAppBarRelativeLayout.setBackgroundDrawable(getResources().getDrawable(R.color.transparent)); } // Close `domainsDatabaseHelper`. @@ -2441,28 +3362,113 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation if (firstPartyCookiesEnabled) { // First-party cookies are enabled. firstPartyCookiesIconMenuItem.setIcon(R.drawable.cookies_enabled); } else { // First-party cookies are disabled. - firstPartyCookiesIconMenuItem.setIcon(R.drawable.cookies_disabled); + if (darkTheme) { + firstPartyCookiesIconMenuItem.setIcon(R.drawable.cookies_disabled_dark); + } else { + firstPartyCookiesIconMenuItem.setIcon(R.drawable.cookies_disabled_light); + } } // Update `domStorageIcon`. if (javaScriptEnabled && domStorageEnabled) { // Both JavaScript and DOM storage are enabled. domStorageIconMenuItem.setIcon(R.drawable.dom_storage_enabled); } else if (javaScriptEnabled) { // JavaScript is enabled but DOM storage is disabled. - domStorageIconMenuItem.setIcon(R.drawable.dom_storage_disabled); + if (darkTheme) { + domStorageIconMenuItem.setIcon(R.drawable.dom_storage_disabled_dark); + } else { + domStorageIconMenuItem.setIcon(R.drawable.dom_storage_disabled_light); + } } else { // JavaScript is disabled, so DOM storage is ghosted. - domStorageIconMenuItem.setIcon(R.drawable.dom_storage_ghosted); + if (darkTheme) { + domStorageIconMenuItem.setIcon(R.drawable.dom_storage_ghosted_dark); + } else { + domStorageIconMenuItem.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. - formDataIconMenuItem.setIcon(R.drawable.form_data_disabled); + if (darkTheme) { + formDataIconMenuItem.setIcon(R.drawable.form_data_disabled_dark); + } else { + formDataIconMenuItem.setIcon(R.drawable.form_data_disabled_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(); + } + } + + private void highlightUrlText() { + String urlString = urlTextBox.getText().toString(); + + if (urlString.startsWith("http://")) { // Highlight the protocol of connections that are not encrypted. + urlTextBox.getText().setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE); + } else if (urlString.startsWith("https://")) { // De-emphasize the protocol of connections that are encrypted. + urlTextBox.getText().setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE); + } + + // Get the index of the `/` immediately after the domain name. + int endOfDomainName = urlString.indexOf("/", (urlString.indexOf("//") + 2)); + + // De-emphasize the text after the domain name. + if (endOfDomainName > 0) { + urlTextBox.getText().setSpan(finalGrayColorSpan, endOfDomainName, urlString.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + } + } + + private void loadBookmarksFolder() { + // Update `bookmarksCursor` 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`. + bookmarksCursorAdapter = new CursorAdapter(this, bookmarksCursor, false) { + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + // Inflate the individual item layout. `false` does not attach it to the root. + return getLayoutInflater().inflate(R.layout.bookmarks_drawer_item_linearlayout, parent, false); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + // Get handles for the views. + ImageView bookmarkFavoriteIcon = view.findViewById(R.id.bookmark_favorite_icon); + TextView bookmarkNameTextView = view.findViewById(R.id.bookmark_name); + + // Get the favorite icon byte array from the `Cursor`. + byte[] favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON)); + + // Convert the byte array to a `Bitmap` beginning at the first byte and ending at the last. + Bitmap favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.length); + + // Display the bitmap in `bookmarkFavoriteIcon`. + bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap); + + // Get the bookmark name from the cursor and display it in `bookmarkNameTextView`. + String bookmarkNameString = cursor.getString(cursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME)); + bookmarkNameTextView.setText(bookmarkNameString); + + // Make the font bold for folders. + if (cursor.getInt(cursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) { + bookmarkNameTextView.setTypeface(Typeface.DEFAULT_BOLD); + } else { // Reset the font to default for normal bookmarks. + bookmarkNameTextView.setTypeface(Typeface.DEFAULT); + } + } + }; + + // Populate the `ListView` with the adapter. + bookmarksListView.setAdapter(bookmarksCursorAdapter); + + // Set the bookmarks drawer title. + if (currentBookmarksFolder.isEmpty()) { + bookmarksTitleTextView.setText(R.string.bookmarks); + } else { + bookmarksTitleTextView.setText(currentBookmarksFolder); } } }