import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentPagerAdapter;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
+import androidx.viewpager.widget.ViewPager;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.navigation.NavigationView;
import com.google.android.material.snackbar.Snackbar;
+import com.google.android.material.tabs.TabLayout;
import com.stoutner.privacybrowser.BuildConfig;
import com.stoutner.privacybrowser.R;
import com.stoutner.privacybrowser.dialogs.AdConsentDialog;
import com.stoutner.privacybrowser.dialogs.PinnedMismatchDialog;
import com.stoutner.privacybrowser.dialogs.UrlHistoryDialog;
import com.stoutner.privacybrowser.dialogs.ViewSslCertificateDialog;
+import com.stoutner.privacybrowser.fragments.WebViewTabFragment;
import com.stoutner.privacybrowser.helpers.AdHelper;
import com.stoutner.privacybrowser.helpers.BlockListHelper;
import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
// AppCompatActivity from android.support.v7.app.AppCompatActivity must be used to have access to the SupportActionBar until the minimum API is >= 21.
public class MainWebViewActivity extends AppCompatActivity implements CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener,
DownloadFileDialog.DownloadFileListener, DownloadImageDialog.DownloadImageListener, DownloadLocationPermissionDialog.DownloadLocationPermissionDialogListener, EditBookmarkDialog.EditBookmarkListener,
- EditBookmarkFolderDialog.EditBookmarkFolderListener, HttpAuthenticationDialog.HttpAuthenticationListener, NavigationView.OnNavigationItemSelectedListener, PinnedMismatchDialog.PinnedMismatchListener,
- SslCertificateErrorDialog.SslCertificateErrorListener, UrlHistoryDialog.UrlHistoryListener {
+ EditBookmarkFolderDialog.EditBookmarkFolderListener, HttpAuthenticationDialog.HttpAuthenticationListener, NavigationView.OnNavigationItemSelectedListener, WebViewTabFragment.NewTabListener,
+ PinnedMismatchDialog.PinnedMismatchListener, SslCertificateErrorDialog.SslCertificateErrorListener, UrlHistoryDialog.UrlHistoryListener {
// `darkTheme` is public static so it can be accessed from everywhere.
public static boolean darkTheme;
private static FragmentManager fragmentManager;
+ // A handle for the activity is set in `onCreate()` and accessed in `WebViewPagerAdapter`.
+ private Activity activity;
+
// `navigatingHistory` is used in `onCreate()`, `onNavigationItemSelected()`, `onSslMismatchBack()`, and `applyDomainSettings()`.
private boolean navigatingHistory;
- // `mainWebView` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`,
+ // The current WebView is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`,
// `findNextOnPage()`, `closeFindOnPage()`, `loadUrlFromTextBox()`, `onSslMismatchBack()`, and `applyProxyThroughOrbot()`.
- private NestedScrollWebView mainWebView;
+ private NestedScrollWebView currentWebView;
// `fullScreenVideoFrameLayout` is used in `onCreate()` and `onConfigurationChanged()`.
private FrameLayout fullScreenVideoFrameLayout;
- // `urlAppBarRelativeLayout` is used in `onCreate()` and `applyDomainSettings()`.
- private RelativeLayout urlAppBarRelativeLayout;
-
- // `favoriteIconImageView` is used in `onCreate()` and `applyDomainSettings()`
- private ImageView favoriteIconImageView;
-
// `cookieManager` is used in `onCreate()`, `onOptionsItemSelected()`, and `onNavigationItemSelected()`, `loadUrlFromTextBox()`, `onDownloadImage()`, `onDownloadFile()`, and `onRestart()`.
private CookieManager cookieManager;
// `refreshMenuItem` is used in `onCreate()` and `onCreateOptionsMenu()`.
private MenuItem refreshMenuItem;
+ // The WebView pager adapter is used in `onCreate()`, `onResume()`, and `addTab()`.
+ private WebViewPagerAdapter webViewPagerAdapter;
+
+ // The navigation requests menu item is used in `onCreate()` and accessed from `WebViewPagerAdapter`.
+ private MenuItem navigationRequestsMenuItem;
+
+ // The blocklist helper is used in `onCreate()` and `WebViewPagerAdapter`.
+ BlockListHelper blockListHelper;
+
+ // The blocklists are populated in `onCreate()` and accessed from `WebViewPagerAdapter`.
+ private ArrayList<List<String[]>> easyList;
+ private ArrayList<List<String[]>> easyPrivacy;
+ private ArrayList<List<String[]>> fanboysAnnoyanceList;
+ private ArrayList<List<String[]>> fanboysSocialList;
+ private ArrayList<List<String[]>> ultraPrivacy;
+
// The blocklist menu items are used in `onCreate()`, `onCreateOptionsMenu()`, and `onPrepareOptionsMenu()`.
private MenuItem blocklistsMenuItem;
private MenuItem easyListMenuItem;
// `fileChooserCallback` is used in `onCreate()` and `onActivityResult()`.
private ValueCallback<Uri[]> fileChooserCallback;
- // The download strings are used in `onCreate()` and `onRequestPermissionResult()`.
+ // The download strings are used in `onCreate()`, `onRequestPermissionResult()` and `initializeWebView()`.
private String downloadUrl;
private String downloadContentDisposition;
private long downloadContentLength;
private ArrayAdapter<CharSequence> userAgentNamesArray;
private String[] userAgentDataArray;
- // The request codes are used in `onCreate()`, `onCreateContextMenu()`, `onCloseDownloadLocationPermissionDialog()`, and `onRequestPermissionResult()`.
+ // The request codes are used in `onCreate()`, `onCreateContextMenu()`, `onCloseDownloadLocationPermissionDialog()`, `onRequestPermissionResult()`, and `initializeWebView()`.
private final int DOWNLOAD_FILE_REQUEST_CODE = 1;
private final int DOWNLOAD_IMAGE_REQUEST_CODE = 2;
setContentView(R.layout.main_framelayout);
// Get handles for views, resources, and managers.
+ activity = this;
Resources resources = getResources();
fragmentManager = getSupportFragmentManager();
inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
// Register `orbotStatusBroadcastReceiver` on `this` context.
this.registerReceiver(orbotStatusBroadcastReceiver, new IntentFilter("org.torproject.android.intent.action.STATUS"));
+ // Instantiate the block list helper.
+ blockListHelper = new BlockListHelper();
+
+ // Initialize the list of resource requests.
+ resourceRequests = new ArrayList<>();
+
+ // Parse the block lists.
+ easyList = blockListHelper.parseBlockList(getAssets(), "blocklists/easylist.txt");
+ easyPrivacy = blockListHelper.parseBlockList(getAssets(), "blocklists/easyprivacy.txt");
+ fanboysAnnoyanceList = blockListHelper.parseBlockList(getAssets(), "blocklists/fanboy-annoyance.txt");
+ fanboysSocialList = blockListHelper.parseBlockList(getAssets(), "blocklists/fanboy-social.txt");
+ ultraPrivacy = blockListHelper.parseBlockList(getAssets(), "blocklists/ultraprivacy.txt");
+
+ // Store the list versions.
+ easyListVersion = easyList.get(0).get(0)[0];
+ easyPrivacyVersion = easyPrivacy.get(0).get(0)[0];
+ fanboysAnnoyanceVersion = fanboysAnnoyanceList.get(0).get(0)[0];
+ fanboysSocialVersion = fanboysSocialList.get(0).get(0)[0];
+ ultraPrivacyVersion = ultraPrivacy.get(0).get(0)[0];
+
// Get handles for views that need to be modified.
- FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
- RelativeLayout mainContentRelativeLayout = findViewById(R.id.main_content_relativelayout);
+ final NavigationView navigationView = findViewById(R.id.navigationview);
+ TabLayout tabLayout = findViewById(R.id.tablayout);
SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
- mainWebView = findViewById(R.id.main_webview);
+ ViewPager webViewPager = findViewById(R.id.webviewpager);
bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
bookmarksTitleTextView = findViewById(R.id.bookmarks_title_textview);
FloatingActionButton launchBookmarksActivityFab = findViewById(R.id.launch_bookmarks_activity_fab);
FloatingActionButton createBookmarkFab = findViewById(R.id.create_bookmark_fab);
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);
+
+ // Listen for touches on the navigation menu.
+ navigationView.setNavigationItemSelectedListener(this);
+
+ // Get handles for the navigation menu and the back and forward menu items. The menu is zero-based.
+ final Menu navigationMenu = navigationView.getMenu();
+ final MenuItem navigationCloseTabMenuItem = navigationMenu.getItem(0);
+ final MenuItem navigationBackMenuItem = navigationMenu.getItem(3);
+ final MenuItem navigationForwardMenuItem = navigationMenu.getItem(4);
+ final MenuItem navigationHistoryMenuItem = navigationMenu.getItem(5);
+ navigationRequestsMenuItem = navigationMenu.getItem(6);
+
+ // Initialize the web view pager adapter.
+ webViewPagerAdapter = new WebViewPagerAdapter(fragmentManager);
+
+ // Set the pager adapter on the web view pager.
+ webViewPager.setAdapter(webViewPagerAdapter);
+
+ // Store up to 100 tabs in memory.
+ webViewPager.setOffscreenPageLimit(100);
+
+ // Update the web view pager every time a tab is modified.
+ webViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
+ @Override
+ public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onPageSelected(int position) {
+ // TODO. Consider using an array of the WebViews.
+ // Get the current WebView fragment. Instantiate item returns the current item if it already exists.
+ Fragment webViewFragment = (Fragment) webViewPagerAdapter.instantiateItem(webViewPager, position);
+
+ // Store the current WebView.
+ currentWebView = (NestedScrollWebView) webViewFragment.getView();
+
+ // Select the corresponding tab if it does not match the currently selected page. This will happen if the page was scrolled via swiping in the view pager.
+ if (tabLayout.getSelectedTabPosition() != position) {
+ // Get a handle for the corresponding tab.
+ TabLayout.Tab correspondingTab = tabLayout.getTabAt(position);
+
+ // Assert that the corresponding tab is not null.
+ assert correspondingTab != null;
+
+ // Select the corresponding tab.
+ correspondingTab.select();
+ }
+ }
+
+ @Override
+ public void onPageScrollStateChanged(int state) {
+ // Do nothing.
+ }
+ });
+
+ // Display the View SSL Certificate dialog when the currently selected tab is reselected.
+ tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
+ @Override
+ public void onTabSelected(TabLayout.Tab tab) {
+ // Select the same page in the view pager.
+ webViewPager.setCurrentItem(tab.getPosition());
+ }
+
+ @Override
+ public void onTabUnselected(TabLayout.Tab tab) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onTabReselected(TabLayout.Tab tab) {
+ // Instantiate the View SSL Certificate dialog.
+ DialogFragment viewSslCertificateDialogFragment = new ViewSslCertificateDialog();
+
+ // Display the View SSL Certificate dialog.
+ viewSslCertificateDialogFragment.show(getSupportFragmentManager(), getString(R.string.view_ssl_certificate));
+ }
+ });
+
+ // Add the first tab.
+ webViewPagerAdapter.addPage();
// 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) {
createBookmarkDialog.show(fragmentManager, resources.getString(R.string.create_bookmark));
});
- // Create a double-tap listener to toggle full-screen mode.
- final GestureDetector gestureDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() {
- // Override `onDoubleTap()`. All other events are handled using the default settings.
- @Override
- public boolean onDoubleTap(MotionEvent event) {
- if (fullScreenBrowsingModeEnabled) { // Only process the double-tap if full screen browsing mode is enabled.
- // Toggle the full screen browsing mode tracker.
- inFullScreenBrowsingMode = !inFullScreenBrowsingMode;
-
- // Toggle the full screen browsing mode.
- if (inFullScreenBrowsingMode) { // Switch to full screen mode.
- // Hide the app bar if specified.
- if (hideAppBar) {
- actionBar.hide();
- }
-
- // Hide the banner ad in the free flavor.
- if (BuildConfig.FLAVOR.contentEquals("free")) {
- AdHelper.hideAd(findViewById(R.id.adview));
- }
-
- // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
- getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
-
- /* Hide the system bars.
- * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
- * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
- * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
- * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
- */
- rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
- View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
- } else { // Switch to normal viewing mode.
- // Show the app bar.
- actionBar.show();
-
- // Show the banner ad in the free flavor.
- if (BuildConfig.FLAVOR.contentEquals("free")) {
- // Reload the ad.
- AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
- }
-
- // Remove the `SYSTEM_UI` flags from the root frame layout.
- rootFrameLayout.setSystemUiVisibility(0);
-
- // Add the translucent status flag.
- getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
- }
-
- // Consume the double-tap.
- return true;
- } else { // Do not consume the double-tap because full screen browsing mode is disabled.
- return false;
- }
- }
- });
-
- // Pass all touch events on `mainWebView` through `gestureDetector` to check for double-taps.
- mainWebView.setOnTouchListener((View view, MotionEvent event) -> {
- // Call `performClick()` on the view, which is required for accessibility.
- view.performClick();
-
- // Send the `event` to `gestureDetector`.
- return gestureDetector.onTouchEvent(event);
- });
-
- // Update `findOnPageCountTextView`.
- mainWebView.setFindListener(new WebView.FindListener() {
- // Get a handle for `findOnPageCountTextView`.
- final TextView findOnPageCountTextView = findViewById(R.id.find_on_page_count_textview);
-
- @Override
- public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) {
- if ((isDoneCounting) && (numberOfMatches == 0)) { // There are no matches.
- // Set `findOnPageCountTextView` to `0/0`.
- findOnPageCountTextView.setText(R.string.zero_of_zero);
- } else if (isDoneCounting) { // There are matches.
- // `activeMatchOrdinal` is zero-based.
- int activeMatch = activeMatchOrdinal + 1;
-
- // Build the match string.
- String matchString = activeMatch + "/" + numberOfMatches;
-
- // Set `findOnPageCountTextView`.
- findOnPageCountTextView.setText(matchString);
- }
- }
- });
-
// Search for the string on the page whenever a character changes in the `findOnPageEditText`.
findOnPageEditText.addTextChangedListener(new TextWatcher() {
@Override
@Override
public void afterTextChanged(Editable s) {
// Search for the text in `mainWebView`.
- mainWebView.findAllAsync(findOnPageEditText.getText().toString());
+ currentWebView.findAllAsync(findOnPageEditText.getText().toString());
}
});
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);
+ inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
// Consume the event.
return true;
});
// Implement swipe to refresh.
- swipeRefreshLayout.setOnRefreshListener(() -> mainWebView.reload());
+ swipeRefreshLayout.setOnRefreshListener(() -> currentWebView.reload());
// The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
swipeRefreshLayout.setProgressViewOffset(false, swipeRefreshLayout.getProgressViewStartOffset() - 10, swipeRefreshLayout.getProgressViewEndOffset());
drawerLayout.setDrawerTitle(GravityCompat.START, getString(R.string.navigation_drawer));
drawerLayout.setDrawerTitle(GravityCompat.END, getString(R.string.bookmarks));
- // Listen for touches on the navigation menu.
- final NavigationView navigationView = findViewById(R.id.navigationview);
- navigationView.setNavigationItemSelectedListener(this);
-
- // Get handles for `navigationMenu` and the back and forward menu items. The menu is zero-based, so items 1, 2, and 3 are the second, third, and fourth entries in the menu.
- final Menu navigationMenu = navigationView.getMenu();
- final MenuItem navigationBackMenuItem = navigationMenu.getItem(1);
- final MenuItem navigationForwardMenuItem = navigationMenu.getItem(2);
- final MenuItem navigationHistoryMenuItem = navigationMenu.getItem(3);
- final MenuItem navigationRequestsMenuItem = navigationMenu.getItem(4);
-
// Initialize the bookmarks database helper. The `0` specifies a database version, but that is ignored and set instead using a constant in `BookmarksDatabaseHelper`.
bookmarksDatabaseHelper = new BookmarksDatabaseHelper(this, null, null, 0);
bookmarksHeaderTextView.setPadding(drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingTop, drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingBottom);
}
- // Update the back, forward, history, and requests menu items.
- navigationBackMenuItem.setEnabled(mainWebView.canGoBack());
- navigationForwardMenuItem.setEnabled(mainWebView.canGoForward());
- navigationHistoryMenuItem.setEnabled((mainWebView.canGoBack() || mainWebView.canGoForward()));
+ // Update the navigation menu items.
+ navigationCloseTabMenuItem.setEnabled(tabLayout.getTabCount() > 1);
+ navigationBackMenuItem.setEnabled(currentWebView.canGoBack());
+ navigationForwardMenuItem.setEnabled(currentWebView.canGoForward());
+ navigationHistoryMenuItem.setEnabled((currentWebView.canGoBack() || currentWebView.canGoForward()));
navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
// Hide the keyboard (if displayed).
- inputMethodManager.hideSoftInputFromWindow(mainWebView.getWindowToken(), 0);
+ inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
// Clear the focus from from the URL text box and the WebView. This removes any text selection markers and context menus, which otherwise draw above the open drawers.
urlTextBox.clearFocus();
- mainWebView.clearFocus();
+ currentWebView.clearFocus();
}
}
});
// Create the hamburger icon at the start of the AppBar.
actionBarDrawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.open_navigation_drawer, R.string.close_navigation_drawer);
- // Get a handle for the progress bar.
- final ProgressBar progressBar = findViewById(R.id.progress_bar);
+ // Initialize cookieManager.
+ cookieManager = CookieManager.getInstance();
- mainWebView.setWebChromeClient(new WebChromeClient() {
- // Update the progress bar when a page is loading.
- @Override
- public void onProgressChanged(WebView view, int progress) {
- // Inject the night mode CSS if night mode is enabled.
- if (nightMode) {
- // `background-color: #212121` sets the background to be dark gray. `color: #BDBDBD` sets the text color to be light gray. `box-shadow: none` removes a lower underline on links
- // used by WordPress. `text-decoration: none` removes all text underlines. `text-shadow: none` removes text shadows, which usually have a hard coded color.
- // `border: none` removes all borders, which can also be used to underline text. `a {color: #1565C0}` sets links to be a dark blue.
- // `::selection {background: #0D47A1}' sets the text selection highlight color to be a dark blue. `!important` takes precedent over any existing sub-settings.
- mainWebView.evaluateJavascript("(function() {var parent = document.getElementsByTagName('head').item(0); var style = document.createElement('style'); style.type = 'text/css'; " +
- "style.innerHTML = '* {background-color: #212121 !important; color: #BDBDBD !important; box-shadow: none !important; text-decoration: none !important;" +
- "text-shadow: none !important; border: none !important;} a {color: #1565C0 !important;} ::selection {background: #0D47A1 !important;}'; parent.appendChild(style)})()", value -> {
- // Initialize a handler to display `mainWebView`.
- Handler displayWebViewHandler = new Handler();
-
- // Setup a runnable to display `mainWebView` after a delay to allow the CSS to be applied.
- Runnable displayWebViewRunnable = () -> {
- // Only display `mainWebView` if the progress bar is gone. This prevents the display of the `WebView` while it is still loading.
- if (progressBar.getVisibility() == View.GONE) {
- mainWebView.setVisibility(View.VISIBLE);
- }
- };
-
- // Displaying of `mainWebView` after 500 milliseconds.
- displayWebViewHandler.postDelayed(displayWebViewRunnable, 500);
- });
- }
+ // Replace the header that `WebView` creates for `X-Requested-With` with a null value. The default value is the application ID (com.stoutner.privacybrowser.standard).
+ customHeaders.put("X-Requested-With", "");
- // Update the progress bar.
- progressBar.setProgress(progress);
+ // Initialize the default preference values the first time the program is run. `false` keeps this command from resetting any current preferences back to default.
+ PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
- // 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);
+ // Get a handle for the `Runtime`.
+ privacyBrowserRuntime = Runtime.getRuntime();
- // 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);
- }
+ // 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`.
- //Stop the swipe to refresh indicator if it is running
- swipeRefreshLayout.setRefreshing(false);
- }
- }
+ // Initialize `inFullScreenBrowsingMode`, which is always false at this point because Privacy Browser never starts in full screen browsing mode.
+ inFullScreenBrowsingMode = false;
- // Set the favorite icon when it changes.
- @Override
- public void onReceivedIcon(WebView view, Bitmap icon) {
- // Only update the favorite icon if the website has finished loading.
- if (progressBar.getVisibility() == View.GONE) {
- // Save a copy of the favorite icon.
- favoriteIconBitmap = icon;
+ // Initialize the privacy settings variables.
+ javaScriptEnabled = false;
+ firstPartyCookiesEnabled = false;
+ thirdPartyCookiesEnabled = false;
+ domStorageEnabled = false;
+ saveFormDataEnabled = false; // Form data can be removed once the minimum API >= 26.
+ nightMode = false;
- // Place the favorite icon in the appBar.
- favoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(icon, 64, 64, true));
- }
- }
+ // Store the default user agent.
+ // TODO webViewDefaultUserAgent = mainWebView.getSettings().getUserAgentString();
- // Save a copy of the title when it changes.
- @Override
- public void onReceivedTitle(WebView view, String title) {
- // Save a copy of the title.
- webViewTitle = title;
- }
+ // Initialize the WebView title.
+ webViewTitle = getString(R.string.no_title);
- // Enter full screen video.
- @Override
- public void onShowCustomView(View video, CustomViewCallback callback) {
- // Set the full screen video flag.
- displayingFullScreenVideo = true;
+ // Initialize the favorite icon bitmap. `ContextCompat` must be used until API >= 21.
+ Drawable favoriteIconDrawable = ContextCompat.getDrawable(getApplicationContext(), R.drawable.world);
+ BitmapDrawable favoriteIconBitmapDrawable = (BitmapDrawable) favoriteIconDrawable;
+ assert favoriteIconBitmapDrawable != null;
+ favoriteIconDefaultBitmap = favoriteIconBitmapDrawable.getBitmap();
- // Pause the ad if this is the free flavor.
- if (BuildConfig.FLAVOR.contentEquals("free")) {
- // The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
- AdHelper.pauseAd(findViewById(R.id.adview));
- }
+ // If the favorite icon is null, load the default.
+ if (favoriteIconBitmap == null) {
+ favoriteIconBitmap = favoriteIconDefaultBitmap;
+ }
- // Hide the keyboard.
- inputMethodManager.hideSoftInputFromWindow(mainWebView.getWindowToken(), 0);
+ // Initialize the user agent array adapter and string array.
+ userAgentNamesArray = ArrayAdapter.createFromResource(this, R.array.user_agent_names, R.layout.spinner_item);
+ userAgentDataArray = resources.getStringArray(R.array.user_agent_data);
- // Hide the main content relative layout.
- mainContentRelativeLayout.setVisibility(View.GONE);
+ // Get the intent that started the app.
+ Intent launchingIntent = getIntent();
- // Remove the translucent status bar overlay on the `Drawer Layout`, which is special and needs its own command.
- drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
+ // Get the information from the intent.
+ String launchingIntentAction = launchingIntent.getAction();
+ Uri launchingIntentUriData = launchingIntent.getData();
- // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
- getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+ // If the intent action is a web search, perform the search.
+ if ((launchingIntentAction != null) && launchingIntentAction.equals(Intent.ACTION_WEB_SEARCH)) {
+ // Create an encoded URL string.
+ String encodedUrlString;
- /* Hide the system bars.
- * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
- * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
- * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
- * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
- */
- rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
- View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
-
- // Disable the sliding drawers.
- drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
-
- // Add the video view to the full screen video frame layout.
- fullScreenVideoFrameLayout.addView(video);
-
- // Show the full screen video frame layout.
- fullScreenVideoFrameLayout.setVisibility(View.VISIBLE);
+ // Sanitize the search input and convert it to a search.
+ try {
+ encodedUrlString = URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8");
+ } catch (UnsupportedEncodingException exception) {
+ encodedUrlString = "";
}
- // Exit full screen video.
- @Override
- public void onHideCustomView() {
- // Unset the full screen video flag.
- displayingFullScreenVideo = false;
-
- // Remove all the views from the full screen video frame layout.
- fullScreenVideoFrameLayout.removeAllViews();
-
- // Hide the full screen video frame layout.
- fullScreenVideoFrameLayout.setVisibility(View.GONE);
+ // Add the base search URL.
+ formattedUrlString = searchURL + encodedUrlString;
+ } else if (launchingIntentUriData != null){ // Check to see if the intent contains a new URL.
+ // Set the formatted URL string.
+ formattedUrlString = launchingIntentUriData.toString();
+ }
+ }
- // Enable the sliding drawers.
- drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
+ @Override
+ protected void onNewIntent(Intent intent) {
+ // Sets the new intent as the activity intent, so that any future `getIntent()`s pick up this one instead of creating a new activity.
+ setIntent(intent);
- // Show the main content relative layout.
- mainContentRelativeLayout.setVisibility(View.VISIBLE);
+ // Get the information from the intent.
+ String intentAction = intent.getAction();
+ Uri intentUriData = intent.getData();
- // Apply the appropriate full screen mode the `SYSTEM_UI` flags.
- if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode.
- // Hide the app bar if specified.
- if (hideAppBar) {
- actionBar.hide();
- }
+ // If the intent action is a web search, perform the search.
+ if ((intentAction != null) && intentAction.equals(Intent.ACTION_WEB_SEARCH)) {
+ // Create an encoded URL string.
+ String encodedUrlString;
- // Hide the banner ad in the free flavor.
- if (BuildConfig.FLAVOR.contentEquals("free")) {
- AdHelper.hideAd(findViewById(R.id.adview));
- }
+ // Sanitize the search input and convert it to a search.
+ try {
+ encodedUrlString = URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8");
+ } catch (UnsupportedEncodingException exception) {
+ encodedUrlString = "";
+ }
- // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
- getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+ // Add the base search URL.
+ formattedUrlString = searchURL + encodedUrlString;
+ } else if (intentUriData != null){ // Check to see if the intent contains a new URL.
+ // Set the formatted URL string.
+ formattedUrlString = intentUriData.toString();
+ }
- /* Hide the system bars.
- * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
- * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
- * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
- * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
- */
- rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
- View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
- } else { // Switch to normal viewing mode.
- // Remove the `SYSTEM_UI` flags from the root frame layout.
- rootFrameLayout.setSystemUiVisibility(0);
+ // Load the URL.
+ loadUrl(formattedUrlString);
- // Add the translucent status flag.
- getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
- }
+ // Get a handle for the drawer layout.
+ DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
- // Reload the ad for the free flavor if not in full screen mode.
- if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
- // Reload the ad.
- AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
- }
- }
+ // Close the navigation drawer if it is open.
+ if (drawerLayout.isDrawerVisible(GravityCompat.START)) {
+ drawerLayout.closeDrawer(GravityCompat.START);
+ }
- // Upload files.
- @Override
- public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
- // Show the file chooser if the device is running API >= 21.
- if (Build.VERSION.SDK_INT >= 21) {
- // Store the file path callback.
- fileChooserCallback = filePathCallback;
+ // Close the bookmarks drawer if it is open.
+ if (drawerLayout.isDrawerVisible(GravityCompat.END)) {
+ drawerLayout.closeDrawer(GravityCompat.END);
+ }
- // Create an intent to open a chooser based ont the file chooser parameters.
- Intent fileChooserIntent = fileChooserParams.createIntent();
+ // Clear the keyboard if displayed and remove the focus on the urlTextBar if it has it.
+ currentWebView.requestFocus();
+ }
- // Open the file chooser. Currently only one `startActivityForResult` exists in this activity, so the request code, used to differentiate them, is simply `0`.
- startActivityForResult(fileChooserIntent, 0);
- }
- return true;
- }
- });
+ @Override
+ public void onRestart() {
+ // Run the default commands.
+ super.onRestart();
- // Register `mainWebView` for a context menu. This is used to see link targets and download images.
- registerForContextMenu(mainWebView);
+ // Make sure Orbot is running if Privacy Browser is proxying through Orbot.
+ if (proxyThroughOrbot) {
+ // Request Orbot to start. If Orbot is already running no hard will be caused by this request.
+ Intent orbotIntent = new Intent("org.torproject.android.intent.action.START");
- // Allow the downloading of files.
- mainWebView.setDownloadListener((String url, String userAgent, String contentDisposition, String mimetype, long contentLength) -> {
- // Check if the download should be processed by an external app.
- if (downloadWithExternalApp) { // Download with an external app.
- openUrlWithExternalApp(url);
- } else { // Download with Android's download manager.
- // Check to see if the WRITE_EXTERNAL_STORAGE permission has already been granted.
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission has not been granted.
- // The WRITE_EXTERNAL_STORAGE permission needs to be requested.
+ // Send the intent to the Orbot package.
+ orbotIntent.setPackage("org.torproject.android");
- // Store the variables for future use by `onRequestPermissionsResult()`.
- downloadUrl = url;
- downloadContentDisposition = contentDisposition;
- downloadContentLength = contentLength;
+ // Make it so.
+ sendBroadcast(orbotIntent);
+ }
- // Show a dialog if the user has previously denied the permission.
- if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
- // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
- DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE);
+ // Apply the app settings if returning from the Settings activity..
+ if (reapplyAppSettingsOnRestart) {
+ // Apply the app settings.
+ applyAppSettings();
- // Show the download location permission alert dialog. The permission will be requested when the the dialog is closed.
- downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
- } else { // Show the permission request directly.
- // Request the permission. The download dialog will be launched by `onRequestPermissionResult()`.
- ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
- }
- } else { // The storage permission has already been granted.
- // Get a handle for the download file alert dialog.
- DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(url, contentDisposition, contentLength);
+ // Reload the webpage if displaying of images has been disabled in the Settings activity.
+ if (reloadOnRestart) {
+ // Reload the WebViews.
+ // TODO
+ currentWebView.reload();
- // Show the download file alert dialog.
- downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
- }
+ // Reset `reloadOnRestartBoolean`.
+ reloadOnRestart = false;
}
- });
-
- // Allow pinch to zoom.
- mainWebView.getSettings().setBuiltInZoomControls(true);
- // Hide zoom controls.
- mainWebView.getSettings().setDisplayZoomControls(false);
-
- // Don't allow mixed content (HTTP and HTTPS) on the same website.
- if (Build.VERSION.SDK_INT >= 21) {
- mainWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW);
+ // Reset the return from settings flag.
+ reapplyAppSettingsOnRestart = false;
}
- // Set the WebView to use a wide viewport. Otherwise, some web pages will be scrunched and some content will render outside the screen.
- mainWebView.getSettings().setUseWideViewPort(true);
+ // Apply the domain settings if returning from the Domains activity.
+ if (reapplyDomainSettingsOnRestart) {
+ // Reapply the domain settings.
+ applyDomainSettings(formattedUrlString, false, true);
- // Set the WebView to load in overview mode (zoomed out to the maximum width).
- mainWebView.getSettings().setLoadWithOverviewMode(true);
+ // Reset `reapplyDomainSettingsOnRestart`.
+ reapplyDomainSettingsOnRestart = false;
+ }
- // Explicitly disable geolocation.
- mainWebView.getSettings().setGeolocationEnabled(false);
+ // Load the URL on restart to apply changes to night mode.
+ if (loadUrlOnRestart) {
+ // Load the current `formattedUrlString`.
+ loadUrl(formattedUrlString);
- // Initialize cookieManager.
- cookieManager = CookieManager.getInstance();
+ // Reset `loadUrlOnRestart.
+ loadUrlOnRestart = false;
+ }
- // Replace the header that `WebView` creates for `X-Requested-With` with a null value. The default value is the application ID (com.stoutner.privacybrowser.standard).
- customHeaders.put("X-Requested-With", "");
+ // Update the bookmarks drawer if returning from the Bookmarks activity.
+ if (restartFromBookmarksActivity) {
+ // Get a handle for the drawer layout.
+ DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
- // Initialize the default preference values the first time the program is run. `false` keeps this command from resetting any current preferences back to default.
- PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
+ // Close the bookmarks drawer.
+ drawerLayout.closeDrawer(GravityCompat.END);
- // Get a handle for the `Runtime`.
- privacyBrowserRuntime = Runtime.getRuntime();
+ // Reload the bookmarks drawer.
+ loadBookmarksFolder();
- // 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`.
+ // Reset `restartFromBookmarksActivity`.
+ restartFromBookmarksActivity = false;
+ }
- // Initialize `inFullScreenBrowsingMode`, which is always false at this point because Privacy Browser never starts in full screen browsing mode.
- inFullScreenBrowsingMode = false;
+ // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. This can be important if the screen was rotated.
+ updatePrivacyIcons(true);
+ }
- // Initialize the privacy settings variables.
- javaScriptEnabled = false;
- firstPartyCookiesEnabled = false;
- thirdPartyCookiesEnabled = false;
- domStorageEnabled = false;
- saveFormDataEnabled = false; // Form data can be removed once the minimum API >= 26.
- nightMode = false;
+ // `onResume()` runs after `onStart()`, which runs after `onCreate()` and `onRestart()`.
+ @Override
+ public void onResume() {
+ // Run the default commands.
+ super.onResume();
- // Store the default user agent.
- webViewDefaultUserAgent = mainWebView.getSettings().getUserAgentString();
+ // Resume JavaScript (if enabled).
+ // TODO mainWebView.resumeTimers();
- // Initialize the WebView title.
- webViewTitle = getString(R.string.no_title);
+ // Resume `mainWebView`.
+ // TODO mainWebView.onResume();
- // Initialize the favorite icon bitmap. `ContextCompat` must be used until API >= 21.
- Drawable favoriteIconDrawable = ContextCompat.getDrawable(getApplicationContext(), R.drawable.world);
- BitmapDrawable favoriteIconBitmapDrawable = (BitmapDrawable) favoriteIconDrawable;
- assert favoriteIconBitmapDrawable != null;
- favoriteIconDefaultBitmap = favoriteIconBitmapDrawable.getBitmap();
+ // Display a message to the user if waiting for Orbot.
+ if (waitingForOrbot && !orbotStatus.equals("ON")) {
+ // Disable the wide view port so that the waiting for Orbot text is displayed correctly.
+ currentWebView.getSettings().setUseWideViewPort(false);
- // If the favorite icon is null, load the default.
- if (favoriteIconBitmap == null) {
- favoriteIconBitmap = favoriteIconDefaultBitmap;
+ // Load a waiting page. `null` specifies no encoding, which defaults to ASCII.
+ currentWebView.loadData(waitingForOrbotHtmlString, "text/html", null);
}
- // Initialize the user agent array adapter and string array.
- userAgentNamesArray = ArrayAdapter.createFromResource(this, R.array.user_agent_names, R.layout.spinner_item);
- userAgentDataArray = resources.getStringArray(R.array.user_agent_data);
-
- // Apply the app settings from the shared preferences.
- applyAppSettings();
-
- // Instantiate the block list helper.
- BlockListHelper blockListHelper = new BlockListHelper();
+ if (displayingFullScreenVideo || inFullScreenBrowsingMode) {
+ // Get a handle for the root frame layouts.
+ FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
- // Initialize the list of resource requests.
- resourceRequests = new ArrayList<>();
+ // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
- // Parse the block lists.
- final ArrayList<List<String[]>> easyList = blockListHelper.parseBlockList(getAssets(), "blocklists/easylist.txt");
- final ArrayList<List<String[]>> easyPrivacy = blockListHelper.parseBlockList(getAssets(), "blocklists/easyprivacy.txt");
- final ArrayList<List<String[]>> fanboysAnnoyanceList = blockListHelper.parseBlockList(getAssets(), "blocklists/fanboy-annoyance.txt");
- final ArrayList<List<String[]>> fanboysSocialList = blockListHelper.parseBlockList(getAssets(), "blocklists/fanboy-social.txt");
- final ArrayList<List<String[]>> ultraPrivacy = blockListHelper.parseBlockList(getAssets(), "blocklists/ultraprivacy.txt");
+ /* Hide the system bars.
+ * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
+ * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
+ * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
+ * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
+ */
+ rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
+ View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
+ } else if (BuildConfig.FLAVOR.contentEquals("free")) { // Resume the adView for the free flavor.
+ // Resume the ad.
+ AdHelper.resumeAd(findViewById(R.id.adview));
+ }
+ }
- // Store the list versions.
- easyListVersion = easyList.get(0).get(0)[0];
- easyPrivacyVersion = easyPrivacy.get(0).get(0)[0];
- fanboysAnnoyanceVersion = fanboysAnnoyanceList.get(0).get(0)[0];
- fanboysSocialVersion = fanboysSocialList.get(0).get(0)[0];
- ultraPrivacyVersion = ultraPrivacy.get(0).get(0)[0];
+ @Override
+ public void onPause() {
+ // Run the default commands.
+ super.onPause();
- // Get a handle for the activity. This is used to update the requests counter while the navigation menu is open.
- Activity activity = this;
+ // Pause `mainWebView`.
+ // TODO
+ currentWebView.onPause();
- mainWebView.setWebViewClient(new WebViewClient() {
- // `shouldOverrideUrlLoading` makes this `WebView` the default handler for URLs inside the app, so that links are not kicked out to other apps.
- // The deprecated `shouldOverrideUrlLoading` must be used until API >= 24.
- @SuppressWarnings("deprecation")
- @Override
- public boolean shouldOverrideUrlLoading(WebView view, String url) {
- if (url.startsWith("http")) { // Load the URL in Privacy Browser.
- // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
- formattedUrlString = "";
+ // Stop all JavaScript.
+ // TODO
+ currentWebView.pauseTimers();
- // Apply the domain settings for the new URL. `applyDomainSettings` doesn't do anything if the domain has not changed.
- boolean userAgentChanged = applyDomainSettings(url, true, false);
+ // Pause the ad or it will continue to consume resources in the background on the free flavor.
+ if (BuildConfig.FLAVOR.contentEquals("free")) {
+ // Pause the ad.
+ AdHelper.pauseAd(findViewById(R.id.adview));
+ }
+ }
- // Check if the user agent has changed.
- if (userAgentChanged) {
- // Manually load the URL. The changing of the user agent will cause WebView to reload the previous URL.
- mainWebView.loadUrl(url, customHeaders);
+ @Override
+ public void onDestroy() {
+ // Unregister the Orbot status broadcast receiver.
+ this.unregisterReceiver(orbotStatusBroadcastReceiver);
- // Returning true indicates that Privacy Browser is manually handling the loading of the URL.
- return true;
- } else {
- // Returning false causes the current WebView to handle the URL and prevents it from adding redirects to the history list.
- return false;
- }
- } else if (url.startsWith("mailto:")) { // Load the email address in an external email program.
- // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
- Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
+ // Close the bookmarks cursor and database.
+ bookmarksCursor.close();
+ bookmarksDatabaseHelper.close();
- // Parse the url and set it as the data for the intent.
- emailIntent.setData(Uri.parse(url));
+ // Run the default commands.
+ super.onDestroy();
+ }
- // Open the email program in a new task instead of as part of Privacy Browser.
- emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // Inflate the menu. This adds items to the action bar if it is present.
+ getMenuInflater().inflate(R.menu.webview_options_menu, menu);
- // Make it so.
- startActivity(emailIntent);
+ // Set mainMenu so it can be used by `onOptionsItemSelected()` and `updatePrivacyIcons`.
+ mainMenu = menu;
- // Returning true indicates Privacy Browser is handling the URL by creating an intent.
- return true;
- } else if (url.startsWith("tel:")) { // Load the phone number in the dialer.
- // Open the dialer and load the phone number, but wait for the user to place the call.
- Intent dialIntent = new Intent(Intent.ACTION_DIAL);
+ // Set the initial status of the privacy icons. `false` does not call `invalidateOptionsMenu` as the last step.
+ updatePrivacyIcons(false);
- // Add the phone number to the intent.
- dialIntent.setData(Uri.parse(url));
+ // Get handles for the menu items.
+ MenuItem toggleFirstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies);
+ MenuItem toggleThirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies);
+ MenuItem toggleDomStorageMenuItem = menu.findItem(R.id.toggle_dom_storage);
+ MenuItem toggleSaveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data); // Form data can be removed once the minimum API >= 26.
+ MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data); // Form data can be removed once the minimum API >= 26.
+ refreshMenuItem = menu.findItem(R.id.refresh);
+ blocklistsMenuItem = menu.findItem(R.id.blocklists);
+ easyListMenuItem = menu.findItem(R.id.easylist);
+ easyPrivacyMenuItem = menu.findItem(R.id.easyprivacy);
+ fanboysAnnoyanceListMenuItem = menu.findItem(R.id.fanboys_annoyance_list);
+ fanboysSocialBlockingListMenuItem = menu.findItem(R.id.fanboys_social_blocking_list);
+ ultraPrivacyMenuItem = menu.findItem(R.id.ultraprivacy);
+ blockAllThirdPartyRequestsMenuItem = menu.findItem(R.id.block_all_third_party_requests);
+ MenuItem adConsentMenuItem = menu.findItem(R.id.ad_consent);
- // Open the dialer in a new task instead of as part of Privacy Browser.
- dialIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ // Only display third-party cookies if API >= 21
+ toggleThirdPartyCookiesMenuItem.setVisible(Build.VERSION.SDK_INT >= 21);
- // Make it so.
- startActivity(dialIntent);
+ // Only display the form data menu items if the API < 26.
+ toggleSaveFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
+ clearFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
- // Returning true indicates Privacy Browser is handling the URL by creating an intent.
- return true;
- } else { // Load a system chooser to select an app that can handle the URL.
- // Open an app that can handle the URL.
- Intent genericIntent = new Intent(Intent.ACTION_VIEW);
+ // Only show Ad Consent if this is the free flavor.
+ adConsentMenuItem.setVisible(BuildConfig.FLAVOR.contentEquals("free"));
- // Add the URL to the intent.
- genericIntent.setData(Uri.parse(url));
+ // Get the shared preference values.
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
- // List all apps that can handle the URL instead of just opening the first one.
- genericIntent.addCategory(Intent.CATEGORY_BROWSABLE);
+ // Get the status of the additional AppBar icons.
+ displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false);
- // Open the app in a new task instead of as part of Privacy Browser.
- genericIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ // Set the status of the additional app bar icons. Setting the refresh menu item to `SHOW_AS_ACTION_ALWAYS` makes it appear even on small devices like phones.
+ if (displayAdditionalAppBarIcons) {
+ toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ refreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+ } else { //Do not display the additional icons.
+ toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
+ toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
+ refreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
+ }
- // Start the app or display a snackbar if no app is available to handle the URL.
- try {
- startActivity(genericIntent);
- } catch (ActivityNotFoundException exception) {
- Snackbar.make(mainWebView, getString(R.string.unrecognized_url) + " " + url, Snackbar.LENGTH_SHORT).show();
- }
+ // Replace Refresh with Stop if a URL is already loading.
+ if (urlIsLoading) {
+ // Set the title.
+ refreshMenuItem.setTitle(R.string.stop);
- // Returning true indicates Privacy Browser is handling the URL by creating an intent.
- return true;
+ // If the icon is displayed in the AppBar, set it according to the theme.
+ if (displayAdditionalAppBarIcons) {
+ if (darkTheme) {
+ refreshMenuItem.setIcon(R.drawable.close_dark);
+ } else {
+ refreshMenuItem.setIcon(R.drawable.close_light);
}
}
+ }
- // 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()));
+ return true;
+ }
- // Reset the whitelist results tracker.
- whiteListResultStringArray = null;
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ // Get a handle for the swipe refresh layout.
+ SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
- // Initialize the third party request tracker.
- boolean isThirdPartyRequest = false;
+ // Get handles for the menu items.
+ MenuItem addOrEditDomain = menu.findItem(R.id.add_or_edit_domain);
+ MenuItem toggleFirstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies);
+ MenuItem toggleThirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies);
+ MenuItem toggleDomStorageMenuItem = menu.findItem(R.id.toggle_dom_storage);
+ MenuItem toggleSaveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data); // Form data can be removed once the minimum API >= 26.
+ MenuItem clearDataMenuItem = menu.findItem(R.id.clear_data);
+ MenuItem clearCookiesMenuItem = menu.findItem(R.id.clear_cookies);
+ MenuItem clearDOMStorageMenuItem = menu.findItem(R.id.clear_dom_storage);
+ MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data); // Form data can be removed once the minimum API >= 26.
+ MenuItem fontSizeMenuItem = menu.findItem(R.id.font_size);
+ MenuItem swipeToRefreshMenuItem = menu.findItem(R.id.swipe_to_refresh);
+ MenuItem displayImagesMenuItem = menu.findItem(R.id.display_images);
+ MenuItem nightModeMenuItem = menu.findItem(R.id.night_mode);
+ MenuItem proxyThroughOrbotMenuItem = menu.findItem(R.id.proxy_through_orbot);
- // Initialize the current domain string.
- String currentDomain = "";
+ // 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);
+ }
- // Nobody is happy when comparing null strings.
- if (!(formattedUrlString == null) && !(url == null)) {
- // Get the domain strings to URIs.
- Uri currentDomainUri = Uri.parse(formattedUrlString);
- Uri requestDomainUri = Uri.parse(url);
+ // Set the status of the menu item checkboxes.
+ toggleFirstPartyCookiesMenuItem.setChecked(firstPartyCookiesEnabled);
+ toggleThirdPartyCookiesMenuItem.setChecked(thirdPartyCookiesEnabled);
+ toggleDomStorageMenuItem.setChecked(domStorageEnabled);
+ toggleSaveFormDataMenuItem.setChecked(saveFormDataEnabled); // Form data can be removed once the minimum API >= 26.
+ easyListMenuItem.setChecked(easyListEnabled);
+ easyPrivacyMenuItem.setChecked(easyPrivacyEnabled);
+ fanboysAnnoyanceListMenuItem.setChecked(fanboysAnnoyanceListEnabled);
+ fanboysSocialBlockingListMenuItem.setChecked(fanboysSocialBlockingListEnabled);
+ ultraPrivacyMenuItem.setChecked(ultraPrivacyEnabled);
+ blockAllThirdPartyRequestsMenuItem.setChecked(blockAllThirdPartyRequests);
+ swipeToRefreshMenuItem.setChecked(swipeRefreshLayout.isEnabled());
+ // TODO displayImagesMenuItem.setChecked(mainWebView.getSettings().getLoadsImagesAutomatically());
+ nightModeMenuItem.setChecked(nightMode);
+ proxyThroughOrbotMenuItem.setChecked(proxyThroughOrbot);
- // Get the domain host names.
- String currentBaseDomain = currentDomainUri.getHost();
- String requestBaseDomain = requestDomainUri.getHost();
+ // Enable third-party cookies if first-party cookies are enabled.
+ toggleThirdPartyCookiesMenuItem.setEnabled(firstPartyCookiesEnabled);
- // Update the current domain variable.
- currentDomain = currentBaseDomain;
+ // Enable DOM Storage if JavaScript is enabled.
+ toggleDomStorageMenuItem.setEnabled(javaScriptEnabled);
- // Only compare the current base domain and the request base domain if neither is null.
- if (!(currentBaseDomain == null) && !(requestBaseDomain == null)) {
- // Determine the current base domain.
- while (currentBaseDomain.indexOf(".", currentBaseDomain.indexOf(".") + 1) > 0) { // There is at least one subdomain.
- // Remove the first subdomain.
- currentBaseDomain = currentBaseDomain.substring(currentBaseDomain.indexOf(".") + 1);
- }
+ // Enable Clear Cookies if there are any.
+ clearCookiesMenuItem.setEnabled(cookieManager.hasCookies());
- // Determine the request base domain.
- while (requestBaseDomain.indexOf(".", requestBaseDomain.indexOf(".") + 1) > 0) { // There is at least one subdomain.
- // Remove the first subdomain.
- requestBaseDomain = requestBaseDomain.substring(requestBaseDomain.indexOf(".") + 1);
- }
+ // Get a count of the number of files in the Local Storage directory.
+ File localStorageDirectory = new File (privateDataDirectoryString + "/app_webview/Local Storage/");
+ int localStorageDirectoryNumberOfFiles = 0;
+ if (localStorageDirectory.exists()) {
+ localStorageDirectoryNumberOfFiles = localStorageDirectory.list().length;
+ }
- // Update the third party request tracker.
- isThirdPartyRequest = !currentBaseDomain.equals(requestBaseDomain);
- }
- }
+ // 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;
+ }
- // Block third-party requests if enabled.
- if (isThirdPartyRequest && blockAllThirdPartyRequests) {
- // Increment the blocked requests counters.
- blockedRequests++;
- thirdPartyBlockedRequests++;
+ // Enable Clear DOM Storage if there is any.
+ clearDOMStorageMenuItem.setEnabled(localStorageDirectoryNumberOfFiles > 0 || indexedDBDirectoryNumberOfFiles > 0);
- // Update the titles of the blocklist menu items. This must be run from the UI thread.
- activity.runOnUiThread(() -> {
- navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
- blocklistsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
- blockAllThirdPartyRequestsMenuItem.setTitle(thirdPartyBlockedRequests + " - " + getString(R.string.block_all_third_party_requests));
- });
+ // Enable Clear Form Data is there is any. This can be removed once the minimum API >= 26.
+ if (Build.VERSION.SDK_INT < 26) {
+ WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(this);
+ clearFormDataMenuItem.setEnabled(mainWebViewDatabase.hasFormData());
+ } else {
+ // Disable clear form data because it is not supported on current version of Android.
+ clearFormDataMenuItem.setEnabled(false);
+ }
- // Add the request to the log.
- resourceRequests.add(new String[]{String.valueOf(REQUEST_THIRD_PARTY), url});
+ // Enable Clear Data if any of the submenu items are enabled.
+ clearDataMenuItem.setEnabled(clearCookiesMenuItem.isEnabled() || clearDOMStorageMenuItem.isEnabled() || clearFormDataMenuItem.isEnabled());
- // Return an empty web resource response.
- return emptyWebResourceResponse;
- }
+ // Disable Fanboy's Social Blocking List if Fanboy's Annoyance List is checked.
+ fanboysSocialBlockingListMenuItem.setEnabled(!fanboysAnnoyanceListEnabled);
- // Check UltraPrivacy if it is enabled.
- if (ultraPrivacyEnabled) {
- if (blockListHelper.isBlocked(currentDomain, url, isThirdPartyRequest, ultraPrivacy)) {
- // Increment the blocked requests counters.
- blockedRequests++;
- ultraPrivacyBlockedRequests++;
+ // Initialize the display names for the blocklists with the number of blocked requests.
+ blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + blockedRequests);
+ easyListMenuItem.setTitle(easyListBlockedRequests + " - " + getString(R.string.easylist));
+ easyPrivacyMenuItem.setTitle(easyPrivacyBlockedRequests + " - " + getString(R.string.easyprivacy));
+ fanboysAnnoyanceListMenuItem.setTitle(fanboysAnnoyanceListBlockedRequests + " - " + getString(R.string.fanboys_annoyance_list));
+ fanboysSocialBlockingListMenuItem.setTitle(fanboysSocialBlockingListBlockedRequests + " - " + getString(R.string.fanboys_social_blocking_list));
+ ultraPrivacyMenuItem.setTitle(ultraPrivacyBlockedRequests + " - " + getString(R.string.ultraprivacy));
+ blockAllThirdPartyRequestsMenuItem.setTitle(thirdPartyBlockedRequests + " - " + getString(R.string.block_all_third_party_requests));
- // Update the titles of the blocklist menu items. This must be run from the UI thread.
- activity.runOnUiThread(() -> {
- navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
- blocklistsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
- ultraPrivacyMenuItem.setTitle(ultraPrivacyBlockedRequests + " - " + getString(R.string.ultraprivacy));
- });
+ // Get the current user agent.
+ // TODO String currentUserAgent = mainWebView.getSettings().getUserAgentString();
+ String currentUserAgent = "";
- // The resource request was blocked. Return an empty web resource response.
- return emptyWebResourceResponse;
- }
+ // Select the current user agent menu item. A switch statement cannot be used because the user agents are not compile time constants.
+ if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[0])) { // Privacy Browser.
+ menu.findItem(R.id.user_agent_privacy_browser).setChecked(true);
+ } else if (currentUserAgent.equals(webViewDefaultUserAgent)) { // WebView Default.
+ menu.findItem(R.id.user_agent_webview_default).setChecked(true);
+ } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[2])) { // Firefox on Android.
+ menu.findItem(R.id.user_agent_firefox_on_android).setChecked(true);
+ } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[3])) { // Chrome on Android.
+ menu.findItem(R.id.user_agent_chrome_on_android).setChecked(true);
+ } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[4])) { // Safari on iOS.
+ menu.findItem(R.id.user_agent_safari_on_ios).setChecked(true);
+ } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[5])) { // Firefox on Linux.
+ menu.findItem(R.id.user_agent_firefox_on_linux).setChecked(true);
+ } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[6])) { // Chromium on Linux.
+ menu.findItem(R.id.user_agent_chromium_on_linux).setChecked(true);
+ } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[7])) { // Firefox on Windows.
+ menu.findItem(R.id.user_agent_firefox_on_windows).setChecked(true);
+ } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[8])) { // Chrome on Windows.
+ menu.findItem(R.id.user_agent_chrome_on_windows).setChecked(true);
+ } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[9])) { // Edge on Windows.
+ menu.findItem(R.id.user_agent_edge_on_windows).setChecked(true);
+ } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[10])) { // Internet Explorer on Windows.
+ menu.findItem(R.id.user_agent_internet_explorer_on_windows).setChecked(true);
+ } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[11])) { // Safari on macOS.
+ menu.findItem(R.id.user_agent_safari_on_macos).setChecked(true);
+ } else { // Custom user agent.
+ menu.findItem(R.id.user_agent_custom).setChecked(true);
+ }
- // If the whitelist result is not null, the request has been allowed by UltraPrivacy.
- if (whiteListResultStringArray != null) {
- // Add a whitelist entry to the resource requests array.
- resourceRequests.add(whiteListResultStringArray);
+ // Initialize font size variables.
+ // TODO int fontSize = mainWebView.getSettings().getTextZoom();
+ int fontSize = 100;
+ String fontSizeTitle;
+ MenuItem selectedFontSizeMenuItem;
- // The resource request has been allowed by UltraPrivacy. `return null` loads the requested resource.
- return null;
- }
+ // Prepare the font size title and current size menu item.
+ switch (fontSize) {
+ case 25:
+ fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.twenty_five_percent);
+ selectedFontSizeMenuItem = menu.findItem(R.id.font_size_twenty_five_percent);
+ break;
+
+ case 50:
+ fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.fifty_percent);
+ selectedFontSizeMenuItem = menu.findItem(R.id.font_size_fifty_percent);
+ break;
+
+ case 75:
+ fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.seventy_five_percent);
+ selectedFontSizeMenuItem = menu.findItem(R.id.font_size_seventy_five_percent);
+ break;
+
+ case 100:
+ fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent);
+ selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent);
+ break;
+
+ case 125:
+ fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_twenty_five_percent);
+ selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_twenty_five_percent);
+ break;
+
+ case 150:
+ fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_fifty_percent);
+ selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_fifty_percent);
+ break;
+
+ case 175:
+ fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_seventy_five_percent);
+ selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_seventy_five_percent);
+ break;
+
+ case 200:
+ 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 = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent);
+ selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent);
+ break;
+ }
+
+ // Set the font size title and select the current size menu item.
+ fontSizeMenuItem.setTitle(fontSizeTitle);
+ selectedFontSizeMenuItem.setChecked(true);
+
+ // Run all the other default commands.
+ super.onPrepareOptionsMenu(menu);
+
+ // Display the menu.
+ return true;
+ }
+
+ @Override
+ // Remove Android Studio's warning about the dangers of using SetJavaScriptEnabled.
+ @SuppressLint("SetJavaScriptEnabled")
+ // removeAllCookies is deprecated, but it is required for API < 21.
+ @SuppressWarnings("deprecation")
+ public boolean onOptionsItemSelected(MenuItem menuItem) {
+ // Reenter full screen browsing mode if it was interrupted by the options menu. <https://redmine.stoutner.com/issues/389>
+ if (inFullScreenBrowsingMode) {
+ // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+
+ FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
+
+ /* Hide the system bars.
+ * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
+ * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
+ * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
+ * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
+ */
+ rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
+ View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
+ }
+
+ // Get the selected menu item ID.
+ int menuItemId = menuItem.getItemId();
+
+ // Run the commands that correlate to the selected menu item.
+ switch (menuItemId) {
+ case R.id.toggle_javascript:
+ // Switch the status of javaScriptEnabled.
+ javaScriptEnabled = !javaScriptEnabled;
+
+ // Apply the new JavaScript status.
+ currentWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled);
+
+ // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
+ updatePrivacyIcons(true);
+
+ // Display a `Snackbar`.
+ if (javaScriptEnabled) { // JavaScrip is enabled.
+ Snackbar.make(findViewById(R.id.webviewpager), R.string.javascript_enabled, Snackbar.LENGTH_SHORT).show();
+ } else if (firstPartyCookiesEnabled) { // JavaScript is disabled, but first-party cookies are enabled.
+ Snackbar.make(findViewById(R.id.webviewpager), R.string.javascript_disabled, Snackbar.LENGTH_SHORT).show();
+ } else { // Privacy mode.
+ Snackbar.make(findViewById(R.id.webviewpager), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
}
- // Check EasyList if it is enabled.
- if (easyListEnabled) {
- if (blockListHelper.isBlocked(currentDomain, url, isThirdPartyRequest, easyList)) {
- // Increment the blocked requests counters.
- blockedRequests++;
- easyListBlockedRequests++;
+ // Reload the current WebView.
+ currentWebView.reload();
+ return true;
- // Update the titles of the blocklist menu items. This must be run from the UI thread.
- activity.runOnUiThread(() -> {
- navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
- blocklistsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
- easyListMenuItem.setTitle(easyListBlockedRequests + " - " + getString(R.string.easylist));
- });
+ case R.id.add_or_edit_domain:
+ if (domainSettingsApplied) { // Edit the current domain settings.
+ // Reapply the domain settings on returning to `MainWebViewActivity`.
+ reapplyDomainSettingsOnRestart = true;
+ currentDomainName = "";
- // Reset the whitelist results tracker (because otherwise it will sometimes add results to the list due to a race condition).
- whiteListResultStringArray = null;
+ // Create an intent to launch the domains activity.
+ Intent domainsIntent = new Intent(this, DomainsActivity.class);
- // The resource request was blocked. Return an empty web resource response.
- return emptyWebResourceResponse;
- }
+ // Put extra information instructing the domains activity to directly load the current domain and close on back instead of returning to the domains list.
+ domainsIntent.putExtra("loadDomain", domainSettingsDatabaseId);
+ domainsIntent.putExtra("closeOnBack", true);
+
+ // Make it so.
+ startActivity(domainsIntent);
+ } else { // Add a new domain.
+ // Apply the new domain settings on returning to `MainWebViewActivity`.
+ reapplyDomainSettingsOnRestart = true;
+ currentDomainName = "";
+
+ // Get the current domain
+ Uri currentUri = Uri.parse(formattedUrlString);
+ String currentDomain = currentUri.getHost();
+
+ // Initialize the database handler. The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
+ DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
+
+ // Create the domain and store the database ID.
+ int newDomainDatabaseId = domainsDatabaseHelper.addDomain(currentDomain);
+
+ // Create an intent to launch the domains activity.
+ Intent domainsIntent = new Intent(this, DomainsActivity.class);
+
+ // Put extra information instructing the domains activity to directly load the new domain and close on back instead of returning to the domains list.
+ domainsIntent.putExtra("loadDomain", newDomainDatabaseId);
+ domainsIntent.putExtra("closeOnBack", true);
+
+ // Make it so.
+ startActivity(domainsIntent);
}
+ return true;
- // Check EasyPrivacy if it is enabled.
- if (easyPrivacyEnabled) {
- if (blockListHelper.isBlocked(currentDomain, url, isThirdPartyRequest, easyPrivacy)) {
- // Increment the blocked requests counters.
- blockedRequests++;
- easyPrivacyBlockedRequests++;
+ case R.id.toggle_first_party_cookies:
+ // Switch the status of firstPartyCookiesEnabled.
+ firstPartyCookiesEnabled = !firstPartyCookiesEnabled;
- // Update the titles of the blocklist menu items. This must be run from the UI thread.
- activity.runOnUiThread(() -> {
- navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
- blocklistsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
- easyPrivacyMenuItem.setTitle(easyPrivacyBlockedRequests + " - " + getString(R.string.easyprivacy));
- });
+ // Update the menu checkbox.
+ menuItem.setChecked(firstPartyCookiesEnabled);
- // Reset the whitelist results tracker (because otherwise it will sometimes add results to the list due to a race condition).
- whiteListResultStringArray = null;
+ // Apply the new cookie status.
+ cookieManager.setAcceptCookie(firstPartyCookiesEnabled);
- // The resource request was blocked. Return an empty web resource response.
- return emptyWebResourceResponse;
- }
+ // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
+ updatePrivacyIcons(true);
+
+ // Display a `Snackbar`.
+ if (firstPartyCookiesEnabled) { // First-party cookies are enabled.
+ Snackbar.make(findViewById(R.id.webviewpager), R.string.first_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
+ } else if (javaScriptEnabled) { // JavaScript is still enabled.
+ Snackbar.make(findViewById(R.id.webviewpager), R.string.first_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
+ } else { // Privacy mode.
+ Snackbar.make(findViewById(R.id.webviewpager), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
}
- // Check Fanboy’s Annoyance List if it is enabled.
- if (fanboysAnnoyanceListEnabled) {
- if (blockListHelper.isBlocked(currentDomain, url, isThirdPartyRequest, fanboysAnnoyanceList)) {
- // Increment the blocked requests counters.
- blockedRequests++;
- fanboysAnnoyanceListBlockedRequests++;
+ // Reload the current WebView.
+ currentWebView.reload();
+ return true;
- // Update the titles of the blocklist menu items. This must be run from the UI thread.
- activity.runOnUiThread(() -> {
- navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
- blocklistsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
- fanboysAnnoyanceListMenuItem.setTitle(fanboysAnnoyanceListBlockedRequests + " - " + getString(R.string.fanboys_annoyance_list));
- });
+ case R.id.toggle_third_party_cookies:
+ if (Build.VERSION.SDK_INT >= 21) {
+ // Switch the status of thirdPartyCookiesEnabled.
+ thirdPartyCookiesEnabled = !thirdPartyCookiesEnabled;
- // Reset the whitelist results tracker (because otherwise it will sometimes add results to the list due to a race condition).
- whiteListResultStringArray = null;
+ // Update the menu checkbox.
+ menuItem.setChecked(thirdPartyCookiesEnabled);
- // The resource request was blocked. Return an empty web resource response.
- return emptyWebResourceResponse;
+ // Apply the new cookie status.
+ cookieManager.setAcceptThirdPartyCookies(currentWebView, thirdPartyCookiesEnabled);
+
+ // Display a `Snackbar`.
+ if (thirdPartyCookiesEnabled) {
+ Snackbar.make(findViewById(R.id.webviewpager), R.string.third_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
+ } else {
+ Snackbar.make(findViewById(R.id.webviewpager), R.string.third_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
}
- } else if (fanboysSocialBlockingListEnabled){ // Only check Fanboy’s Social Blocking List if Fanboy’s Annoyance List is disabled.
- if (blockListHelper.isBlocked(currentDomain, url, isThirdPartyRequest, fanboysSocialList)) {
- // Increment the blocked requests counters.
- blockedRequests++;
- fanboysSocialBlockingListBlockedRequests++;
- // Update the titles of the blocklist menu items. This must be run from the UI thread.
- activity.runOnUiThread(() -> {
- navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
- blocklistsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
- fanboysSocialBlockingListMenuItem.setTitle(fanboysSocialBlockingListBlockedRequests + " - " + getString(R.string.fanboys_social_blocking_list));
- });
+ // Reload the current WebView.
+ currentWebView.reload();
+ } // Else do nothing because SDK < 21.
+ return true;
- // Reset the whitelist results tracker (because otherwise it will sometimes add results to the list due to a race condition).
- whiteListResultStringArray = null;
+ case R.id.toggle_dom_storage:
+ // Switch the status of domStorageEnabled.
+ domStorageEnabled = !domStorageEnabled;
- // The resource request was blocked. Return an empty web resource response.
- return emptyWebResourceResponse;
- }
+ // Update the menu checkbox.
+ menuItem.setChecked(domStorageEnabled);
+
+ // Apply the new DOM Storage status.
+ currentWebView.getSettings().setDomStorageEnabled(domStorageEnabled);
+
+ // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
+ updatePrivacyIcons(true);
+
+ // Display a `Snackbar`.
+ if (domStorageEnabled) {
+ Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_enabled, Snackbar.LENGTH_SHORT).show();
+ } else {
+ Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_disabled, Snackbar.LENGTH_SHORT).show();
}
- // Add the request to the log because it hasn't been processed by any of the previous checks.
- if (whiteListResultStringArray != null ) { // The request was processed by a whitelist.
- resourceRequests.add(whiteListResultStringArray);
- } else { // The request didn't match any blocklist entry. Log it as a default request.
- resourceRequests.add(new String[]{String.valueOf(REQUEST_DEFAULT), url});
+ // Reload the current WebView.
+ currentWebView.reload();
+ return true;
+
+ // Form data can be removed once the minimum API >= 26.
+ case R.id.toggle_save_form_data:
+ // Switch the status of saveFormDataEnabled.
+ saveFormDataEnabled = !saveFormDataEnabled;
+
+ // Update the menu checkbox.
+ menuItem.setChecked(saveFormDataEnabled);
+
+ // Apply the new form data status.
+ currentWebView.getSettings().setSaveFormData(saveFormDataEnabled);
+
+ // Display a `Snackbar`.
+ if (saveFormDataEnabled) {
+ Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_enabled, Snackbar.LENGTH_SHORT).show();
+ } else {
+ Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_disabled, Snackbar.LENGTH_SHORT).show();
}
- // The resource request has not been blocked. `return null` loads the requested resource.
- return null;
- }
+ // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
+ updatePrivacyIcons(true);
+
+ // Reload the current WebView.
+ currentWebView.reload();
+ return true;
+
+ case R.id.clear_cookies:
+ Snackbar.make(findViewById(R.id.webviewpager), R.string.cookies_deleted, Snackbar.LENGTH_LONG)
+ .setAction(R.string.undo, v -> {
+ // Do nothing because everything will be handled by `onDismissed()` below.
+ })
+ .addCallback(new Snackbar.Callback() {
+ @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
+ @Override
+ public void onDismissed(Snackbar snackbar, int event) {
+ switch (event) {
+ // The user pushed the undo button.
+ case Snackbar.Callback.DISMISS_EVENT_ACTION:
+ // Do nothing.
+ break;
+
+ // The snackbar was dismissed without the undo button being pushed.
+ default:
+ // `cookieManager.removeAllCookie()` varies by SDK.
+ if (Build.VERSION.SDK_INT < 21) {
+ cookieManager.removeAllCookie();
+ } else {
+ cookieManager.removeAllCookies(null);
+ }
+ }
+ }
+ })
+ .show();
+ return true;
+
+ case R.id.clear_dom_storage:
+ Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_deleted, Snackbar.LENGTH_LONG)
+ .setAction(R.string.undo, v -> {
+ // Do nothing because everything will be handled by `onDismissed()` below.
+ })
+ .addCallback(new Snackbar.Callback() {
+ @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
+ @Override
+ public void onDismissed(Snackbar snackbar, int event) {
+ switch (event) {
+ // The user pushed the undo button.
+ case Snackbar.Callback.DISMISS_EVENT_ACTION:
+ // Do nothing.
+ break;
+
+ // The snackbar was dismissed without the undo button being pushed.
+ default:
+ // Delete the DOM Storage.
+ WebStorage webStorage = WebStorage.getInstance();
+ webStorage.deleteAllData();
+
+ // Initialize a handler to manually delete the DOM storage files and directories.
+ Handler deleteDomStorageHandler = new Handler();
+
+ // Setup a runnable to manually delete the DOM storage files and directories.
+ Runnable deleteDomStorageRunnable = () -> {
+ try {
+ // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
+ Process deleteLocalStorageProcess = privacyBrowserRuntime.exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
+
+ // Multiple commands must be used because `Runtime.exec()` does not like `*`.
+ Process deleteIndexProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
+ Process deleteQuotaManagerProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
+ Process deleteQuotaManagerJournalProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
+ Process deleteDatabasesProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
- // 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;
+ // Wait for the processes to finish.
+ deleteLocalStorageProcess.waitFor();
+ deleteIndexProcess.waitFor();
+ deleteQuotaManagerProcess.waitFor();
+ deleteQuotaManagerJournalProcess.waitFor();
+ deleteDatabasesProcess.waitFor();
+ } catch (Exception exception) {
+ // Do nothing if an error is thrown.
+ }
+ };
- // Display the HTTP authentication dialog.
- DialogFragment httpAuthenticationDialogFragment = HttpAuthenticationDialog.displayDialog(host, realm);
- httpAuthenticationDialogFragment.show(fragmentManager, getString(R.string.http_authentication));
- }
+ // Manually delete the DOM storage files after 200 milliseconds.
+ deleteDomStorageHandler.postDelayed(deleteDomStorageRunnable, 200);
+ }
+ }
+ })
+ .show();
+ return true;
- // Update the URL in urlTextBox when the page starts to load.
- @Override
- public void onPageStarted(WebView view, String url, Bitmap favicon) {
- // Set `urlIsLoading` to `true`, so that redirects while loading do not trigger changes in the user agent, which forces another reload of the existing page.
- // This is also used to determine when to check for pinned mismatches.
- urlIsLoading = true;
+ // Form data can be remove once the minimum API >= 26.
+ case R.id.clear_form_data:
+ Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_deleted, Snackbar.LENGTH_LONG)
+ .setAction(R.string.undo, v -> {
+ // Do nothing because everything will be handled by `onDismissed()` below.
+ })
+ .addCallback(new Snackbar.Callback() {
+ @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
+ @Override
+ public void onDismissed(Snackbar snackbar, int event) {
+ switch (event) {
+ // The user pushed the undo button.
+ case Snackbar.Callback.DISMISS_EVENT_ACTION:
+ // Do nothing.
+ break;
- // Reset the list of host IP addresses.
- currentHostIpAddresses = "";
+ // The snackbar was dismissed without the `Undo` button being pushed.
+ default:
+ // Delete the form data.
+ WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(getApplicationContext());
+ mainWebViewDatabase.clearFormData();
+ }
+ }
+ })
+ .show();
+ return true;
- // Reset the list of resource requests.
- resourceRequests.clear();
+ case R.id.easylist:
+ // Toggle the EasyList status.
+ easyListEnabled = !easyListEnabled;
- // Initialize the counters for requests blocked by each blocklist.
- blockedRequests = 0;
- easyListBlockedRequests = 0;
- easyPrivacyBlockedRequests = 0;
- fanboysAnnoyanceListBlockedRequests = 0;
- fanboysSocialBlockingListBlockedRequests = 0;
- ultraPrivacyBlockedRequests = 0;
- thirdPartyBlockedRequests = 0;
+ // Update the menu checkbox.
+ menuItem.setChecked(easyListEnabled);
- // If night mode is enabled, hide `mainWebView` until after the night mode CSS is applied.
- if (nightMode) {
- mainWebView.setVisibility(View.INVISIBLE);
- }
+ // Reload the current WebView.
+ currentWebView.reload();
+ return true;
- // Hide the keyboard.
- inputMethodManager.hideSoftInputFromWindow(mainWebView.getWindowToken(), 0);
+ case R.id.easyprivacy:
+ // Toggle the EasyPrivacy status.
+ easyPrivacyEnabled = !easyPrivacyEnabled;
- // Check to see if Privacy Browser is waiting on Orbot.
- if (!waitingForOrbot) { // Process the URL.
- // The formatted URL string must be updated at the beginning of the load, so that if the user toggles JavaScript during the load the new website is reloaded.
- formattedUrlString = url;
+ // Update the menu checkbox.
+ menuItem.setChecked(easyPrivacyEnabled);
- // Display the formatted URL text.
- urlTextBox.setText(formattedUrlString);
+ // Reload the current WebView.
+ currentWebView.reload();
+ return true;
- // Apply text highlighting to `urlTextBox`.
- highlightUrlText();
+ case R.id.fanboys_annoyance_list:
+ // Toggle Fanboy's Annoyance List status.
+ fanboysAnnoyanceListEnabled = !fanboysAnnoyanceListEnabled;
- // Get a URI for the current URL.
- Uri currentUri = Uri.parse(formattedUrlString);
+ // Update the menu checkbox.
+ menuItem.setChecked(fanboysAnnoyanceListEnabled);
- // Get the IP addresses for the host.
- new GetHostIpAddresses(activity).execute(currentUri.getHost());
+ // Update the staus of Fanboy's Social Blocking List.
+ MenuItem fanboysSocialBlockingListMenuItem = mainMenu.findItem(R.id.fanboys_social_blocking_list);
+ fanboysSocialBlockingListMenuItem.setEnabled(!fanboysAnnoyanceListEnabled);
- // Apply any custom domain settings if the URL was loaded by navigating history.
- if (navigatingHistory) {
- // Apply the domain settings.
- boolean userAgentChanged = applyDomainSettings(url, true, false);
+ // Reload the current WebView.
+ currentWebView.reload();
+ return true;
- // Reset `navigatingHistory`.
- navigatingHistory = false;
+ case R.id.fanboys_social_blocking_list:
+ // Toggle Fanboy's Social Blocking List status.
+ fanboysSocialBlockingListEnabled = !fanboysSocialBlockingListEnabled;
- // Manually load the URL if the user agent has changed, which will have caused the previous URL to be reloaded.
- if (userAgentChanged) {
- loadUrl(formattedUrlString);
- }
- }
+ // Update the menu checkbox.
+ menuItem.setChecked(fanboysSocialBlockingListEnabled);
- // Replace Refresh with Stop if the menu item has been created. (The WebView typically begins loading before the menu items are instantiated.)
- if (refreshMenuItem != null) {
- // Set the title.
- refreshMenuItem.setTitle(R.string.stop);
+ // Reload the current WebView.
+ currentWebView.reload();
+ return true;
- // If the icon is displayed in the AppBar, set it according to the theme.
- if (displayAdditionalAppBarIcons) {
- if (darkTheme) {
- refreshMenuItem.setIcon(R.drawable.close_dark);
- } else {
- refreshMenuItem.setIcon(R.drawable.close_light);
- }
- }
- }
- }
- }
+ case R.id.ultraprivacy:
+ // Toggle the UltraPrivacy status.
+ ultraPrivacyEnabled = !ultraPrivacyEnabled;
- // It is necessary to update `formattedUrlString` and `urlTextBox` after the page finishes loading because the final URL can change during load.
- @Override
- public void onPageFinished(WebView view, String url) {
- // Reset the wide view port if it has been turned off by the waiting for Orbot message.
- if (!waitingForOrbot) {
- // Only use a wide view port if the URL starts with `http`, not for `file://` and `content://`.
- mainWebView.getSettings().setUseWideViewPort(url.startsWith("http"));
- }
+ // Update the menu checkbox.
+ menuItem.setChecked(ultraPrivacyEnabled);
- // 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();
- }
+ // Reload the current WebView.
+ currentWebView.reload();
+ return true;
- // Update the Refresh menu item if it has been created.
- if (refreshMenuItem != null) {
- // Reset the Refresh title.
- refreshMenuItem.setTitle(R.string.refresh);
+ case R.id.block_all_third_party_requests:
+ //Toggle the third-party requests blocker status.
+ blockAllThirdPartyRequests = !blockAllThirdPartyRequests;
- // If the icon is displayed in the AppBar, reset it according to the theme.
- if (displayAdditionalAppBarIcons) {
- if (darkTheme) {
- refreshMenuItem.setIcon(R.drawable.refresh_enabled_dark);
- } else {
- refreshMenuItem.setIcon(R.drawable.refresh_enabled_light);
- }
- }
- }
+ // Update the menu checkbox.
+ menuItem.setChecked(blockAllThirdPartyRequests);
+ // Reload the current WebView.
+ currentWebView.reload();
+ return true;
+ case R.id.user_agent_privacy_browser:
+ // Update the user agent.
+ currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[0]);
- // Clear the cache and history if Incognito Mode is enabled.
- if (incognitoModeEnabled) {
- // Clear the cache. `true` includes disk files.
- mainWebView.clearCache(true);
+ // Reload the current WebView.
+ currentWebView.reload();
+ return true;
- // Clear the back/forward history.
- mainWebView.clearHistory();
+ case R.id.user_agent_webview_default:
+ // Update the user agent.
+ currentWebView.getSettings().setUserAgentString("");
- // Manually delete cache folders.
- try {
- // Delete the main cache directory.
- privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/cache");
+ // Reload the current WebView.
+ currentWebView.reload();
+ return true;
- // Delete the secondary `Service Worker` cache directory.
- // A `String[]` must be used because the directory contains a space and `Runtime.exec` will not escape the string correctly otherwise.
- privacyBrowserRuntime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
- } catch (IOException e) {
- // Do nothing if an error is thrown.
- }
- }
+ case R.id.user_agent_firefox_on_android:
+ // Update the user agent.
+ currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[2]);
- // Update the URL text box and apply domain settings if not waiting on Orbot.
- if (!waitingForOrbot) {
- // Check to see if `WebView` has set `url` to be `about:blank`.
- if (url.equals("about:blank")) { // `WebView` is blank, so `formattedUrlString` should be `""` and `urlTextBox` should display a hint.
- // Set `formattedUrlString` to `""`.
- formattedUrlString = "";
+ // Reload the current WebView.
+ currentWebView.reload();
+ return true;
- urlTextBox.setText(formattedUrlString);
+ case R.id.user_agent_chrome_on_android:
+ // Update the user agent.
+ currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[3]);
- // Request focus for `urlTextBox`.
- urlTextBox.requestFocus();
+ // Reload the current WebView.
+ currentWebView.reload();
+ return true;
- // Display the keyboard.
- inputMethodManager.showSoftInput(urlTextBox, 0);
+ case R.id.user_agent_safari_on_ios:
+ // Update the user agent.
+ currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[4]);
- // Apply the domain settings. This clears any settings from the previous domain.
- applyDomainSettings(formattedUrlString, true, false);
- } else { // `WebView` has loaded a webpage.
- // Set the formatted URL string. Getting the URL from the WebView instead of using the one provided by `onPageFinished` makes websites like YouTube function correctly.
- formattedUrlString = mainWebView.getUrl();
+ // Reload the current WebView.
+ currentWebView.reload();
+ return true;
- // Only update the URL text box if the user is not typing in it.
- if (!urlTextBox.hasFocus()) {
- // Display the formatted URL text.
- urlTextBox.setText(formattedUrlString);
+ case R.id.user_agent_firefox_on_linux:
+ // Update the user agent.
+ currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[5]);
- // Apply text highlighting to `urlTextBox`.
- highlightUrlText();
- }
- }
+ // Reload the current WebView.
+ currentWebView.reload();
+ return true;
- // Store the SSL certificate so it can be accessed from `ViewSslCertificateDialog` and `PinnedMismatchDialog`.
- sslCertificate = mainWebView.getCertificate();
+ case R.id.user_agent_chromium_on_linux:
+ // Update the user agent.
+ currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[6]);
- // Check the current website information against any pinned domain information if the current IP addresses have been loaded.
- if (!gettingIpAddresses) {
- checkPinnedMismatch();
- }
- }
+ // Reload the current WebView.
+ currentWebView.reload();
+ return true;
- // Reset `urlIsLoading`, which is used to prevent reloads on redirect if the user agent changes. It is also used to determine when to check for pinned mismatches.
- urlIsLoading = false;
- }
+ case R.id.user_agent_firefox_on_windows:
+ // Update the user agent.
+ currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[7]);
- // Handle SSL Certificate errors.
- @Override
- public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
- // Get the current website SSL certificate.
- SslCertificate currentWebsiteSslCertificate = error.getCertificate();
+ // Reload the current WebView.
+ currentWebView.reload();
+ return true;
- // 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();
+ case R.id.user_agent_chrome_on_windows:
+ // Update the user agent.
+ currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[8]);
- // Proceed to the website if the current SSL website certificate matches the pinned domain certificate.
- if (pinnedSslCertificate &&
- currentWebsiteIssuedToCName.equals(pinnedSslIssuedToCName) && currentWebsiteIssuedToOName.equals(pinnedSslIssuedToOName) &&
- currentWebsiteIssuedToUName.equals(pinnedSslIssuedToUName) && currentWebsiteIssuedByCName.equals(pinnedSslIssuedByCName) &&
- currentWebsiteIssuedByOName.equals(pinnedSslIssuedByOName) && currentWebsiteIssuedByUName.equals(pinnedSslIssuedByUName) &&
- currentWebsiteSslStartDate.equals(pinnedSslStartDate) && currentWebsiteSslEndDate.equals(pinnedSslEndDate)) {
+ // Reload the current WebView.
+ currentWebView.reload();
+ return true;
- // 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;
+ case R.id.user_agent_edge_on_windows:
+ // Update the user agent.
+ currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[9]);
- // Display the SSL error `AlertDialog`.
- DialogFragment sslCertificateErrorDialogFragment = SslCertificateErrorDialog.displayDialog(error);
- sslCertificateErrorDialogFragment.show(fragmentManager, getString(R.string.ssl_certificate_error));
- }
- }
- });
+ // Reload the current WebView.
+ currentWebView.reload();
+ return true;
- // Get the intent that started the app.
- Intent launchingIntent = getIntent();
+ case R.id.user_agent_internet_explorer_on_windows:
+ // Update the user agent.
+ currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[10]);
- // Get the information from the intent.
- String launchingIntentAction = launchingIntent.getAction();
- Uri launchingIntentUriData = launchingIntent.getData();
+ // Reload the current WebView.
+ currentWebView.reload();
+ return true;
- // If the intent action is a web search, perform the search.
- if ((launchingIntentAction != null) && launchingIntentAction.equals(Intent.ACTION_WEB_SEARCH)) {
- // Create an encoded URL string.
- String encodedUrlString;
+ case R.id.user_agent_safari_on_macos:
+ // Update the user agent.
+ currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[11]);
- // Sanitize the search input and convert it to a search.
- try {
- encodedUrlString = URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8");
- } catch (UnsupportedEncodingException exception) {
- encodedUrlString = "";
- }
+ // Reload the current WebView.
+ currentWebView.reload();
+ return true;
- // Add the base search URL.
- formattedUrlString = searchURL + encodedUrlString;
- } else if (launchingIntentUriData != null){ // Check to see if the intent contains a new URL.
- // Set the formatted URL string.
- formattedUrlString = launchingIntentUriData.toString();
- }
+ case R.id.user_agent_custom:
+ // Update the user agent.
+ currentWebView.getSettings().setUserAgentString(defaultCustomUserAgentString);
- // Load the website if not waiting for Orbot to connect.
- if (!waitingForOrbot) {
- loadUrl(formattedUrlString);
- }
- }
+ // Reload the current WebView.
+ currentWebView.reload();
+ return true;
- @Override
- protected void onNewIntent(Intent intent) {
- // Sets the new intent as the activity intent, so that any future `getIntent()`s pick up this one instead of creating a new activity.
- setIntent(intent);
+ case R.id.font_size_twenty_five_percent:
+ currentWebView.getSettings().setTextZoom(25);
+ return true;
- // Get the information from the intent.
- String intentAction = intent.getAction();
- Uri intentUriData = intent.getData();
+ case R.id.font_size_fifty_percent:
+ currentWebView.getSettings().setTextZoom(50);
+ return true;
- // If the intent action is a web search, perform the search.
- if ((intentAction != null) && intentAction.equals(Intent.ACTION_WEB_SEARCH)) {
- // Create an encoded URL string.
- String encodedUrlString;
+ case R.id.font_size_seventy_five_percent:
+ currentWebView.getSettings().setTextZoom(75);
+ return true;
- // Sanitize the search input and convert it to a search.
- try {
- encodedUrlString = URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8");
- } catch (UnsupportedEncodingException exception) {
- encodedUrlString = "";
- }
+ case R.id.font_size_one_hundred_percent:
+ currentWebView.getSettings().setTextZoom(100);
+ return true;
- // Add the base search URL.
- formattedUrlString = searchURL + encodedUrlString;
- } else if (intentUriData != null){ // Check to see if the intent contains a new URL.
- // Set the formatted URL string.
- formattedUrlString = intentUriData.toString();
- }
+ case R.id.font_size_one_hundred_twenty_five_percent:
+ currentWebView.getSettings().setTextZoom(125);
+ return true;
- // Load the URL.
- loadUrl(formattedUrlString);
+ case R.id.font_size_one_hundred_fifty_percent:
+ currentWebView.getSettings().setTextZoom(150);
+ return true;
- // Get a handle for the drawer layout.
- DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
+ case R.id.font_size_one_hundred_seventy_five_percent:
+ currentWebView.getSettings().setTextZoom(175);
+ return true;
- // Close the navigation drawer if it is open.
- if (drawerLayout.isDrawerVisible(GravityCompat.START)) {
- drawerLayout.closeDrawer(GravityCompat.START);
- }
+ case R.id.font_size_two_hundred_percent:
+ currentWebView.getSettings().setTextZoom(200);
+ return true;
- // Close the bookmarks drawer if it is open.
- if (drawerLayout.isDrawerVisible(GravityCompat.END)) {
- drawerLayout.closeDrawer(GravityCompat.END);
- }
+ case R.id.swipe_to_refresh:
+ // Get a handle for the swipe refresh layout.
+ SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
- // Clear the keyboard if displayed and remove the focus on the urlTextBar if it has it.
- mainWebView.requestFocus();
- }
+ // Toggle swipe to refresh.
+ swipeRefreshLayout.setEnabled(!swipeRefreshLayout.isEnabled());
+ return true;
- @Override
- public void onRestart() {
- // Run the default commands.
- super.onRestart();
+ case R.id.display_images:
+ if (currentWebView.getSettings().getLoadsImagesAutomatically()) { // Images are currently loaded automatically.
+ // Disable loading of images.
+ currentWebView.getSettings().setLoadsImagesAutomatically(false);
- // Make sure Orbot is running if Privacy Browser is proxying through Orbot.
- if (proxyThroughOrbot) {
- // Request Orbot to start. If Orbot is already running no hard will be caused by this request.
- Intent orbotIntent = new Intent("org.torproject.android.intent.action.START");
+ // Reload the website to remove existing images.
+ currentWebView.reload();
+ } else { // Images are not currently loaded automatically.
+ // Enable loading of images. Missing images will be loaded without the need for a reload.
+ currentWebView.getSettings().setLoadsImagesAutomatically(true);
+ }
+ return true;
- // Send the intent to the Orbot package.
- orbotIntent.setPackage("org.torproject.android");
+ case R.id.night_mode:
+ // Toggle night mode.
+ nightMode = !nightMode;
- // Make it so.
- sendBroadcast(orbotIntent);
- }
+ // Enable or disable JavaScript according to night mode, the global preference, and any domain settings.
+ if (nightMode) { // Night mode is enabled. Enable JavaScript.
+ // Update the global variable.
+ javaScriptEnabled = true;
+ } else if (domainSettingsApplied) { // Night mode is disabled and domain settings are applied. Set JavaScript according to the domain settings.
+ // Get the JavaScript preference that was stored the last time domain settings were loaded.
+ javaScriptEnabled = domainSettingsJavaScriptEnabled;
+ } else { // Night mode is disabled and domain settings are not applied. Set JavaScript according to the global preference.
+ // Get a handle for the shared preference.
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
- // Apply the app settings if returning from the Settings activity..
- if (reapplyAppSettingsOnRestart) {
- // Apply the app settings.
- applyAppSettings();
+ // Get the JavaScript preference.
+ javaScriptEnabled = sharedPreferences.getBoolean("javascript", false);
+ }
- // Reload the webpage if displaying of images has been disabled in the Settings activity.
- if (reloadOnRestart) {
- // Reload `mainWebView`.
- mainWebView.reload();
+ // Apply the JavaScript setting to the WebView.
+ currentWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled);
- // Reset `reloadOnRestartBoolean`.
- reloadOnRestart = false;
- }
+ // Update the privacy icons.
+ updatePrivacyIcons(false);
- // Reset the return from settings flag.
- reapplyAppSettingsOnRestart = false;
- }
+ // Reload the website.
+ currentWebView.reload();
+ return true;
- // Apply the domain settings if returning from the Domains activity.
- if (reapplyDomainSettingsOnRestart) {
- // Reapply the domain settings.
- applyDomainSettings(formattedUrlString, false, true);
+ case R.id.find_on_page:
+ // Get a handle for the views.
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
- // Reset `reapplyDomainSettingsOnRestart`.
- reapplyDomainSettingsOnRestart = false;
- }
+ // Hide the toolbar.
+ toolbar.setVisibility(View.GONE);
- // Load the URL on restart to apply changes to night mode.
- if (loadUrlOnRestart) {
- // Load the current `formattedUrlString`.
- loadUrl(formattedUrlString);
+ // Show the find on page linear layout.
+ findOnPageLinearLayout.setVisibility(View.VISIBLE);
- // Reset `loadUrlOnRestart.
- loadUrlOnRestart = false;
- }
+ // Display the keyboard. The app must wait 200 ms before running the command to work around a bug in Android.
+ // http://stackoverflow.com/questions/5520085/android-show-softkeyboard-with-showsoftinput-is-not-working
+ findOnPageEditText.postDelayed(() -> {
+ // Set the focus on `findOnPageEditText`.
+ findOnPageEditText.requestFocus();
- // Update the bookmarks drawer if returning from the Bookmarks activity.
- if (restartFromBookmarksActivity) {
- // Get a handle for the drawer layout.
- DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
+ // Display the keyboard. `0` sets no input flags.
+ inputMethodManager.showSoftInput(findOnPageEditText, 0);
+ }, 200);
+ return true;
- // Close the bookmarks drawer.
- drawerLayout.closeDrawer(GravityCompat.END);
+ case R.id.view_source:
+ // Launch the View Source activity.
+ Intent viewSourceIntent = new Intent(this, ViewSourceActivity.class);
+ startActivity(viewSourceIntent);
+ return true;
- // Reload the bookmarks drawer.
- loadBookmarksFolder();
+ case R.id.share_url:
+ // Setup the share string.
+ String shareString = webViewTitle + " – " + urlTextBox.getText().toString();
- // Reset `restartFromBookmarksActivity`.
- restartFromBookmarksActivity = false;
- }
+ // Create the share intent.
+ Intent shareIntent = new Intent(Intent.ACTION_SEND);
+ shareIntent.putExtra(Intent.EXTRA_TEXT, shareString);
+ shareIntent.setType("text/plain");
- // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. This can be important if the screen was rotated.
- updatePrivacyIcons(true);
- }
+ // Make it so.
+ startActivity(Intent.createChooser(shareIntent, getString(R.string.share_url)));
+ return true;
- // `onResume()` runs after `onStart()`, which runs after `onCreate()` and `onRestart()`.
- @Override
- public void onResume() {
- // Run the default commands.
- super.onResume();
+ case R.id.print:
+ // Get a `PrintManager` instance.
+ PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);
- // Resume JavaScript (if enabled).
- mainWebView.resumeTimers();
+ // Create a print document adapter form the current WebView.
+ PrintDocumentAdapter printDocumentAdapter = currentWebView.createPrintDocumentAdapter();
- // Resume `mainWebView`.
- mainWebView.onResume();
+ // Remove the lint error below that `printManager` might be `null`.
+ assert printManager != null;
- // Display a message to the user if waiting for Orbot.
- if (waitingForOrbot && !orbotStatus.equals("ON")) {
- // Disable the wide view port so that the waiting for Orbot text is displayed correctly.
- mainWebView.getSettings().setUseWideViewPort(false);
+ // Print the document. The print attributes are `null`.
+ printManager.print(getString(R.string.privacy_browser_web_page), printDocumentAdapter, null);
+ return true;
+
+ case R.id.open_with_app:
+ openWithApp(formattedUrlString);
+ return true;
- // Load a waiting page. `null` specifies no encoding, which defaults to ASCII.
- mainWebView.loadData(waitingForOrbotHtmlString, "text/html", null);
- }
+ case R.id.open_with_browser:
+ openWithBrowser(formattedUrlString);
+ return true;
- if (displayingFullScreenVideo || inFullScreenBrowsingMode) {
- // Get a handle for the root frame layouts.
- FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
+ case R.id.add_to_homescreen:
+ // Instantiate the create home screen shortcut dialog.
+ DialogFragment createHomeScreenShortcutDialogFragment = CreateHomeScreenShortcutDialog.createDialog(currentWebView.getTitle(), formattedUrlString, favoriteIconBitmap);
- // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
- getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+ // Show the create home screen shortcut dialog.
+ createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getString(R.string.create_shortcut));
+ return true;
- /* Hide the system bars.
- * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
- * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
- * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
- * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
- */
- rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
- View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
- } else if (BuildConfig.FLAVOR.contentEquals("free")) { // Resume the adView for the free flavor.
- // Resume the ad.
- AdHelper.resumeAd(findViewById(R.id.adview));
- }
- }
+ case R.id.proxy_through_orbot:
+ // Toggle the proxy through Orbot variable.
+ proxyThroughOrbot = !proxyThroughOrbot;
- @Override
- public void onPause() {
- // Run the default commands.
- super.onPause();
+ // Apply the proxy through Orbot settings.
+ applyProxyThroughOrbot(true);
+ return true;
- // Pause `mainWebView`.
- mainWebView.onPause();
+ case R.id.refresh:
+ if (menuItem.getTitle().equals(getString(R.string.refresh))) { // The refresh button was pushed.
+ // Reload the current WebView.
+ currentWebView.reload();
+ } else { // The stop button was pushed.
+ // Stop the loading of the WebView.
+ currentWebView.stopLoading();
+ }
+ return true;
- // Stop all JavaScript.
- mainWebView.pauseTimers();
+ case R.id.ad_consent:
+ // Display the ad consent dialog.
+ DialogFragment adConsentDialogFragment = new AdConsentDialog();
+ adConsentDialogFragment.show(getSupportFragmentManager(), getString(R.string.ad_consent));
+ return true;
- // Pause the ad or it will continue to consume resources in the background on the free flavor.
- if (BuildConfig.FLAVOR.contentEquals("free")) {
- // Pause the ad.
- AdHelper.pauseAd(findViewById(R.id.adview));
+ default:
+ // Don't consume the event.
+ return super.onOptionsItemSelected(menuItem);
}
}
+ // removeAllCookies is deprecated, but it is required for API < 21.
+ @SuppressWarnings("deprecation")
@Override
- public void onDestroy() {
- // Unregister the Orbot status broadcast receiver.
- this.unregisterReceiver(orbotStatusBroadcastReceiver);
+ public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
+ // Get the menu item ID.
+ int menuItemId = menuItem.getItemId();
- // Close the bookmarks cursor and database.
- bookmarksCursor.close();
- bookmarksDatabaseHelper.close();
+ // Run the commands that correspond to the selected menu item.
+ switch (menuItemId) {
+ case R.id.close_tab:
+ // Get a handle for the tab layout.
+ TabLayout tabLayout = findViewById(R.id.tablayout);
- // Run the default commands.
- super.onDestroy();
- }
+ // Get the current tab number.
+ int currentTabNumber = tabLayout.getSelectedTabPosition();
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- // Inflate the menu. This adds items to the action bar if it is present.
- getMenuInflater().inflate(R.menu.webview_options_menu, menu);
+ // Delete the tab and page.
+ webViewPagerAdapter.deletePage(currentTabNumber);
+ break;
- // Set mainMenu so it can be used by `onOptionsItemSelected()` and `updatePrivacyIcons`.
- mainMenu = menu;
+ case R.id.clear_and_exit:
+ // Close the bookmarks cursor and database.
+ bookmarksCursor.close();
+ bookmarksDatabaseHelper.close();
- // Set the initial status of the privacy icons. `false` does not call `invalidateOptionsMenu` as the last step.
- updatePrivacyIcons(false);
+ // Get a handle for the shared preferences.
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
- // Get handles for the menu items.
- MenuItem toggleFirstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies);
- MenuItem toggleThirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies);
- MenuItem toggleDomStorageMenuItem = menu.findItem(R.id.toggle_dom_storage);
- MenuItem toggleSaveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data); // Form data can be removed once the minimum API >= 26.
- MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data); // Form data can be removed once the minimum API >= 26.
- refreshMenuItem = menu.findItem(R.id.refresh);
- blocklistsMenuItem = menu.findItem(R.id.blocklists);
- easyListMenuItem = menu.findItem(R.id.easylist);
- easyPrivacyMenuItem = menu.findItem(R.id.easyprivacy);
- fanboysAnnoyanceListMenuItem = menu.findItem(R.id.fanboys_annoyance_list);
- fanboysSocialBlockingListMenuItem = menu.findItem(R.id.fanboys_social_blocking_list);
- ultraPrivacyMenuItem = menu.findItem(R.id.ultraprivacy);
- blockAllThirdPartyRequestsMenuItem = menu.findItem(R.id.block_all_third_party_requests);
- MenuItem adConsentMenuItem = menu.findItem(R.id.ad_consent);
+ // Get the status of the clear everything preference.
+ boolean clearEverything = sharedPreferences.getBoolean("clear_everything", true);
- // Only display third-party cookies if API >= 21
- toggleThirdPartyCookiesMenuItem.setVisible(Build.VERSION.SDK_INT >= 21);
+ // 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();
+ }
- // Only display the form data menu items if the API < 26.
- toggleSaveFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
- clearFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
+ // Manually delete the cookies database, as `CookieManager` sometimes will not flush its changes to disk before `System.exit(0)` is run.
+ try {
+ // Two commands must be used because `Runtime.exec()` does not like `*`.
+ Process deleteCookiesProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies");
+ Process deleteCookiesJournalProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies-journal");
- // Only show Ad Consent if this is the free flavor.
- adConsentMenuItem.setVisible(BuildConfig.FLAVOR.contentEquals("free"));
+ // Wait until the processes have finished.
+ deleteCookiesProcess.waitFor();
+ deleteCookiesJournalProcess.waitFor();
+ } catch (Exception exception) {
+ // Do nothing if an error is thrown.
+ }
+ }
- // Get the shared preference values.
- SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+ // Clear DOM storage.
+ if (clearEverything || sharedPreferences.getBoolean("clear_dom_storage", true)) {
+ // Ask `WebStorage` to clear the DOM storage.
+ WebStorage webStorage = WebStorage.getInstance();
+ webStorage.deleteAllData();
- // Get the status of the additional AppBar icons.
- displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false);
+ // Manually delete the DOM storage files and directories, as `WebStorage` sometimes will not flush its changes to disk before `System.exit(0)` is run.
+ try {
+ // A `String[]` must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
+ Process deleteLocalStorageProcess = privacyBrowserRuntime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
- // Set the status of the additional app bar icons. Setting the refresh menu item to `SHOW_AS_ACTION_ALWAYS` makes it appear even on small devices like phones.
- if (displayAdditionalAppBarIcons) {
- toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
- toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
- refreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
- } else { //Do not display the additional icons.
- toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
- toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
- refreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
- }
+ // Multiple commands must be used because `Runtime.exec()` does not like `*`.
+ Process deleteIndexProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
+ Process deleteQuotaManagerProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
+ Process deleteQuotaManagerJournalProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
+ Process deleteDatabaseProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
- // Replace Refresh with Stop if a URL is already loading.
- if (urlIsLoading) {
- // Set the title.
- refreshMenuItem.setTitle(R.string.stop);
+ // Wait until the processes have finished.
+ deleteLocalStorageProcess.waitFor();
+ deleteIndexProcess.waitFor();
+ deleteQuotaManagerProcess.waitFor();
+ deleteQuotaManagerJournalProcess.waitFor();
+ deleteDatabaseProcess.waitFor();
+ } catch (Exception exception) {
+ // Do nothing if an error is thrown.
+ }
+ }
- // If the icon is displayed in the AppBar, set it according to the theme.
- if (displayAdditionalAppBarIcons) {
- if (darkTheme) {
- refreshMenuItem.setIcon(R.drawable.close_dark);
- } else {
- refreshMenuItem.setIcon(R.drawable.close_light);
+ // Clear form data if the API < 26.
+ if ((Build.VERSION.SDK_INT < 26) && (clearEverything || sharedPreferences.getBoolean("clear_form_data", true))) {
+ WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
+ webViewDatabase.clearFormData();
+
+ // Manually delete the form data database, as `WebViewDatabase` sometimes will not flush its changes to disk before `System.exit(0)` is run.
+ try {
+ // A string array must be used because the database contains a space and `Runtime.exec` will not otherwise escape the string correctly.
+ Process deleteWebDataProcess = privacyBrowserRuntime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data"});
+ Process deleteWebDataJournalProcess = privacyBrowserRuntime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data-journal"});
+
+ // Wait until the processes have finished.
+ deleteWebDataProcess.waitFor();
+ deleteWebDataJournalProcess.waitFor();
+ } catch (Exception exception) {
+ // Do nothing if an error is thrown.
+ }
}
- }
- }
- return true;
- }
+ // Clear the cache.
+ if (clearEverything || sharedPreferences.getBoolean("clear_cache", true)) {
+ // Clear the cache.
+ // TODO
+ currentWebView.clearCache(true);
- @Override
- public boolean onPrepareOptionsMenu(Menu menu) {
- // Get a handle for the swipe refresh layout.
- SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
+ // Manually delete the cache directories.
+ try {
+ // Delete the main cache directory.
+ Process deleteCacheProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/cache");
- // Get handles for the menu items.
- MenuItem addOrEditDomain = menu.findItem(R.id.add_or_edit_domain);
- MenuItem toggleFirstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies);
- MenuItem toggleThirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies);
- MenuItem toggleDomStorageMenuItem = menu.findItem(R.id.toggle_dom_storage);
- MenuItem toggleSaveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data); // Form data can be removed once the minimum API >= 26.
- MenuItem clearDataMenuItem = menu.findItem(R.id.clear_data);
- MenuItem clearCookiesMenuItem = menu.findItem(R.id.clear_cookies);
- MenuItem clearDOMStorageMenuItem = menu.findItem(R.id.clear_dom_storage);
- MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data); // Form data can be removed once the minimum API >= 26.
- MenuItem fontSizeMenuItem = menu.findItem(R.id.font_size);
- MenuItem swipeToRefreshMenuItem = menu.findItem(R.id.swipe_to_refresh);
- MenuItem displayImagesMenuItem = menu.findItem(R.id.display_images);
- MenuItem nightModeMenuItem = menu.findItem(R.id.night_mode);
- MenuItem proxyThroughOrbotMenuItem = menu.findItem(R.id.proxy_through_orbot);
+ // Delete the secondary `Service Worker` cache directory.
+ // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
+ Process deleteServiceWorkerProcess = privacyBrowserRuntime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
- // 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);
- }
+ // Wait until the processes have finished.
+ deleteCacheProcess.waitFor();
+ deleteServiceWorkerProcess.waitFor();
+ } catch (Exception exception) {
+ // Do nothing if an error is thrown.
+ }
+ }
- // Set the status of the menu item checkboxes.
- toggleFirstPartyCookiesMenuItem.setChecked(firstPartyCookiesEnabled);
- toggleThirdPartyCookiesMenuItem.setChecked(thirdPartyCookiesEnabled);
- toggleDomStorageMenuItem.setChecked(domStorageEnabled);
- toggleSaveFormDataMenuItem.setChecked(saveFormDataEnabled); // Form data can be removed once the minimum API >= 26.
- easyListMenuItem.setChecked(easyListEnabled);
- easyPrivacyMenuItem.setChecked(easyPrivacyEnabled);
- fanboysAnnoyanceListMenuItem.setChecked(fanboysAnnoyanceListEnabled);
- fanboysSocialBlockingListMenuItem.setChecked(fanboysSocialBlockingListEnabled);
- ultraPrivacyMenuItem.setChecked(ultraPrivacyEnabled);
- blockAllThirdPartyRequestsMenuItem.setChecked(blockAllThirdPartyRequests);
- swipeToRefreshMenuItem.setChecked(swipeRefreshLayout.isEnabled());
- displayImagesMenuItem.setChecked(mainWebView.getSettings().getLoadsImagesAutomatically());
- nightModeMenuItem.setChecked(nightMode);
- proxyThroughOrbotMenuItem.setChecked(proxyThroughOrbot);
+ // Clear SSL certificate preferences.
+ // TODO
+ currentWebView.clearSslPreferences();
- // Enable third-party cookies if first-party cookies are enabled.
- toggleThirdPartyCookiesMenuItem.setEnabled(firstPartyCookiesEnabled);
+ // Clear the back/forward history.
+ // TODO
+ currentWebView.clearHistory();
- // Enable DOM Storage if JavaScript is enabled.
- toggleDomStorageMenuItem.setEnabled(javaScriptEnabled);
+ // Clear `formattedUrlString`.
+ formattedUrlString = null;
- // Enable Clear Cookies if there are any.
- clearCookiesMenuItem.setEnabled(cookieManager.hasCookies());
+ // Clear `customHeaders`.
+ customHeaders.clear();
- // 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;
- }
+ // Destroy the internal state of `mainWebView`.
+ // TODO
+ currentWebView.destroy();
- // 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;
- }
+ // Manually delete the `app_webview` folder, which contains the cookies, DOM storage, form data, and `Service Worker` cache.
+ // See `https://code.google.com/p/android/issues/detail?id=233826&thanks=233826&ts=1486670530`.
+ if (clearEverything) {
+ try {
+ // Delete the folder.
+ Process deleteAppWebviewProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview");
- // Enable Clear DOM Storage if there is any.
- clearDOMStorageMenuItem.setEnabled(localStorageDirectoryNumberOfFiles > 0 || indexedDBDirectoryNumberOfFiles > 0);
+ // Wait until the process has finished.
+ deleteAppWebviewProcess.waitFor();
+ } catch (Exception exception) {
+ // Do nothing if an error is thrown.
+ }
+ }
- // Enable Clear Form Data is there is any. This can be removed once the minimum API >= 26.
- if (Build.VERSION.SDK_INT < 26) {
- WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(this);
- clearFormDataMenuItem.setEnabled(mainWebViewDatabase.hasFormData());
- } else {
- // Disable clear form data because it is not supported on current version of Android.
- clearFormDataMenuItem.setEnabled(false);
- }
+ // Close Privacy Browser. `finishAndRemoveTask` also removes Privacy Browser from the recent app list.
+ if (Build.VERSION.SDK_INT >= 21) {
+ finishAndRemoveTask();
+ } else {
+ finish();
+ }
- // Enable Clear Data if any of the submenu items are enabled.
- clearDataMenuItem.setEnabled(clearCookiesMenuItem.isEnabled() || clearDOMStorageMenuItem.isEnabled() || clearFormDataMenuItem.isEnabled());
+ // Remove the terminated program from RAM. The status code is `0`.
+ System.exit(0);
+ break;
- // Disable Fanboy's Social Blocking List if Fanboy's Annoyance List is checked.
- fanboysSocialBlockingListMenuItem.setEnabled(!fanboysAnnoyanceListEnabled);
+ case R.id.home:
+ loadUrl(homepage);
+ break;
- // Initialize the display names for the blocklists with the number of blocked requests.
- blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + blockedRequests);
- easyListMenuItem.setTitle(easyListBlockedRequests + " - " + getString(R.string.easylist));
- easyPrivacyMenuItem.setTitle(easyPrivacyBlockedRequests + " - " + getString(R.string.easyprivacy));
- fanboysAnnoyanceListMenuItem.setTitle(fanboysAnnoyanceListBlockedRequests + " - " + getString(R.string.fanboys_annoyance_list));
- fanboysSocialBlockingListMenuItem.setTitle(fanboysSocialBlockingListBlockedRequests + " - " + getString(R.string.fanboys_social_blocking_list));
- ultraPrivacyMenuItem.setTitle(ultraPrivacyBlockedRequests + " - " + getString(R.string.ultraprivacy));
- blockAllThirdPartyRequestsMenuItem.setTitle(thirdPartyBlockedRequests + " - " + getString(R.string.block_all_third_party_requests));
+ case R.id.back:
+ if (currentWebView.canGoBack()) {
+ // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
+ formattedUrlString = "";
- // Get the current user agent.
- String currentUserAgent = mainWebView.getSettings().getUserAgentString();
+ // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
+ navigatingHistory = true;
- // Select the current user agent menu item. A switch statement cannot be used because the user agents are not compile time constants.
- if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[0])) { // Privacy Browser.
- menu.findItem(R.id.user_agent_privacy_browser).setChecked(true);
- } else if (currentUserAgent.equals(webViewDefaultUserAgent)) { // WebView Default.
- menu.findItem(R.id.user_agent_webview_default).setChecked(true);
- } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[2])) { // Firefox on Android.
- menu.findItem(R.id.user_agent_firefox_on_android).setChecked(true);
- } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[3])) { // Chrome on Android.
- menu.findItem(R.id.user_agent_chrome_on_android).setChecked(true);
- } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[4])) { // Safari on iOS.
- menu.findItem(R.id.user_agent_safari_on_ios).setChecked(true);
- } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[5])) { // Firefox on Linux.
- menu.findItem(R.id.user_agent_firefox_on_linux).setChecked(true);
- } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[6])) { // Chromium on Linux.
- menu.findItem(R.id.user_agent_chromium_on_linux).setChecked(true);
- } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[7])) { // Firefox on Windows.
- menu.findItem(R.id.user_agent_firefox_on_windows).setChecked(true);
- } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[8])) { // Chrome on Windows.
- menu.findItem(R.id.user_agent_chrome_on_windows).setChecked(true);
- } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[9])) { // Edge on Windows.
- menu.findItem(R.id.user_agent_edge_on_windows).setChecked(true);
- } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[10])) { // Internet Explorer on Windows.
- menu.findItem(R.id.user_agent_internet_explorer_on_windows).setChecked(true);
- } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[11])) { // Safari on macOS.
- menu.findItem(R.id.user_agent_safari_on_macos).setChecked(true);
- } else { // Custom user agent.
- menu.findItem(R.id.user_agent_custom).setChecked(true);
- }
+ // Load the previous website in the history.
+ currentWebView.goBack();
+ }
+ break;
- // Initialize font size variables.
- int fontSize = mainWebView.getSettings().getTextZoom();
- String fontSizeTitle;
- MenuItem selectedFontSizeMenuItem;
+ case R.id.forward:
+ if (currentWebView.canGoForward()) {
+ // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
+ formattedUrlString = "";
- // Prepare the font size title and current size menu item.
- switch (fontSize) {
- case 25:
- fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.twenty_five_percent);
- selectedFontSizeMenuItem = menu.findItem(R.id.font_size_twenty_five_percent);
+ // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
+ navigatingHistory = true;
+
+ // Load the next website in the history.
+ currentWebView.goForward();
+ }
break;
- case 50:
- fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.fifty_percent);
- selectedFontSizeMenuItem = menu.findItem(R.id.font_size_fifty_percent);
+ case R.id.history:
+ // Get the `WebBackForwardList`.
+ WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList();
+
+ // Show the URL history dialog and name this instance `R.string.history`.
+ DialogFragment urlHistoryDialogFragment = UrlHistoryDialog.loadBackForwardList(this, webBackForwardList);
+ urlHistoryDialogFragment.show(getSupportFragmentManager(), getString(R.string.history));
break;
- case 75:
- fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.seventy_five_percent);
- selectedFontSizeMenuItem = menu.findItem(R.id.font_size_seventy_five_percent);
+ case R.id.requests:
+ // Launch the requests activity.
+ Intent requestsIntent = new Intent(this, RequestsActivity.class);
+ startActivity(requestsIntent);
break;
- case 100:
- fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent);
- selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent);
+ case R.id.downloads:
+ // Launch the system Download Manager.
+ Intent downloadManagerIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
+
+ // Launch as a new task so that Download Manager and Privacy Browser show as separate windows in the recent tasks list.
+ downloadManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ startActivity(downloadManagerIntent);
break;
- case 125:
- fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_twenty_five_percent);
- selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_twenty_five_percent);
+ case R.id.domains:
+ // Set the flag to reapply the domain settings on restart when returning from Domain Settings.
+ reapplyDomainSettingsOnRestart = true;
+ currentDomainName = "";
+
+ // Launch the domains activity.
+ Intent domainsIntent = new Intent(this, DomainsActivity.class);
+ startActivity(domainsIntent);
break;
- case 150:
- fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_fifty_percent);
- selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_fifty_percent);
+ case R.id.settings:
+ // Set the flag to reapply app settings on restart when returning from Settings.
+ reapplyAppSettingsOnRestart = true;
+
+ // Set the flag to reapply the domain settings on restart when returning from Settings.
+ reapplyDomainSettingsOnRestart = true;
+ currentDomainName = "";
+
+ // Launch the settings activity.
+ Intent settingsIntent = new Intent(this, SettingsActivity.class);
+ startActivity(settingsIntent);
break;
- case 175:
- fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_seventy_five_percent);
- selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_seventy_five_percent);
+ case R.id.import_export:
+ // Launch the import/export activity.
+ Intent importExportIntent = new Intent (this, ImportExportActivity.class);
+ startActivity(importExportIntent);
break;
- case 200:
- fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.two_hundred_percent);
- selectedFontSizeMenuItem = menu.findItem(R.id.font_size_two_hundred_percent);
+ case R.id.logcat:
+ // Launch the logcat activity.
+ Intent logcatIntent = new Intent(this, LogcatActivity.class);
+ startActivity(logcatIntent);
break;
- default:
- fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent);
- selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent);
+ case R.id.guide:
+ // Launch `GuideActivity`.
+ Intent guideIntent = new Intent(this, GuideActivity.class);
+ startActivity(guideIntent);
break;
- }
- // Set the font size title and select the current size menu item.
- fontSizeMenuItem.setTitle(fontSizeTitle);
- selectedFontSizeMenuItem.setChecked(true);
+ case R.id.about:
+ // Launch `AboutActivity`.
+ Intent aboutIntent = new Intent(this, AboutActivity.class);
+ startActivity(aboutIntent);
+ break;
+ }
- // Run all the other default commands.
- super.onPrepareOptionsMenu(menu);
+ // Get a handle for the drawer layout.
+ DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
- // Display the menu.
+ // Close the navigation drawer.
+ drawerLayout.closeDrawer(GravityCompat.START);
return true;
}
@Override
- // Remove Android Studio's warning about the dangers of using SetJavaScriptEnabled.
- @SuppressLint("SetJavaScriptEnabled")
- // removeAllCookies is deprecated, but it is required for API < 21.
- @SuppressWarnings("deprecation")
- public boolean onOptionsItemSelected(MenuItem menuItem) {
- // Reenter full screen browsing mode if it was interrupted by the options menu. <https://redmine.stoutner.com/issues/389>
- if (inFullScreenBrowsingMode) {
- // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
- getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+ public void onPostCreate(Bundle savedInstanceState) {
+ // Run the default commands.
+ super.onPostCreate(savedInstanceState);
+
+ // Sync the state of the DrawerToggle after the default `onRestoreInstanceState()` has finished. This creates the navigation drawer icon.
+ actionBarDrawerToggle.syncState();
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ // Run the default commands.
+ super.onConfigurationChanged(newConfig);
+
+ // Get the status bar pixel size.
+ int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
+ int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId);
+
+ // Get the resource density.
+ float screenDensity = getResources().getDisplayMetrics().density;
+
+ // Recalculate the drawer header padding.
+ drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity);
+ drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity);
+ drawerHeaderPaddingBottom = (int) (8 * screenDensity);
+
+ // Reload the ad for the free flavor if not in full screen mode.
+ if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
+ // Reload the ad. The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
+ AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
+ }
+
+ // `invalidateOptionsMenu` should recalculate the number of action buttons from the menu to display on the app bar, but it doesn't because of the this bug:
+ // https://code.google.com/p/android/issues/detail?id=20493#c8
+ // ActivityCompat.invalidateOptionsMenu(this);
+ }
- FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ // Store the `HitTestResult`.
+ final WebView.HitTestResult hitTestResult = currentWebView.getHitTestResult();
- /* Hide the system bars.
- * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
- * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
- * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
- * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
- */
- rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
- View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
- }
+ // Create strings.
+ final String imageUrl;
+ final String linkUrl;
- // Get the selected menu item ID.
- int menuItemId = menuItem.getItemId();
+ // Get a handle for the the clipboard and fragment managers.
+ final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
+ FragmentManager fragmentManager = getSupportFragmentManager();
- // Run the commands that correlate to the selected menu item.
- switch (menuItemId) {
- case R.id.toggle_javascript:
- // Switch the status of javaScriptEnabled.
- javaScriptEnabled = !javaScriptEnabled;
+ // Remove the lint errors below that `clipboardManager` might be `null`.
+ assert clipboardManager != null;
- // Apply the new JavaScript status.
- mainWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled);
+ switch (hitTestResult.getType()) {
+ // `SRC_ANCHOR_TYPE` is a link.
+ case WebView.HitTestResult.SRC_ANCHOR_TYPE:
+ // Get the target URL.
+ linkUrl = hitTestResult.getExtra();
- // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
- updatePrivacyIcons(true);
+ // Set the target URL as the title of the `ContextMenu`.
+ menu.setHeaderTitle(linkUrl);
- // Display a `Snackbar`.
- if (javaScriptEnabled) { // JavaScrip is enabled.
- Snackbar.make(findViewById(R.id.main_webview), R.string.javascript_enabled, Snackbar.LENGTH_SHORT).show();
- } else if (firstPartyCookiesEnabled) { // JavaScript is disabled, but first-party cookies are enabled.
- Snackbar.make(findViewById(R.id.main_webview), R.string.javascript_disabled, Snackbar.LENGTH_SHORT).show();
- } else { // Privacy mode.
- Snackbar.make(findViewById(R.id.main_webview), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
- }
+ // Add a Load URL entry.
+ menu.add(R.string.load_url).setOnMenuItemClickListener((MenuItem item) -> {
+ loadUrl(linkUrl);
+ return false;
+ });
- // Reload the WebView.
- mainWebView.reload();
- return true;
+ // Add a Copy URL entry.
+ menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
+ // Save the link URL in a `ClipData`.
+ ClipData srcAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl);
- case R.id.add_or_edit_domain:
- if (domainSettingsApplied) { // Edit the current domain settings.
- // Reapply the domain settings on returning to `MainWebViewActivity`.
- reapplyDomainSettingsOnRestart = true;
- currentDomainName = "";
+ // Set the `ClipData` as the clipboard's primary clip.
+ clipboardManager.setPrimaryClip(srcAnchorTypeClipData);
+ return false;
+ });
- // Create an intent to launch the domains activity.
- Intent domainsIntent = new Intent(this, DomainsActivity.class);
+ // Add a Download URL entry.
+ menu.add(R.string.download_url).setOnMenuItemClickListener((MenuItem item) -> {
+ // Check if the download should be processed by an external app.
+ if (downloadWithExternalApp) { // Download with an external app.
+ openUrlWithExternalApp(linkUrl);
+ } else { // Download with Android's download manager.
+ // Check to see if the storage permission has already been granted.
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
+ // Store the variables for future use by `onRequestPermissionsResult()`.
+ downloadUrl = linkUrl;
+ downloadContentDisposition = "none";
+ downloadContentLength = -1;
- // Put extra information instructing the domains activity to directly load the current domain and close on back instead of returning to the domains list.
- domainsIntent.putExtra("loadDomain", domainSettingsDatabaseId);
- domainsIntent.putExtra("closeOnBack", true);
+ // Show a dialog if the user has previously denied the permission.
+ if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
+ // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
+ DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE);
- // Make it so.
- startActivity(domainsIntent);
- } else { // Add a new domain.
- // Apply the new domain settings on returning to `MainWebViewActivity`.
- reapplyDomainSettingsOnRestart = true;
- currentDomainName = "";
+ // Show the download location permission alert dialog. The permission will be requested when the the dialog is closed.
+ downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
+ } else { // Show the permission request directly.
+ // Request the permission. The download dialog will be launched by `onRequestPermissionResult()`.
+ ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
+ }
+ } else { // The storage permission has already been granted.
+ // Get a handle for the download file alert dialog.
+ DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(linkUrl, "none", -1);
- // Get the current domain
- Uri currentUri = Uri.parse(formattedUrlString);
- String currentDomain = currentUri.getHost();
+ // Show the download file alert dialog.
+ downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
+ }
+ }
+ return false;
+ });
- // Initialize the database handler. The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
- DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
+ // Add an Open with App entry.
+ menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
+ openWithApp(linkUrl);
+ return false;
+ });
- // Create the domain and store the database ID.
- int newDomainDatabaseId = domainsDatabaseHelper.addDomain(currentDomain);
+ // Add an Open with Browser entry.
+ menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
+ openWithBrowser(linkUrl);
+ return false;
+ });
- // Create an intent to launch the domains activity.
- Intent domainsIntent = new Intent(this, DomainsActivity.class);
+ // Add a Cancel entry, which by default closes the context menu.
+ menu.add(R.string.cancel);
+ break;
- // Put extra information instructing the domains activity to directly load the new domain and close on back instead of returning to the domains list.
- domainsIntent.putExtra("loadDomain", newDomainDatabaseId);
- domainsIntent.putExtra("closeOnBack", true);
+ case WebView.HitTestResult.EMAIL_TYPE:
+ // Get the target URL.
+ linkUrl = hitTestResult.getExtra();
- // Make it so.
- startActivity(domainsIntent);
- }
- return true;
+ // Set the target URL as the title of the `ContextMenu`.
+ menu.setHeaderTitle(linkUrl);
- case R.id.toggle_first_party_cookies:
- // Switch the status of firstPartyCookiesEnabled.
- firstPartyCookiesEnabled = !firstPartyCookiesEnabled;
+ // Add a Write Email entry.
+ menu.add(R.string.write_email).setOnMenuItemClickListener(item -> {
+ // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
+ Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
- // Update the menu checkbox.
- menuItem.setChecked(firstPartyCookiesEnabled);
+ // Parse the url and set it as the data for the `Intent`.
+ emailIntent.setData(Uri.parse("mailto:" + linkUrl));
- // Apply the new cookie status.
- cookieManager.setAcceptCookie(firstPartyCookiesEnabled);
+ // `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);
- // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
- updatePrivacyIcons(true);
+ // Make it so.
+ startActivity(emailIntent);
+ return false;
+ });
- // Display a `Snackbar`.
- if (firstPartyCookiesEnabled) { // First-party cookies are enabled.
- Snackbar.make(findViewById(R.id.main_webview), R.string.first_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
- } else if (javaScriptEnabled) { // JavaScript is still enabled.
- Snackbar.make(findViewById(R.id.main_webview), R.string.first_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
- } else { // Privacy mode.
- Snackbar.make(findViewById(R.id.main_webview), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
- }
+ // Add a Copy Email Address entry.
+ menu.add(R.string.copy_email_address).setOnMenuItemClickListener(item -> {
+ // Save the email address in a `ClipData`.
+ ClipData srcEmailTypeClipData = ClipData.newPlainText(getString(R.string.email_address), linkUrl);
- // Reload the WebView.
- mainWebView.reload();
- return true;
+ // Set the `ClipData` as the clipboard's primary clip.
+ clipboardManager.setPrimaryClip(srcEmailTypeClipData);
+ return false;
+ });
- case R.id.toggle_third_party_cookies:
- if (Build.VERSION.SDK_INT >= 21) {
- // Switch the status of thirdPartyCookiesEnabled.
- thirdPartyCookiesEnabled = !thirdPartyCookiesEnabled;
+ // Add a `Cancel` entry, which by default closes the `ContextMenu`.
+ menu.add(R.string.cancel);
+ break;
- // Update the menu checkbox.
- menuItem.setChecked(thirdPartyCookiesEnabled);
+ // `IMAGE_TYPE` is an image.
+ case WebView.HitTestResult.IMAGE_TYPE:
+ // Get the image URL.
+ imageUrl = hitTestResult.getExtra();
- // Apply the new cookie status.
- cookieManager.setAcceptThirdPartyCookies(mainWebView, thirdPartyCookiesEnabled);
+ // Set the image URL as the title of the `ContextMenu`.
+ menu.setHeaderTitle(imageUrl);
- // Display a `Snackbar`.
- if (thirdPartyCookiesEnabled) {
- Snackbar.make(findViewById(R.id.main_webview), R.string.third_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
- } else {
- Snackbar.make(findViewById(R.id.main_webview), R.string.third_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
- }
+ // Add a View Image entry.
+ menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
+ loadUrl(imageUrl);
+ return false;
+ });
- // Reload the WebView.
- mainWebView.reload();
- } // Else do nothing because SDK < 21.
- return true;
+ // Add a Download Image entry.
+ menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
+ // Check if the download should be processed by an external app.
+ if (downloadWithExternalApp) { // Download with an external app.
+ openUrlWithExternalApp(imageUrl);
+ } else { // Download with Android's download manager.
+ // Check to see if the storage permission has already been granted.
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
+ // Store the image URL for use by `onRequestPermissionResult()`.
+ downloadImageUrl = imageUrl;
- case R.id.toggle_dom_storage:
- // Switch the status of domStorageEnabled.
- domStorageEnabled = !domStorageEnabled;
+ // Show a dialog if the user has previously denied the permission.
+ if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
+ // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
+ DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
- // Update the menu checkbox.
- menuItem.setChecked(domStorageEnabled);
+ // Show the download location permission alert dialog. The permission will be requested when the dialog is closed.
+ downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
+ } else { // Show the permission request directly.
+ // Request the permission. The download dialog will be launched by `onRequestPermissionResult().
+ ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
+ }
+ } else { // The storage permission has already been granted.
+ // Get a handle for the download image alert dialog.
+ DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
- // Apply the new DOM Storage status.
- mainWebView.getSettings().setDomStorageEnabled(domStorageEnabled);
+ // Show the download image alert dialog.
+ downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
+ }
+ }
+ return false;
+ });
- // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
- updatePrivacyIcons(true);
+ // Add a Copy URL entry.
+ menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> {
+ // Save the image URL in a `ClipData`.
+ ClipData srcImageTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
- // Display a `Snackbar`.
- if (domStorageEnabled) {
- Snackbar.make(findViewById(R.id.main_webview), R.string.dom_storage_enabled, Snackbar.LENGTH_SHORT).show();
- } else {
- Snackbar.make(findViewById(R.id.main_webview), R.string.dom_storage_disabled, Snackbar.LENGTH_SHORT).show();
- }
+ // Set the `ClipData` as the clipboard's primary clip.
+ clipboardManager.setPrimaryClip(srcImageTypeClipData);
+ return false;
+ });
+
+ // Add an Open with App entry.
+ menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
+ openWithApp(imageUrl);
+ return false;
+ });
- // Reload the WebView.
- mainWebView.reload();
- return true;
+ // Add an Open with Browser entry.
+ menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
+ openWithBrowser(imageUrl);
+ return false;
+ });
- // Form data can be removed once the minimum API >= 26.
- case R.id.toggle_save_form_data:
- // Switch the status of saveFormDataEnabled.
- saveFormDataEnabled = !saveFormDataEnabled;
+ // Add a `Cancel` entry, which by default closes the `ContextMenu`.
+ menu.add(R.string.cancel);
+ break;
- // Update the menu checkbox.
- menuItem.setChecked(saveFormDataEnabled);
- // Apply the new form data status.
- mainWebView.getSettings().setSaveFormData(saveFormDataEnabled);
+ // `SRC_IMAGE_ANCHOR_TYPE` is an image that is also a link.
+ case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
+ // Get the image URL.
+ imageUrl = hitTestResult.getExtra();
- // Display a `Snackbar`.
- if (saveFormDataEnabled) {
- Snackbar.make(findViewById(R.id.main_webview), R.string.form_data_enabled, Snackbar.LENGTH_SHORT).show();
- } else {
- Snackbar.make(findViewById(R.id.main_webview), R.string.form_data_disabled, Snackbar.LENGTH_SHORT).show();
- }
+ // Set the image URL as the title of the `ContextMenu`.
+ menu.setHeaderTitle(imageUrl);
- // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step.
- updatePrivacyIcons(true);
+ // Add a `View Image` entry.
+ menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
+ loadUrl(imageUrl);
+ return false;
+ });
- // Reload the WebView.
- mainWebView.reload();
- return true;
+ // Add a `Download Image` entry.
+ menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
+ // Check if the download should be processed by an external app.
+ if (downloadWithExternalApp) { // Download with an external app.
+ openUrlWithExternalApp(imageUrl);
+ } else { // Download with Android's download manager.
+ // Check to see if the storage permission has already been granted.
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
+ // Store the image URL for use by `onRequestPermissionResult()`.
+ downloadImageUrl = imageUrl;
- case R.id.clear_cookies:
- Snackbar.make(findViewById(R.id.main_webview), R.string.cookies_deleted, Snackbar.LENGTH_LONG)
- .setAction(R.string.undo, v -> {
- // Do nothing because everything will be handled by `onDismissed()` below.
- })
- .addCallback(new Snackbar.Callback() {
- @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
- @Override
- public void onDismissed(Snackbar snackbar, int event) {
- switch (event) {
- // The user pushed the undo button.
- case Snackbar.Callback.DISMISS_EVENT_ACTION:
- // Do nothing.
- break;
+ // Show a dialog if the user has previously denied the permission.
+ if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
+ // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
+ DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
- // The snackbar was dismissed without the undo button being pushed.
- default:
- // `cookieManager.removeAllCookie()` varies by SDK.
- if (Build.VERSION.SDK_INT < 21) {
- cookieManager.removeAllCookie();
- } else {
- cookieManager.removeAllCookies(null);
- }
- }
+ // Show the download location permission alert dialog. The permission will be requested when the dialog is closed.
+ downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
+ } else { // Show the permission request directly.
+ // Request the permission. The download dialog will be launched by `onRequestPermissionResult().
+ ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
}
- })
- .show();
- return true;
+ } else { // The storage permission has already been granted.
+ // Get a handle for the download image alert dialog.
+ DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
- case R.id.clear_dom_storage:
- Snackbar.make(findViewById(R.id.main_webview), R.string.dom_storage_deleted, Snackbar.LENGTH_LONG)
- .setAction(R.string.undo, v -> {
- // Do nothing because everything will be handled by `onDismissed()` below.
- })
- .addCallback(new Snackbar.Callback() {
- @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
- @Override
- public void onDismissed(Snackbar snackbar, int event) {
- switch (event) {
- // The user pushed the undo button.
- case Snackbar.Callback.DISMISS_EVENT_ACTION:
- // Do nothing.
- break;
+ // Show the download image alert dialog.
+ downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
+ }
+ }
+ return false;
+ });
- // The snackbar was dismissed without the undo button being pushed.
- default:
- // Delete the DOM Storage.
- WebStorage webStorage = WebStorage.getInstance();
- webStorage.deleteAllData();
+ // Add a `Copy URL` entry.
+ menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> {
+ // Save the image URL in a `ClipData`.
+ ClipData srcImageAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
- // Initialize a handler to manually delete the DOM storage files and directories.
- Handler deleteDomStorageHandler = new Handler();
+ // Set the `ClipData` as the clipboard's primary clip.
+ clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData);
+ return false;
+ });
- // Setup a runnable to manually delete the DOM storage files and directories.
- Runnable deleteDomStorageRunnable = () -> {
- try {
- // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
- Process deleteLocalStorageProcess = privacyBrowserRuntime.exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
+ // Add an Open with App entry.
+ menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
+ openWithApp(imageUrl);
+ return false;
+ });
- // Multiple commands must be used because `Runtime.exec()` does not like `*`.
- Process deleteIndexProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
- Process deleteQuotaManagerProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
- Process deleteQuotaManagerJournalProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
- Process deleteDatabasesProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
+ // Add an Open with Browser entry.
+ menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
+ openWithBrowser(imageUrl);
+ return false;
+ });
- // Wait for the processes to finish.
- deleteLocalStorageProcess.waitFor();
- deleteIndexProcess.waitFor();
- deleteQuotaManagerProcess.waitFor();
- deleteQuotaManagerJournalProcess.waitFor();
- deleteDatabasesProcess.waitFor();
- } catch (Exception exception) {
- // Do nothing if an error is thrown.
- }
- };
+ // Add a `Cancel` entry, which by default closes the `ContextMenu`.
+ menu.add(R.string.cancel);
+ break;
+ }
+ }
- // Manually delete the DOM storage files after 200 milliseconds.
- deleteDomStorageHandler.postDelayed(deleteDomStorageRunnable, 200);
- }
- }
- })
- .show();
- return true;
+ @Override
+ public void onCreateBookmark(DialogFragment dialogFragment) {
+ // Get the views from the dialog fragment.
+ EditText createBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_name_edittext);
+ EditText createBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_url_edittext);
- // Form data can be remove once the minimum API >= 26.
- case R.id.clear_form_data:
- Snackbar.make(findViewById(R.id.main_webview), R.string.form_data_deleted, Snackbar.LENGTH_LONG)
- .setAction(R.string.undo, v -> {
- // Do nothing because everything will be handled by `onDismissed()` below.
- })
- .addCallback(new Snackbar.Callback() {
- @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
- @Override
- public void onDismissed(Snackbar snackbar, int event) {
- switch (event) {
- // The user pushed the undo button.
- case Snackbar.Callback.DISMISS_EVENT_ACTION:
- // Do nothing.
- break;
+ // Extract the strings from the edit texts.
+ String bookmarkNameString = createBookmarkNameEditText.getText().toString();
+ String bookmarkUrlString = createBookmarkUrlEditText.getText().toString();
- // The snackbar was dismissed without the `Undo` button being pushed.
- default:
- // Delete the form data.
- WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(getApplicationContext());
- mainWebViewDatabase.clearFormData();
- }
- }
- })
- .show();
- return true;
+ // Get a copy of the favorite icon bitmap.
+ Bitmap favoriteIcon = favoriteIconBitmap;
- case R.id.easylist:
- // Toggle the EasyList status.
- easyListEnabled = !easyListEnabled;
+ // Scale the favorite icon bitmap down if it is larger than 256 x 256. Filtering uses bilinear interpolation.
+ if ((favoriteIcon.getHeight() > 256) || (favoriteIcon.getWidth() > 256)) {
+ favoriteIcon = Bitmap.createScaledBitmap(favoriteIcon, 256, 256, true);
+ }
- // Update the menu checkbox.
- menuItem.setChecked(easyListEnabled);
+ // Create a favorite icon byte array output stream.
+ ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
- // Reload the main WebView.
- mainWebView.reload();
- return true;
+ // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
+ favoriteIcon.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
- case R.id.easyprivacy:
- // Toggle the EasyPrivacy status.
- easyPrivacyEnabled = !easyPrivacyEnabled;
+ // Convert the favorite icon byte array stream to a byte array.
+ byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
- // Update the menu checkbox.
- menuItem.setChecked(easyPrivacyEnabled);
+ // Display the new bookmark below the current items in the (0 indexed) list.
+ int newBookmarkDisplayOrder = bookmarksListView.getCount();
- // Reload the main WebView.
- mainWebView.reload();
- return true;
+ // Create the bookmark.
+ bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolder, newBookmarkDisplayOrder, favoriteIconByteArray);
- case R.id.fanboys_annoyance_list:
- // Toggle Fanboy's Annoyance List status.
- fanboysAnnoyanceListEnabled = !fanboysAnnoyanceListEnabled;
+ // Update the bookmarks cursor with the current contents of this folder.
+ bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
- // Update the menu checkbox.
- menuItem.setChecked(fanboysAnnoyanceListEnabled);
+ // Update the list view.
+ bookmarksCursorAdapter.changeCursor(bookmarksCursor);
- // Update the staus of Fanboy's Social Blocking List.
- MenuItem fanboysSocialBlockingListMenuItem = mainMenu.findItem(R.id.fanboys_social_blocking_list);
- fanboysSocialBlockingListMenuItem.setEnabled(!fanboysAnnoyanceListEnabled);
+ // Scroll to the new bookmark.
+ bookmarksListView.setSelection(newBookmarkDisplayOrder);
+ }
- // Reload the main WebView.
- mainWebView.reload();
- return true;
+ @Override
+ public void onCreateBookmarkFolder(DialogFragment dialogFragment) {
+ // Get handles for the views in the dialog fragment.
+ EditText createFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.create_folder_name_edittext);
+ RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon_radiobutton);
+ ImageView folderIconImageView = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon);
- case R.id.fanboys_social_blocking_list:
- // Toggle Fanboy's Social Blocking List status.
- fanboysSocialBlockingListEnabled = !fanboysSocialBlockingListEnabled;
+ // Get new folder name string.
+ String folderNameString = createFolderNameEditText.getText().toString();
- // Update the menu checkbox.
- menuItem.setChecked(fanboysSocialBlockingListEnabled);
+ // Create a folder icon bitmap.
+ Bitmap folderIconBitmap;
- // Reload the main WebView.
- mainWebView.reload();
- return true;
+ // Set the folder icon bitmap according to the dialog.
+ if (defaultFolderIconRadioButton.isChecked()) { // Use the default folder icon.
+ // Get the default folder icon drawable.
+ Drawable folderIconDrawable = folderIconImageView.getDrawable();
- case R.id.ultraprivacy:
- // Toggle the UltraPrivacy status.
- ultraPrivacyEnabled = !ultraPrivacyEnabled;
+ // Convert the folder icon drawable to a bitmap drawable.
+ BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
- // Update the menu checkbox.
- menuItem.setChecked(ultraPrivacyEnabled);
+ // Convert the folder icon bitmap drawable to a bitmap.
+ folderIconBitmap = folderIconBitmapDrawable.getBitmap();
+ } else { // Use the WebView favorite icon.
+ // Get a copy of the favorite icon bitmap.
+ folderIconBitmap = favoriteIconBitmap;
- // Reload the main WebView.
- mainWebView.reload();
- return true;
+ // Scale the folder icon bitmap down if it is larger than 256 x 256. Filtering uses bilinear interpolation.
+ if ((folderIconBitmap.getHeight() > 256) || (folderIconBitmap.getWidth() > 256)) {
+ folderIconBitmap = Bitmap.createScaledBitmap(folderIconBitmap, 256, 256, true);
+ }
+ }
- case R.id.block_all_third_party_requests:
- //Toggle the third-party requests blocker status.
- blockAllThirdPartyRequests = !blockAllThirdPartyRequests;
+ // Create a folder icon byte array output stream.
+ ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream();
- // Update the menu checkbox.
- menuItem.setChecked(blockAllThirdPartyRequests);
+ // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
+ folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream);
- // Reload the main WebView.
- mainWebView.reload();
- return true;
+ // Convert the folder icon byte array stream to a byte array.
+ byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray();
- case R.id.user_agent_privacy_browser:
- // Update the user agent.
- mainWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[0]);
+ // 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);
+ }
- // Reload the WebView.
- mainWebView.reload();
- return true;
+ // Create the folder, which will be placed at the top of the `ListView`.
+ bookmarksDatabaseHelper.createFolder(folderNameString, currentBookmarksFolder, folderIconByteArray);
- case R.id.user_agent_webview_default:
- // Update the user agent.
- mainWebView.getSettings().setUserAgentString("");
+ // Update the bookmarks cursor with the current contents of this folder.
+ bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
- // Reload the WebView.
- mainWebView.reload();
- return true;
+ // Update the `ListView`.
+ bookmarksCursorAdapter.changeCursor(bookmarksCursor);
- case R.id.user_agent_firefox_on_android:
- // Update the user agent.
- mainWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[2]);
+ // Scroll to the new folder.
+ bookmarksListView.setSelection(0);
+ }
- // Reload the WebView.
- mainWebView.reload();
- return true;
+ @Override
+ public void onSaveBookmark(DialogFragment 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);
- case R.id.user_agent_chrome_on_android:
- // Update the user agent.
- mainWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[3]);
+ // Store the bookmark strings.
+ String bookmarkNameString = editBookmarkNameEditText.getText().toString();
+ String bookmarkUrlString = editBookmarkUrlEditText.getText().toString();
- // Reload the WebView.
- mainWebView.reload();
- return true;
+ // 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.
+ // Get a copy of the favorite icon bitmap.
+ Bitmap favoriteIcon = favoriteIconBitmap;
- case R.id.user_agent_safari_on_ios:
- // Update the user agent.
- mainWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[4]);
+ // Scale the favorite icon bitmap down if it is larger than 256 x 256. Filtering uses bilinear interpolation.
+ if ((favoriteIcon.getHeight() > 256) || (favoriteIcon.getWidth() > 256)) {
+ favoriteIcon = Bitmap.createScaledBitmap(favoriteIcon, 256, 256, true);
+ }
- // Reload the WebView.
- mainWebView.reload();
- return true;
+ // Create a favorite icon byte array output stream.
+ ByteArrayOutputStream newFavoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
- case R.id.user_agent_firefox_on_linux:
- // Update the user agent.
- mainWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[5]);
+ // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
+ favoriteIcon.compress(Bitmap.CompressFormat.PNG, 0, newFavoriteIconByteArrayOutputStream);
- // Reload the WebView.
- mainWebView.reload();
- return true;
+ // Convert the favorite icon byte array stream to a byte array.
+ byte[] newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray();
- case R.id.user_agent_chromium_on_linux:
- // Update the user agent.
- mainWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[6]);
+ // Update the bookmark and the favorite icon.
+ bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, newFavoriteIconByteArray);
+ }
- // Reload the WebView.
- mainWebView.reload();
- return true;
+ // Update the bookmarks cursor with the current contents of this folder.
+ bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
- case R.id.user_agent_firefox_on_windows:
- // Update the user agent.
- mainWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[7]);
+ // Update the list view.
+ bookmarksCursorAdapter.changeCursor(bookmarksCursor);
+ }
- // Reload the WebView.
- mainWebView.reload();
- return true;
+ @Override
+ public void onSaveBookmarkFolder(DialogFragment 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 defaultFolderIconImageView = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_imageview);
- case R.id.user_agent_chrome_on_windows:
- // Update the user agent.
- mainWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[8]);
+ // Get the new folder name.
+ String newFolderNameString = editFolderNameEditText.getText().toString();
- // Reload the WebView.
- mainWebView.reload();
- return true;
+ // Check if the favorite icon has changed.
+ if (currentFolderIconRadioButton.isChecked()) { // Only the name has changed.
+ // Update the name in the database.
+ bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString);
+ } else if (!currentFolderIconRadioButton.isChecked() && newFolderNameString.equals(oldFolderNameString)) { // Only the icon has changed.
+ // Create the new folder icon Bitmap.
+ Bitmap folderIconBitmap;
- case R.id.user_agent_edge_on_windows:
- // Update the user agent.
- mainWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[9]);
+ // Populate the new folder icon bitmap.
+ if (defaultFolderIconRadioButton.isChecked()) {
+ // Get the default folder icon drawable.
+ Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
- // Reload the WebView.
- mainWebView.reload();
- return true;
+ // Convert the folder icon drawable to a bitmap drawable.
+ BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
- case R.id.user_agent_internet_explorer_on_windows:
- // Update the user agent.
- mainWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[10]);
+ // Convert the folder icon bitmap drawable to a bitmap.
+ folderIconBitmap = folderIconBitmapDrawable.getBitmap();
+ } else { // Use the `WebView` favorite icon.
+ // Get a copy of the favorite icon bitmap.
+ folderIconBitmap = MainWebViewActivity.favoriteIconBitmap;
- // Reload the WebView.
- mainWebView.reload();
- return true;
+ // Scale the folder icon bitmap down if it is larger than 256 x 256. Filtering uses bilinear interpolation.
+ if ((folderIconBitmap.getHeight() > 256) || (folderIconBitmap.getWidth() > 256)) {
+ folderIconBitmap = Bitmap.createScaledBitmap(folderIconBitmap, 256, 256, true);
+ }
+ }
- case R.id.user_agent_safari_on_macos:
- // Update the user agent.
- mainWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[11]);
+ // Create a folder icon byte array output stream.
+ ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
- // Reload the WebView.
- mainWebView.reload();
- return true;
+ // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
+ folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
- case R.id.user_agent_custom:
- // Update the user agent.
- mainWebView.getSettings().setUserAgentString(defaultCustomUserAgentString);
+ // Convert the folder icon byte array stream to a byte array.
+ byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
- // Reload the WebView.
- mainWebView.reload();
- return true;
+ // Update the folder icon in the database.
+ bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderIconByteArray);
+ } else { // The folder icon and the name have changed.
+ // Get the new folder icon `Bitmap`.
+ Bitmap folderIconBitmap;
+ if (defaultFolderIconRadioButton.isChecked()) {
+ // Get the default folder icon drawable.
+ Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
- case R.id.font_size_twenty_five_percent:
- mainWebView.getSettings().setTextZoom(25);
- return true;
+ // Convert the folder icon drawable to a bitmap drawable.
+ BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
- case R.id.font_size_fifty_percent:
- mainWebView.getSettings().setTextZoom(50);
- return true;
+ // Convert the folder icon bitmap drawable to a bitmap.
+ folderIconBitmap = folderIconBitmapDrawable.getBitmap();
+ } else { // Use the `WebView` favorite icon.
+ // Get a copy of the favorite icon bitmap.
+ folderIconBitmap = MainWebViewActivity.favoriteIconBitmap;
- case R.id.font_size_seventy_five_percent:
- mainWebView.getSettings().setTextZoom(75);
- return true;
+ // Scale the folder icon bitmap down if it is larger than 256 x 256. Filtering uses bilinear interpolation.
+ if ((folderIconBitmap.getHeight() > 256) || (folderIconBitmap.getWidth() > 256)) {
+ folderIconBitmap = Bitmap.createScaledBitmap(folderIconBitmap, 256, 256, true);
+ }
+ }
- case R.id.font_size_one_hundred_percent:
- mainWebView.getSettings().setTextZoom(100);
- return true;
+ // Create a folder icon byte array output stream.
+ ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
- case R.id.font_size_one_hundred_twenty_five_percent:
- mainWebView.getSettings().setTextZoom(125);
- return true;
+ // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
+ folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
- case R.id.font_size_one_hundred_fifty_percent:
- mainWebView.getSettings().setTextZoom(150);
- return true;
+ // Convert the folder icon byte array stream to a byte array.
+ byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
- case R.id.font_size_one_hundred_seventy_five_percent:
- mainWebView.getSettings().setTextZoom(175);
- return true;
+ // Update the folder name and icon in the database.
+ bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, newFolderIconByteArray);
+ }
- case R.id.font_size_two_hundred_percent:
- mainWebView.getSettings().setTextZoom(200);
- return true;
+ // Update the bookmarks cursor with the current contents of this folder.
+ bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
- case R.id.swipe_to_refresh:
- // Get a handle for the swipe refresh layout.
- SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
+ // Update the `ListView`.
+ bookmarksCursorAdapter.changeCursor(bookmarksCursor);
+ }
- // Toggle swipe to refresh.
- swipeRefreshLayout.setEnabled(!swipeRefreshLayout.isEnabled());
- return true;
+ @Override
+ public void onCloseDownloadLocationPermissionDialog(int downloadType) {
+ switch (downloadType) {
+ case DownloadLocationPermissionDialog.DOWNLOAD_FILE:
+ // Request the WRITE_EXTERNAL_STORAGE permission with a file request code.
+ ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
+ break;
- case R.id.display_images:
- if (mainWebView.getSettings().getLoadsImagesAutomatically()) { // Images are currently loaded automatically.
- mainWebView.getSettings().setLoadsImagesAutomatically(false);
- mainWebView.reload();
- } else { // Images are not currently loaded automatically.
- mainWebView.getSettings().setLoadsImagesAutomatically(true);
- }
- return true;
+ case DownloadLocationPermissionDialog.DOWNLOAD_IMAGE:
+ // Request the WRITE_EXTERNAL_STORAGE permission with an image request code.
+ ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
+ break;
+ }
+ }
- case R.id.night_mode:
- // Toggle night mode.
- nightMode = !nightMode;
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ // Get a handle for the fragment manager.
+ FragmentManager fragmentManager = getSupportFragmentManager();
- // Enable or disable JavaScript according to night mode, the global preference, and any domain settings.
- if (nightMode) { // Night mode is enabled. Enable JavaScript.
- // Update the global variable.
- javaScriptEnabled = true;
- } else if (domainSettingsApplied) { // Night mode is disabled and domain settings are applied. Set JavaScript according to the domain settings.
- // Get the JavaScript preference that was stored the last time domain settings were loaded.
- javaScriptEnabled = domainSettingsJavaScriptEnabled;
- } else { // Night mode is disabled and domain settings are not applied. Set JavaScript according to the global preference.
- // Get a handle for the shared preference.
- SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+ switch (requestCode) {
+ case DOWNLOAD_FILE_REQUEST_CODE:
+ // Show the download file alert dialog. When the dialog closes, the correct command will be used based on the permission status.
+ DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(downloadUrl, downloadContentDisposition, downloadContentLength);
- // Get the JavaScript preference.
- javaScriptEnabled = sharedPreferences.getBoolean("javascript", false);
+ // On API 23, displaying the fragment must be delayed or the app will crash.
+ if (Build.VERSION.SDK_INT == 23) {
+ new Handler().postDelayed(() -> downloadFileDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
+ } else {
+ downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
}
- // Apply the JavaScript setting to the WebView.
- mainWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled);
+ // Reset the download variables.
+ downloadUrl = "";
+ downloadContentDisposition = "";
+ downloadContentLength = 0;
+ break;
+
+ case DOWNLOAD_IMAGE_REQUEST_CODE:
+ // Show the download image alert dialog. When the dialog closes, the correct command will be used based on the permission status.
+ DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(downloadImageUrl);
- // Update the privacy icons.
- updatePrivacyIcons(false);
+ // On API 23, displaying the fragment must be delayed or the app will crash.
+ if (Build.VERSION.SDK_INT == 23) {
+ new Handler().postDelayed(() -> downloadImageDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
+ } else {
+ downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
+ }
- // Reload the website.
- mainWebView.reload();
- return true;
+ // Reset the image URL variable.
+ downloadImageUrl = "";
+ break;
+ }
+ }
- case R.id.find_on_page:
- // Get a handle for the views.
- Toolbar toolbar = findViewById(R.id.toolbar);
- LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
+ @Override
+ public void onDownloadImage(DialogFragment dialogFragment, String imageUrl) {
+ // Download the image if it has an HTTP or HTTPS URI.
+ if (imageUrl.startsWith("http")) {
+ // Get a handle for the system `DOWNLOAD_SERVICE`.
+ DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
- // Hide the toolbar.
- toolbar.setVisibility(View.GONE);
+ // Parse `imageUrl`.
+ DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(imageUrl));
- // Show the find on page linear layout.
- findOnPageLinearLayout.setVisibility(View.VISIBLE);
+ // Pass cookies to download manager if cookies are enabled. This is required to download images from websites that require a login.
+ // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
+ if (firstPartyCookiesEnabled) {
+ // Get the cookies for `imageUrl`.
+ String cookies = cookieManager.getCookie(imageUrl);
- // Display the keyboard. The app must wait 200 ms before running the command to work around a bug in Android.
- // http://stackoverflow.com/questions/5520085/android-show-softkeyboard-with-showsoftinput-is-not-working
- findOnPageEditText.postDelayed(() -> {
- // Set the focus on `findOnPageEditText`.
- findOnPageEditText.requestFocus();
+ // Add the cookies to `downloadRequest`. In the HTTP request header, cookies are named `Cookie`.
+ downloadRequest.addRequestHeader("Cookie", cookies);
+ }
- // Display the keyboard. `0` sets no input flags.
- inputMethodManager.showSoftInput(findOnPageEditText, 0);
- }, 200);
- return true;
+ // Get the file name from the dialog fragment.
+ EditText downloadImageNameEditText = dialogFragment.getDialog().findViewById(R.id.download_image_name);
+ String imageName = downloadImageNameEditText.getText().toString();
- case R.id.view_source:
- // Launch the View Source activity.
- Intent viewSourceIntent = new Intent(this, ViewSourceActivity.class);
- startActivity(viewSourceIntent);
- return true;
+ // Specify the download location.
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // External write permission granted.
+ // Download to the public download directory.
+ downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, imageName);
+ } else { // External write permission denied.
+ // Download to the app's external download directory.
+ downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, imageName);
+ }
- case R.id.share_url:
- // Setup the share string.
- String shareString = webViewTitle + " – " + urlTextBox.getText().toString();
+ // Allow `MediaScanner` to index the download if it is a media file.
+ downloadRequest.allowScanningByMediaScanner();
- // Create the share intent.
- Intent shareIntent = new Intent(Intent.ACTION_SEND);
- shareIntent.putExtra(Intent.EXTRA_TEXT, shareString);
- shareIntent.setType("text/plain");
+ // Add the URL as the description for the download.
+ downloadRequest.setDescription(imageUrl);
- // Make it so.
- startActivity(Intent.createChooser(shareIntent, getString(R.string.share_url)));
- return true;
+ // Show the download notification after the download is completed.
+ downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
- case R.id.print:
- // Get a `PrintManager` instance.
- PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);
+ // Remove the lint warning below that `downloadManager` might be `null`.
+ assert downloadManager != null;
- // Convert `mainWebView` to `printDocumentAdapter`.
- PrintDocumentAdapter printDocumentAdapter = mainWebView.createPrintDocumentAdapter();
+ // Initiate the download.
+ downloadManager.enqueue(downloadRequest);
+ } else { // The image is not an HTTP or HTTPS URI.
+ Snackbar.make(currentWebView, R.string.cannot_download_image, Snackbar.LENGTH_INDEFINITE).show();
+ }
+ }
- // Remove the lint error below that `printManager` might be `null`.
- assert printManager != null;
+ @Override
+ public void onDownloadFile(DialogFragment dialogFragment, String downloadUrl) {
+ // Download the file if it has an HTTP or HTTPS URI.
+ if (downloadUrl.startsWith("http")) {
+ // Get a handle for the system `DOWNLOAD_SERVICE`.
+ DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
- // Print the document. The print attributes are `null`.
- printManager.print(getString(R.string.privacy_browser_web_page), printDocumentAdapter, null);
- return true;
+ // Parse `downloadUrl`.
+ DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(downloadUrl));
- case R.id.open_with_app:
- openWithApp(formattedUrlString);
- return true;
+ // Pass cookies to download manager if cookies are enabled. This is required to download files from websites that require a login.
+ // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
+ if (firstPartyCookiesEnabled) {
+ // Get the cookies for `downloadUrl`.
+ String cookies = cookieManager.getCookie(downloadUrl);
- case R.id.open_with_browser:
- openWithBrowser(formattedUrlString);
- return true;
+ // Add the cookies to `downloadRequest`. In the HTTP request header, cookies are named `Cookie`.
+ downloadRequest.addRequestHeader("Cookie", cookies);
+ }
- case R.id.add_to_homescreen:
- // Instantiate the create home screen shortcut dialog.
- DialogFragment createHomeScreenShortcutDialogFragment = CreateHomeScreenShortcutDialog.createDialog(mainWebView.getTitle(), formattedUrlString, favoriteIconBitmap);
+ // Get the file name from the dialog fragment.
+ EditText downloadFileNameEditText = dialogFragment.getDialog().findViewById(R.id.download_file_name);
+ String fileName = downloadFileNameEditText.getText().toString();
- // Show the create home screen shortcut dialog.
- createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getString(R.string.create_shortcut));
- return true;
+ // Specify the download location.
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // External write permission granted.
+ // Download to the public download directory.
+ downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
+ } else { // External write permission denied.
+ // Download to the app's external download directory.
+ downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, fileName);
+ }
- case R.id.proxy_through_orbot:
- // Toggle the proxy through Orbot variable.
- proxyThroughOrbot = !proxyThroughOrbot;
+ // Allow `MediaScanner` to index the download if it is a media file.
+ downloadRequest.allowScanningByMediaScanner();
- // Apply the proxy through Orbot settings.
- applyProxyThroughOrbot(true);
- return true;
+ // Add the URL as the description for the download.
+ downloadRequest.setDescription(downloadUrl);
- case R.id.refresh:
- if (menuItem.getTitle().equals(getString(R.string.refresh))) { // The refresh button was pushed.
- // Reload the WebView.
- mainWebView.reload();
- } else { // The stop button was pushed.
- // Stop the loading of the WebView.
- mainWebView.stopLoading();
- }
- return true;
+ // Show the download notification after the download is completed.
+ downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
- case R.id.ad_consent:
- // Display the ad consent dialog.
- DialogFragment adConsentDialogFragment = new AdConsentDialog();
- adConsentDialogFragment.show(getSupportFragmentManager(), getString(R.string.ad_consent));
- return true;
+ // Remove the lint warning below that `downloadManager` might be `null`.
+ assert downloadManager != null;
- default:
- // Don't consume the event.
- return super.onOptionsItemSelected(menuItem);
+ // Initiate the download.
+ downloadManager.enqueue(downloadRequest);
+ } else { // The download is not an HTTP or HTTPS URI.
+ Snackbar.make(currentWebView, R.string.cannot_download_file, Snackbar.LENGTH_INDEFINITE).show();
}
}
- // removeAllCookies is deprecated, but it is required for API < 21.
- @SuppressWarnings("deprecation")
@Override
- public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
- int menuItemId = menuItem.getItemId();
-
- switch (menuItemId) {
- case R.id.home:
- loadUrl(homepage);
- break;
+ public void onHttpAuthenticationCancel() {
+ // Cancel the `HttpAuthHandler`.
+ httpAuthHandler.cancel();
+ }
- case R.id.back:
- if (mainWebView.canGoBack()) {
- // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
- formattedUrlString = "";
+ @Override
+ public void onHttpAuthenticationProceed(DialogFragment 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);
- // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
- navigatingHistory = true;
+ // Proceed with the HTTP authentication.
+ httpAuthHandler.proceed(usernameEditText.getText().toString(), passwordEditText.getText().toString());
+ }
- // Load the previous website in the history.
- mainWebView.goBack();
- }
- break;
+ @Override
+ public void onSslErrorCancel() {
+ sslErrorHandler.cancel();
+ }
- case R.id.forward:
- if (mainWebView.canGoForward()) {
- // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
- formattedUrlString = "";
+ @Override
+ public void onSslErrorProceed() {
+ sslErrorHandler.proceed();
+ }
- // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
- navigatingHistory = true;
+ @Override
+ public void onPinnedMismatchBack() {
+ if (currentWebView.canGoBack()) { // There is a back page in the history.
+ // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
+ formattedUrlString = "";
- // Load the next website in the history.
- mainWebView.goForward();
- }
- break;
+ // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
+ navigatingHistory = true;
- case R.id.history:
- // Get the `WebBackForwardList`.
- WebBackForwardList webBackForwardList = mainWebView.copyBackForwardList();
+ // Go back.
+ currentWebView.goBack();
+ } else { // There are no pages to go back to.
+ // Load a blank page
+ loadUrl("");
+ }
+ }
- // Show the URL history dialog and name this instance `R.string.history`.
- DialogFragment urlHistoryDialogFragment = UrlHistoryDialog.loadBackForwardList(this, webBackForwardList);
- urlHistoryDialogFragment.show(getSupportFragmentManager(), getString(R.string.history));
- break;
+ @Override
+ public void onPinnedMismatchProceed() {
+ // Do not check the pinned information for this domain again until the domain changes.
+ ignorePinnedDomainInformation = true;
+ }
- case R.id.requests:
- // Launch the requests activity.
- Intent requestsIntent = new Intent(this, RequestsActivity.class);
- startActivity(requestsIntent);
- break;
+ @Override
+ public void onUrlHistoryEntrySelected(int moveBackOrForwardSteps) {
+ // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
+ formattedUrlString = "";
- case R.id.downloads:
- // Launch the system Download Manager.
- Intent downloadManagerIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
+ // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
+ navigatingHistory = true;
- // Launch as a new task so that Download Manager and Privacy Browser show as separate windows in the recent tasks list.
- downloadManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ // Load the history entry.
+ currentWebView.goBackOrForward(moveBackOrForwardSteps);
+ }
- startActivity(downloadManagerIntent);
- break;
+ @Override
+ public void onClearHistory() {
+ // Clear the history.
+ currentWebView.clearHistory();
+ }
- case R.id.domains:
- // Set the flag to reapply the domain settings on restart when returning from Domain Settings.
- reapplyDomainSettingsOnRestart = true;
- currentDomainName = "";
+ // Override `onBackPressed` to handle the navigation drawer and `mainWebView`.
+ @Override
+ public void onBackPressed() {
+ // Get a handle for the drawer layout.
+ DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
- // Launch the domains activity.
- Intent domainsIntent = new Intent(this, DomainsActivity.class);
- startActivity(domainsIntent);
- break;
+ if (drawerLayout.isDrawerVisible(GravityCompat.START)) { // The navigation drawer is open.
+ // Close the navigation drawer.
+ drawerLayout.closeDrawer(GravityCompat.START);
+ } else if (drawerLayout.isDrawerVisible(GravityCompat.END)){ // The bookmarks drawer is open.
+ if (currentBookmarksFolder.isEmpty()) { // The home folder is displayed.
+ // close the bookmarks drawer.
+ drawerLayout.closeDrawer(GravityCompat.END);
+ } else { // A subfolder is displayed.
+ // Place the former parent folder in `currentFolder`.
+ currentBookmarksFolder = bookmarksDatabaseHelper.getParentFolderName(currentBookmarksFolder);
- case R.id.settings:
- // Set the flag to reapply app settings on restart when returning from Settings.
- reapplyAppSettingsOnRestart = true;
+ // Load the new folder.
+ loadBookmarksFolder();
+ }
- // Set the flag to reapply the domain settings on restart when returning from Settings.
- reapplyDomainSettingsOnRestart = true;
- currentDomainName = "";
+ } else if (currentWebView.canGoBack()) { // There is at least one item in the current WebView history.
+ // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
+ formattedUrlString = "";
- // Launch the settings activity.
- Intent settingsIntent = new Intent(this, SettingsActivity.class);
- startActivity(settingsIntent);
- break;
+ // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
+ navigatingHistory = true;
- case R.id.import_export:
- // Launch the import/export activity.
- Intent importExportIntent = new Intent (this, ImportExportActivity.class);
- startActivity(importExportIntent);
- break;
+ // Go back.
+ currentWebView.goBack();
+ } else { // There isn't anything to do in Privacy Browser.
+ // Pass `onBackPressed()` to the system.
+ super.onBackPressed();
+ }
+ }
- case R.id.logcat:
- // Launch the logcat activity.
- Intent logcatIntent = new Intent(this, LogcatActivity.class);
- startActivity(logcatIntent);
- break;
+ // Process the results of an upload file chooser. Currently there is only one `startActivityForResult` in this activity, so the request code, used to differentiate them, is ignored.
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ // File uploads only work on API >= 21.
+ if (Build.VERSION.SDK_INT >= 21) {
+ // Pass the file to the WebView.
+ fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, data));
+ }
+ }
- case R.id.guide:
- // Launch `GuideActivity`.
- Intent guideIntent = new Intent(this, GuideActivity.class);
- startActivity(guideIntent);
- break;
+ private void loadUrlFromTextBox() {
+ // Get the text from urlTextBox and convert it to a string. trim() removes white spaces from the beginning and end of the string.
+ String unformattedUrlString = urlTextBox.getText().toString().trim();
- case R.id.about:
- // Launch `AboutActivity`.
- Intent aboutIntent = new Intent(this, AboutActivity.class);
- startActivity(aboutIntent);
- break;
+ // Check to see if `unformattedUrlString` is a valid URL. Otherwise, convert it into a search.
+ if (unformattedUrlString.startsWith("content://")) {
+ // Load the entire content URL.
+ formattedUrlString = unformattedUrlString;
+ } else if (Patterns.WEB_URL.matcher(unformattedUrlString).matches() || unformattedUrlString.startsWith("http://") || unformattedUrlString.startsWith("https://")
+ || unformattedUrlString.startsWith("file://")) {
+ // Add `https://` at the beginning if there is no protocol. Otherwise the app will segfault.
+ if (!unformattedUrlString.startsWith("http") && !unformattedUrlString.startsWith("file://") && !unformattedUrlString.startsWith("content://")) {
+ unformattedUrlString = "https://" + unformattedUrlString;
+ }
- case R.id.clear_and_exit:
- // Close the bookmarks cursor and database.
- bookmarksCursor.close();
- bookmarksDatabaseHelper.close();
+ // Initialize `unformattedUrl`.
+ URL unformattedUrl = null;
- // Get a handle for the shared preferences.
- SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+ // Convert `unformattedUrlString` to a `URL`, then to a `URI`, and then back to a `String`, which sanitizes the input and adds in any missing components.
+ try {
+ unformattedUrl = new URL(unformattedUrlString);
+ } catch (MalformedURLException e) {
+ e.printStackTrace();
+ }
- // Get the status of the clear everything preference.
- boolean clearEverything = sharedPreferences.getBoolean("clear_everything", true);
+ // The ternary operator (? :) makes sure that a null pointer exception is not thrown, which would happen if `.get` was called on a `null` value.
+ String scheme = unformattedUrl != null ? unformattedUrl.getProtocol() : null;
+ String authority = unformattedUrl != null ? unformattedUrl.getAuthority() : null;
+ String path = unformattedUrl != null ? unformattedUrl.getPath() : null;
+ String query = unformattedUrl != null ? unformattedUrl.getQuery() : null;
+ String fragment = unformattedUrl != null ? unformattedUrl.getRef() : null;
- // 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();
- }
+ // Build the URI.
+ Uri.Builder formattedUri = new Uri.Builder();
+ formattedUri.scheme(scheme).authority(authority).path(path).query(query).fragment(fragment);
- // Manually delete the cookies database, as `CookieManager` sometimes will not flush its changes to disk before `System.exit(0)` is run.
- try {
- // Two commands must be used because `Runtime.exec()` does not like `*`.
- Process deleteCookiesProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies");
- Process deleteCookiesJournalProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies-journal");
+ // Decode `formattedUri` as a `String` in `UTF-8`.
+ try {
+ formattedUrlString = URLDecoder.decode(formattedUri.build().toString(), "UTF-8");
+ } catch (UnsupportedEncodingException exception) {
+ // Load a blank string.
+ formattedUrlString = "";
+ }
+ } else if (unformattedUrlString.isEmpty()){ // Load a blank web site.
+ // Load a blank string.
+ formattedUrlString = "";
+ } else { // Search for the contents of the URL box.
+ // Create an encoded URL String.
+ String encodedUrlString;
- // Wait until the processes have finished.
- deleteCookiesProcess.waitFor();
- deleteCookiesJournalProcess.waitFor();
- } catch (Exception exception) {
- // Do nothing if an error is thrown.
- }
- }
+ // Sanitize the search input.
+ try {
+ encodedUrlString = URLEncoder.encode(unformattedUrlString, "UTF-8");
+ } catch (UnsupportedEncodingException exception) {
+ encodedUrlString = "";
+ }
- // Clear DOM storage.
- if (clearEverything || sharedPreferences.getBoolean("clear_dom_storage", true)) {
- // Ask `WebStorage` to clear the DOM storage.
- WebStorage webStorage = WebStorage.getInstance();
- webStorage.deleteAllData();
+ // Add the base search URL.
+ formattedUrlString = searchURL + encodedUrlString;
+ }
- // Manually delete the DOM storage files and directories, as `WebStorage` sometimes will not flush its changes to disk before `System.exit(0)` is run.
- try {
- // A `String[]` must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
- Process deleteLocalStorageProcess = privacyBrowserRuntime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
+ // 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();
- // Multiple commands must be used because `Runtime.exec()` does not like `*`.
- Process deleteIndexProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
- Process deleteQuotaManagerProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
- Process deleteQuotaManagerJournalProcess = privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
- Process deleteDatabaseProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
+ // Make it so.
+ loadUrl(formattedUrlString);
+ }
- // Wait until the processes have finished.
- deleteLocalStorageProcess.waitFor();
- deleteIndexProcess.waitFor();
- deleteQuotaManagerProcess.waitFor();
- deleteQuotaManagerJournalProcess.waitFor();
- deleteDatabaseProcess.waitFor();
- } catch (Exception exception) {
- // Do nothing if an error is thrown.
- }
- }
+ private void loadUrl(String url) {// Apply any custom domain settings.
+ // Set the URL as the formatted URL string so that checking third-party requests works correctly.
+ formattedUrlString = url;
- // Clear form data if the API < 26.
- if ((Build.VERSION.SDK_INT < 26) && (clearEverything || sharedPreferences.getBoolean("clear_form_data", true))) {
- WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
- webViewDatabase.clearFormData();
+ // Apply the domain settings.
+ applyDomainSettings(url, true, false);
- // Manually delete the form data database, as `WebViewDatabase` sometimes will not flush its changes to disk before `System.exit(0)` is run.
- try {
- // A string array must be used because the database contains a space and `Runtime.exec` will not otherwise escape the string correctly.
- Process deleteWebDataProcess = privacyBrowserRuntime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data"});
- Process deleteWebDataJournalProcess = privacyBrowserRuntime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data-journal"});
+ // If loading a website, set `urlIsLoading` to prevent changes in the user agent on websites with redirects from reloading the current website.
+ urlIsLoading = !url.equals("");
- // Wait until the processes have finished.
- deleteWebDataProcess.waitFor();
- deleteWebDataJournalProcess.waitFor();
- } catch (Exception exception) {
- // Do nothing if an error is thrown.
- }
- }
+ // Load the URL.
+ currentWebView.loadUrl(url, customHeaders);
+ }
- // Clear the cache.
- if (clearEverything || sharedPreferences.getBoolean("clear_cache", true)) {
- // `true` includes disk files.
- mainWebView.clearCache(true);
+ public void findPreviousOnPage(View view) {
+ // Go to the previous highlighted phrase on the page. `false` goes backwards instead of forwards.
+ currentWebView.findNext(false);
+ }
- // Manually delete the cache directories.
- try {
- // Delete the main cache directory.
- Process deleteCacheProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/cache");
+ public void findNextOnPage(View view) {
+ // Go to the next highlighted phrase on the page. `true` goes forwards instead of backwards.
+ currentWebView.findNext(true);
+ }
- // Delete the secondary `Service Worker` cache directory.
- // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
- Process deleteServiceWorkerProcess = privacyBrowserRuntime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
+ public void closeFindOnPage(View view) {
+ // Get a handle for the views.
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
- // Wait until the processes have finished.
- deleteCacheProcess.waitFor();
- deleteServiceWorkerProcess.waitFor();
- } catch (Exception exception) {
- // Do nothing if an error is thrown.
- }
- }
+ // Delete the contents of `find_on_page_edittext`.
+ findOnPageEditText.setText(null);
- // Clear SSL certificate preferences.
- mainWebView.clearSslPreferences();
+ // Clear the highlighted phrases.
+ currentWebView.clearMatches();
- // Clear the back/forward history.
- mainWebView.clearHistory();
+ // Hide the find on page linear layout.
+ findOnPageLinearLayout.setVisibility(View.GONE);
- // Clear `formattedUrlString`.
- formattedUrlString = null;
+ // Show the toolbar.
+ toolbar.setVisibility(View.VISIBLE);
- // Clear `customHeaders`.
- customHeaders.clear();
+ // Hide the keyboard.
+ inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
+ }
- // Destroy the internal state of `mainWebView`.
- mainWebView.destroy();
+ private void applyAppSettings() {
+ // Get a handle for the shared preferences.
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
- // Manually delete the `app_webview` folder, which contains the cookies, DOM storage, form data, and `Service Worker` cache.
- // See `https://code.google.com/p/android/issues/detail?id=233826&thanks=233826&ts=1486670530`.
- if (clearEverything) {
- try {
- // Delete the folder.
- Process deleteAppWebviewProcess = privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview");
+ // Store the values from the shared preferences in variables.
+ incognitoModeEnabled = sharedPreferences.getBoolean("incognito_mode", false);
+ boolean doNotTrackEnabled = sharedPreferences.getBoolean("do_not_track", false);
+ proxyThroughOrbot = sharedPreferences.getBoolean("proxy_through_orbot", false);
+ fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean("full_screen_browsing_mode", false);
+ hideAppBar = sharedPreferences.getBoolean("hide_app_bar", true);
+ downloadWithExternalApp = sharedPreferences.getBoolean("download_with_external_app", false);
- // Wait until the process has finished.
- deleteAppWebviewProcess.waitFor();
- } catch (Exception exception) {
- // Do nothing if an error is thrown.
- }
- }
+ // Get handles for the views that need to be modified. `getSupportActionBar()` must be used until the minimum API >= 21.
+ FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
+ ActionBar actionBar = getSupportActionBar();
- // Close Privacy Browser. `finishAndRemoveTask` also removes Privacy Browser from the recent app list.
- if (Build.VERSION.SDK_INT >= 21) {
- finishAndRemoveTask();
- } else {
- finish();
- }
+ // Remove the incorrect lint warnings below that the action bar might be null.
+ assert actionBar != null;
- // Remove the terminated program from RAM. The status code is `0`.
- System.exit(0);
- break;
+ // Apply the proxy through Orbot settings.
+ applyProxyThroughOrbot(false);
+
+ // Set Do Not Track status.
+ if (doNotTrackEnabled) {
+ customHeaders.put("DNT", "1");
+ } else {
+ customHeaders.remove("DNT");
}
- // Get a handle for the drawer layout.
- DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
+ // Set the app bar scrolling.
+ currentWebView.setNestedScrollingEnabled(sharedPreferences.getBoolean("scroll_app_bar", true));
- // Close the navigation drawer.
- drawerLayout.closeDrawer(GravityCompat.START);
- return true;
- }
+ // Update the full screen browsing mode settings.
+ if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode.
+ // Update the visibility of the app bar, which might have changed in the settings.
+ if (hideAppBar) {
+ actionBar.hide();
+ } else {
+ actionBar.show();
+ }
- @Override
- public void onPostCreate(Bundle savedInstanceState) {
- // Run the default commands.
- super.onPostCreate(savedInstanceState);
+ // Hide the banner ad in the free flavor.
+ if (BuildConfig.FLAVOR.contentEquals("free")) {
+ AdHelper.hideAd(findViewById(R.id.adview));
+ }
- // Sync the state of the DrawerToggle after the default `onRestoreInstanceState()` has finished. This creates the navigation drawer icon.
- actionBarDrawerToggle.syncState();
- }
+ // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
- @Override
- public void onConfigurationChanged(Configuration newConfig) {
- // Run the default commands.
- super.onConfigurationChanged(newConfig);
+ /* Hide the system bars.
+ * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
+ * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
+ * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
+ * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
+ */
+ rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
+ View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
+ } else { // Privacy Browser is not in full screen browsing mode.
+ // Reset the full screen tracker, which could be true if Privacy Browser was in full screen mode before entering settings and full screen browsing was disabled.
+ inFullScreenBrowsingMode = false;
- // Get the status bar pixel size.
- int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
- int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId);
+ // Show the app bar.
+ actionBar.show();
- // Get the resource density.
- float screenDensity = getResources().getDisplayMetrics().density;
+ // Show the banner ad in the free flavor.
+ if (BuildConfig.FLAVOR.contentEquals("free")) {
+ // Initialize the ads. If this isn't the first run, `loadAd()` will be automatically called instead.
+ AdHelper.initializeAds(findViewById(R.id.adview), getApplicationContext(), fragmentManager, getString(R.string.google_app_id), getString(R.string.ad_unit_id));
+ }
- // Recalculate the drawer header padding.
- drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity);
- drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity);
- drawerHeaderPaddingBottom = (int) (8 * screenDensity);
+ // Remove the `SYSTEM_UI` flags from the root frame layout.
+ rootFrameLayout.setSystemUiVisibility(0);
- // Reload the ad for the free flavor if not in full screen mode.
- if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
- // Reload the ad. The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
- AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
+ // Add the translucent status flag.
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
}
-
- // `invalidateOptionsMenu` should recalculate the number of action buttons from the menu to display on the app bar, but it doesn't because of the this bug:
- // https://code.google.com/p/android/issues/detail?id=20493#c8
- // ActivityCompat.invalidateOptionsMenu(this);
}
- @Override
- public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
- // Store the `HitTestResult`.
- final WebView.HitTestResult hitTestResult = mainWebView.getHitTestResult();
-
- // Create strings.
- final String imageUrl;
- final String linkUrl;
-
- // Get a handle for the the clipboard and fragment managers.
- final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
- FragmentManager fragmentManager = getSupportFragmentManager();
-
- // 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:
- // Get the target URL.
- linkUrl = hitTestResult.getExtra();
+ // `reloadWebsite` is used if returning from the Domains activity. Otherwise JavaScript might not function correctly if it is newly enabled.
+ // The deprecated `.getDrawable()` must be used until the minimum API >= 21.
+ @SuppressWarnings("deprecation")
+ private boolean applyDomainSettings(String url, boolean resetFavoriteIcon, boolean reloadWebsite) {
+ // Get a handle for the URL edit text.
+ EditText urlEditText = findViewById(R.id.url_edittext);
- // Set the target URL as the title of the `ContextMenu`.
- menu.setHeaderTitle(linkUrl);
+ // Get the current user agent.
+ String initialUserAgent = currentWebView.getSettings().getUserAgentString();
- // Add a Load URL entry.
- menu.add(R.string.load_url).setOnMenuItemClickListener((MenuItem item) -> {
- loadUrl(linkUrl);
- return false;
- });
+ // Initialize a variable to track if the user agent changes.
+ boolean userAgentChanged = false;
- // Add a Copy URL entry.
- menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
- // Save the link URL in a `ClipData`.
- ClipData srcAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl);
+ // Parse the URL into a URI.
+ Uri uri = Uri.parse(url);
- // Set the `ClipData` as the clipboard's primary clip.
- clipboardManager.setPrimaryClip(srcAnchorTypeClipData);
- return false;
- });
+ // Extract the domain from `uri`.
+ String hostName = uri.getHost();
- // Add a Download URL entry.
- menu.add(R.string.download_url).setOnMenuItemClickListener((MenuItem item) -> {
- // Check if the download should be processed by an external app.
- if (downloadWithExternalApp) { // Download with an external app.
- openUrlWithExternalApp(linkUrl);
- } else { // Download with Android's download manager.
- // Check to see if the storage permission has already been granted.
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
- // Store the variables for future use by `onRequestPermissionsResult()`.
- downloadUrl = linkUrl;
- downloadContentDisposition = "none";
- downloadContentLength = -1;
+ // Initialize `loadingNewDomainName`.
+ boolean loadingNewDomainName;
- // Show a dialog if the user has previously denied the permission.
- if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
- // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
- DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE);
+ // If either `hostName` or `currentDomainName` are `null`, run the options for loading a new domain name.
+ // The lint suggestion to simplify the `if` statement is incorrect, because `hostName.equals(currentDomainName)` can produce a `null object reference.`
+ //noinspection SimplifiableIfStatement
+ if ((hostName == null) || (currentDomainName == null)) {
+ loadingNewDomainName = true;
+ } else { // Determine if `hostName` equals `currentDomainName`.
+ loadingNewDomainName = !hostName.equals(currentDomainName);
+ }
- // Show the download location permission alert dialog. The permission will be requested when the the dialog is closed.
- downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
- } else { // Show the permission request directly.
- // Request the permission. The download dialog will be launched by `onRequestPermissionResult()`.
- ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
- }
- } else { // The storage permission has already been granted.
- // Get a handle for the download file alert dialog.
- DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(linkUrl, "none", -1);
+ // Strings don't like to be null.
+ if (hostName == null) {
+ hostName = "";
+ }
- // Show the download file alert dialog.
- downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
- }
- }
- return false;
- });
+ // 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;
- // Add an Open with App entry.
- menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
- openWithApp(linkUrl);
- return false;
- });
+ // Reset the ignoring of pinned domain information.
+ ignorePinnedDomainInformation = false;
- // Add an Open with Browser entry.
- menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
- openWithBrowser(linkUrl);
- return false;
- });
+ // Reset the favorite icon if specified.
+ if (resetFavoriteIcon) {
+ // Store the favorite icon bitmap.
+ favoriteIconBitmap = favoriteIconDefaultBitmap;
- // Add a Cancel entry, which by default closes the context menu.
- menu.add(R.string.cancel);
- break;
+ // Get a handle for the tab layout.
+ TabLayout tabLayout = findViewById(R.id.tablayout);
- case WebView.HitTestResult.EMAIL_TYPE:
- // Get the target URL.
- linkUrl = hitTestResult.getExtra();
+ // Get the current tab.
+ TabLayout.Tab currentTab = tabLayout.getTabAt(tabLayout.getSelectedTabPosition());
- // Set the target URL as the title of the `ContextMenu`.
- menu.setHeaderTitle(linkUrl);
+ // Remove the warning below that the current tab might be null.
+ assert currentTab != null;
- // Add a Write Email entry.
- menu.add(R.string.write_email).setOnMenuItemClickListener(item -> {
- // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
- Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
+ // Get the current tab custom view.
+ View currentTabCustomView = currentTab.getCustomView();
- // Parse the url and set it as the data for the `Intent`.
- emailIntent.setData(Uri.parse("mailto:" + linkUrl));
+ // Remove the warning below that the current tab custom view might be null.
+ assert currentTabCustomView != null;
- // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser.
- emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ // Get the current tab favorite icon image view.
+ ImageView currentTabFavoriteIconImageView = currentTabCustomView.findViewById(R.id.favorite_icon_imageview);
- // Make it so.
- startActivity(emailIntent);
- return false;
- });
+ // Set the default favorite icon as the favorite icon for this tab.
+ currentTabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(favoriteIconBitmap, 64, 64, true));
+ }
- // Add a Copy Email Address entry.
- menu.add(R.string.copy_email_address).setOnMenuItemClickListener(item -> {
- // Save the email address in a `ClipData`.
- ClipData srcEmailTypeClipData = ClipData.newPlainText(getString(R.string.email_address), linkUrl);
+ // Get a handle for the swipe refresh layout.
+ SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
- // Set the `ClipData` as the clipboard's primary clip.
- clipboardManager.setPrimaryClip(srcEmailTypeClipData);
- return false;
- });
+ // Initialize the database handler. The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
+ DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
- // Add a `Cancel` entry, which by default closes the `ContextMenu`.
- menu.add(R.string.cancel);
- break;
+ // Get a full cursor from `domainsDatabaseHelper`.
+ Cursor domainNameCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomain();
- // `IMAGE_TYPE` is an image.
- case WebView.HitTestResult.IMAGE_TYPE:
- // Get the image URL.
- imageUrl = hitTestResult.getExtra();
+ // Initialize `domainSettingsSet`.
+ Set<String> domainSettingsSet = new HashSet<>();
- // Set the image URL as the title of the `ContextMenu`.
- menu.setHeaderTitle(imageUrl);
+ // Get the domain name column index.
+ int domainNameColumnIndex = domainNameCursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME);
- // Add a View Image entry.
- menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
- loadUrl(imageUrl);
- return false;
- });
+ // Populate `domainSettingsSet`.
+ for (int i = 0; i < domainNameCursor.getCount(); i++) {
+ // Move `domainsCursor` to the current row.
+ domainNameCursor.moveToPosition(i);
- // Add a Download Image entry.
- menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
- // Check if the download should be processed by an external app.
- if (downloadWithExternalApp) { // Download with an external app.
- openUrlWithExternalApp(imageUrl);
- } else { // Download with Android's download manager.
- // Check to see if the storage permission has already been granted.
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
- // Store the image URL for use by `onRequestPermissionResult()`.
- downloadImageUrl = imageUrl;
+ // Store the domain name in `domainSettingsSet`.
+ domainSettingsSet.add(domainNameCursor.getString(domainNameColumnIndex));
+ }
- // Show a dialog if the user has previously denied the permission.
- if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
- // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
- DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
+ // Close `domainNameCursor.
+ domainNameCursor.close();
- // Show the download location permission alert dialog. The permission will be requested when the dialog is closed.
- downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
- } else { // Show the permission request directly.
- // Request the permission. The download dialog will be launched by `onRequestPermissionResult().
- ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
- }
- } else { // The storage permission has already been granted.
- // Get a handle for the download image alert dialog.
- DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
+ // Initialize variables to track if domain settings will be applied and, if so, under which name.
+ domainSettingsApplied = false;
+ String domainNameInDatabase = null;
- // Show the download image alert dialog.
- downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
- }
- }
- return false;
- });
+ // Check the hostname.
+ if (domainSettingsSet.contains(hostName)) {
+ domainSettingsApplied = true;
+ domainNameInDatabase = hostName;
+ }
- // Add a Copy URL entry.
- menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> {
- // Save the image URL in a `ClipData`.
- ClipData srcImageTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
+ // Check all the subdomains of the host name against wildcard domains in the domain cursor.
+ while (!domainSettingsApplied && hostName.contains(".")) { // Stop checking if domain settings are already applied or there are no more `.` in the host name.
+ if (domainSettingsSet.contains("*." + hostName)) { // Check the host name prepended by `*.`.
+ // Apply the domain settings.
+ domainSettingsApplied = true;
- // Set the `ClipData` as the clipboard's primary clip.
- clipboardManager.setPrimaryClip(srcImageTypeClipData);
- return false;
- });
+ // Store the applied domain names as it appears in the database.
+ domainNameInDatabase = "*." + hostName;
+ }
- // Add an Open with App entry.
- menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
- openWithApp(imageUrl);
- return false;
- });
+ // Strip out the lowest subdomain of of the host name.
+ hostName = hostName.substring(hostName.indexOf(".") + 1);
+ }
- // Add an Open with Browser entry.
- menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
- openWithBrowser(imageUrl);
- return false;
- });
- // Add a `Cancel` entry, which by default closes the `ContextMenu`.
- menu.add(R.string.cancel);
- break;
+ // Get a handle for the shared preference.
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+ // Store the general preference information.
+ String defaultFontSizeString = sharedPreferences.getString("font_size", getString(R.string.font_size_default_value));
+ String defaultUserAgentName = sharedPreferences.getString("user_agent", getString(R.string.user_agent_default_value));
+ defaultCustomUserAgentString = sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value));
+ boolean defaultSwipeToRefresh = sharedPreferences.getBoolean("swipe_to_refresh", true);
+ nightMode = sharedPreferences.getBoolean("night_mode", false);
+ boolean displayWebpageImages = sharedPreferences.getBoolean("display_webpage_images", true);
- // `SRC_IMAGE_ANCHOR_TYPE` is an image that is also a link.
- case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
- // Get the image URL.
- imageUrl = hitTestResult.getExtra();
+ if (domainSettingsApplied) { // The url has custom domain settings.
+ // Get a cursor for the current host and move it to the first position.
+ Cursor currentHostDomainSettingsCursor = domainsDatabaseHelper.getCursorForDomainName(domainNameInDatabase);
+ currentHostDomainSettingsCursor.moveToFirst();
- // Set the image URL as the title of the `ContextMenu`.
- menu.setHeaderTitle(imageUrl);
+ // Get the settings from the cursor.
+ domainSettingsDatabaseId = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper._ID)));
+ javaScriptEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)) == 1);
+ firstPartyCookiesEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FIRST_PARTY_COOKIES)) == 1);
+ thirdPartyCookiesEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_THIRD_PARTY_COOKIES)) == 1);
+ domStorageEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_DOM_STORAGE)) == 1);
+ // Form data can be removed once the minimum API >= 26.
+ saveFormDataEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FORM_DATA)) == 1);
+ easyListEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYLIST)) == 1);
+ easyPrivacyEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYPRIVACY)) == 1);
+ fanboysAnnoyanceListEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST)) == 1);
+ fanboysSocialBlockingListEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)) == 1);
+ ultraPrivacyEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY)) == 1);
+ blockAllThirdPartyRequests = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS)) == 1);
+ String userAgentName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.USER_AGENT));
+ int fontSize = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.FONT_SIZE));
+ int swipeToRefreshInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SWIPE_TO_REFRESH));
+ int nightModeInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.NIGHT_MODE));
+ int displayWebpageImagesInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.DISPLAY_IMAGES));
+ pinnedSslCertificate = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)) == 1);
+ pinnedSslIssuedToCName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME));
+ pinnedSslIssuedToOName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION));
+ pinnedSslIssuedToUName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT));
+ pinnedSslIssuedByCName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME));
+ pinnedSslIssuedByOName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION));
+ pinnedSslIssuedByUName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT));
+ pinnedIpAddresses = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_IP_ADDRESSES)) == 1);
+ pinnedHostIpAddresses = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.IP_ADDRESSES));
- // Add a `View Image` entry.
- menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
- loadUrl(imageUrl);
- return false;
- });
+ // 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;
- // Add a `Download Image` entry.
- menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
- // Check if the download should be processed by an external app.
- if (downloadWithExternalApp) { // Download with an external app.
- openUrlWithExternalApp(imageUrl);
- } else { // Download with Android's download manager.
- // Check to see if the storage permission has already been granted.
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
- // Store the image URL for use by `onRequestPermissionResult()`.
- downloadImageUrl = imageUrl;
+ case DomainsDatabaseHelper.NIGHT_MODE_DISABLED:
+ nightMode = false;
+ break;
+ }
- // Show a dialog if the user has previously denied the permission.
- if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
- // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
- DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
+ // Store the domain JavaScript status. This is used by the options menu night mode toggle.
+ domainSettingsJavaScriptEnabled = javaScriptEnabled;
- // Show the download location permission alert dialog. The permission will be requested when the dialog is closed.
- downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
- } else { // Show the permission request directly.
- // Request the permission. The download dialog will be launched by `onRequestPermissionResult().
- ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
- }
- } else { // The storage permission has already been granted.
- // Get a handle for the download image alert dialog.
- DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
+ // Enable JavaScript if night mode is enabled.
+ if (nightMode) {
+ javaScriptEnabled = true;
+ }
- // Show the download image alert dialog.
- downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
- }
- }
- return false;
- });
+ // Set the pinned SSL certificate start date to `null` if the saved date `long` is 0.
+ if (currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)) == 0) {
+ pinnedSslStartDate = null;
+ } else {
+ pinnedSslStartDate = new Date(currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)));
+ }
- // Add a `Copy URL` entry.
- menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> {
- // Save the image URL in a `ClipData`.
- ClipData srcImageAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
+ // Set the pinned SSL certificate end date to `null` if the saved date `long` is 0.
+ if (currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)) == 0) {
+ pinnedSslEndDate = null;
+ } else {
+ pinnedSslEndDate = new Date(currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)));
+ }
- // Set the `ClipData` as the clipboard's primary clip.
- clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData);
- return false;
- });
+ // Close `currentHostDomainSettingsCursor`.
+ currentHostDomainSettingsCursor.close();
- // Add an Open with App entry.
- menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
- openWithApp(imageUrl);
- return false;
- });
+ // Apply the domain settings.
+ currentWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled);
+ cookieManager.setAcceptCookie(firstPartyCookiesEnabled);
+ currentWebView.getSettings().setDomStorageEnabled(domStorageEnabled);
- // Add an Open with Browser entry.
- menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
- openWithBrowser(imageUrl);
- return false;
- });
+ // Apply the form data setting if the API < 26.
+ if (Build.VERSION.SDK_INT < 26) {
+ currentWebView.getSettings().setSaveFormData(saveFormDataEnabled);
+ }
- // Add a `Cancel` entry, which by default closes the `ContextMenu`.
- menu.add(R.string.cancel);
- break;
- }
- }
+ // Apply the font size.
+ if (fontSize == 0) { // Apply the default font size.
+ currentWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
+ } else { // Apply the specified font size.
+ currentWebView.getSettings().setTextZoom(fontSize);
+ }
- @Override
- public void onCreateBookmark(DialogFragment dialogFragment) {
- // Get the views from the dialog fragment.
- EditText createBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_name_edittext);
- EditText createBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_url_edittext);
+ // Set third-party cookies status if API >= 21.
+ if (Build.VERSION.SDK_INT >= 21) {
+ cookieManager.setAcceptThirdPartyCookies(currentWebView, thirdPartyCookiesEnabled);
+ }
+
+ // Only set the user agent if the webpage is not currently loading. Otherwise, changing the user agent on redirects can cause the original website to reload.
+ // <https://redmine.stoutner.com/issues/160>
+ if (!urlIsLoading) {
+ // Set the user agent.
+ if (userAgentName.equals(getString(R.string.system_default_user_agent))) { // Use the system default user agent.
+ // Get the array position of the default user agent name.
+ int defaultUserAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
- // Extract the strings from the edit texts.
- String bookmarkNameString = createBookmarkNameEditText.getText().toString();
- String bookmarkUrlString = createBookmarkUrlEditText.getText().toString();
+ // Set the user agent according to the system default.
+ switch (defaultUserAgentArrayPosition) {
+ case UNRECOGNIZED_USER_AGENT: // The default user agent name is not on the canonical list.
+ // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
+ currentWebView.getSettings().setUserAgentString(defaultUserAgentName);
+ break;
- // Get a copy of the favorite icon bitmap.
- Bitmap favoriteIcon = favoriteIconBitmap;
+ case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
+ // Set the user agent to `""`, which uses the default value.
+ currentWebView.getSettings().setUserAgentString("");
+ break;
- // Scale the favorite icon bitmap down if it is larger than 256 x 256. Filtering uses bilinear interpolation.
- if ((favoriteIcon.getHeight() > 256) || (favoriteIcon.getWidth() > 256)) {
- favoriteIcon = Bitmap.createScaledBitmap(favoriteIcon, 256, 256, true);
- }
+ case SETTINGS_CUSTOM_USER_AGENT:
+ // Set the custom user agent.
+ currentWebView.getSettings().setUserAgentString(defaultCustomUserAgentString);
+ break;
- // Create a favorite icon byte array output stream.
- ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
+ default:
+ // Get the user agent string from the user agent data array
+ currentWebView.getSettings().setUserAgentString(userAgentDataArray[defaultUserAgentArrayPosition]);
+ }
+ } else { // Set the user agent according to the stored name.
+ // Get the array position of the user agent name.
+ int userAgentArrayPosition = userAgentNamesArray.getPosition(userAgentName);
- // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
- favoriteIcon.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
+ switch (userAgentArrayPosition) {
+ case UNRECOGNIZED_USER_AGENT: // The user agent name contains a custom user agent.
+ currentWebView.getSettings().setUserAgentString(userAgentName);
+ break;
- // Convert the favorite icon byte array stream to a byte array.
- byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
+ case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
+ // Set the user agent to `""`, which uses the default value.
+ currentWebView.getSettings().setUserAgentString("");
+ break;
- // Display the new bookmark below the current items in the (0 indexed) list.
- int newBookmarkDisplayOrder = bookmarksListView.getCount();
+ default:
+ // Get the user agent string from the user agent data array.
+ currentWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
+ }
+ }
- // Create the bookmark.
- bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolder, newBookmarkDisplayOrder, favoriteIconByteArray);
+ // Store the applied user agent string, which is used in the View Source activity.
+ appliedUserAgentString = currentWebView.getSettings().getUserAgentString();
- // Update the bookmarks cursor with the current contents of this folder.
- bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
+ // Update the user agent change tracker.
+ userAgentChanged = !appliedUserAgentString.equals(initialUserAgent);
+ }
- // Update the list view.
- bookmarksCursorAdapter.changeCursor(bookmarksCursor);
+ // Set swipe to refresh.
+ switch (swipeToRefreshInt) {
+ case DomainsDatabaseHelper.SWIPE_TO_REFRESH_SYSTEM_DEFAULT:
+ // Set swipe to refresh according to the default.
+ swipeRefreshLayout.setEnabled(defaultSwipeToRefresh);
+ break;
- // Scroll to the new bookmark.
- bookmarksListView.setSelection(newBookmarkDisplayOrder);
- }
+ case DomainsDatabaseHelper.SWIPE_TO_REFRESH_ENABLED:
+ // Enable swipe to refresh.
+ swipeRefreshLayout.setEnabled(true);
+ break;
- @Override
- public void onCreateBookmarkFolder(DialogFragment dialogFragment) {
- // Get handles for the views in the dialog fragment.
- EditText createFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.create_folder_name_edittext);
- RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon_radiobutton);
- ImageView folderIconImageView = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon);
+ case DomainsDatabaseHelper.SWIPE_TO_REFRESH_DISABLED:
+ // Disable swipe to refresh.
+ swipeRefreshLayout.setEnabled(false);
+ }
- // Get new folder name string.
- String folderNameString = createFolderNameEditText.getText().toString();
+ // Set the loading of webpage images.
+ switch (displayWebpageImagesInt) {
+ case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_SYSTEM_DEFAULT:
+ currentWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
+ break;
- // Create a folder icon bitmap.
- Bitmap folderIconBitmap;
+ case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_ENABLED:
+ currentWebView.getSettings().setLoadsImagesAutomatically(true);
+ break;
- // Set the folder icon bitmap according to the dialog.
- if (defaultFolderIconRadioButton.isChecked()) { // Use the default folder icon.
- // Get the default folder icon drawable.
- Drawable folderIconDrawable = folderIconImageView.getDrawable();
+ case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_DISABLED:
+ currentWebView.getSettings().setLoadsImagesAutomatically(false);
+ break;
+ }
- // Convert the folder icon drawable to a bitmap drawable.
- BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
+ // Set a green background on `urlTextBox` to indicate that custom domain settings are being used. We have to use the deprecated `.getDrawable()` until the minimum API >= 21.
+ if (darkTheme) {
+ urlEditText.setBackground(getResources().getDrawable(R.drawable.url_bar_background_dark_blue));
+ } else {
+ urlEditText.setBackground(getResources().getDrawable(R.drawable.url_bar_background_light_green));
+ }
+ } else { // The new URL does not have custom domain settings. Load the defaults.
+ // Store the values from `sharedPreferences` in variables.
+ javaScriptEnabled = sharedPreferences.getBoolean("javascript", false);
+ firstPartyCookiesEnabled = sharedPreferences.getBoolean("first_party_cookies", false);
+ thirdPartyCookiesEnabled = sharedPreferences.getBoolean("third_party_cookies", false);
+ domStorageEnabled = sharedPreferences.getBoolean("dom_storage", false);
+ saveFormDataEnabled = sharedPreferences.getBoolean("save_form_data", false); // Form data can be removed once the minimum API >= 26.
+ easyListEnabled = sharedPreferences.getBoolean("easylist", true);
+ easyPrivacyEnabled = sharedPreferences.getBoolean("easyprivacy", true);
+ fanboysAnnoyanceListEnabled = sharedPreferences.getBoolean("fanboys_annoyance_list", true);
+ fanboysSocialBlockingListEnabled = sharedPreferences.getBoolean("fanboys_social_blocking_list", true);
+ ultraPrivacyEnabled = sharedPreferences.getBoolean("ultraprivacy", true);
+ blockAllThirdPartyRequests = sharedPreferences.getBoolean("block_all_third_party_requests", false);
- // Convert the folder icon bitmap drawable to a bitmap.
- folderIconBitmap = folderIconBitmapDrawable.getBitmap();
- } else { // Use the WebView favorite icon.
- // Get a copy of the favorite icon bitmap.
- folderIconBitmap = favoriteIconBitmap;
+ // Set `javaScriptEnabled` to be `true` if `night_mode` is `true`.
+ if (nightMode) {
+ javaScriptEnabled = true;
+ }
- // Scale the folder icon bitmap down if it is larger than 256 x 256. Filtering uses bilinear interpolation.
- if ((folderIconBitmap.getHeight() > 256) || (folderIconBitmap.getWidth() > 256)) {
- folderIconBitmap = Bitmap.createScaledBitmap(folderIconBitmap, 256, 256, true);
- }
- }
+ // Apply the default settings.
+ currentWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled);
+ cookieManager.setAcceptCookie(firstPartyCookiesEnabled);
+ currentWebView.getSettings().setDomStorageEnabled(domStorageEnabled);
+ currentWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
+ swipeRefreshLayout.setEnabled(defaultSwipeToRefresh);
- // Create a folder icon byte array output stream.
- ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream();
+ // Apply the form data setting if the API < 26.
+ if (Build.VERSION.SDK_INT < 26) {
+ currentWebView.getSettings().setSaveFormData(saveFormDataEnabled);
+ }
- // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
- folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream);
+ // Reset the pinned variables.
+ domainSettingsDatabaseId = -1;
+ pinnedSslCertificate = false;
+ pinnedSslIssuedToCName = "";
+ pinnedSslIssuedToOName = "";
+ pinnedSslIssuedToUName = "";
+ pinnedSslIssuedByCName = "";
+ pinnedSslIssuedByOName = "";
+ pinnedSslIssuedByUName = "";
+ pinnedSslStartDate = null;
+ pinnedSslEndDate = null;
+ pinnedIpAddresses = false;
+ pinnedHostIpAddresses = "";
- // Convert the folder icon byte array stream to a byte array.
- byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray();
+ // Set third-party cookies status if API >= 21.
+ if (Build.VERSION.SDK_INT >= 21) {
+ cookieManager.setAcceptThirdPartyCookies(currentWebView, thirdPartyCookiesEnabled);
+ }
- // 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);
- }
+ // 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.
+ // <https://redmine.stoutner.com/issues/160>
+ if (!urlIsLoading) {
+ // Get the array position of the user agent name.
+ int userAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
- // Create the folder, which will be placed at the top of the `ListView`.
- bookmarksDatabaseHelper.createFolder(folderNameString, currentBookmarksFolder, folderIconByteArray);
+ // Set the user agent.
+ switch (userAgentArrayPosition) {
+ case UNRECOGNIZED_USER_AGENT: // The default user agent name is not on the canonical list.
+ // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
+ currentWebView.getSettings().setUserAgentString(defaultUserAgentName);
+ break;
- // Update the bookmarks cursor with the current contents of this folder.
- bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
+ case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
+ // Set the user agent to `""`, which uses the default value.
+ currentWebView.getSettings().setUserAgentString("");
+ break;
- // Update the `ListView`.
- bookmarksCursorAdapter.changeCursor(bookmarksCursor);
+ case SETTINGS_CUSTOM_USER_AGENT:
+ // Set the custom user agent.
+ currentWebView.getSettings().setUserAgentString(defaultCustomUserAgentString);
+ break;
- // Scroll to the new folder.
- bookmarksListView.setSelection(0);
- }
+ default:
+ // Get the user agent string from the user agent data array
+ currentWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
+ }
- @Override
- public void onSaveBookmark(DialogFragment 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 applied user agent string, which is used in the View Source activity.
+ appliedUserAgentString = currentWebView.getSettings().getUserAgentString();
- // Store the bookmark strings.
- String bookmarkNameString = editBookmarkNameEditText.getText().toString();
- String bookmarkUrlString = editBookmarkUrlEditText.getText().toString();
+ // Update the user agent change tracker.
+ userAgentChanged = !appliedUserAgentString.equals(initialUserAgent);
+ }
- // 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.
- // Get a copy of the favorite icon bitmap.
- Bitmap favoriteIcon = favoriteIconBitmap;
+ // Set the loading of webpage images.
+ currentWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
- // Scale the favorite icon bitmap down if it is larger than 256 x 256. Filtering uses bilinear interpolation.
- if ((favoriteIcon.getHeight() > 256) || (favoriteIcon.getWidth() > 256)) {
- favoriteIcon = Bitmap.createScaledBitmap(favoriteIcon, 256, 256, true);
+ // Set a transparent background on `urlTextBox`. The deprecated `.getDrawable()` must be used until the minimum API >= 21.
+ urlEditText.setBackgroundDrawable(getResources().getDrawable(R.color.transparent));
}
- // Create a favorite icon byte array output stream.
- ByteArrayOutputStream newFavoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
-
- // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
- favoriteIcon.compress(Bitmap.CompressFormat.PNG, 0, newFavoriteIconByteArrayOutputStream);
-
- // Convert the favorite icon byte array stream to a byte array.
- byte[] newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray();
+ // Close the domains database helper.
+ domainsDatabaseHelper.close();
- // Update the bookmark and the favorite icon.
- bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, newFavoriteIconByteArray);
+ // Update the privacy icons, but only if `mainMenu` has already been populated.
+ if (mainMenu != null) {
+ updatePrivacyIcons(true);
+ }
}
- // Update the bookmarks cursor with the current contents of this folder.
- bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
+ // Reload the website if returning from the Domains activity.
+ if (reloadWebsite) {
+ currentWebView.reload();
+ }
- // Update the list view.
- bookmarksCursorAdapter.changeCursor(bookmarksCursor);
+ // Return the user agent changed status.
+ return userAgentChanged;
}
- @Override
- public void onSaveBookmarkFolder(DialogFragment 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 defaultFolderIconImageView = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_imageview);
+ private void applyProxyThroughOrbot(boolean reloadWebsite) {
+ // Get a handle for the shared preferences.
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
- // Get the new folder name.
- String newFolderNameString = editFolderNameEditText.getText().toString();
+ // Get the search preferences.
+ String homepageString = sharedPreferences.getString("homepage", getString(R.string.homepage_default_value));
+ String torHomepageString = sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value));
+ String torSearchString = sharedPreferences.getString("tor_search", getString(R.string.tor_search_default_value));
+ String torSearchCustomUrlString = sharedPreferences.getString("tor_search_custom_url", getString(R.string.tor_search_custom_url_default_value));
+ String searchString = sharedPreferences.getString("search", getString(R.string.search_default_value));
+ String searchCustomUrlString = sharedPreferences.getString("search_custom_url", getString(R.string.search_custom_url_default_value));
- // Check if the favorite icon has changed.
- if (currentFolderIconRadioButton.isChecked()) { // Only the name has changed.
- // Update the name in the database.
- bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString);
- } else if (!currentFolderIconRadioButton.isChecked() && newFolderNameString.equals(oldFolderNameString)) { // Only the icon has changed.
- // Create the new folder icon Bitmap.
- Bitmap folderIconBitmap;
+ // Get a handle for the action bar. `getSupportActionBar()` must be used until the minimum API >= 21.
+ ActionBar actionBar = getSupportActionBar();
- // Populate the new folder icon bitmap.
- if (defaultFolderIconRadioButton.isChecked()) {
- // Get the default folder icon drawable.
- Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
+ // Remove the incorrect lint warning later that the action bar might be null.
+ assert actionBar != null;
- // Convert the folder icon drawable to a bitmap drawable.
- BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
+ // Set the homepage, search, and proxy options.
+ if (proxyThroughOrbot) { // Set the Tor options.
+ // Set `torHomepageString` as `homepage`.
+ homepage = torHomepageString;
- // Convert the folder icon bitmap drawable to a bitmap.
- folderIconBitmap = folderIconBitmapDrawable.getBitmap();
- } else { // Use the `WebView` favorite icon.
- // Get a copy of the favorite icon bitmap.
- folderIconBitmap = MainWebViewActivity.favoriteIconBitmap;
+ // If formattedUrlString is null assign the homepage to it.
+ if (formattedUrlString == null) {
+ formattedUrlString = homepage;
+ }
- // Scale the folder icon bitmap down if it is larger than 256 x 256. Filtering uses bilinear interpolation.
- if ((folderIconBitmap.getHeight() > 256) || (folderIconBitmap.getWidth() > 256)) {
- folderIconBitmap = Bitmap.createScaledBitmap(folderIconBitmap, 256, 256, true);
- }
+ // Set the search URL.
+ if (torSearchString.equals("Custom URL")) { // Get the custom URL string.
+ searchURL = torSearchCustomUrlString;
+ } else { // Use the string from the pre-built list.
+ searchURL = torSearchString;
}
- // Create a folder icon byte array output stream.
- ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
+ // Set the proxy. `this` refers to the current activity where an `AlertDialog` might be displayed.
+ OrbotProxyHelper.setProxy(getApplicationContext(), this, "localhost", "8118");
- // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
- folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
+ // Set the `appBar` background to indicate proxying through Orbot is enabled. `this` refers to the context.
+ if (darkTheme) {
+ actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.dark_blue_30));
+ } else {
+ actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.blue_50));
+ }
- // Convert the folder icon byte array stream to a byte array.
- byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
+ // Check to see if Orbot is ready.
+ if (!orbotStatus.equals("ON")) { // Orbot is not ready.
+ // Set `waitingForOrbot`.
+ waitingForOrbot = true;
- // Update the folder icon in the database.
- bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderIconByteArray);
- } else { // The folder icon and the name have changed.
- // Get the new folder icon `Bitmap`.
- Bitmap folderIconBitmap;
- if (defaultFolderIconRadioButton.isChecked()) {
- // Get the default folder icon drawable.
- Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
+ // Disable the wide view port so that the waiting for Orbot text is displayed correctly.
+ currentWebView.getSettings().setUseWideViewPort(false);
- // Convert the folder icon drawable to a bitmap drawable.
- BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
+ // Load a waiting page. `null` specifies no encoding, which defaults to ASCII.
+ currentWebView.loadData(waitingForOrbotHtmlString, "text/html", null);
+ } else if (reloadWebsite) { // Orbot is ready and the website should be reloaded.
+ // Reload the website.
+ currentWebView.reload();
+ }
+ } else { // Set the non-Tor options.
+ // Set `homepageString` as `homepage`.
+ homepage = homepageString;
- // Convert the folder icon bitmap drawable to a bitmap.
- folderIconBitmap = folderIconBitmapDrawable.getBitmap();
- } else { // Use the `WebView` favorite icon.
- // Get a copy of the favorite icon bitmap.
- folderIconBitmap = MainWebViewActivity.favoriteIconBitmap;
+ // If formattedUrlString is null assign the homepage to it.
+ if (formattedUrlString == null) {
+ formattedUrlString = homepage;
+ }
- // Scale the folder icon bitmap down if it is larger than 256 x 256. Filtering uses bilinear interpolation.
- if ((folderIconBitmap.getHeight() > 256) || (folderIconBitmap.getWidth() > 256)) {
- folderIconBitmap = Bitmap.createScaledBitmap(folderIconBitmap, 256, 256, true);
- }
+ // Set the search URL.
+ if (searchString.equals("Custom URL")) { // Get the custom URL string.
+ searchURL = searchCustomUrlString;
+ } else { // Use the string from the pre-built list.
+ searchURL = searchString;
}
- // Create a folder icon byte array output stream.
- ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
+ // Reset the proxy to default. The host is `""` and the port is `"0"`.
+ OrbotProxyHelper.setProxy(getApplicationContext(), this, "", "0");
- // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
- folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
+ // Set the default `appBar` background. `this` refers to the context.
+ if (darkTheme) {
+ actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.gray_900));
+ } else {
+ actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.gray_100));
+ }
- // Convert the folder icon byte array stream to a byte array.
- byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
+ // Reset `waitingForOrbot.
+ waitingForOrbot = false;
- // Update the folder name and icon in the database.
- bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, newFolderIconByteArray);
+ // Reload the website if requested.
+ if (reloadWebsite) {
+ currentWebView.reload();
+ }
}
-
- // Update the bookmarks cursor with the current contents of this folder.
- bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
-
- // Update the `ListView`.
- bookmarksCursorAdapter.changeCursor(bookmarksCursor);
}
- @Override
- public void onCloseDownloadLocationPermissionDialog(int downloadType) {
- switch (downloadType) {
- case DownloadLocationPermissionDialog.DOWNLOAD_FILE:
- // Request the WRITE_EXTERNAL_STORAGE permission with a file request code.
- ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
- break;
+ private void updatePrivacyIcons(boolean runInvalidateOptionsMenu) {
+ // Get handles for the menu items.
+ MenuItem privacyMenuItem = mainMenu.findItem(R.id.toggle_javascript);
+ MenuItem firstPartyCookiesMenuItem = mainMenu.findItem(R.id.toggle_first_party_cookies);
+ MenuItem domStorageMenuItem = mainMenu.findItem(R.id.toggle_dom_storage);
+ MenuItem refreshMenuItem = mainMenu.findItem(R.id.refresh);
- case DownloadLocationPermissionDialog.DOWNLOAD_IMAGE:
- // Request the WRITE_EXTERNAL_STORAGE permission with an image request code.
- ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
- break;
+ // Update the privacy icon.
+ if (javaScriptEnabled) { // JavaScript is enabled.
+ privacyMenuItem.setIcon(R.drawable.javascript_enabled);
+ } else if (firstPartyCookiesEnabled) { // JavaScript is disabled but cookies are enabled.
+ privacyMenuItem.setIcon(R.drawable.warning);
+ } else { // All the dangerous features are disabled.
+ privacyMenuItem.setIcon(R.drawable.privacy_mode);
}
- }
-
- @Override
- public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
- // Get a handle for the fragment manager.
- FragmentManager fragmentManager = getSupportFragmentManager();
-
- switch (requestCode) {
- case DOWNLOAD_FILE_REQUEST_CODE:
- // Show the download file alert dialog. When the dialog closes, the correct command will be used based on the permission status.
- DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(downloadUrl, downloadContentDisposition, downloadContentLength);
-
- // On API 23, displaying the fragment must be delayed or the app will crash.
- if (Build.VERSION.SDK_INT == 23) {
- new Handler().postDelayed(() -> downloadFileDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
- } else {
- downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
- }
- // Reset the download variables.
- downloadUrl = "";
- downloadContentDisposition = "";
- downloadContentLength = 0;
- break;
+ // Update the first-party cookies icon.
+ if (firstPartyCookiesEnabled) { // First-party cookies are enabled.
+ firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_enabled);
+ } else { // First-party cookies are disabled.
+ if (darkTheme) {
+ firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_dark);
+ } else {
+ firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_light);
+ }
+ }
- case DOWNLOAD_IMAGE_REQUEST_CODE:
- // Show the download image alert dialog. When the dialog closes, the correct command will be used based on the permission status.
- DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(downloadImageUrl);
+ // Update the DOM storage icon.
+ if (javaScriptEnabled && domStorageEnabled) { // Both JavaScript and DOM storage are enabled.
+ domStorageMenuItem.setIcon(R.drawable.dom_storage_enabled);
+ } else if (javaScriptEnabled) { // JavaScript is enabled but DOM storage is disabled.
+ if (darkTheme) {
+ domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_dark);
+ } else {
+ domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_light);
+ }
+ } else { // JavaScript is disabled, so DOM storage is ghosted.
+ if (darkTheme) {
+ domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_dark);
+ } else {
+ domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_light);
+ }
+ }
- // On API 23, displaying the fragment must be delayed or the app will crash.
- if (Build.VERSION.SDK_INT == 23) {
- new Handler().postDelayed(() -> downloadImageDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
- } else {
- downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
- }
+ // Update the refresh icon.
+ if (darkTheme) {
+ refreshMenuItem.setIcon(R.drawable.refresh_enabled_dark);
+ } else {
+ refreshMenuItem.setIcon(R.drawable.refresh_enabled_light);
+ }
- // Reset the image URL variable.
- downloadImageUrl = "";
- break;
+ // `invalidateOptionsMenu` calls `onPrepareOptionsMenu()` and redraws the icons in the `AppBar`.
+ if (runInvalidateOptionsMenu) {
+ invalidateOptionsMenu();
}
}
- @Override
- public void onDownloadImage(DialogFragment dialogFragment, String imageUrl) {
- // Download the image if it has an HTTP or HTTPS URI.
- if (imageUrl.startsWith("http")) {
- // Get a handle for the system `DOWNLOAD_SERVICE`.
- DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
+ private void openUrlWithExternalApp(String url) {
+ // Create a download intent. Not specifying the action type will display the maximum number of options.
+ Intent downloadIntent = new Intent();
- // Parse `imageUrl`.
- DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(imageUrl));
+ // Set the URI and the MIME type. Specifying `text/html` displays a good number of options.
+ downloadIntent.setDataAndType(Uri.parse(url), "text/html");
- // Pass cookies to download manager if cookies are enabled. This is required to download images from websites that require a login.
- // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
- if (firstPartyCookiesEnabled) {
- // Get the cookies for `imageUrl`.
- String cookies = cookieManager.getCookie(imageUrl);
+ // Flag the intent to open in a new task.
+ downloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- // Add the cookies to `downloadRequest`. In the HTTP request header, cookies are named `Cookie`.
- downloadRequest.addRequestHeader("Cookie", cookies);
- }
+ // Show the chooser.
+ startActivity(Intent.createChooser(downloadIntent, getString(R.string.open_with)));
+ }
+
+ private void highlightUrlText() {
+ // Only highlight the URL text if the box is not currently selected.
+ if (!urlTextBox.hasFocus()) {
+ // Get the URL string.
+ String urlString = urlTextBox.getText().toString();
+
+ // Highlight the URL according to the protocol.
+ if (urlString.startsWith("file://")) { // This is a file URL.
+ // De-emphasize only the protocol.
+ urlTextBox.getText().setSpan(initialGrayColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+ } else if (urlString.startsWith("content://")) {
+ // De-emphasize only the protocol.
+ urlTextBox.getText().setSpan(initialGrayColorSpan, 0, 10, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+ } else { // This is a web URL.
+ // Get the index of the `/` immediately after the domain name.
+ int endOfDomainName = urlString.indexOf("/", (urlString.indexOf("//") + 2));
- // Get the file name from the dialog fragment.
- EditText downloadImageNameEditText = dialogFragment.getDialog().findViewById(R.id.download_image_name);
- String imageName = downloadImageNameEditText.getText().toString();
+ // Create a base URL string.
+ String baseUrl;
- // Specify the download location.
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // External write permission granted.
- // Download to the public download directory.
- downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, imageName);
- } else { // External write permission denied.
- // Download to the app's external download directory.
- downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, imageName);
- }
+ // Get the base URL.
+ if (endOfDomainName > 0) { // There is at least one character after the base URL.
+ // Get the base URL.
+ baseUrl = urlString.substring(0, endOfDomainName);
+ } else { // There are no characters after the base URL.
+ // Set the base URL to be the entire URL string.
+ baseUrl = urlString;
+ }
- // Allow `MediaScanner` to index the download if it is a media file.
- downloadRequest.allowScanningByMediaScanner();
+ // Get the index of the last `.` in the domain.
+ int lastDotIndex = baseUrl.lastIndexOf(".");
- // Add the URL as the description for the download.
- downloadRequest.setDescription(imageUrl);
+ // Get the index of the penultimate `.` in the domain.
+ int penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1);
- // Show the download notification after the download is completed.
- downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
+ // Markup the beginning of the URL.
+ if (urlString.startsWith("http://")) { // Highlight the protocol of connections that are not encrypted.
+ urlTextBox.getText().setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
- // Remove the lint warning below that `downloadManager` might be `null`.
- assert downloadManager != null;
+ // De-emphasize subdomains.
+ if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name.
+ urlTextBox.getText().setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+ }
+ } else if (urlString.startsWith("https://")) { // De-emphasize the protocol of connections that are encrypted.
+ if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name.
+ // De-emphasize the protocol and the additional subdomains.
+ urlTextBox.getText().setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+ } else { // There is only one subdomain in the domain name.
+ // De-emphasize only the protocol.
+ urlTextBox.getText().setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+ }
+ }
- // Initiate the download.
- downloadManager.enqueue(downloadRequest);
- } else { // The image is not an HTTP or HTTPS URI.
- Snackbar.make(mainWebView, R.string.cannot_download_image, Snackbar.LENGTH_INDEFINITE).show();
+ // De-emphasize the text after the domain name.
+ if (endOfDomainName > 0) {
+ urlTextBox.getText().setSpan(finalGrayColorSpan, endOfDomainName, urlString.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+ }
+ }
}
}
- @Override
- public void onDownloadFile(DialogFragment dialogFragment, String downloadUrl) {
- // Download the file if it has an HTTP or HTTPS URI.
- if (downloadUrl.startsWith("http")) {
- // Get a handle for the system `DOWNLOAD_SERVICE`.
- DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
-
- // Parse `downloadUrl`.
- DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(downloadUrl));
-
- // Pass cookies to download manager if cookies are enabled. This is required to download files from websites that require a login.
- // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
- if (firstPartyCookiesEnabled) {
- // Get the cookies for `downloadUrl`.
- String cookies = cookieManager.getCookie(downloadUrl);
+ private void loadBookmarksFolder() {
+ // Update the bookmarks cursor with the contents of the bookmarks database for the current folder.
+ bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
- // Add the cookies to `downloadRequest`. In the HTTP request header, cookies are named `Cookie`.
- downloadRequest.addRequestHeader("Cookie", cookies);
+ // Populate the bookmarks cursor adapter. `this` specifies the `Context`. `false` disables `autoRequery`.
+ bookmarksCursorAdapter = new CursorAdapter(this, bookmarksCursor, false) {
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ // Inflate the individual item layout. `false` does not attach it to the root.
+ return getLayoutInflater().inflate(R.layout.bookmarks_drawer_item_linearlayout, parent, false);
}
- // Get the file name from the dialog fragment.
- EditText downloadFileNameEditText = dialogFragment.getDialog().findViewById(R.id.download_file_name);
- String fileName = downloadFileNameEditText.getText().toString();
+ @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);
- // Specify the download location.
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // External write permission granted.
- // Download to the public download directory.
- downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
- } else { // External write permission denied.
- // Download to the app's external download directory.
- downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, fileName);
- }
+ // Get the favorite icon byte array from the cursor.
+ byte[] favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON));
- // Allow `MediaScanner` to index the download if it is a media file.
- downloadRequest.allowScanningByMediaScanner();
+ // 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);
- // Add the URL as the description for the download.
- downloadRequest.setDescription(downloadUrl);
+ // Display the bitmap in `bookmarkFavoriteIcon`.
+ bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap);
- // Show the download notification after the download is completed.
- downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
+ // Get the bookmark name from the cursor and display it in `bookmarkNameTextView`.
+ String bookmarkNameString = cursor.getString(cursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
+ bookmarkNameTextView.setText(bookmarkNameString);
- // Remove the lint warning below that `downloadManager` might be `null`.
- assert downloadManager != null;
+ // 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);
+ }
+ }
+ };
- // Initiate the download.
- downloadManager.enqueue(downloadRequest);
- } else { // The download is not an HTTP or HTTPS URI.
- Snackbar.make(mainWebView, R.string.cannot_download_file, Snackbar.LENGTH_INDEFINITE).show();
- }
- }
+ // Populate the `ListView` with the adapter.
+ bookmarksListView.setAdapter(bookmarksCursorAdapter);
- @Override
- public void onHttpAuthenticationCancel() {
- // Cancel the `HttpAuthHandler`.
- httpAuthHandler.cancel();
+ // Set the bookmarks drawer title.
+ if (currentBookmarksFolder.isEmpty()) {
+ bookmarksTitleTextView.setText(R.string.bookmarks);
+ } else {
+ bookmarksTitleTextView.setText(currentBookmarksFolder);
+ }
}
- @Override
- public void onHttpAuthenticationProceed(DialogFragment 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());
- }
+ private void openWithApp(String url) {
+ // Create the open with intent with `ACTION_VIEW`.
+ Intent openWithAppIntent = new Intent(Intent.ACTION_VIEW);
- public void viewSslCertificate(View view) {
- // Show the `ViewSslCertificateDialog` `AlertDialog` and name this instance `@string/view_ssl_certificate`.
- DialogFragment viewSslCertificateDialogFragment = new ViewSslCertificateDialog();
- viewSslCertificateDialogFragment.show(getSupportFragmentManager(), getString(R.string.view_ssl_certificate));
- }
+ // Set the URI but not the MIME type. This should open all available apps.
+ openWithAppIntent.setData(Uri.parse(url));
- @Override
- public void onSslErrorCancel() {
- sslErrorHandler.cancel();
- }
+ // Flag the intent to open in a new task.
+ openWithAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- @Override
- public void onSslErrorProceed() {
- sslErrorHandler.proceed();
+ // Show the chooser.
+ startActivity(openWithAppIntent);
}
- @Override
- public void onPinnedMismatchBack() {
- if (mainWebView.canGoBack()) { // There is a back page in the history.
- // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
- formattedUrlString = "";
+ private void openWithBrowser(String url) {
+ // Create the open with intent with `ACTION_VIEW`.
+ Intent openWithBrowserIntent = new Intent(Intent.ACTION_VIEW);
- // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
- navigatingHistory = true;
+ // Set the URI and the MIME type. `"text/html"` should load browser options.
+ openWithBrowserIntent.setDataAndType(Uri.parse(url), "text/html");
- // Go back.
- mainWebView.goBack();
- } else { // There are no pages to go back to.
- // Load a blank page
- loadUrl("");
- }
- }
+ // Flag the intent to open in a new task.
+ openWithBrowserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- @Override
- public void onPinnedMismatchProceed() {
- // Do not check the pinned information for this domain again until the domain changes.
- ignorePinnedDomainInformation = true;
+ // Show the chooser.
+ startActivity(openWithBrowserIntent);
}
- @Override
- public void onUrlHistoryEntrySelected(int moveBackOrForwardSteps) {
- // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
- formattedUrlString = "";
+ private static void checkPinnedMismatch() {
+ if ((pinnedSslCertificate || pinnedIpAddresses) && !ignorePinnedDomainInformation) {
+ // Initialize the current SSL certificate variables.
+ String currentWebsiteIssuedToCName = "";
+ String currentWebsiteIssuedToOName = "";
+ String currentWebsiteIssuedToUName = "";
+ String currentWebsiteIssuedByCName = "";
+ String currentWebsiteIssuedByOName = "";
+ String currentWebsiteIssuedByUName = "";
+ Date currentWebsiteSslStartDate = null;
+ Date currentWebsiteSslEndDate = null;
- // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
- navigatingHistory = true;
- // Load the history entry.
- mainWebView.goBackOrForward(moveBackOrForwardSteps);
- }
+ // 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();
+ }
- @Override
- public void onClearHistory() {
- // Clear the history.
- mainWebView.clearHistory();
- }
+ // Initialize string variables to store the SSL certificate dates. Strings are needed to compare the values below, which doesn't work with `Dates` if they are `null`.
+ String currentWebsiteSslStartDateString = "";
+ String currentWebsiteSslEndDateString = "";
+ String pinnedSslStartDateString = "";
+ String pinnedSslEndDateString = "";
- // Override `onBackPressed` to handle the navigation drawer and `mainWebView`.
- @Override
- public void onBackPressed() {
- // Get a handle for the drawer layout.
- DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
+ // Convert the `Dates` to `Strings` if they are not `null`.
+ if (currentWebsiteSslStartDate != null) {
+ currentWebsiteSslStartDateString = currentWebsiteSslStartDate.toString();
+ }
- if (drawerLayout.isDrawerVisible(GravityCompat.START)) { // The navigation drawer is open.
- // Close the navigation drawer.
- drawerLayout.closeDrawer(GravityCompat.START);
- } else if (drawerLayout.isDrawerVisible(GravityCompat.END)){ // The bookmarks drawer is open.
- if (currentBookmarksFolder.isEmpty()) { // The home folder is displayed.
- // close the bookmarks drawer.
- drawerLayout.closeDrawer(GravityCompat.END);
- } else { // A subfolder is displayed.
- // Place the former parent folder in `currentFolder`.
- currentBookmarksFolder = bookmarksDatabaseHelper.getParentFolderName(currentBookmarksFolder);
+ if (currentWebsiteSslEndDate != null) {
+ currentWebsiteSslEndDateString = currentWebsiteSslEndDate.toString();
+ }
- // Load the new folder.
- loadBookmarksFolder();
+ if (pinnedSslStartDate != null) {
+ pinnedSslStartDateString = pinnedSslStartDate.toString();
}
- } else if (mainWebView.canGoBack()) { // There is at least one item in the `WebView` history.
- // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
- formattedUrlString = "";
+ if (pinnedSslEndDate != null) {
+ pinnedSslEndDateString = pinnedSslEndDate.toString();
+ }
- // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded.
- navigatingHistory = true;
+ // Check to see if the pinned information matches the current information.
+ if ((pinnedIpAddresses && !currentHostIpAddresses.equals(pinnedHostIpAddresses)) || (pinnedSslCertificate && (!currentWebsiteIssuedToCName.equals(pinnedSslIssuedToCName) ||
+ !currentWebsiteIssuedToOName.equals(pinnedSslIssuedToOName) || !currentWebsiteIssuedToUName.equals(pinnedSslIssuedToUName) ||
+ !currentWebsiteIssuedByCName.equals(pinnedSslIssuedByCName) || !currentWebsiteIssuedByOName.equals(pinnedSslIssuedByOName) ||
+ !currentWebsiteIssuedByUName.equals(pinnedSslIssuedByUName) || !currentWebsiteSslStartDateString.equals(pinnedSslStartDateString) ||
+ !currentWebsiteSslEndDateString.equals(pinnedSslEndDateString)))) {
- // Go back.
- mainWebView.goBack();
- } else { // There isn't anything to do in Privacy Browser.
- // Pass `onBackPressed()` to the system.
- super.onBackPressed();
- }
- }
+ // Get a handle for the pinned mismatch alert dialog.
+ DialogFragment pinnedMismatchDialogFragment = PinnedMismatchDialog.displayDialog(pinnedSslCertificate, pinnedIpAddresses);
- // Process the results of an upload file chooser. Currently there is only one `startActivityForResult` in this activity, so the request code, used to differentiate them, is ignored.
- @Override
- public void onActivityResult(int requestCode, int resultCode, Intent data) {
- // File uploads only work on API >= 21.
- if (Build.VERSION.SDK_INT >= 21) {
- // Pass the file to the WebView.
- fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, data));
+ // Show the pinned mismatch alert dialog.
+ pinnedMismatchDialogFragment.show(fragmentManager, "Pinned Mismatch");
+ }
}
}
- private void loadUrlFromTextBox() {
- // Get the text from urlTextBox and convert it to a string. trim() removes white spaces from the beginning and end of the string.
- String unformattedUrlString = urlTextBox.getText().toString().trim();
+ // This must run asynchronously because it involves a network request. `String` declares the parameters. `Void` does not declare progress units. `String` contains the results.
+ private static class GetHostIpAddresses extends AsyncTask<String, Void, String> {
+ // The weak references are used to determine if the activity have disappeared while the AsyncTask is running.
+ private final WeakReference<Activity> activityWeakReference;
- // Check to see if `unformattedUrlString` is a valid URL. Otherwise, convert it into a search.
- if (unformattedUrlString.startsWith("content://")) {
- // Load the entire content URL.
- formattedUrlString = unformattedUrlString;
- } else if (Patterns.WEB_URL.matcher(unformattedUrlString).matches() || unformattedUrlString.startsWith("http://") || unformattedUrlString.startsWith("https://")
- || unformattedUrlString.startsWith("file://")) {
- // Add `https://` at the beginning if there is no protocol. Otherwise the app will segfault.
- if (!unformattedUrlString.startsWith("http") && !unformattedUrlString.startsWith("file://") && !unformattedUrlString.startsWith("content://")) {
- unformattedUrlString = "https://" + unformattedUrlString;
- }
+ GetHostIpAddresses(Activity activity) {
+ // Populate the weak references.
+ activityWeakReference = new WeakReference<>(activity);
+ }
- // Initialize `unformattedUrl`.
- URL unformattedUrl = null;
+ // `onPreExecute()` operates on the UI thread.
+ @Override
+ protected void onPreExecute() {
+ // Get a handle for the activity.
+ Activity activity = activityWeakReference.get();
- // Convert `unformattedUrlString` to a `URL`, then to a `URI`, and then back to a `String`, which sanitizes the input and adds in any missing components.
- try {
- unformattedUrl = new URL(unformattedUrlString);
- } catch (MalformedURLException e) {
- e.printStackTrace();
+ // Abort if the activity is gone.
+ if ((activity == null) || activity.isFinishing()) {
+ return;
}
- // The ternary operator (? :) makes sure that a null pointer exception is not thrown, which would happen if `.get` was called on a `null` value.
- String scheme = unformattedUrl != null ? unformattedUrl.getProtocol() : null;
- String authority = unformattedUrl != null ? unformattedUrl.getAuthority() : null;
- String path = unformattedUrl != null ? unformattedUrl.getPath() : null;
- String query = unformattedUrl != null ? unformattedUrl.getQuery() : null;
- String fragment = unformattedUrl != null ? unformattedUrl.getRef() : null;
+ // Set the getting IP addresses tracker.
+ gettingIpAddresses = true;
+ }
- // Build the URI.
- Uri.Builder formattedUri = new Uri.Builder();
- formattedUri.scheme(scheme).authority(authority).path(path).query(query).fragment(fragment);
- // Decode `formattedUri` as a `String` in `UTF-8`.
- try {
- formattedUrlString = URLDecoder.decode(formattedUri.build().toString(), "UTF-8");
- } catch (UnsupportedEncodingException exception) {
- // Load a blank string.
- formattedUrlString = "";
+ @Override
+ protected String doInBackground(String... domainName) {
+ // Get a handle for the activity.
+ Activity activity = activityWeakReference.get();
+
+ // Abort if the activity is gone.
+ if ((activity == null) || activity.isFinishing()) {
+ // Return an empty spannable string builder.
+ return "";
}
- } else if (unformattedUrlString.isEmpty()){ // Load a blank web site.
- // Load a blank string.
- formattedUrlString = "";
- } else { // Search for the contents of the URL box.
- // Create an encoded URL String.
- String encodedUrlString;
- // Sanitize the search input.
+ // Initialize an IP address string builder.
+ StringBuilder ipAddresses = new StringBuilder();
+
+ // Get an array with the IP addresses for the host.
try {
- encodedUrlString = URLEncoder.encode(unformattedUrlString, "UTF-8");
- } catch (UnsupportedEncodingException exception) {
- encodedUrlString = "";
+ // Get an array with all the IP addresses for the domain.
+ InetAddress[] inetAddressesArray = InetAddress.getAllByName(domainName[0]);
+
+ // Add each IP address to the string builder.
+ for (InetAddress inetAddress : inetAddressesArray) {
+ if (ipAddresses.length() == 0) { // This is the first IP address.
+ // Add the IP address to the string builder.
+ ipAddresses.append(inetAddress.getHostAddress());
+ } else { // This is not the first IP address.
+ // Add a line break to the string builder first.
+ ipAddresses.append("\n");
+
+ // Add the IP address to the string builder.
+ ipAddresses.append(inetAddress.getHostAddress());
+ }
+ }
+ } catch (UnknownHostException exception) {
+ // Do nothing.
}
- // Add the base search URL.
- formattedUrlString = searchURL + encodedUrlString;
+ // Return the string.
+ return ipAddresses.toString();
}
- // Clear the focus from the URL text box. Otherwise, proximate typing in the box will retain the colorized formatting instead of being reset during refocus.
- urlTextBox.clearFocus();
-
- // Make it so.
- loadUrl(formattedUrlString);
- }
+ // `onPostExecute()` operates on the UI thread.
+ @Override
+ protected void onPostExecute(String ipAddresses) {
+ // Get a handle for the activity.
+ Activity activity = activityWeakReference.get();
- private void loadUrl(String url) {// Apply any custom domain settings.
- // Set the URL as the formatted URL string so that checking third-party requests works correctly.
- formattedUrlString = url;
+ // Abort if the activity is gone.
+ if ((activity == null) || activity.isFinishing()) {
+ return;
+ }
- // Apply the domain settings.
- applyDomainSettings(url, true, false);
+ // Store the IP addresses.
+ currentHostIpAddresses = ipAddresses;
- // If loading a website, set `urlIsLoading` to prevent changes in the user agent on websites with redirects from reloading the current website.
- urlIsLoading = !url.equals("");
+ if (!urlIsLoading) {
+ checkPinnedMismatch();
+ }
- // Load the URL.
- mainWebView.loadUrl(url, customHeaders);
+ // Reset the getting IP addresses tracker.
+ gettingIpAddresses = false;
+ }
}
- public void findPreviousOnPage(View view) {
- // Go to the previous highlighted phrase on the page. `false` goes backwards instead of forwards.
- mainWebView.findNext(false);
- }
+ private class WebViewPagerAdapter extends FragmentPagerAdapter {
+ // The WebView fragments list contains all the WebViews.
+ private LinkedList<WebViewTabFragment> webViewFragmentsList = new LinkedList<>();
- public void findNextOnPage(View view) {
- // Go to the next highlighted phrase on the page. `true` goes forwards instead of backwards.
- mainWebView.findNext(true);
- }
+ // Define the constructor.
+ private WebViewPagerAdapter(FragmentManager fragmentManager){
+ // Run the default commands.
+ super(fragmentManager);
+ }
- public void closeFindOnPage(View view) {
- // Get a handle for the views.
- Toolbar toolbar = findViewById(R.id.toolbar);
- LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
+ @Override
+ public int getCount() {
+ // Return the number of pages.
+ return webViewFragmentsList.size();
+ }
- // Delete the contents of `find_on_page_edittext`.
- findOnPageEditText.setText(null);
+ @Override
+ public int getItemPosition(@NonNull Object object) {
+ //noinspection SuspiciousMethodCalls
+ if (webViewFragmentsList.contains(object)) {
+ // The tab has not been deleted.
+ return POSITION_UNCHANGED;
+ } else {
+ // The tab has been deleted.
+ return POSITION_NONE;
+ }
+ }
- // Clear the highlighted phrases.
- mainWebView.clearMatches();
+ @Override
+ public Fragment getItem(int pageNumber) {
+ // Get a WebView for a particular page. Page numbers are 0 indexed.
+ return webViewFragmentsList.get(pageNumber);
+ }
- // Hide the find on page linear layout.
- findOnPageLinearLayout.setVisibility(View.GONE);
+ private void addPage() {
+ // Add a new page. The pages and tabs are 0 indexed, so the size of the current list equals the number of the next page.
+ webViewFragmentsList.add(WebViewTabFragment.createTab(webViewFragmentsList.size()));
- // Show the toolbar.
- toolbar.setVisibility(View.VISIBLE);
+ // Update the view pager.
+ notifyDataSetChanged();
+ }
- // Hide the keyboard.
- inputMethodManager.hideSoftInputFromWindow(mainWebView.getWindowToken(), 0);
- }
+ private void deletePage(int pageNumber) {
+ // Get a handle for the tab layout.
+ TabLayout tabLayout = findViewById(R.id.tablayout);
- private void applyAppSettings() {
- // Get a handle for the shared preferences.
- SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+ // TODO always move to the next tab if possible.
+ // Select a tab that is not being deleted.
+ if (pageNumber == 0) { // The first tab is being deleted.
+ // Get a handle for the second tab. The tabs are 0 indexed.
+ TabLayout.Tab secondTab = tabLayout.getTabAt(1);
- // Store the values from the shared preferences in variables.
- incognitoModeEnabled = sharedPreferences.getBoolean("incognito_mode", false);
- boolean doNotTrackEnabled = sharedPreferences.getBoolean("do_not_track", false);
- proxyThroughOrbot = sharedPreferences.getBoolean("proxy_through_orbot", false);
- fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean("full_screen_browsing_mode", false);
- hideAppBar = sharedPreferences.getBoolean("hide_app_bar", true);
- downloadWithExternalApp = sharedPreferences.getBoolean("download_with_external_app", false);
+ // Remove the incorrect lint warning below that the second tab might be null.
+ assert secondTab != null;
- // Get handles for the views that need to be modified. `getSupportActionBar()` must be used until the minimum API >= 21.
- FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
- ActionBar actionBar = getSupportActionBar();
+ // Select the second tab.
+ secondTab.select();
+ } else { // The first tab is not being deleted.
+ // Get a handle for the previous tab.
+ TabLayout.Tab previousTab = tabLayout.getTabAt(pageNumber - 1);
- // Remove the incorrect lint warnings below that the action bar might be null.
- assert actionBar != null;
+ // Remove the incorrect lint warning below tha the previous tab might be null.
+ assert previousTab != null;
- // Apply the proxy through Orbot settings.
- applyProxyThroughOrbot(false);
+ // Select the previous tab.
+ previousTab.select();
+ }
- // Set Do Not Track status.
- if (doNotTrackEnabled) {
- customHeaders.put("DNT", "1");
- } else {
- customHeaders.remove("DNT");
+ // Delete the page.
+ webViewFragmentsList.remove(pageNumber);
+
+ // Delete the tab.
+ tabLayout.removeTabAt(pageNumber);
+
+ // Update the view pager.
+ notifyDataSetChanged();
}
+ }
- // Set the app bar scrolling.
- mainWebView.setNestedScrollingEnabled(sharedPreferences.getBoolean("scroll_app_bar", true));
+ public void addTab(View view) {
+ // Add the new WebView page.
+ webViewPagerAdapter.addPage();
- // Update the full screen browsing mode settings.
- if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode.
- // Update the visibility of the app bar, which might have changed in the settings.
- if (hideAppBar) {
- actionBar.hide();
- } else {
- actionBar.show();
- }
+ // Get a handle for the tab layout.
+ TabLayout tabLayout = findViewById(R.id.tablayout);
- // Hide the banner ad in the free flavor.
- if (BuildConfig.FLAVOR.contentEquals("free")) {
- AdHelper.hideAd(findViewById(R.id.adview));
- }
+ // Get a handle for the new tab. The tabs are 0 indexed.
+ TabLayout.Tab newTab = tabLayout.getTabAt(tabLayout.getTabCount() - 1);
- // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
- getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+ // Remove the incorrect warning below that the new tab might be null.
+ assert newTab != null;
- /* Hide the system bars.
- * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
- * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
- * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
- * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
- */
- rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
- View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
- } else { // Privacy Browser is not in full screen browsing mode.
- // Reset the full screen tracker, which could be true if Privacy Browser was in full screen mode before entering settings and full screen browsing was disabled.
- inFullScreenBrowsingMode = false;
+ // Move the tab layout to the new tab.
+ newTab.select();
+ }
- // Show the app bar.
- actionBar.show();
+ @Override
+ public void initializeWebView(NestedScrollWebView nestedScrollWebView, int tabNumber) {
+ // Get handles for the activity views.
+ final FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
+ final DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
+ final RelativeLayout mainContentRelativeLayout = findViewById(R.id.main_content_relativelayout);
+ final ActionBar actionBar = getSupportActionBar();
+ final TabLayout tabLayout = findViewById(R.id.tablayout);
+ final SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
+
+ // Remove the incorrect lint warnings below that the some of the views might be null.
+ assert actionBar != null;
- // Show the banner ad in the free flavor.
- if (BuildConfig.FLAVOR.contentEquals("free")) {
- // Initialize the ads. If this isn't the first run, `loadAd()` will be automatically called instead.
- AdHelper.initializeAds(findViewById(R.id.adview), getApplicationContext(), fragmentManager, getString(R.string.google_app_id), getString(R.string.ad_unit_id));
- }
+ // TODO. Still doesn't work right.
+ // Create the tab if it doesn't already exist.
+ try {
+ TabLayout.Tab tab = tabLayout.getTabAt(tabNumber);
- // Remove the `SYSTEM_UI` flags from the root frame layout.
- rootFrameLayout.setSystemUiVisibility(0);
+ assert tab != null;
- // Add the translucent status flag.
- getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+ tab.getCustomView();
+ } catch (Exception exception) {
+ tabLayout.addTab(tabLayout.newTab());
}
- }
+ // Get the current tab.
+ TabLayout.Tab currentTab = tabLayout.getTabAt(tabNumber);
- // `reloadWebsite` is used if returning from the Domains activity. Otherwise JavaScript might not function correctly if it is newly enabled.
- // The deprecated `.getDrawable()` must be used until the minimum API >= 21.
- @SuppressWarnings("deprecation")
- private boolean applyDomainSettings(String url, boolean resetFavoriteIcon, boolean reloadWebsite) {
- // Get the current user agent.
- String initialUserAgent = mainWebView.getSettings().getUserAgentString();
+ // Remove the lint warning below that the current tab might be null.
+ assert currentTab != null;
- // Initialize a variable to track if the user agent changes.
- boolean userAgentChanged = false;
+ // Set a custom view on the current tab.
+ currentTab.setCustomView(R.layout.custom_tab_view);
- // Parse the URL into a URI.
- Uri uri = Uri.parse(url);
+ // Get the custom view from the tab.
+ View currentTabView = currentTab.getCustomView();
- // Extract the domain from `uri`.
- String hostName = uri.getHost();
+ // Remove the incorrect warning below that the current tab view might be null.
+ assert currentTabView != null;
- // Initialize `loadingNewDomainName`.
- boolean loadingNewDomainName;
+ // Get the current views from the tab.
+ ImageView tabFavoriteIconImageView = currentTabView.findViewById(R.id.favorite_icon_imageview);
+ TextView tabTitleTextView = currentTabView.findViewById(R.id.title_textview);
- // If either `hostName` or `currentDomainName` are `null`, run the options for loading a new domain name.
- // The lint suggestion to simplify the `if` statement is incorrect, because `hostName.equals(currentDomainName)` can produce a `null object reference.`
- //noinspection SimplifiableIfStatement
- if ((hostName == null) || (currentDomainName == null)) {
- loadingNewDomainName = true;
- } else { // Determine if `hostName` equals `currentDomainName`.
- loadingNewDomainName = !hostName.equals(currentDomainName);
- }
- // Strings don't like to be null.
- if (hostName == null) {
- hostName = "";
- }
+ //TODO
+ final ProgressBar progressBar = findViewById(R.id.progress_bar);
- // 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;
+ // Get a handle for the shared preferences.
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
- // Reset the ignoring of pinned domain information.
- ignorePinnedDomainInformation = false;
+ // Get the relevant preferences.
+ boolean downloadWithExternalApp = sharedPreferences.getBoolean("download_with_external_app", false);
- // Reset the favorite icon if specified.
- if (resetFavoriteIcon) {
- favoriteIconBitmap = favoriteIconDefaultBitmap;
- favoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(favoriteIconBitmap, 64, 64, true));
- }
+ // Allow pinch to zoom.
+ nestedScrollWebView.getSettings().setBuiltInZoomControls(true);
- // Get a handle for the swipe refresh layout.
- SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
+ // Hide zoom controls.
+ nestedScrollWebView.getSettings().setDisplayZoomControls(false);
- // Initialize the database handler. The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
- DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
+ // Don't allow mixed content (HTTP and HTTPS) on the same website.
+ if (Build.VERSION.SDK_INT >= 21) {
+ nestedScrollWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW);
+ }
- // Get a full cursor from `domainsDatabaseHelper`.
- Cursor domainNameCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomain();
+ // Set the WebView to use a wide viewport. Otherwise, some web pages will be scrunched and some content will render outside the screen.
+ nestedScrollWebView.getSettings().setUseWideViewPort(true);
- // Initialize `domainSettingsSet`.
- Set<String> domainSettingsSet = new HashSet<>();
+ // Set the WebView to load in overview mode (zoomed out to the maximum width).
+ nestedScrollWebView.getSettings().setLoadWithOverviewMode(true);
- // Get the domain name column index.
- int domainNameColumnIndex = domainNameCursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME);
+ // Explicitly disable geolocation.
+ nestedScrollWebView.getSettings().setGeolocationEnabled(false);
- // Populate `domainSettingsSet`.
- for (int i = 0; i < domainNameCursor.getCount(); i++) {
- // Move `domainsCursor` to the current row.
- domainNameCursor.moveToPosition(i);
+ // Create a double-tap gesture detector to toggle full-screen mode.
+ GestureDetector doubleTapGestureDetector = new GestureDetector(getApplicationContext(), new GestureDetector.SimpleOnGestureListener() {
+ // Override `onDoubleTap()`. All other events are handled using the default settings.
+ @Override
+ public boolean onDoubleTap(MotionEvent event) {
+ if (fullScreenBrowsingModeEnabled) { // Only process the double-tap if full screen browsing mode is enabled.
+ // Toggle the full screen browsing mode tracker.
+ inFullScreenBrowsingMode = !inFullScreenBrowsingMode;
- // Store the domain name in `domainSettingsSet`.
- domainSettingsSet.add(domainNameCursor.getString(domainNameColumnIndex));
- }
+ // Toggle the full screen browsing mode.
+ if (inFullScreenBrowsingMode) { // Switch to full screen mode.
+ // Hide the app bar if specified.
+ if (hideAppBar) {
+ actionBar.hide();
+ }
- // Close `domainNameCursor.
- domainNameCursor.close();
+ // Hide the banner ad in the free flavor.
+ if (BuildConfig.FLAVOR.contentEquals("free")) {
+ AdHelper.hideAd(findViewById(R.id.adview));
+ }
- // Initialize variables to track if domain settings will be applied and, if so, under which name.
- domainSettingsApplied = false;
- String domainNameInDatabase = null;
+ // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
- // Check the hostname.
- if (domainSettingsSet.contains(hostName)) {
- domainSettingsApplied = true;
- domainNameInDatabase = hostName;
- }
+ /* Hide the system bars.
+ * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
+ * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
+ * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
+ * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
+ */
+ rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
+ View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
+ } else { // Switch to normal viewing mode.
+ // Show the app bar.
+ actionBar.show();
- // Check all the subdomains of the host name against wildcard domains in the domain cursor.
- while (!domainSettingsApplied && hostName.contains(".")) { // Stop checking if domain settings are already applied or there are no more `.` in the host name.
- if (domainSettingsSet.contains("*." + hostName)) { // Check the host name prepended by `*.`.
- // Apply the domain settings.
- domainSettingsApplied = true;
+ // Show the banner ad in the free flavor.
+ if (BuildConfig.FLAVOR.contentEquals("free")) {
+ // Reload the ad.
+ AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
+ }
- // Store the applied domain names as it appears in the database.
- domainNameInDatabase = "*." + hostName;
- }
+ // Remove the `SYSTEM_UI` flags from the root frame layout.
+ rootFrameLayout.setSystemUiVisibility(0);
- // Strip out the lowest subdomain of of the host name.
- hostName = hostName.substring(hostName.indexOf(".") + 1);
+ // Add the translucent status flag.
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+ }
+
+ // Consume the double-tap.
+ return true;
+ } else { // Do not consume the double-tap because full screen browsing mode is disabled.
+ return false;
+ }
}
+ });
+ // Pass all touch events on the WebView through the double-tap gesture detector.
+ nestedScrollWebView.setOnTouchListener((View view, MotionEvent event) -> {
+ // Call `performClick()` on the view, which is required for accessibility.
+ view.performClick();
- // Get a handle for the shared preference.
- SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+ // Send the event to the gesture detector.
+ return doubleTapGestureDetector.onTouchEvent(event);
+ });
- // Store the general preference information.
- String defaultFontSizeString = sharedPreferences.getString("font_size", getString(R.string.font_size_default_value));
- String defaultUserAgentName = sharedPreferences.getString("user_agent", getString(R.string.user_agent_default_value));
- defaultCustomUserAgentString = sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value));
- boolean defaultSwipeToRefresh = sharedPreferences.getBoolean("swipe_to_refresh", true);
- nightMode = sharedPreferences.getBoolean("night_mode", false);
- boolean displayWebpageImages = sharedPreferences.getBoolean("display_webpage_images", true);
+ // Register the WebView for a context menu. This is used to see link targets and download images.
+ registerForContextMenu(nestedScrollWebView);
- if (domainSettingsApplied) { // The url has custom domain settings.
- // Get a cursor for the current host and move it to the first position.
- Cursor currentHostDomainSettingsCursor = domainsDatabaseHelper.getCursorForDomainName(domainNameInDatabase);
- currentHostDomainSettingsCursor.moveToFirst();
+ // Allow the downloading of files.
+ nestedScrollWebView.setDownloadListener((String url, String userAgent, String contentDisposition, String mimetype, long contentLength) -> {
+ // Check if the download should be processed by an external app.
+ if (downloadWithExternalApp) { // Download with an external app.
+ // Create a download intent. Not specifying the action type will display the maximum number of options.
+ Intent downloadIntent = new Intent();
- // Get the settings from the cursor.
- domainSettingsDatabaseId = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper._ID)));
- javaScriptEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)) == 1);
- firstPartyCookiesEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FIRST_PARTY_COOKIES)) == 1);
- thirdPartyCookiesEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_THIRD_PARTY_COOKIES)) == 1);
- domStorageEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_DOM_STORAGE)) == 1);
- // Form data can be removed once the minimum API >= 26.
- saveFormDataEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FORM_DATA)) == 1);
- easyListEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYLIST)) == 1);
- easyPrivacyEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYPRIVACY)) == 1);
- fanboysAnnoyanceListEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST)) == 1);
- fanboysSocialBlockingListEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)) == 1);
- ultraPrivacyEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY)) == 1);
- blockAllThirdPartyRequests = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS)) == 1);
- String userAgentName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.USER_AGENT));
- int fontSize = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.FONT_SIZE));
- int swipeToRefreshInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SWIPE_TO_REFRESH));
- int nightModeInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.NIGHT_MODE));
- int displayWebpageImagesInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.DISPLAY_IMAGES));
- pinnedSslCertificate = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)) == 1);
- pinnedSslIssuedToCName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME));
- pinnedSslIssuedToOName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION));
- pinnedSslIssuedToUName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT));
- pinnedSslIssuedByCName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME));
- pinnedSslIssuedByOName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION));
- pinnedSslIssuedByUName = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT));
- pinnedIpAddresses = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_IP_ADDRESSES)) == 1);
- pinnedHostIpAddresses = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.IP_ADDRESSES));
+ // Set the URI and the MIME type. Specifying `text/html` displays a good number of options.
+ downloadIntent.setDataAndType(Uri.parse(url), "text/html");
- // 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;
+ // Flag the intent to open in a new task.
+ downloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- case DomainsDatabaseHelper.NIGHT_MODE_DISABLED:
- nightMode = false;
- break;
- }
+ // Show the chooser.
+ startActivity(Intent.createChooser(downloadIntent, getString(R.string.open_with)));
+ } else { // Download with Android's download manager.
+ // Check to see if the WRITE_EXTERNAL_STORAGE permission has already been granted.
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission has not been granted.
+ // The WRITE_EXTERNAL_STORAGE permission needs to be requested.
- // Store the domain JavaScript status. This is used by the options menu night mode toggle.
- domainSettingsJavaScriptEnabled = javaScriptEnabled;
+ // Store the variables for future use by `onRequestPermissionsResult()`.
+ downloadUrl = url;
+ downloadContentDisposition = contentDisposition;
+ downloadContentLength = contentLength;
- // Enable JavaScript if night mode is enabled.
- if (nightMode) {
- javaScriptEnabled = true;
+ // Show a dialog if the user has previously denied the permission.
+ if (ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
+ // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
+ DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE);
+
+ // Show the download location permission alert dialog. The permission will be requested when the the dialog is closed.
+ downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
+ } else { // Show the permission request directly.
+ // Request the permission. The download dialog will be launched by `onRequestPermissionResult()`.
+ ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
+ }
+ } else { // The storage permission has already been granted.
+ // Get a handle for the download file alert dialog.
+ DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(url, contentDisposition, contentLength);
+
+ // Show the download file alert dialog.
+ downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
}
+ }
+ });
- // Set the pinned SSL certificate start date to `null` if the saved date `long` is 0.
- if (currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)) == 0) {
- pinnedSslStartDate = null;
- } else {
- pinnedSslStartDate = new Date(currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)));
+ // Update the find on page count.
+ nestedScrollWebView.setFindListener(new WebView.FindListener() {
+ // Get a handle for `findOnPageCountTextView`.
+ final TextView findOnPageCountTextView = findViewById(R.id.find_on_page_count_textview);
+
+ @Override
+ public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) {
+ if ((isDoneCounting) && (numberOfMatches == 0)) { // There are no matches.
+ // Set `findOnPageCountTextView` to `0/0`.
+ findOnPageCountTextView.setText(R.string.zero_of_zero);
+ } else if (isDoneCounting) { // There are matches.
+ // `activeMatchOrdinal` is zero-based.
+ int activeMatch = activeMatchOrdinal + 1;
+
+ // Build the match string.
+ String matchString = activeMatch + "/" + numberOfMatches;
+
+ // Set `findOnPageCountTextView`.
+ findOnPageCountTextView.setText(matchString);
}
+ }
+ });
+
+ // Set the web chrome client.
+ nestedScrollWebView.setWebChromeClient(new WebChromeClient() {
+ // Update the progress bar when a page is loading.
+ @Override
+ public void onProgressChanged(WebView view, int progress) {
+ // Inject the night mode CSS if night mode is enabled.
+ if (nightMode) {
+ // `background-color: #212121` sets the background to be dark gray. `color: #BDBDBD` sets the text color to be light gray. `box-shadow: none` removes a lower underline on links
+ // used by WordPress. `text-decoration: none` removes all text underlines. `text-shadow: none` removes text shadows, which usually have a hard coded color.
+ // `border: none` removes all borders, which can also be used to underline text. `a {color: #1565C0}` sets links to be a dark blue.
+ // `::selection {background: #0D47A1}' sets the text selection highlight color to be a dark blue. `!important` takes precedent over any existing sub-settings.
+ nestedScrollWebView.evaluateJavascript("(function() {var parent = document.getElementsByTagName('head').item(0); var style = document.createElement('style'); style.type = 'text/css'; " +
+ "style.innerHTML = '* {background-color: #212121 !important; color: #BDBDBD !important; box-shadow: none !important; text-decoration: none !important;" +
+ "text-shadow: none !important; border: none !important;} a {color: #1565C0 !important;} ::selection {background: #0D47A1 !important;}'; parent.appendChild(style)})()", value -> {
+ // Initialize a handler to display `mainWebView`.
+ Handler displayWebViewHandler = new Handler();
+
+ // Setup a runnable to display `mainWebView` after a delay to allow the CSS to be applied.
+ Runnable displayWebViewRunnable = () -> {
+ // Only display `mainWebView` if the progress bar is gone. This prevents the display of the `WebView` while it is still loading.
+ if (progressBar.getVisibility() == View.GONE) {
+ nestedScrollWebView.setVisibility(View.VISIBLE);
+ }
+ };
- // Set the pinned SSL certificate end date to `null` if the saved date `long` is 0.
- if (currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)) == 0) {
- pinnedSslEndDate = null;
- } else {
- pinnedSslEndDate = new Date(currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)));
+ // Displaying of `mainWebView` after 500 milliseconds.
+ displayWebViewHandler.postDelayed(displayWebViewRunnable, 500);
+ });
}
- // Close `currentHostDomainSettingsCursor`.
- currentHostDomainSettingsCursor.close();
+ // Update the progress bar.
+ progressBar.setProgress(progress);
- // Apply the domain settings.
- mainWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled);
- cookieManager.setAcceptCookie(firstPartyCookiesEnabled);
- mainWebView.getSettings().setDomStorageEnabled(domStorageEnabled);
+ // Set the visibility of the progress bar.
+ if (progress < 100) {
+ // Show the progress bar.
+ progressBar.setVisibility(View.VISIBLE);
+ } else {
+ // Hide the progress bar.
+ progressBar.setVisibility(View.GONE);
- // Apply the form data setting if the API < 26.
- if (Build.VERSION.SDK_INT < 26) {
- mainWebView.getSettings().setSaveFormData(saveFormDataEnabled);
- }
+ // 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) {
+ nestedScrollWebView.setVisibility(View.VISIBLE);
+ }
- // 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);
+ //Stop the swipe to refresh indicator if it is running
+ swipeRefreshLayout.setRefreshing(false);
}
+ }
- // Set third-party cookies status if API >= 21.
- if (Build.VERSION.SDK_INT >= 21) {
- cookieManager.setAcceptThirdPartyCookies(mainWebView, thirdPartyCookiesEnabled);
+ // Set the favorite icon when it changes.
+ @Override
+ public void onReceivedIcon(WebView view, Bitmap icon) {
+ // Only update the favorite icon if the website has finished loading.
+ if (progressBar.getVisibility() == View.GONE) {
+ // Save a copy of the favorite icon.
+ // TODO. We need to save and access the icons for each tab.
+ favoriteIconBitmap = icon;
+
+ tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(icon, 64, 64, true));
}
+ }
- // 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.
- // <https://redmine.stoutner.com/issues/160>
- if (!urlIsLoading) {
- // Set the user agent.
- if (userAgentName.equals(getString(R.string.system_default_user_agent))) { // Use the system default user agent.
- // Get the array position of the default user agent name.
- int defaultUserAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
+ // Save a copy of the title when it changes.
+ @Override
+ public void onReceivedTitle(WebView view, String title) {
+ // Save a copy of the title.
+ // TODO. Replace `webViewTitle` with `currentWebView.getTitle()`.
+ webViewTitle = title;
- // Set the user agent according to the system default.
- switch (defaultUserAgentArrayPosition) {
- case UNRECOGNIZED_USER_AGENT: // The default user agent name is not on the canonical list.
- // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
- mainWebView.getSettings().setUserAgentString(defaultUserAgentName);
- break;
+ // Set the title as the tab text.
+ tabTitleTextView.setText(webViewTitle);
+ }
- case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
- // Set the user agent to `""`, which uses the default value.
- mainWebView.getSettings().setUserAgentString("");
- break;
+ // Enter full screen video.
+ @Override
+ public void onShowCustomView(View video, CustomViewCallback callback) {
+ // Set the full screen video flag.
+ displayingFullScreenVideo = true;
- case SETTINGS_CUSTOM_USER_AGENT:
- // Set the custom user agent.
- mainWebView.getSettings().setUserAgentString(defaultCustomUserAgentString);
- break;
+ // Pause the ad if this is the free flavor.
+ if (BuildConfig.FLAVOR.contentEquals("free")) {
+ // The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
+ AdHelper.pauseAd(findViewById(R.id.adview));
+ }
- default:
- // Get the user agent string from the user agent data array
- mainWebView.getSettings().setUserAgentString(userAgentDataArray[defaultUserAgentArrayPosition]);
- }
- } else { // Set the user agent according to the stored name.
- // Get the array position of the user agent name.
- int userAgentArrayPosition = userAgentNamesArray.getPosition(userAgentName);
+ // Hide the keyboard.
+ inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0);
- switch (userAgentArrayPosition) {
- case UNRECOGNIZED_USER_AGENT: // The user agent name contains a custom user agent.
- mainWebView.getSettings().setUserAgentString(userAgentName);
- break;
+ // Hide the main content relative layout.
+ mainContentRelativeLayout.setVisibility(View.GONE);
- case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
- // Set the user agent to `""`, which uses the default value.
- mainWebView.getSettings().setUserAgentString("");
- break;
+ // Remove the translucent status bar overlay on the `Drawer Layout`, which is special and needs its own command.
+ drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
- default:
- // Get the user agent string from the user agent data array.
- mainWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
- }
- }
+ // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
- // Store the applied user agent string, which is used in the View Source activity.
- appliedUserAgentString = mainWebView.getSettings().getUserAgentString();
+ /* Hide the system bars.
+ * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
+ * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
+ * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
+ * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
+ */
+ rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
+ View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
- // Update the user agent change tracker.
- userAgentChanged = !appliedUserAgentString.equals(initialUserAgent);
- }
+ // Disable the sliding drawers.
+ drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
- // Set swipe to refresh.
- switch (swipeToRefreshInt) {
- case DomainsDatabaseHelper.SWIPE_TO_REFRESH_SYSTEM_DEFAULT:
- // Set swipe to refresh according to the default.
- swipeRefreshLayout.setEnabled(defaultSwipeToRefresh);
- break;
+ // Add the video view to the full screen video frame layout.
+ fullScreenVideoFrameLayout.addView(video);
- case DomainsDatabaseHelper.SWIPE_TO_REFRESH_ENABLED:
- // Enable swipe to refresh.
- swipeRefreshLayout.setEnabled(true);
- break;
+ // Show the full screen video frame layout.
+ fullScreenVideoFrameLayout.setVisibility(View.VISIBLE);
+ }
- case DomainsDatabaseHelper.SWIPE_TO_REFRESH_DISABLED:
- // Disable swipe to refresh.
- swipeRefreshLayout.setEnabled(false);
- }
+ // Exit full screen video.
+ @Override
+ public void onHideCustomView() {
+ // Unset the full screen video flag.
+ displayingFullScreenVideo = false;
- // Set the loading of webpage images.
- switch (displayWebpageImagesInt) {
- case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_SYSTEM_DEFAULT:
- mainWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
- break;
+ // Remove all the views from the full screen video frame layout.
+ fullScreenVideoFrameLayout.removeAllViews();
- case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_ENABLED:
- mainWebView.getSettings().setLoadsImagesAutomatically(true);
- break;
+ // Hide the full screen video frame layout.
+ fullScreenVideoFrameLayout.setVisibility(View.GONE);
- case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_DISABLED:
- mainWebView.getSettings().setLoadsImagesAutomatically(false);
- break;
- }
+ // Enable the sliding drawers.
+ drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
- // Set a green background on `urlTextBox` to indicate that custom domain settings are being used. We have to use the deprecated `.getDrawable()` until the minimum API >= 21.
- if (darkTheme) {
- urlAppBarRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_dark_blue));
- } else {
- urlAppBarRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_light_green));
- }
- } else { // The new URL does not have custom domain settings. Load the defaults.
- // Store the values from `sharedPreferences` in variables.
- javaScriptEnabled = sharedPreferences.getBoolean("javascript", false);
- firstPartyCookiesEnabled = sharedPreferences.getBoolean("first_party_cookies", false);
- thirdPartyCookiesEnabled = sharedPreferences.getBoolean("third_party_cookies", false);
- domStorageEnabled = sharedPreferences.getBoolean("dom_storage", false);
- saveFormDataEnabled = sharedPreferences.getBoolean("save_form_data", false); // Form data can be removed once the minimum API >= 26.
- easyListEnabled = sharedPreferences.getBoolean("easylist", true);
- easyPrivacyEnabled = sharedPreferences.getBoolean("easyprivacy", true);
- fanboysAnnoyanceListEnabled = sharedPreferences.getBoolean("fanboys_annoyance_list", true);
- fanboysSocialBlockingListEnabled = sharedPreferences.getBoolean("fanboys_social_blocking_list", true);
- ultraPrivacyEnabled = sharedPreferences.getBoolean("ultraprivacy", true);
- blockAllThirdPartyRequests = sharedPreferences.getBoolean("block_all_third_party_requests", false);
+ // Show the main content relative layout.
+ mainContentRelativeLayout.setVisibility(View.VISIBLE);
- // Set `javaScriptEnabled` to be `true` if `night_mode` is `true`.
- if (nightMode) {
- javaScriptEnabled = true;
- }
+ // Apply the appropriate full screen mode the `SYSTEM_UI` flags.
+ if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode.
+ // Hide the app bar if specified.
+ if (hideAppBar) {
+ actionBar.hide();
+ }
- // Apply the default settings.
- mainWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled);
- cookieManager.setAcceptCookie(firstPartyCookiesEnabled);
- mainWebView.getSettings().setDomStorageEnabled(domStorageEnabled);
- mainWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
- swipeRefreshLayout.setEnabled(defaultSwipeToRefresh);
+ // Hide the banner ad in the free flavor.
+ if (BuildConfig.FLAVOR.contentEquals("free")) {
+ AdHelper.hideAd(findViewById(R.id.adview));
+ }
- // Apply the form data setting if the API < 26.
- if (Build.VERSION.SDK_INT < 26) {
- mainWebView.getSettings().setSaveFormData(saveFormDataEnabled);
- }
+ // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
- // Reset the pinned variables.
- domainSettingsDatabaseId = -1;
- pinnedSslCertificate = false;
- pinnedSslIssuedToCName = "";
- pinnedSslIssuedToOName = "";
- pinnedSslIssuedToUName = "";
- pinnedSslIssuedByCName = "";
- pinnedSslIssuedByOName = "";
- pinnedSslIssuedByUName = "";
- pinnedSslStartDate = null;
- pinnedSslEndDate = null;
- pinnedIpAddresses = false;
- pinnedHostIpAddresses = "";
+ /* Hide the system bars.
+ * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
+ * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
+ * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
+ * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
+ */
+ rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
+ View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
+ } else { // Switch to normal viewing mode.
+ // Remove the `SYSTEM_UI` flags from the root frame layout.
+ rootFrameLayout.setSystemUiVisibility(0);
- // Set third-party cookies status if API >= 21.
- if (Build.VERSION.SDK_INT >= 21) {
- cookieManager.setAcceptThirdPartyCookies(mainWebView, thirdPartyCookiesEnabled);
+ // Add the translucent status flag.
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
}
- // 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.
- // <https://redmine.stoutner.com/issues/160>
- if (!urlIsLoading) {
- // Get the array position of the user agent name.
- int userAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
-
- // Set the user agent.
- switch (userAgentArrayPosition) {
- case UNRECOGNIZED_USER_AGENT: // The default user agent name is not on the canonical list.
- // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
- mainWebView.getSettings().setUserAgentString(defaultUserAgentName);
- break;
-
- case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
- // Set the user agent to `""`, which uses the default value.
- mainWebView.getSettings().setUserAgentString("");
- break;
-
- case SETTINGS_CUSTOM_USER_AGENT:
- // Set the custom user agent.
- mainWebView.getSettings().setUserAgentString(defaultCustomUserAgentString);
- break;
+ // Reload the ad for the free flavor if not in full screen mode.
+ if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
+ // Reload the ad.
+ AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
+ }
+ }
- default:
- // Get the user agent string from the user agent data array
- mainWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
- }
+ // Upload files.
+ @Override
+ public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
+ // Show the file chooser if the device is running API >= 21.
+ if (Build.VERSION.SDK_INT >= 21) {
+ // Store the file path callback.
+ fileChooserCallback = filePathCallback;
- // Store the applied user agent string, which is used in the View Source activity.
- appliedUserAgentString = mainWebView.getSettings().getUserAgentString();
+ // Create an intent to open a chooser based ont the file chooser parameters.
+ Intent fileChooserIntent = fileChooserParams.createIntent();
- // Update the user agent change tracker.
- userAgentChanged = !appliedUserAgentString.equals(initialUserAgent);
+ // Open the file chooser. Currently only one `startActivityForResult` exists in this activity, so the request code, used to differentiate them, is simply `0`.
+ startActivityForResult(fileChooserIntent, 0);
}
-
- // Set the loading of webpage images.
- mainWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
-
- // Set a transparent background on `urlTextBox`. The deprecated `.getDrawable()` must be used until the minimum API >= 21.
- urlAppBarRelativeLayout.setBackgroundDrawable(getResources().getDrawable(R.color.transparent));
+ return true;
}
+ });
- // Close the domains database helper.
- domainsDatabaseHelper.close();
+ nestedScrollWebView.setWebViewClient(new WebViewClient() {
+ // `shouldOverrideUrlLoading` makes this `WebView` the default handler for URLs inside the app, so that links are not kicked out to other apps.
+ // The deprecated `shouldOverrideUrlLoading` must be used until API >= 24.
+ @SuppressWarnings("deprecation")
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView view, String url) {
+ if (url.startsWith("http")) { // Load the URL in Privacy Browser.
+ // Reset the formatted URL string so the page will load correctly if blocking of third-party requests is enabled.
+ formattedUrlString = "";
- // Update the privacy icons, but only if `mainMenu` has already been populated.
- if (mainMenu != null) {
- updatePrivacyIcons(true);
- }
- }
+ // Apply the domain settings for the new URL. `applyDomainSettings` doesn't do anything if the domain has not changed.
+ boolean userAgentChanged = applyDomainSettings(url, true, false);
- // Reload the website if returning from the Domains activity.
- if (reloadWebsite) {
- mainWebView.reload();
- }
+ // Check if the user agent has changed.
+ if (userAgentChanged) {
+ // Manually load the URL. The changing of the user agent will cause WebView to reload the previous URL.
+ nestedScrollWebView.loadUrl(url, customHeaders);
- // Return the user agent changed status.
- return userAgentChanged;
- }
+ // Returning true indicates that Privacy Browser is manually handling the loading of the URL.
+ return true;
+ } else {
+ // Returning false causes the current WebView to handle the URL and prevents it from adding redirects to the history list.
+ return false;
+ }
+ } else if (url.startsWith("mailto:")) { // Load the email address in an external email program.
+ // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
+ Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
- private void applyProxyThroughOrbot(boolean reloadWebsite) {
- // Get a handle for the shared preferences.
- SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+ // Parse the url and set it as the data for the intent.
+ emailIntent.setData(Uri.parse(url));
- // Get the search preferences.
- String homepageString = sharedPreferences.getString("homepage", getString(R.string.homepage_default_value));
- String torHomepageString = sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value));
- String torSearchString = sharedPreferences.getString("tor_search", getString(R.string.tor_search_default_value));
- String torSearchCustomUrlString = sharedPreferences.getString("tor_search_custom_url", getString(R.string.tor_search_custom_url_default_value));
- String searchString = sharedPreferences.getString("search", getString(R.string.search_default_value));
- String searchCustomUrlString = sharedPreferences.getString("search_custom_url", getString(R.string.search_custom_url_default_value));
+ // Open the email program in a new task instead of as part of Privacy Browser.
+ emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- // Get a handle for the action bar. `getSupportActionBar()` must be used until the minimum API >= 21.
- ActionBar actionBar = getSupportActionBar();
+ // Make it so.
+ startActivity(emailIntent);
- // Remove the incorrect lint warning later that the action bar might be null.
- assert actionBar != null;
+ // Returning true indicates Privacy Browser is handling the URL by creating an intent.
+ return true;
+ } else if (url.startsWith("tel:")) { // Load the phone number in the dialer.
+ // Open the dialer and load the phone number, but wait for the user to place the call.
+ Intent dialIntent = new Intent(Intent.ACTION_DIAL);
- // Set the homepage, search, and proxy options.
- if (proxyThroughOrbot) { // Set the Tor options.
- // Set `torHomepageString` as `homepage`.
- homepage = torHomepageString;
+ // Add the phone number to the intent.
+ dialIntent.setData(Uri.parse(url));
- // If formattedUrlString is null assign the homepage to it.
- if (formattedUrlString == null) {
- formattedUrlString = homepage;
- }
+ // Open the dialer in a new task instead of as part of Privacy Browser.
+ dialIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- // Set the search URL.
- if (torSearchString.equals("Custom URL")) { // Get the custom URL string.
- searchURL = torSearchCustomUrlString;
- } else { // Use the string from the pre-built list.
- searchURL = torSearchString;
- }
+ // Make it so.
+ startActivity(dialIntent);
- // Set the proxy. `this` refers to the current activity where an `AlertDialog` might be displayed.
- OrbotProxyHelper.setProxy(getApplicationContext(), this, "localhost", "8118");
+ // Returning true indicates Privacy Browser is handling the URL by creating an intent.
+ return true;
+ } else { // Load a system chooser to select an app that can handle the URL.
+ // Open an app that can handle the URL.
+ Intent genericIntent = new Intent(Intent.ACTION_VIEW);
- // Set the `appBar` background to indicate proxying through Orbot is enabled. `this` refers to the context.
- if (darkTheme) {
- actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.dark_blue_30));
- } else {
- actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.blue_50));
- }
+ // Add the URL to the intent.
+ genericIntent.setData(Uri.parse(url));
- // Check to see if Orbot is ready.
- if (!orbotStatus.equals("ON")) { // Orbot is not ready.
- // Set `waitingForOrbot`.
- waitingForOrbot = true;
+ // List all apps that can handle the URL instead of just opening the first one.
+ genericIntent.addCategory(Intent.CATEGORY_BROWSABLE);
- // Disable the wide view port so that the waiting for Orbot text is displayed correctly.
- mainWebView.getSettings().setUseWideViewPort(false);
+ // Open the app in a new task instead of as part of Privacy Browser.
+ genericIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- // Load a waiting page. `null` specifies no encoding, which defaults to ASCII.
- mainWebView.loadData(waitingForOrbotHtmlString, "text/html", null);
- } else if (reloadWebsite) { // Orbot is ready and the website should be reloaded.
- // Reload the website.
- mainWebView.reload();
- }
- } else { // Set the non-Tor options.
- // Set `homepageString` as `homepage`.
- homepage = homepageString;
+ // Start the app or display a snackbar if no app is available to handle the URL.
+ try {
+ startActivity(genericIntent);
+ } catch (ActivityNotFoundException exception) {
+ Snackbar.make(nestedScrollWebView, getString(R.string.unrecognized_url) + " " + url, Snackbar.LENGTH_SHORT).show();
+ }
- // If formattedUrlString is null assign the homepage to it.
- if (formattedUrlString == null) {
- formattedUrlString = homepage;
+ // Returning true indicates Privacy Browser is handling the URL by creating an intent.
+ return true;
+ }
}
- // Set the search URL.
- if (searchString.equals("Custom URL")) { // Get the custom URL string.
- searchURL = searchCustomUrlString;
- } else { // Use the string from the pre-built list.
- searchURL = searchString;
- }
+ // 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()));
- // Reset the proxy to default. The host is `""` and the port is `"0"`.
- OrbotProxyHelper.setProxy(getApplicationContext(), this, "", "0");
+ // Reset the whitelist results tracker.
+ whiteListResultStringArray = null;
- // Set the default `appBar` background. `this` refers to the context.
- if (darkTheme) {
- actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.gray_900));
- } else {
- actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.gray_100));
- }
+ // Initialize the third party request tracker.
+ boolean isThirdPartyRequest = false;
- // Reset `waitingForOrbot.
- waitingForOrbot = false;
+ // Initialize the current domain string.
+ String currentDomain = "";
- // Reload the website if requested.
- if (reloadWebsite) {
- mainWebView.reload();
- }
- }
- }
+ // Nobody is happy when comparing null strings.
+ if (!(formattedUrlString == null) && !(url == null)) {
+ // Get the domain strings to URIs.
+ Uri currentDomainUri = Uri.parse(formattedUrlString);
+ Uri requestDomainUri = Uri.parse(url);
- private void updatePrivacyIcons(boolean runInvalidateOptionsMenu) {
- // Get handles for the menu items.
- MenuItem privacyMenuItem = mainMenu.findItem(R.id.toggle_javascript);
- MenuItem firstPartyCookiesMenuItem = mainMenu.findItem(R.id.toggle_first_party_cookies);
- MenuItem domStorageMenuItem = mainMenu.findItem(R.id.toggle_dom_storage);
- MenuItem refreshMenuItem = mainMenu.findItem(R.id.refresh);
+ // Get the domain host names.
+ String currentBaseDomain = currentDomainUri.getHost();
+ String requestBaseDomain = requestDomainUri.getHost();
- // Update the privacy icon.
- if (javaScriptEnabled) { // JavaScript is enabled.
- privacyMenuItem.setIcon(R.drawable.javascript_enabled);
- } else if (firstPartyCookiesEnabled) { // JavaScript is disabled but cookies are enabled.
- privacyMenuItem.setIcon(R.drawable.warning);
- } else { // All the dangerous features are disabled.
- privacyMenuItem.setIcon(R.drawable.privacy_mode);
- }
+ // Update the current domain variable.
+ currentDomain = currentBaseDomain;
- // Update the first-party cookies icon.
- if (firstPartyCookiesEnabled) { // First-party cookies are enabled.
- firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_enabled);
- } else { // First-party cookies are disabled.
- if (darkTheme) {
- firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_dark);
- } else {
- firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_light);
- }
- }
+ // Only compare the current base domain and the request base domain if neither is null.
+ if (!(currentBaseDomain == null) && !(requestBaseDomain == null)) {
+ // Determine the current base domain.
+ while (currentBaseDomain.indexOf(".", currentBaseDomain.indexOf(".") + 1) > 0) { // There is at least one subdomain.
+ // Remove the first subdomain.
+ currentBaseDomain = currentBaseDomain.substring(currentBaseDomain.indexOf(".") + 1);
+ }
- // Update the DOM storage icon.
- if (javaScriptEnabled && domStorageEnabled) { // Both JavaScript and DOM storage are enabled.
- domStorageMenuItem.setIcon(R.drawable.dom_storage_enabled);
- } else if (javaScriptEnabled) { // JavaScript is enabled but DOM storage is disabled.
- if (darkTheme) {
- domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_dark);
- } else {
- domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_light);
- }
- } else { // JavaScript is disabled, so DOM storage is ghosted.
- if (darkTheme) {
- domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_dark);
- } else {
- domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_light);
- }
- }
+ // Determine the request base domain.
+ while (requestBaseDomain.indexOf(".", requestBaseDomain.indexOf(".") + 1) > 0) { // There is at least one subdomain.
+ // Remove the first subdomain.
+ requestBaseDomain = requestBaseDomain.substring(requestBaseDomain.indexOf(".") + 1);
+ }
- // Update the refresh icon.
- if (darkTheme) {
- refreshMenuItem.setIcon(R.drawable.refresh_enabled_dark);
- } else {
- refreshMenuItem.setIcon(R.drawable.refresh_enabled_light);
- }
+ // Update the third party request tracker.
+ isThirdPartyRequest = !currentBaseDomain.equals(requestBaseDomain);
+ }
+ }
- // `invalidateOptionsMenu` calls `onPrepareOptionsMenu()` and redraws the icons in the `AppBar`.
- if (runInvalidateOptionsMenu) {
- invalidateOptionsMenu();
- }
- }
+ // Block third-party requests if enabled.
+ if (isThirdPartyRequest && blockAllThirdPartyRequests) {
+ // Increment the blocked requests counters.
+ blockedRequests++;
+ thirdPartyBlockedRequests++;
- private void openUrlWithExternalApp(String url) {
- // Create a download intent. Not specifying the action type will display the maximum number of options.
- Intent downloadIntent = new Intent();
+ // Update the titles of the blocklist menu items. This must be run from the UI thread.
+ activity.runOnUiThread(() -> {
+ navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
+ blocklistsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
+ blockAllThirdPartyRequestsMenuItem.setTitle(thirdPartyBlockedRequests + " - " + getString(R.string.block_all_third_party_requests));
+ });
- // Set the URI and the MIME type. Specifying `text/html` displays a good number of options.
- downloadIntent.setDataAndType(Uri.parse(url), "text/html");
+ // Add the request to the log.
+ resourceRequests.add(new String[]{String.valueOf(REQUEST_THIRD_PARTY), url});
- // Flag the intent to open in a new task.
- downloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ // Return an empty web resource response.
+ return emptyWebResourceResponse;
+ }
- // Show the chooser.
- startActivity(Intent.createChooser(downloadIntent, getString(R.string.open_with)));
- }
+ // Check UltraPrivacy if it is enabled.
+ if (ultraPrivacyEnabled) {
+ if (blockListHelper.isBlocked(currentDomain, url, isThirdPartyRequest, ultraPrivacy)) {
+ // Increment the blocked requests counters.
+ blockedRequests++;
+ ultraPrivacyBlockedRequests++;
- private void highlightUrlText() {
- // Only highlight the URL text if the box is not currently selected.
- if (!urlTextBox.hasFocus()) {
- // Get the URL string.
- String urlString = urlTextBox.getText().toString();
+ // Update the titles of the blocklist menu items. This must be run from the UI thread.
+ activity.runOnUiThread(() -> {
+ navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
+ blocklistsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
+ ultraPrivacyMenuItem.setTitle(ultraPrivacyBlockedRequests + " - " + getString(R.string.ultraprivacy));
+ });
- // Highlight the URL according to the protocol.
- if (urlString.startsWith("file://")) { // This is a file URL.
- // De-emphasize only the protocol.
- urlTextBox.getText().setSpan(initialGrayColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
- } else if (urlString.startsWith("content://")) {
- // De-emphasize only the protocol.
- urlTextBox.getText().setSpan(initialGrayColorSpan, 0, 10, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
- } else { // This is a web URL.
- // Get the index of the `/` immediately after the domain name.
- int endOfDomainName = urlString.indexOf("/", (urlString.indexOf("//") + 2));
+ // The resource request was blocked. Return an empty web resource response.
+ return emptyWebResourceResponse;
+ }
- // Create a base URL string.
- String baseUrl;
+ // If the whitelist result is not null, the request has been allowed by UltraPrivacy.
+ if (whiteListResultStringArray != null) {
+ // Add a whitelist entry to the resource requests array.
+ resourceRequests.add(whiteListResultStringArray);
- // Get the base URL.
- if (endOfDomainName > 0) { // There is at least one character after the base URL.
- // Get the base URL.
- baseUrl = urlString.substring(0, endOfDomainName);
- } else { // There are no characters after the base URL.
- // Set the base URL to be the entire URL string.
- baseUrl = urlString;
+ // The resource request has been allowed by UltraPrivacy. `return null` loads the requested resource.
+ return null;
+ }
}
- // Get the index of the last `.` in the domain.
- int lastDotIndex = baseUrl.lastIndexOf(".");
+ // Check EasyList if it is enabled.
+ if (easyListEnabled) {
+ if (blockListHelper.isBlocked(currentDomain, url, isThirdPartyRequest, easyList)) {
+ // Increment the blocked requests counters.
+ blockedRequests++;
+ easyListBlockedRequests++;
- // Get the index of the penultimate `.` in the domain.
- int penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1);
+ // Update the titles of the blocklist menu items. This must be run from the UI thread.
+ activity.runOnUiThread(() -> {
+ navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
+ blocklistsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
+ easyListMenuItem.setTitle(easyListBlockedRequests + " - " + getString(R.string.easylist));
+ });
- // Markup the beginning of the URL.
- if (urlString.startsWith("http://")) { // Highlight the protocol of connections that are not encrypted.
- urlTextBox.getText().setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+ // Reset the whitelist results tracker (because otherwise it will sometimes add results to the list due to a race condition).
+ whiteListResultStringArray = null;
- // De-emphasize subdomains.
- if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name.
- urlTextBox.getText().setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
- }
- } else if (urlString.startsWith("https://")) { // De-emphasize the protocol of connections that are encrypted.
- if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name.
- // De-emphasize the protocol and the additional subdomains.
- urlTextBox.getText().setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
- } else { // There is only one subdomain in the domain name.
- // De-emphasize only the protocol.
- urlTextBox.getText().setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+ // The resource request was blocked. Return an empty web resource response.
+ return emptyWebResourceResponse;
}
}
- // De-emphasize the text after the domain name.
- if (endOfDomainName > 0) {
- urlTextBox.getText().setSpan(finalGrayColorSpan, endOfDomainName, urlString.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+ // Check EasyPrivacy if it is enabled.
+ if (easyPrivacyEnabled) {
+ if (blockListHelper.isBlocked(currentDomain, url, isThirdPartyRequest, easyPrivacy)) {
+ // Increment the blocked requests counters.
+ blockedRequests++;
+ easyPrivacyBlockedRequests++;
+
+ // Update the titles of the blocklist menu items. This must be run from the UI thread.
+ activity.runOnUiThread(() -> {
+ navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
+ blocklistsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
+ easyPrivacyMenuItem.setTitle(easyPrivacyBlockedRequests + " - " + getString(R.string.easyprivacy));
+ });
+
+ // Reset the whitelist results tracker (because otherwise it will sometimes add results to the list due to a race condition).
+ whiteListResultStringArray = null;
+
+ // The resource request was blocked. Return an empty web resource response.
+ return emptyWebResourceResponse;
+ }
}
- }
- }
- }
- private void loadBookmarksFolder() {
- // Update the bookmarks cursor with the contents of the bookmarks database for the current folder.
- bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
+ // Check Fanboy’s Annoyance List if it is enabled.
+ if (fanboysAnnoyanceListEnabled) {
+ if (blockListHelper.isBlocked(currentDomain, url, isThirdPartyRequest, fanboysAnnoyanceList)) {
+ // Increment the blocked requests counters.
+ blockedRequests++;
+ fanboysAnnoyanceListBlockedRequests++;
- // 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);
- }
+ // Update the titles of the blocklist menu items. This must be run from the UI thread.
+ activity.runOnUiThread(() -> {
+ navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
+ blocklistsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
+ fanboysAnnoyanceListMenuItem.setTitle(fanboysAnnoyanceListBlockedRequests + " - " + getString(R.string.fanboys_annoyance_list));
+ });
- @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);
+ // Reset the whitelist results tracker (because otherwise it will sometimes add results to the list due to a race condition).
+ whiteListResultStringArray = null;
- // Get the favorite icon byte array from the cursor.
- byte[] favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON));
+ // The resource request was blocked. Return an empty web resource response.
+ return emptyWebResourceResponse;
+ }
+ } else if (fanboysSocialBlockingListEnabled) { // Only check Fanboy’s Social Blocking List if Fanboy’s Annoyance List is disabled.
+ if (blockListHelper.isBlocked(currentDomain, url, isThirdPartyRequest, fanboysSocialList)) {
+ // Increment the blocked requests counters.
+ blockedRequests++;
+ fanboysSocialBlockingListBlockedRequests++;
- // Convert the byte array to a `Bitmap` beginning at the first byte and ending at the last.
- Bitmap favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.length);
+ // Update the titles of the blocklist menu items. This must be run from the UI thread.
+ activity.runOnUiThread(() -> {
+ navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
+ blocklistsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
+ fanboysSocialBlockingListMenuItem.setTitle(fanboysSocialBlockingListBlockedRequests + " - " + getString(R.string.fanboys_social_blocking_list));
+ });
- // Display the bitmap in `bookmarkFavoriteIcon`.
- bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap);
+ // Reset the whitelist results tracker (because otherwise it will sometimes add results to the list due to a race condition).
+ whiteListResultStringArray = null;
- // Get the bookmark name from the cursor and display it in `bookmarkNameTextView`.
- String bookmarkNameString = cursor.getString(cursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
- bookmarkNameTextView.setText(bookmarkNameString);
+ // The resource request was blocked. Return an empty web resource response.
+ return emptyWebResourceResponse;
+ }
+ }
- // 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);
+ // Add the request to the log because it hasn't been processed by any of the previous checks.
+ if (whiteListResultStringArray != null) { // The request was processed by a whitelist.
+ resourceRequests.add(whiteListResultStringArray);
+ } else { // The request didn't match any blocklist entry. Log it as a default request.
+ resourceRequests.add(new String[]{String.valueOf(REQUEST_DEFAULT), url});
}
+
+ // The resource request has not been blocked. `return null` loads the requested resource.
+ return null;
}
- };
- // Populate the `ListView` with the adapter.
- bookmarksListView.setAdapter(bookmarksCursorAdapter);
+ // 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;
- // Set the bookmarks drawer title.
- if (currentBookmarksFolder.isEmpty()) {
- bookmarksTitleTextView.setText(R.string.bookmarks);
- } else {
- bookmarksTitleTextView.setText(currentBookmarksFolder);
- }
- }
+ // Display the HTTP authentication dialog.
+ DialogFragment httpAuthenticationDialogFragment = HttpAuthenticationDialog.displayDialog(host, realm);
+ httpAuthenticationDialogFragment.show(fragmentManager, getString(R.string.http_authentication));
+ }
- private void openWithApp(String url) {
- // Create the open with intent with `ACTION_VIEW`.
- Intent openWithAppIntent = new Intent(Intent.ACTION_VIEW);
+ // Update the URL in urlTextBox when the page starts to load.
+ @Override
+ public void onPageStarted(WebView view, String url, Bitmap favicon) {
+ // Set `urlIsLoading` to `true`, so that redirects while loading do not trigger changes in the user agent, which forces another reload of the existing page.
+ // This is also used to determine when to check for pinned mismatches.
+ urlIsLoading = true;
- // Set the URI but not the MIME type. This should open all available apps.
- openWithAppIntent.setData(Uri.parse(url));
+ // Reset the list of host IP addresses.
+ currentHostIpAddresses = "";
- // Flag the intent to open in a new task.
- openWithAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ // Reset the list of resource requests.
+ resourceRequests.clear();
- // Show the chooser.
- startActivity(openWithAppIntent);
- }
+ // Initialize the counters for requests blocked by each blocklist.
+ blockedRequests = 0;
+ easyListBlockedRequests = 0;
+ easyPrivacyBlockedRequests = 0;
+ fanboysAnnoyanceListBlockedRequests = 0;
+ fanboysSocialBlockingListBlockedRequests = 0;
+ ultraPrivacyBlockedRequests = 0;
+ thirdPartyBlockedRequests = 0;
- private void openWithBrowser(String url) {
- // Create the open with intent with `ACTION_VIEW`.
- Intent openWithBrowserIntent = new Intent(Intent.ACTION_VIEW);
+ // If night mode is enabled, hide `mainWebView` until after the night mode CSS is applied.
+ if (nightMode) {
+ nestedScrollWebView.setVisibility(View.INVISIBLE);
+ }
- // Set the URI and the MIME type. `"text/html"` should load browser options.
- openWithBrowserIntent.setDataAndType(Uri.parse(url), "text/html");
+ // Hide the keyboard.
+ inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0);
+
+ // Check to see if Privacy Browser is waiting on Orbot.
+ if (!waitingForOrbot) { // Process the URL.
+ // The formatted URL string must be updated at the beginning of the load, so that if the user toggles JavaScript during the load the new website is reloaded.
+ formattedUrlString = url;
+
+ // Display the formatted URL text.
+ urlTextBox.setText(formattedUrlString);
- // Flag the intent to open in a new task.
- openWithBrowserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ // Apply text highlighting to `urlTextBox`.
+ highlightUrlText();
- // Show the chooser.
- startActivity(openWithBrowserIntent);
- }
+ // Get a URI for the current URL.
+ Uri currentUri = Uri.parse(formattedUrlString);
- private static void checkPinnedMismatch() {
- if ((pinnedSslCertificate || pinnedIpAddresses) && !ignorePinnedDomainInformation) {
- // Initialize the current SSL certificate variables.
- String currentWebsiteIssuedToCName = "";
- String currentWebsiteIssuedToOName = "";
- String currentWebsiteIssuedToUName = "";
- String currentWebsiteIssuedByCName = "";
- String currentWebsiteIssuedByOName = "";
- String currentWebsiteIssuedByUName = "";
- Date currentWebsiteSslStartDate = null;
- Date currentWebsiteSslEndDate = null;
+ // Get the IP addresses for the host.
+ new GetHostIpAddresses(activity).execute(currentUri.getHost());
+ // Apply any custom domain settings if the URL was loaded by navigating history.
+ if (navigatingHistory) {
+ // Apply the domain settings.
+ boolean userAgentChanged = applyDomainSettings(url, true, false);
- // 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();
- }
+ // Reset `navigatingHistory`.
+ navigatingHistory = false;
- // Initialize string variables to store the SSL certificate dates. Strings are needed to compare the values below, which doesn't work with `Dates` if they are `null`.
- String currentWebsiteSslStartDateString = "";
- String currentWebsiteSslEndDateString = "";
- String pinnedSslStartDateString = "";
- String pinnedSslEndDateString = "";
+ // Manually load the URL if the user agent has changed, which will have caused the previous URL to be reloaded.
+ if (userAgentChanged) {
+ loadUrl(formattedUrlString);
+ }
+ }
- // Convert the `Dates` to `Strings` if they are not `null`.
- if (currentWebsiteSslStartDate != null) {
- currentWebsiteSslStartDateString = currentWebsiteSslStartDate.toString();
- }
+ // Replace Refresh with Stop if the menu item has been created. (The WebView typically begins loading before the menu items are instantiated.)
+ if (refreshMenuItem != null) {
+ // Set the title.
+ refreshMenuItem.setTitle(R.string.stop);
- if (currentWebsiteSslEndDate != null) {
- currentWebsiteSslEndDateString = currentWebsiteSslEndDate.toString();
+ // If the icon is displayed in the AppBar, set it according to the theme.
+ if (displayAdditionalAppBarIcons) {
+ if (darkTheme) {
+ refreshMenuItem.setIcon(R.drawable.close_dark);
+ } else {
+ refreshMenuItem.setIcon(R.drawable.close_light);
+ }
+ }
+ }
+ }
}
- if (pinnedSslStartDate != null) {
- pinnedSslStartDateString = pinnedSslStartDate.toString();
- }
+ // It is necessary to update `formattedUrlString` and `urlTextBox` after the page finishes loading because the final URL can change during load.
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ // Reset the wide view port if it has been turned off by the waiting for Orbot message.
+ if (!waitingForOrbot) {
+ // Only use a wide view port if the URL starts with `http`, not for `file://` and `content://`.
+ nestedScrollWebView.getSettings().setUseWideViewPort(url.startsWith("http"));
+ }
- if (pinnedSslEndDate != null) {
- pinnedSslEndDateString = pinnedSslEndDate.toString();
- }
+ // 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();
+ }
- // Check to see if the pinned information matches the current information.
- if ((pinnedIpAddresses && !currentHostIpAddresses.equals(pinnedHostIpAddresses)) || (pinnedSslCertificate && (!currentWebsiteIssuedToCName.equals(pinnedSslIssuedToCName) ||
- !currentWebsiteIssuedToOName.equals(pinnedSslIssuedToOName) || !currentWebsiteIssuedToUName.equals(pinnedSslIssuedToUName) ||
- !currentWebsiteIssuedByCName.equals(pinnedSslIssuedByCName) || !currentWebsiteIssuedByOName.equals(pinnedSslIssuedByOName) ||
- !currentWebsiteIssuedByUName.equals(pinnedSslIssuedByUName) || !currentWebsiteSslStartDateString.equals(pinnedSslStartDateString) ||
- !currentWebsiteSslEndDateString.equals(pinnedSslEndDateString)))) {
+ // Update the Refresh menu item if it has been created.
+ if (refreshMenuItem != null) {
+ // Reset the Refresh title.
+ refreshMenuItem.setTitle(R.string.refresh);
- // Get a handle for the pinned mismatch alert dialog.
- DialogFragment pinnedMismatchDialogFragment = PinnedMismatchDialog.displayDialog(pinnedSslCertificate, pinnedIpAddresses);
+ // If the icon is displayed in the AppBar, reset it according to the theme.
+ if (displayAdditionalAppBarIcons) {
+ if (darkTheme) {
+ refreshMenuItem.setIcon(R.drawable.refresh_enabled_dark);
+ } else {
+ refreshMenuItem.setIcon(R.drawable.refresh_enabled_light);
+ }
+ }
+ }
- // Show the pinned mismatch alert dialog.
- pinnedMismatchDialogFragment.show(fragmentManager, "Pinned Mismatch");
- }
- }
- }
- // This must run asynchronously because it involves a network request. `String` declares the parameters. `Void` does not declare progress units. `String` contains the results.
- private static class GetHostIpAddresses extends AsyncTask<String, Void, String> {
- // The weak references are used to determine if the activity have disappeared while the AsyncTask is running.
- private final WeakReference<Activity> activityWeakReference;
+ // Clear the cache and history if Incognito Mode is enabled.
+ if (incognitoModeEnabled) {
+ // Clear the cache. `true` includes disk files.
+ nestedScrollWebView.clearCache(true);
- GetHostIpAddresses(Activity activity) {
- // Populate the weak references.
- activityWeakReference = new WeakReference<>(activity);
- }
+ // Clear the back/forward history.
+ nestedScrollWebView.clearHistory();
- // `onPreExecute()` operates on the UI thread.
- @Override
- protected void onPreExecute() {
- // Get a handle for the activity.
- Activity activity = activityWeakReference.get();
+ // Manually delete cache folders.
+ try {
+ // Delete the main cache directory.
+ privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/cache");
- // Abort if the activity is gone.
- if ((activity == null) || activity.isFinishing()) {
- return;
- }
+ // Delete the secondary `Service Worker` cache directory.
+ // A `String[]` must be used because the directory contains a space and `Runtime.exec` will not escape the string correctly otherwise.
+ privacyBrowserRuntime.exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
+ } catch (IOException e) {
+ // Do nothing if an error is thrown.
+ }
+ }
- // Set the getting IP addresses tracker.
- gettingIpAddresses = true;
- }
+ // Update the URL text box and apply domain settings if not waiting on Orbot.
+ if (!waitingForOrbot) {
+ // Check to see if `WebView` has set `url` to be `about:blank`.
+ if (url.equals("about:blank")) { // `WebView` is blank, so `formattedUrlString` should be `""` and `urlTextBox` should display a hint.
+ // Set `formattedUrlString` to `""`.
+ formattedUrlString = "";
+ urlTextBox.setText(formattedUrlString);
- @Override
- protected String doInBackground(String... domainName) {
- // Get a handle for the activity.
- Activity activity = activityWeakReference.get();
+ // Request focus for `urlTextBox`.
+ urlTextBox.requestFocus();
- // Abort if the activity is gone.
- if ((activity == null) || activity.isFinishing()) {
- // Return an empty spannable string builder.
- return "";
- }
+ // Display the keyboard.
+ inputMethodManager.showSoftInput(urlTextBox, 0);
- // Initialize an IP address string builder.
- StringBuilder ipAddresses = new StringBuilder();
+ // Apply the domain settings. This clears any settings from the previous domain.
+ applyDomainSettings(formattedUrlString, true, false);
+ } else { // `WebView` has loaded a webpage.
+ // Set the formatted URL string. Getting the URL from the WebView instead of using the one provided by `onPageFinished` makes websites like YouTube function correctly.
+ formattedUrlString = nestedScrollWebView.getUrl();
- // Get an array with the IP addresses for the host.
- try {
- // Get an array with all the IP addresses for the domain.
- InetAddress[] inetAddressesArray = InetAddress.getAllByName(domainName[0]);
+ // Only update the URL text box if the user is not typing in it.
+ if (!urlTextBox.hasFocus()) {
+ // Display the formatted URL text.
+ urlTextBox.setText(formattedUrlString);
- // Add each IP address to the string builder.
- for (InetAddress inetAddress : inetAddressesArray) {
- if (ipAddresses.length() == 0) { // This is the first IP address.
- // Add the IP address to the string builder.
- ipAddresses.append(inetAddress.getHostAddress());
- } else { // This is not the first IP address.
- // Add a line break to the string builder first.
- ipAddresses.append("\n");
+ // Apply text highlighting to `urlTextBox`.
+ highlightUrlText();
+ }
+ }
- // Add the IP address to the string builder.
- ipAddresses.append(inetAddress.getHostAddress());
+ // Store the SSL certificate so it can be accessed from `ViewSslCertificateDialog` and `PinnedMismatchDialog`.
+ sslCertificate = nestedScrollWebView.getCertificate();
+
+ // Check the current website information against any pinned domain information if the current IP addresses have been loaded.
+ if (!gettingIpAddresses) {
+ checkPinnedMismatch();
}
}
- } catch (UnknownHostException exception) {
- // Do nothing.
+
+ // Reset `urlIsLoading`, which is used to prevent reloads on redirect if the user agent changes. It is also used to determine when to check for pinned mismatches.
+ urlIsLoading = false;
}
- // Return the string.
- return ipAddresses.toString();
- }
+ // Handle SSL Certificate errors.
+ @Override
+ public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
+ // Get the current website SSL certificate.
+ SslCertificate currentWebsiteSslCertificate = error.getCertificate();
- // `onPostExecute()` operates on the UI thread.
- @Override
- protected void onPostExecute(String ipAddresses) {
- // Get a handle for the activity.
- Activity activity = activityWeakReference.get();
+ // 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();
- // Abort if the activity is gone.
- if ((activity == null) || activity.isFinishing()) {
- return;
- }
+ // Proceed to the website if the current SSL website certificate matches the pinned domain certificate.
+ if (pinnedSslCertificate &&
+ currentWebsiteIssuedToCName.equals(pinnedSslIssuedToCName) && currentWebsiteIssuedToOName.equals(pinnedSslIssuedToOName) &&
+ currentWebsiteIssuedToUName.equals(pinnedSslIssuedToUName) && currentWebsiteIssuedByCName.equals(pinnedSslIssuedByCName) &&
+ currentWebsiteIssuedByOName.equals(pinnedSslIssuedByOName) && currentWebsiteIssuedByUName.equals(pinnedSslIssuedByUName) &&
+ currentWebsiteSslStartDate.equals(pinnedSslStartDate) && currentWebsiteSslEndDate.equals(pinnedSslEndDate)) {
- // Store the IP addresses.
- currentHostIpAddresses = ipAddresses;
+ // 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;
- if (!urlIsLoading) {
- checkPinnedMismatch();
+ // Display the SSL error `AlertDialog`.
+ DialogFragment sslCertificateErrorDialogFragment = SslCertificateErrorDialog.displayDialog(error);
+ sslCertificateErrorDialogFragment.show(fragmentManager, getString(R.string.ssl_certificate_error));
+ }
}
+ });
- // Reset the getting IP addresses tracker.
- gettingIpAddresses = false;
+ // Check to see if this is the first tab.
+ if (tabNumber == 0) {
+ // Set this nested scroll WebView as the current WebView.
+ currentWebView = nestedScrollWebView;
+
+ // Apply the app settings from the shared preferences.
+ applyAppSettings();
+
+ // Load the website if not waiting for Orbot to connect.
+ if (!waitingForOrbot) {
+ loadUrl(formattedUrlString);
+ }
}
}
}
\ No newline at end of file