From c59144b04912acea0dd664cb677b4fc19effe9c2 Mon Sep 17 00:00:00 2001 From: Soren Stoutner Date: Thu, 23 May 2019 13:43:56 -0700 Subject: [PATCH] Speed up the initial loading of Privacy Browser. --- .idea/dictionaries/soren.xml | 1 + app/build.gradle | 2 +- .../activities/MainWebViewActivity.java | 4504 +++++++++-------- 3 files changed, 2261 insertions(+), 2246 deletions(-) diff --git a/.idea/dictionaries/soren.xml b/.idea/dictionaries/soren.xml index ad8034ee..a355e63b 100644 --- a/.idea/dictionaries/soren.xml +++ b/.idea/dictionaries/soren.xml @@ -177,6 +177,7 @@ uncheck undelete unported + unthemed useragent useragentname useragentstring diff --git a/app/build.gradle b/app/build.gradle index 5a5d0859..c88c78d4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -86,5 +86,5 @@ dependencies { implementation 'com.google.android.material:material:1.0.0' // Only compile Firebase ads for the free flavor. - freeImplementation 'com.google.firebase:firebase-ads:17.2.0' + freeImplementation 'com.google.firebase:firebase-ads:17.2.1' } \ No newline at end of file diff --git a/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java b/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java index a90f85c7..895f20ad 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java @@ -301,6 +301,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Remove the warning about needing to override `performClick()` when using an `OnTouchListener` with `WebView`. @SuppressLint("ClickableViewAccessibility") protected void onCreate(Bundle savedInstanceState) { + // Initialize the default preference values the first time the program is run. `false` keeps this command from resetting any current preferences back to default. + PreferenceManager.setDefaultValues(this, R.xml.preferences, false); + // Get a handle for the shared preferences. SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); @@ -325,15 +328,10 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Set the content view. setContentView(R.layout.main_framelayout); - - // Get a handle for the input method. - InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - - // Remove the lint warning below that the input method manager might be null. - assert inputMethodManager != null; - - // Get a handle for the toolbar. + // Get handles for the views that need to be modified. + DrawerLayout drawerLayout = findViewById(R.id.drawerlayout); Toolbar toolbar = findViewById(R.id.toolbar); + ViewPager webViewPager = findViewById(R.id.webviewpager); // Set the action bar. `SupportActionBar` must be used until the minimum API is >= 21. setSupportActionBar(toolbar); @@ -348,126 +346,11 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook actionBar.setCustomView(R.layout.url_app_bar); actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM); - // Initialize the foreground color spans for highlighting the URLs. We have to use the deprecated `getColor()` until API >= 23. - redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_a700)); - initialGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500)); - finalGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500)); - - // Get handles for the URL views. - EditText urlEditText = findViewById(R.id.url_edittext); - - // Remove the formatting from `urlTextBar` when the user is editing the text. - urlEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> { - if (hasFocus) { // The user is editing the URL text box. - // Remove the highlighting. - urlEditText.getText().removeSpan(redColorSpan); - urlEditText.getText().removeSpan(initialGrayColorSpan); - urlEditText.getText().removeSpan(finalGrayColorSpan); - } else { // The user has stopped editing the URL text box. - // Move to the beginning of the string. - urlEditText.setSelection(0); - - // Reapply the highlighting. - highlightUrlText(); - } - }); - - // Set the go button on the keyboard to load the URL in `urlTextBox`. - urlEditText.setOnKeyListener((View v, int keyCode, KeyEvent event) -> { - // If the event is a key-down event on the `enter` button, load the URL. - if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { - // Load the URL into the mainWebView and consume the event. - loadUrlFromTextBox(); - - // If the enter key was pressed, consume the event. - return true; - } else { - // If any other key was pressed, do not consume the event. - return false; - } - }); - - // Initialize the Orbot status and the waiting for Orbot trackers. - orbotStatus = "unknown"; - waitingForOrbot = false; - - // Create an Orbot status `BroadcastReceiver`. - orbotStatusBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - // Store the content of the status message in `orbotStatus`. - orbotStatus = intent.getStringExtra("org.torproject.android.intent.extra.STATUS"); - - // If Privacy Browser is waiting on Orbot, load the website now that Orbot is connected. - if (orbotStatus.equals("ON") && waitingForOrbot) { - // Reset the waiting for Orbot status. - waitingForOrbot = false; - - // Get the intent that started the app. - Intent launchingIntent = getIntent(); - - // Get the information from the intent. - String launchingIntentAction = launchingIntent.getAction(); - Uri launchingIntentUriData = launchingIntent.getData(); - - // If the intent action is a web search, perform the search. - if ((launchingIntentAction != null) && launchingIntentAction.equals(Intent.ACTION_WEB_SEARCH)) { - // Create an encoded URL string. - String encodedUrlString; - - // Sanitize the search input and convert it to a search. - try { - encodedUrlString = URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8"); - } catch (UnsupportedEncodingException exception) { - encodedUrlString = ""; - } - - // Load the completed search URL. - loadUrl(searchURL + encodedUrlString); - } else if (launchingIntentUriData != null){ // Check to see if the intent contains a new URL. - // Load the URL from the intent. - loadUrl(launchingIntentUriData.toString()); - } else { // The is no URL in the intent. - // Select the homepage based on the proxy through Orbot status. - if (proxyThroughOrbot) { - // Load the Tor homepage. - loadUrl(sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value))); - } else { - // Load the normal homepage. - loadUrl(sharedPreferences.getString("homepage", getString(R.string.homepage_default_value))); - } - } - } - } - }; - - // Register `orbotStatusBroadcastReceiver` on `this` context. - this.registerReceiver(orbotStatusBroadcastReceiver, new IntentFilter("org.torproject.android.intent.action.STATUS")); - - // Get handles for views that need to be modified. - DrawerLayout drawerLayout = findViewById(R.id.drawerlayout); - NavigationView navigationView = findViewById(R.id.navigationview); - TabLayout tabLayout = findViewById(R.id.tablayout); - SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout); - ViewPager webViewPager = findViewById(R.id.webviewpager); - ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview); - FloatingActionButton launchBookmarksActivityFab = findViewById(R.id.launch_bookmarks_activity_fab); - FloatingActionButton createBookmarkFolderFab = findViewById(R.id.create_bookmark_folder_fab); - FloatingActionButton createBookmarkFab = findViewById(R.id.create_bookmark_fab); - EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext); - // Initially disable the sliding drawers. They will be enabled once the blocklists are loaded. drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); - // 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. - Menu navigationMenu = navigationView.getMenu(); - MenuItem navigationBackMenuItem = navigationMenu.getItem(2); - MenuItem navigationForwardMenuItem = navigationMenu.getItem(3); - MenuItem navigationHistoryMenuItem = navigationMenu.getItem(4); - MenuItem navigationRequestsMenuItem = navigationMenu.getItem(5); + // 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); // Initialize the web view pager adapter. webViewPagerAdapter = new WebViewPagerAdapter(getSupportFragmentManager()); @@ -478,519 +361,556 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // 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) { - // Close the find on page bar if it is open. - closeFindOnPage(null); + // Populate the blocklists. + new PopulateBlocklists(this, this).execute(); + } - // Set the current WebView. - setCurrentWebView(position); + @Override + protected void onNewIntent(Intent intent) { + // Get the information from the intent. + String intentAction = intent.getAction(); + Uri intentUriData = intent.getData(); - // Select the corresponding tab if it does not match the currently selected page. This will happen if the page was scrolled via swiping in the view pager or by creating a new tab. - if (tabLayout.getSelectedTabPosition() != position) { - // Create a handler to select the tab. - Handler selectTabHandler = new Handler(); + // Determine if this is a web search. + boolean isWebSearch = ((intentAction != null) && intentAction.equals(Intent.ACTION_WEB_SEARCH)); - // Create a runnable to select the tab. - Runnable selectTabRunnable = () -> { - // Get a handle for the tab. - TabLayout.Tab tab = tabLayout.getTabAt(position); + // Only process the URI if it contains data or it is a web search. If the user pressed the desktop icon after the app was already running the URI will be null. + if (intentUriData != null || isWebSearch) { + // Get the shared preferences. + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - // Assert that the tab is not null. - assert tab != null; + // Create a URL string. + String url; - // Select the tab. - tab.select(); - }; + // If the intent action is a web search, perform the search. + if (isWebSearch) { + // Create an encoded URL string. + String encodedUrlString; - // Select the tab layout after 150 milliseconds, which leaves enough time for a new tab to be inflated. - selectTabHandler.postDelayed(selectTabRunnable, 150); + // Sanitize the search input and convert it to a search. + try { + encodedUrlString = URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8"); + } catch (UnsupportedEncodingException exception) { + encodedUrlString = ""; } - } - @Override - public void onPageScrollStateChanged(int state) { - // Do nothing. + // Add the base search URL. + url = searchURL + encodedUrlString; + } else { // The intent should contain a URL. + // Set the intent data as the URL. + url = intentUriData.toString(); } - }); - // 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()); - } + // Add a new tab if specified in the preferences. + if (sharedPreferences.getBoolean("open_intents_in_new_tab", true)) { // Load the URL in a new tab. + // Set the loading new intent flag. + loadingNewIntent = true; - @Override - public void onTabUnselected(TabLayout.Tab tab) { - // Do nothing. + // Add a new tab. + addNewTab(url); + } else { // Load the URL in the current tab. + // Make it so. + loadUrl(url); } - @Override - public void onTabReselected(TabLayout.Tab tab) { - // Instantiate the View SSL Certificate dialog. - DialogFragment viewSslCertificateDialogFragment = ViewSslCertificateDialog.displayDialog(currentWebView.getWebViewFragmentId()); + // Get a handle for the drawer layout. + DrawerLayout drawerLayout = findViewById(R.id.drawerlayout); - // Display the View SSL Certificate dialog. - viewSslCertificateDialogFragment.show(getSupportFragmentManager(), getString(R.string.view_ssl_certificate)); + // Close the navigation drawer if it is open. + if (drawerLayout.isDrawerVisible(GravityCompat.START)) { + drawerLayout.closeDrawer(GravityCompat.START); } - }); - // Set the bookmarks drawer resources according to the theme. This can't be done in the layout due to compatibility issues with the `DrawerLayout` support widget. - // The deprecated `getResources().getDrawable()` must be used until the minimum API >= 21 and and `getResources().getColor()` must be used until the minimum API >= 23. - if (darkTheme) { - launchBookmarksActivityFab.setImageDrawable(getResources().getDrawable(R.drawable.bookmarks_dark)); - createBookmarkFolderFab.setImageDrawable(getResources().getDrawable(R.drawable.create_folder_dark)); - createBookmarkFab.setImageDrawable(getResources().getDrawable(R.drawable.create_bookmark_dark)); - bookmarksListView.setBackgroundColor(getResources().getColor(R.color.gray_850)); - } else { - launchBookmarksActivityFab.setImageDrawable(getResources().getDrawable(R.drawable.bookmarks_light)); - createBookmarkFolderFab.setImageDrawable(getResources().getDrawable(R.drawable.create_folder_light)); - createBookmarkFab.setImageDrawable(getResources().getDrawable(R.drawable.create_bookmark_light)); - bookmarksListView.setBackgroundColor(getResources().getColor(R.color.white)); + // Close the bookmarks drawer if it is open. + if (drawerLayout.isDrawerVisible(GravityCompat.END)) { + drawerLayout.closeDrawer(GravityCompat.END); + } } + } - // Set the launch bookmarks activity FAB to launch the bookmarks activity. - launchBookmarksActivityFab.setOnClickListener(v -> { - // Get a copy of the favorite icon bitmap. - Bitmap favoriteIconBitmap = currentWebView.getFavoriteOrDefaultIcon(); + @Override + public void onRestart() { + // Run the default commands. + super.onRestart(); - // Create a favorite icon byte array output stream. - ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream(); + // 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"); - // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG). - favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream); - - // Convert the favorite icon byte array stream to a byte array. - byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray(); - - // Create an intent to launch the bookmarks activity. - Intent bookmarksIntent = new Intent(getApplicationContext(), BookmarksActivity.class); - - // Add the extra information to the intent. - bookmarksIntent.putExtra("current_url", currentWebView.getUrl()); - bookmarksIntent.putExtra("current_title", currentWebView.getTitle()); - bookmarksIntent.putExtra("current_folder", currentBookmarksFolder); - bookmarksIntent.putExtra("favorite_icon_byte_array", favoriteIconByteArray); + // Send the intent to the Orbot package. + orbotIntent.setPackage("org.torproject.android"); // Make it so. - startActivity(bookmarksIntent); - }); + sendBroadcast(orbotIntent); + } - // Set the create new bookmark folder FAB to display an alert dialog. - createBookmarkFolderFab.setOnClickListener(v -> { - // Create a create bookmark folder dialog. - DialogFragment createBookmarkFolderDialog = CreateBookmarkFolderDialog.createBookmarkFolder(currentWebView.getFavoriteOrDefaultIcon()); + // Apply the app settings if returning from the Settings activity. + if (reapplyAppSettingsOnRestart) { + // Reset the reapply app settings on restart tracker. + reapplyAppSettingsOnRestart = false; - // Show the create bookmark folder dialog. - createBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.create_folder)); - }); + // Apply the app settings. + applyAppSettings(); + } - // Set the create new bookmark FAB to display an alert dialog. - createBookmarkFab.setOnClickListener(view -> { - // Instantiate the create bookmark dialog. - DialogFragment createBookmarkDialog = CreateBookmarkDialog.createBookmark(currentWebView.getUrl(), currentWebView.getTitle(), currentWebView.getFavoriteOrDefaultIcon()); + // Apply the domain settings if returning from the settings or domains activity. + if (reapplyDomainSettingsOnRestart) { + // Reset the reapply domain settings on restart tracker. + reapplyDomainSettingsOnRestart = false; - // Display the create bookmark dialog. - createBookmarkDialog.show(getSupportFragmentManager(), getString(R.string.create_bookmark)); - }); + // Reapply the domain settings for each tab. + for (int i = 0; i < webViewPagerAdapter.getCount(); i++) { + // Get the WebView tab fragment. + WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i); - // Search for the string on the page whenever a character changes in the `findOnPageEditText`. - findOnPageEditText.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - // Do nothing. - } + // Get the fragment view. + View fragmentView = webViewTabFragment.getView(); - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - // Do nothing. - } + // Only reload the WebViews if they exist. + if (fragmentView != null) { + // Get the nested scroll WebView from the tab fragment. + NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview); - @Override - public void afterTextChanged(Editable s) { - // Search for the text in the WebView if it is not null. Sometimes on resume after a period of non-use the WebView will be null. - if (currentWebView != null) { - currentWebView.findAllAsync(findOnPageEditText.getText().toString()); + // Reset the current domain name so the domain settings will be reapplied. + nestedScrollWebView.resetCurrentDomainName(); + + // Reapply the domain settings if the URL is not null, which can happen if an empty tab is active when returning from settings. + if (nestedScrollWebView.getUrl() != null) { + applyDomainSettings(nestedScrollWebView, nestedScrollWebView.getUrl(), false, true); + } } } - }); - - // Set the `check mark` button for the `findOnPageEditText` keyboard to close the soft keyboard. - findOnPageEditText.setOnKeyListener((v, keyCode, event) -> { - if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { // The `enter` key was pressed. - // Hide the soft keyboard. - inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0); + } - // Consume the event. - return true; - } else { // A different key was pressed. - // Do not consume the event. - return false; - } - }); + // Load the URL on restart (used when loading a bookmark). + if (loadUrlOnRestart) { + // Load the specified URL. + loadUrl(urlToLoadOnRestart); - // Implement swipe to refresh. - swipeRefreshLayout.setOnRefreshListener(() -> currentWebView.reload()); + // Reset the load on restart tracker. + loadUrlOnRestart = false; + } - // Store the default progress view offsets for use later in `initializeWebView()`. - defaultProgressViewStartOffset = swipeRefreshLayout.getProgressViewStartOffset(); - defaultProgressViewEndOffset = swipeRefreshLayout.getProgressViewEndOffset(); + // 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); - // Set the swipe to refresh color according to the theme. - if (darkTheme) { - swipeRefreshLayout.setColorSchemeResources(R.color.blue_800); - swipeRefreshLayout.setProgressBackgroundColorSchemeResource(R.color.gray_850); - } else { - swipeRefreshLayout.setColorSchemeResources(R.color.blue_500); - } + // Close the bookmarks drawer. + drawerLayout.closeDrawer(GravityCompat.END); - // `DrawerTitle` identifies the `DrawerLayouts` in accessibility mode. - drawerLayout.setDrawerTitle(GravityCompat.START, getString(R.string.navigation_drawer)); - drawerLayout.setDrawerTitle(GravityCompat.END, getString(R.string.bookmarks)); + // Reload the bookmarks drawer. + loadBookmarksFolder(); - // 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); + // Reset `restartFromBookmarksActivity`. + restartFromBookmarksActivity = false; + } - // Initialize `currentBookmarksFolder`. `""` is the home folder in the database. - currentBookmarksFolder = ""; + // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. This can be important if the screen was rotated. + updatePrivacyIcons(true); + } - // Load the home folder, which is `""` in the database. - loadBookmarksFolder(); + // `onResume()` runs after `onStart()`, which runs after `onCreate()` and `onRestart()`. + @Override + public void onResume() { + // Run the default commands. + super.onResume(); - bookmarksListView.setOnItemClickListener((parent, view, position, id) -> { - // Convert the id from long to int to match the format of the bookmarks database. - int databaseID = (int) id; + for (int i = 0; i < webViewPagerAdapter.getCount(); i++) { + // Get the WebView tab fragment. + WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i); - // Get the bookmark cursor for this ID and move it to the first row. - Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseID); - bookmarkCursor.moveToFirst(); + // Get the fragment view. + View fragmentView = webViewTabFragment.getView(); - // Act upon the bookmark according to the type. - if (bookmarkCursor.getInt(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) { // The selected bookmark is a folder. - // Store the new folder name in `currentBookmarksFolder`. - currentBookmarksFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME)); + // Only resume the WebViews if they exist (they won't when the app is first created). + if (fragmentView != null) { + // Get the nested scroll WebView from the tab fragment. + NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview); - // Load the new folder. - loadBookmarksFolder(); - } else { // The selected bookmark is not a folder. - // Load the bookmark URL. - loadUrl(bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_URL))); + // Resume the nested scroll WebView JavaScript timers. + nestedScrollWebView.resumeTimers(); - // Close the bookmarks drawer. - drawerLayout.closeDrawer(GravityCompat.END); + // Resume the nested scroll WebView. + nestedScrollWebView.onResume(); } + } - // Close the `Cursor`. - bookmarkCursor.close(); - }); - - bookmarksListView.setOnItemLongClickListener((parent, view, position, id) -> { - // Convert the database ID from `long` to `int`. - int databaseId = (int) id; + // 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); - // Find out if the selected bookmark is a folder. - boolean isFolder = bookmarksDatabaseHelper.isFolder(databaseId); + // Load a waiting page. `null` specifies no encoding, which defaults to ASCII. + currentWebView.loadData("

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

", "text/html", null); + } - if (isFolder) { - // Save the current folder name, which is used in `onSaveEditBookmarkFolder()`. - oldFolderNameString = bookmarksCursor.getString(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME)); + if (displayingFullScreenVideo || inFullScreenBrowsingMode) { + // Get a handle for the root frame layouts. + FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout); - // Show the edit bookmark folder `AlertDialog` and name the instance `@string/edit_folder`. - DialogFragment editBookmarkFolderDialog = EditBookmarkFolderDialog.folderDatabaseId(databaseId, currentWebView.getFavoriteOrDefaultIcon()); - editBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.edit_folder)); - } else { - // Show the edit bookmark `AlertDialog` and name the instance `@string/edit_bookmark`. - DialogFragment editBookmarkDialog = EditBookmarkDialog.bookmarkDatabaseId(databaseId, currentWebView.getFavoriteOrDefaultIcon()); - editBookmarkDialog.show(getSupportFragmentManager(), getString(R.string.edit_bookmark)); - } + // 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); - // Consume the event. - 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)); + } + } - // Get the status bar pixel size. - int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android"); - int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId); + @Override + public void onPause() { + // Run the default commands. + super.onPause(); - // Get the resource density. - float screenDensity = getResources().getDisplayMetrics().density; + for (int i = 0; i < webViewPagerAdapter.getCount(); i++) { + // Get the WebView tab fragment. + WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i); - // Calculate the drawer header padding. This is used to move the text in the drawer headers below any cutouts. - drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity); - drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity); - drawerHeaderPaddingBottom = (int) (8 * screenDensity); + // Get the fragment view. + View fragmentView = webViewTabFragment.getView(); - // The drawer listener is used to update the navigation menu.` - drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() { - @Override - public void onDrawerSlide(@NonNull View drawerView, float slideOffset) { - } + // Only pause the WebViews if they exist (they won't when the app is first created). + if (fragmentView != null) { + // Get the nested scroll WebView from the tab fragment. + NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview); - @Override - public void onDrawerOpened(@NonNull View drawerView) { - } + // Pause the nested scroll WebView. + nestedScrollWebView.onPause(); - @Override - public void onDrawerClosed(@NonNull View drawerView) { + // Pause the nested scroll WebView JavaScript timers. + nestedScrollWebView.pauseTimers(); } + } - @Override - public void onDrawerStateChanged(int newState) { - if ((newState == DrawerLayout.STATE_SETTLING) || (newState == DrawerLayout.STATE_DRAGGING)) { // A drawer is opening or closing. - // Get handles for the drawer headers. - TextView navigationHeaderTextView = findViewById(R.id.navigationText); - TextView bookmarksHeaderTextView = findViewById(R.id.bookmarks_title_textview); + // Pause the ad or it will continue to consume resources in the background on the free flavor. + if (BuildConfig.FLAVOR.contentEquals("free")) { + // Pause the ad. + AdHelper.pauseAd(findViewById(R.id.adview)); + } + } - // Apply the navigation header paddings if the view is not null (sometimes it is null if another activity has already started). This moves the text in the header below any cutouts. - if (navigationHeaderTextView != null) { - navigationHeaderTextView.setPadding(drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingTop, drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingBottom); - } + @Override + public void onDestroy() { + // Unregister the Orbot status broadcast receiver. + this.unregisterReceiver(orbotStatusBroadcastReceiver); - // Apply the bookmarks header paddings if the view is not null (sometimes it is null if another activity has already started). This moves the text in the header below any cutouts. - if (bookmarksHeaderTextView != null) { - bookmarksHeaderTextView.setPadding(drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingTop, drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingBottom); - } + // Close the bookmarks cursor and database. + bookmarksCursor.close(); + bookmarksDatabaseHelper.close(); - // Update the navigation menu items if the WebView is not null. - if (currentWebView != null) { - navigationBackMenuItem.setEnabled(currentWebView.canGoBack()); - navigationForwardMenuItem.setEnabled(currentWebView.canGoForward()); - navigationHistoryMenuItem.setEnabled((currentWebView.canGoBack() || currentWebView.canGoForward())); - navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); + // Run the default commands. + super.onDestroy(); + } - // Hide the keyboard (if displayed). - inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0); - } + @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); - // Clear the focus from from the URL text box and the WebView. This removes any text selection markers and context menus, which otherwise draw above the open drawers. - urlEditText.clearFocus(); - currentWebView.clearFocus(); - } - } - }); + // Store a handle for the options menu so it can be used by `onOptionsItemSelected()` and `updatePrivacyIcons()`. + optionsMenu = menu; - // 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); + // Set the initial status of the privacy icons. `false` does not call `invalidateOptionsMenu` as the last step. + updatePrivacyIcons(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", ""); + // Get handles for the menu items. + MenuItem toggleFirstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies); + MenuItem toggleThirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies); + MenuItem toggleDomStorageMenuItem = menu.findItem(R.id.toggle_dom_storage); + MenuItem toggleSaveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data); // Form data can be removed once the minimum API >= 26. + MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data); // Form data can be removed once the minimum API >= 26. + MenuItem refreshMenuItem = menu.findItem(R.id.refresh); + MenuItem adConsentMenuItem = menu.findItem(R.id.ad_consent); - // 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); + // Only display third-party cookies if API >= 21 + toggleThirdPartyCookiesMenuItem.setVisible(Build.VERSION.SDK_INT >= 21); - // Inflate a bare WebView to get the default user agent. It is not used to render content on the screen. - @SuppressLint("InflateParams") View webViewLayout = getLayoutInflater().inflate(R.layout.bare_webview, null, false); + // 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); - // Get a handle for the WebView. - WebView bareWebView = webViewLayout.findViewById(R.id.bare_webview); + // Disable the clear form data menu item if the API >= 26 so that the status of the main Clear Data is calculated correctly. + clearFormDataMenuItem.setEnabled(Build.VERSION.SDK_INT < 26); - // Store the default user agent. - webViewDefaultUserAgent = bareWebView.getSettings().getUserAgentString(); + // Only show Ad Consent if this is the free flavor. + adConsentMenuItem.setVisible(BuildConfig.FLAVOR.contentEquals("free")); - // Destroy the bare WebView. - bareWebView.destroy(); + // Get the shared preferences. + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - // Populate the blocklists. - new PopulateBlocklists(this, this).execute(); - } + // Get the dark theme and app bar preferences.. + boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false); + boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false); - @Override - protected void onNewIntent(Intent intent) { - // Get the information from the intent. - String intentAction = intent.getAction(); - Uri intentUriData = intent.getData(); + // 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); + } - // Determine if this is a web search. - boolean isWebSearch = ((intentAction != null) && intentAction.equals(Intent.ACTION_WEB_SEARCH)); + // Replace Refresh with Stop if a URL is already loading. + if (currentWebView != null && currentWebView.getProgress() != 100) { + // Set the title. + refreshMenuItem.setTitle(R.string.stop); - // Only process the URI if it contains data or it is a web search. If the user pressed the desktop icon after the app was already running the URI will be null. - if (intentUriData != null || isWebSearch) { - // Get the shared preferences. - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + // 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); + } + } + } - // Create a URL string. - String url; + // Done. + return true; + } - // If the intent action is a web search, perform the search. - if (isWebSearch) { - // Create an encoded URL string. - String encodedUrlString; + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + // Get handles for the menu items. + MenuItem addOrEditDomain = menu.findItem(R.id.add_or_edit_domain); + MenuItem firstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies); + MenuItem thirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies); + MenuItem domStorageMenuItem = menu.findItem(R.id.toggle_dom_storage); + MenuItem saveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data); // Form data can be removed once the minimum API >= 26. + MenuItem clearDataMenuItem = menu.findItem(R.id.clear_data); + MenuItem clearCookiesMenuItem = menu.findItem(R.id.clear_cookies); + MenuItem clearDOMStorageMenuItem = menu.findItem(R.id.clear_dom_storage); + MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data); // Form data can be removed once the minimum API >= 26. + MenuItem blocklistsMenuItem = menu.findItem(R.id.blocklists); + MenuItem easyListMenuItem = menu.findItem(R.id.easylist); + MenuItem easyPrivacyMenuItem = menu.findItem(R.id.easyprivacy); + MenuItem fanboysAnnoyanceListMenuItem = menu.findItem(R.id.fanboys_annoyance_list); + MenuItem fanboysSocialBlockingListMenuItem = menu.findItem(R.id.fanboys_social_blocking_list); + MenuItem ultraPrivacyMenuItem = menu.findItem(R.id.ultraprivacy); + MenuItem blockAllThirdPartyRequestsMenuItem = menu.findItem(R.id.block_all_third_party_requests); + MenuItem fontSizeMenuItem = menu.findItem(R.id.font_size); + MenuItem swipeToRefreshMenuItem = menu.findItem(R.id.swipe_to_refresh); + MenuItem wideViewportMenuItem = menu.findItem(R.id.wide_viewport); + 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); - // Sanitize the search input and convert it to a search. - try { - encodedUrlString = URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8"); - } catch (UnsupportedEncodingException exception) { - encodedUrlString = ""; - } + // Get a handle for the cookie manager. + CookieManager cookieManager = CookieManager.getInstance(); - // Add the base search URL. - url = searchURL + encodedUrlString; - } else { // The intent should contain a URL. - // Set the intent data as the URL. - url = intentUriData.toString(); + // Initialize the current user agent string and the font size. + String currentUserAgent = getString(R.string.user_agent_privacy_browser); + int fontSize = 100; + + // Set items that require the current web view to be populated. It will be null when the program is first opened, as `onPrepareOptionsMenu()` is called before the first WebView is initialized. + if (currentWebView != null) { + // Set the add or edit domain text. + if (currentWebView.getDomainSettingsApplied()) { + addOrEditDomain.setTitle(R.string.edit_domain_settings); + } else { + addOrEditDomain.setTitle(R.string.add_domain_settings); } - // Add a new tab if specified in the preferences. - if (sharedPreferences.getBoolean("open_intents_in_new_tab", true)) { // Load the URL in a new tab. - // Set the loading new intent flag. - loadingNewIntent = true; + // Get the current user agent from the WebView. + currentUserAgent = currentWebView.getSettings().getUserAgentString(); - // Add a new tab. - addNewTab(url); - } else { // Load the URL in the current tab. - // Make it so. - loadUrl(url); - } + // Get the current font size from the + fontSize = currentWebView.getSettings().getTextZoom(); - // Get a handle for the drawer layout. - DrawerLayout drawerLayout = findViewById(R.id.drawerlayout); + // Set the status of the menu item checkboxes. + domStorageMenuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled()); + saveFormDataMenuItem.setChecked(currentWebView.getSettings().getSaveFormData()); // Form data can be removed once the minimum API >= 26. + easyListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST)); + easyPrivacyMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY)); + fanboysAnnoyanceListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST)); + fanboysSocialBlockingListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST)); + ultraPrivacyMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY)); + blockAllThirdPartyRequestsMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS)); + swipeToRefreshMenuItem.setChecked(currentWebView.getSwipeToRefresh()); + wideViewportMenuItem.setChecked(currentWebView.getSettings().getUseWideViewPort()); + displayImagesMenuItem.setChecked(currentWebView.getSettings().getLoadsImagesAutomatically()); + nightModeMenuItem.setChecked(currentWebView.getNightMode()); - // Close the navigation drawer if it is open. - if (drawerLayout.isDrawerVisible(GravityCompat.START)) { - drawerLayout.closeDrawer(GravityCompat.START); - } + // Initialize the display names for the blocklists with the number of blocked requests. + blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); + easyListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASY_LIST) + " - " + getString(R.string.easylist)); + easyPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASY_PRIVACY) + " - " + getString(R.string.easyprivacy)); + fanboysAnnoyanceListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST) + " - " + getString(R.string.fanboys_annoyance_list)); + fanboysSocialBlockingListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST) + " - " + getString(R.string.fanboys_social_blocking_list)); + ultraPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.ULTRA_PRIVACY) + " - " + getString(R.string.ultraprivacy)); + blockAllThirdPartyRequestsMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " + getString(R.string.block_all_third_party_requests)); - // Close the bookmarks drawer if it is open. - if (drawerLayout.isDrawerVisible(GravityCompat.END)) { - drawerLayout.closeDrawer(GravityCompat.END); + // Only modify third-party cookies if the API >= 21. + if (Build.VERSION.SDK_INT >= 21) { + // Set the status of the third-party cookies checkbox. + thirdPartyCookiesMenuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView)); + + // Enable third-party cookies if first-party cookies are enabled. + thirdPartyCookiesMenuItem.setEnabled(cookieManager.acceptCookie()); } + + // Enable DOM Storage if JavaScript is enabled. + domStorageMenuItem.setEnabled(currentWebView.getSettings().getJavaScriptEnabled()); } - } - @Override - public void onRestart() { - // Run the default commands. - super.onRestart(); + // Set the status of the menu item checkboxes. + firstPartyCookiesMenuItem.setChecked(cookieManager.acceptCookie()); + proxyThroughOrbotMenuItem.setChecked(proxyThroughOrbot); - // 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"); + // Enable Clear Cookies if there are any. + clearCookiesMenuItem.setEnabled(cookieManager.hasCookies()); - // Send the intent to the Orbot package. - orbotIntent.setPackage("org.torproject.android"); + // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`, which links to `/data/data/com.stoutner.privacybrowser.standard`. + String privateDataDirectoryString = getApplicationInfo().dataDir; - // Make it so. - sendBroadcast(orbotIntent); + // 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; } - // Apply the app settings if returning from the Settings activity. - if (reapplyAppSettingsOnRestart) { - // Reset the reapply app settings on restart tracker. - reapplyAppSettingsOnRestart = false; - - // Apply the app settings. - applyAppSettings(); + // 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; } - // Apply the domain settings if returning from the settings or domains activity. - if (reapplyDomainSettingsOnRestart) { - // Reset the reapply domain settings on restart tracker. - reapplyDomainSettingsOnRestart = false; - - // Reapply the domain settings for each tab. - for (int i = 0; i < webViewPagerAdapter.getCount(); i++) { - // Get the WebView tab fragment. - WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i); - - // Get the fragment view. - View fragmentView = webViewTabFragment.getView(); - - // Only reload the WebViews if they exist. - if (fragmentView != null) { - // Get the nested scroll WebView from the tab fragment. - NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview); + // Enable Clear DOM Storage if there is any. + clearDOMStorageMenuItem.setEnabled(localStorageDirectoryNumberOfFiles > 0 || indexedDBDirectoryNumberOfFiles > 0); - // Reset the current domain name so the domain settings will be reapplied. - nestedScrollWebView.resetCurrentDomainName(); + // Enable Clear Form Data is there is any. This can be removed once the minimum API >= 26. + if (Build.VERSION.SDK_INT < 26) { + // Get the WebView database. + WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this); - // Reapply the domain settings if the URL is not null, which can happen if an empty tab is active when returning from settings. - if (nestedScrollWebView.getUrl() != null) { - applyDomainSettings(nestedScrollWebView, nestedScrollWebView.getUrl(), false, true); - } - } - } + // Enable the clear form data menu item if there is anything to clear. + clearFormDataMenuItem.setEnabled(webViewDatabase.hasFormData()); } - // Load the URL on restart (used when loading a bookmark). - if (loadUrlOnRestart) { - // Load the specified URL. - loadUrl(urlToLoadOnRestart); + // Enable Clear Data if any of the submenu items are enabled. + clearDataMenuItem.setEnabled(clearCookiesMenuItem.isEnabled() || clearDOMStorageMenuItem.isEnabled() || clearFormDataMenuItem.isEnabled()); - // Reset the load on restart tracker. - loadUrlOnRestart = false; - } + // Disable Fanboy's Social Blocking List menu item if Fanboy's Annoyance List is checked. + fanboysSocialBlockingListMenuItem.setEnabled(!fanboysAnnoyanceListMenuItem.isChecked()); - // 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); + // 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); + } - // Close the bookmarks drawer. - drawerLayout.closeDrawer(GravityCompat.END); + // Instantiate the font size title and the selected font size menu item. + String fontSizeTitle; + MenuItem selectedFontSizeMenuItem; - // Reload the bookmarks drawer. - loadBookmarksFolder(); + // 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; - // Reset `restartFromBookmarksActivity`. - restartFromBookmarksActivity = false; - } + case 50: + fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.fifty_percent); + selectedFontSizeMenuItem = menu.findItem(R.id.font_size_fifty_percent); + break; - // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. This can be important if the screen was rotated. - updatePrivacyIcons(true); - } + case 75: + fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.seventy_five_percent); + selectedFontSizeMenuItem = menu.findItem(R.id.font_size_seventy_five_percent); + break; - // `onResume()` runs after `onStart()`, which runs after `onCreate()` and `onRestart()`. - @Override - public void onResume() { - // Run the default commands. - super.onResume(); + 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; - for (int i = 0; i < webViewPagerAdapter.getCount(); i++) { - // Get the WebView tab fragment. - WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i); + 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; - // Get the fragment view. - View fragmentView = webViewTabFragment.getView(); + 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; - // Only resume the WebViews if they exist (they won't when the app is first created). - if (fragmentView != null) { - // Get the nested scroll WebView from the tab fragment. - NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview); + 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; - // Resume the nested scroll WebView JavaScript timers. - nestedScrollWebView.resumeTimers(); + 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; - // Resume the nested scroll WebView. - nestedScrollWebView.onResume(); - } + default: + fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent); + selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent); + break; } - // 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); + // Set the font size title and select the current size menu item. + fontSizeMenuItem.setTitle(fontSizeTitle); + selectedFontSizeMenuItem.setChecked(true); - // Load a waiting page. `null` specifies no encoding, which defaults to ASCII. - currentWebView.loadData("

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

", "text/html", null); - } + // Run all the other default commands. + super.onPrepareOptionsMenu(menu); - if (displayingFullScreenVideo || inFullScreenBrowsingMode) { - // Get a handle for the root frame layouts. - FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout); + // Display the menu. + return true; + } + @Override + // Remove Android Studio's warning about the dangers of using SetJavaScriptEnabled. + @SuppressLint("SetJavaScriptEnabled") + public boolean onOptionsItemSelected(MenuItem menuItem) { + // Reenter full screen browsing mode if it was interrupted by the options menu. + if (inFullScreenBrowsingMode) { // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen. getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + 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. @@ -999,2201 +919,2295 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook */ 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)); } - } - @Override - public void onPause() { - // Run the default commands. - super.onPause(); + // Get the selected menu item ID. + int menuItemId = menuItem.getItemId(); - for (int i = 0; i < webViewPagerAdapter.getCount(); i++) { - // Get the WebView tab fragment. - WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i); + // Get a handle for the shared preferences. + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - // Get the fragment view. - View fragmentView = webViewTabFragment.getView(); + // Get a handle for the cookie manager. + CookieManager cookieManager = CookieManager.getInstance(); - // Only pause the WebViews if they exist (they won't when the app is first created). - if (fragmentView != null) { - // Get the nested scroll WebView from the tab fragment. - NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview); + // Run the commands that correlate to the selected menu item. + switch (menuItemId) { + case R.id.toggle_javascript: + // Toggle the JavaScript status. + currentWebView.getSettings().setJavaScriptEnabled(!currentWebView.getSettings().getJavaScriptEnabled()); - // Pause the nested scroll WebView. - nestedScrollWebView.onPause(); + // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. + updatePrivacyIcons(true); - // Pause the nested scroll WebView JavaScript timers. - nestedScrollWebView.pauseTimers(); - } - } + // Display a `Snackbar`. + if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScrip is enabled. + Snackbar.make(findViewById(R.id.webviewpager), R.string.javascript_enabled, Snackbar.LENGTH_SHORT).show(); + } else if (cookieManager.acceptCookie()) { // JavaScript is disabled, but first-party cookies are enabled. + Snackbar.make(findViewById(R.id.webviewpager), R.string.javascript_disabled, Snackbar.LENGTH_SHORT).show(); + } else { // Privacy mode. + Snackbar.make(findViewById(R.id.webviewpager), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show(); + } - // 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)); - } - } + // Reload the current WebView. + currentWebView.reload(); + return true; - @Override - public void onDestroy() { - // Unregister the Orbot status broadcast receiver. - this.unregisterReceiver(orbotStatusBroadcastReceiver); + case R.id.add_or_edit_domain: + if (currentWebView.getDomainSettingsApplied()) { // Edit the current domain settings. + // Reapply the domain settings on returning to `MainWebViewActivity`. + reapplyDomainSettingsOnRestart = true; - // Close the bookmarks cursor and database. - bookmarksCursor.close(); - bookmarksDatabaseHelper.close(); + // Create an intent to launch the domains activity. + Intent domainsIntent = new Intent(this, DomainsActivity.class); - // Run the default commands. - super.onDestroy(); - } + // Add the extra information to the intent. + domainsIntent.putExtra("load_domain", currentWebView.getDomainSettingsDatabaseId()); + domainsIntent.putExtra("close_on_back", true); + domainsIntent.putExtra("current_url", currentWebView.getUrl()); - @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); + // Get the current certificate. + SslCertificate sslCertificate = currentWebView.getCertificate(); - // Store a handle for the options menu so it can be used by `onOptionsItemSelected()` and `updatePrivacyIcons()`. - optionsMenu = menu; + // Check to see if the SSL certificate is populated. + if (sslCertificate != null) { + // Extract the certificate to strings. + String issuedToCName = sslCertificate.getIssuedTo().getCName(); + String issuedToOName = sslCertificate.getIssuedTo().getOName(); + String issuedToUName = sslCertificate.getIssuedTo().getUName(); + String issuedByCName = sslCertificate.getIssuedBy().getCName(); + String issuedByOName = sslCertificate.getIssuedBy().getOName(); + String issuedByUName = sslCertificate.getIssuedBy().getUName(); + long startDateLong = sslCertificate.getValidNotBeforeDate().getTime(); + long endDateLong = sslCertificate.getValidNotAfterDate().getTime(); - // Set the initial status of the privacy icons. `false` does not call `invalidateOptionsMenu` as the last step. - updatePrivacyIcons(false); + // Add the certificate to the intent. + domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName); + domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName); + domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName); + domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName); + domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName); + domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName); + domainsIntent.putExtra("ssl_start_date", startDateLong); + domainsIntent.putExtra("ssl_end_date", endDateLong); + } - // Get handles for the menu items. - MenuItem toggleFirstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies); - MenuItem toggleThirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies); - MenuItem toggleDomStorageMenuItem = menu.findItem(R.id.toggle_dom_storage); - MenuItem toggleSaveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data); // Form data can be removed once the minimum API >= 26. - MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data); // Form data can be removed once the minimum API >= 26. - MenuItem refreshMenuItem = menu.findItem(R.id.refresh); - MenuItem adConsentMenuItem = menu.findItem(R.id.ad_consent); + // Check to see if the current IP addresses have been received. + if (currentWebView.hasCurrentIpAddresses()) { + // Add the current IP addresses to the intent. + domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses()); + } - // Only display third-party cookies if API >= 21 - toggleThirdPartyCookiesMenuItem.setVisible(Build.VERSION.SDK_INT >= 21); + // Make it so. + startActivity(domainsIntent); + } else { // Add a new domain. + // Apply the new domain settings on returning to `MainWebViewActivity`. + reapplyDomainSettingsOnRestart = true; - // Only display the form data menu items if the API < 26. - toggleSaveFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26); - clearFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26); + // Get the current domain + Uri currentUri = Uri.parse(currentWebView.getUrl()); + String currentDomain = currentUri.getHost(); - // Disable the clear form data menu item if the API >= 26 so that the status of the main Clear Data is calculated correctly. - clearFormDataMenuItem.setEnabled(Build.VERSION.SDK_INT < 26); + // 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); - // Only show Ad Consent if this is the free flavor. - adConsentMenuItem.setVisible(BuildConfig.FLAVOR.contentEquals("free")); + // Create the domain and store the database ID. + int newDomainDatabaseId = domainsDatabaseHelper.addDomain(currentDomain); - // Get the shared preferences. - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + // Create an intent to launch the domains activity. + Intent domainsIntent = new Intent(this, DomainsActivity.class); - // Get the dark theme and app bar preferences.. - boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false); - boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false); + // Add the extra information to the intent. + domainsIntent.putExtra("load_domain", newDomainDatabaseId); + domainsIntent.putExtra("close_on_back", true); + domainsIntent.putExtra("current_url", currentWebView.getUrl()); - // 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); - } + // Get the current certificate. + SslCertificate sslCertificate = currentWebView.getCertificate(); - // Replace Refresh with Stop if a URL is already loading. - if (currentWebView != null && currentWebView.getProgress() != 100) { - // Set the title. - refreshMenuItem.setTitle(R.string.stop); + // Check to see if the SSL certificate is populated. + if (sslCertificate != null) { + // Extract the certificate to strings. + String issuedToCName = sslCertificate.getIssuedTo().getCName(); + String issuedToOName = sslCertificate.getIssuedTo().getOName(); + String issuedToUName = sslCertificate.getIssuedTo().getUName(); + String issuedByCName = sslCertificate.getIssuedBy().getCName(); + String issuedByOName = sslCertificate.getIssuedBy().getOName(); + String issuedByUName = sslCertificate.getIssuedBy().getUName(); + long startDateLong = sslCertificate.getValidNotBeforeDate().getTime(); + long endDateLong = sslCertificate.getValidNotAfterDate().getTime(); - // If the icon is displayed in the AppBar, set it according to the theme. - if (displayAdditionalAppBarIcons) { - if (darkTheme) { - refreshMenuItem.setIcon(R.drawable.close_dark); - } else { - refreshMenuItem.setIcon(R.drawable.close_light); + // Add the certificate to the intent. + domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName); + domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName); + domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName); + domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName); + domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName); + domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName); + domainsIntent.putExtra("ssl_start_date", startDateLong); + domainsIntent.putExtra("ssl_end_date", endDateLong); + } + + // Check to see if the current IP addresses have been received. + if (currentWebView.hasCurrentIpAddresses()) { + // Add the current IP addresses to the intent. + domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses()); + } + + // Make it so. + startActivity(domainsIntent); } - } - } + return true; - return true; - } + case R.id.toggle_first_party_cookies: + // Switch the first-party cookie status. + cookieManager.setAcceptCookie(!cookieManager.acceptCookie()); - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - // Get handles for the menu items. - MenuItem addOrEditDomain = menu.findItem(R.id.add_or_edit_domain); - MenuItem firstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies); - MenuItem thirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies); - MenuItem domStorageMenuItem = menu.findItem(R.id.toggle_dom_storage); - MenuItem saveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data); // Form data can be removed once the minimum API >= 26. - MenuItem clearDataMenuItem = menu.findItem(R.id.clear_data); - MenuItem clearCookiesMenuItem = menu.findItem(R.id.clear_cookies); - MenuItem clearDOMStorageMenuItem = menu.findItem(R.id.clear_dom_storage); - MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data); // Form data can be removed once the minimum API >= 26. - MenuItem blocklistsMenuItem = menu.findItem(R.id.blocklists); - MenuItem easyListMenuItem = menu.findItem(R.id.easylist); - MenuItem easyPrivacyMenuItem = menu.findItem(R.id.easyprivacy); - MenuItem fanboysAnnoyanceListMenuItem = menu.findItem(R.id.fanboys_annoyance_list); - MenuItem fanboysSocialBlockingListMenuItem = menu.findItem(R.id.fanboys_social_blocking_list); - MenuItem ultraPrivacyMenuItem = menu.findItem(R.id.ultraprivacy); - MenuItem blockAllThirdPartyRequestsMenuItem = menu.findItem(R.id.block_all_third_party_requests); - MenuItem fontSizeMenuItem = menu.findItem(R.id.font_size); - MenuItem swipeToRefreshMenuItem = menu.findItem(R.id.swipe_to_refresh); - MenuItem wideViewportMenuItem = menu.findItem(R.id.wide_viewport); - 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); + // Store the first-party cookie status. + currentWebView.setAcceptFirstPartyCookies(cookieManager.acceptCookie()); - // Get a handle for the cookie manager. - CookieManager cookieManager = CookieManager.getInstance(); + // Update the menu checkbox. + menuItem.setChecked(cookieManager.acceptCookie()); - // Initialize the current user agent string and the font size. - String currentUserAgent = getString(R.string.user_agent_privacy_browser); - int fontSize = 100; + // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. + updatePrivacyIcons(true); - // Set items that require the current web view to be populated. It will be null when the program is first opened, as `onPrepareOptionsMenu()` is called before the first WebView is initialized. - if (currentWebView != null) { - // Set the add or edit domain text. - if (currentWebView.getDomainSettingsApplied()) { - addOrEditDomain.setTitle(R.string.edit_domain_settings); - } else { - addOrEditDomain.setTitle(R.string.add_domain_settings); - } + // Display a snackbar. + if (cookieManager.acceptCookie()) { // First-party cookies are enabled. + Snackbar.make(findViewById(R.id.webviewpager), R.string.first_party_cookies_enabled, Snackbar.LENGTH_SHORT).show(); + } else if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScript is still enabled. + Snackbar.make(findViewById(R.id.webviewpager), R.string.first_party_cookies_disabled, Snackbar.LENGTH_SHORT).show(); + } else { // Privacy mode. + Snackbar.make(findViewById(R.id.webviewpager), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show(); + } - // Get the current user agent from the WebView. - currentUserAgent = currentWebView.getSettings().getUserAgentString(); + // Reload the current WebView. + currentWebView.reload(); + return true; - // Get the current font size from the - fontSize = currentWebView.getSettings().getTextZoom(); + case R.id.toggle_third_party_cookies: + if (Build.VERSION.SDK_INT >= 21) { + // Switch the status of thirdPartyCookiesEnabled. + cookieManager.setAcceptThirdPartyCookies(currentWebView, !cookieManager.acceptThirdPartyCookies(currentWebView)); - // Set the status of the menu item checkboxes. - domStorageMenuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled()); - saveFormDataMenuItem.setChecked(currentWebView.getSettings().getSaveFormData()); // Form data can be removed once the minimum API >= 26. - easyListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST)); - easyPrivacyMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY)); - fanboysAnnoyanceListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST)); - fanboysSocialBlockingListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST)); - ultraPrivacyMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY)); - blockAllThirdPartyRequestsMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS)); - swipeToRefreshMenuItem.setChecked(currentWebView.getSwipeToRefresh()); - wideViewportMenuItem.setChecked(currentWebView.getSettings().getUseWideViewPort()); - displayImagesMenuItem.setChecked(currentWebView.getSettings().getLoadsImagesAutomatically()); - nightModeMenuItem.setChecked(currentWebView.getNightMode()); + // Update the menu checkbox. + menuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView)); - // Initialize the display names for the blocklists with the number of blocked requests. - blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); - easyListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASY_LIST) + " - " + getString(R.string.easylist)); - easyPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASY_PRIVACY) + " - " + getString(R.string.easyprivacy)); - fanboysAnnoyanceListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST) + " - " + getString(R.string.fanboys_annoyance_list)); - fanboysSocialBlockingListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST) + " - " + getString(R.string.fanboys_social_blocking_list)); - ultraPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.ULTRA_PRIVACY) + " - " + getString(R.string.ultraprivacy)); - blockAllThirdPartyRequestsMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " + getString(R.string.block_all_third_party_requests)); + // Display a snackbar. + if (cookieManager.acceptThirdPartyCookies(currentWebView)) { + Snackbar.make(findViewById(R.id.webviewpager), R.string.third_party_cookies_enabled, Snackbar.LENGTH_SHORT).show(); + } else { + Snackbar.make(findViewById(R.id.webviewpager), R.string.third_party_cookies_disabled, Snackbar.LENGTH_SHORT).show(); + } - // Only modify third-party cookies if the API >= 21. - if (Build.VERSION.SDK_INT >= 21) { - // Set the status of the third-party cookies checkbox. - thirdPartyCookiesMenuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView)); + // Reload the current WebView. + currentWebView.reload(); + } // Else do nothing because SDK < 21. + return true; - // Enable third-party cookies if first-party cookies are enabled. - thirdPartyCookiesMenuItem.setEnabled(cookieManager.acceptCookie()); - } + case R.id.toggle_dom_storage: + // Toggle the status of domStorageEnabled. + currentWebView.getSettings().setDomStorageEnabled(!currentWebView.getSettings().getDomStorageEnabled()); - // Enable DOM Storage if JavaScript is enabled. - domStorageMenuItem.setEnabled(currentWebView.getSettings().getJavaScriptEnabled()); - } + // Update the menu checkbox. + menuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled()); - // Set the status of the menu item checkboxes. - firstPartyCookiesMenuItem.setChecked(cookieManager.acceptCookie()); - proxyThroughOrbotMenuItem.setChecked(proxyThroughOrbot); + // Update the privacy icon. `true` refreshes the app bar icons. + updatePrivacyIcons(true); - // Enable Clear Cookies if there are any. - clearCookiesMenuItem.setEnabled(cookieManager.hasCookies()); + // Display a snackbar. + if (currentWebView.getSettings().getDomStorageEnabled()) { + Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_enabled, Snackbar.LENGTH_SHORT).show(); + } else { + Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_disabled, Snackbar.LENGTH_SHORT).show(); + } - // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`, which links to `/data/data/com.stoutner.privacybrowser.standard`. - String privateDataDirectoryString = getApplicationInfo().dataDir; + // Reload the current WebView. + currentWebView.reload(); + return true; - // Get a count of the number of files in the Local Storage directory. - File localStorageDirectory = new File (privateDataDirectoryString + "/app_webview/Local Storage/"); - int localStorageDirectoryNumberOfFiles = 0; - if (localStorageDirectory.exists()) { - localStorageDirectoryNumberOfFiles = localStorageDirectory.list().length; - } + // Form data can be removed once the minimum API >= 26. + case R.id.toggle_save_form_data: + // Switch the status of saveFormDataEnabled. + currentWebView.getSettings().setSaveFormData(!currentWebView.getSettings().getSaveFormData()); - // 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; - } + // Update the menu checkbox. + menuItem.setChecked(currentWebView.getSettings().getSaveFormData()); - // Enable Clear DOM Storage if there is any. - clearDOMStorageMenuItem.setEnabled(localStorageDirectoryNumberOfFiles > 0 || indexedDBDirectoryNumberOfFiles > 0); + // Display a snackbar. + if (currentWebView.getSettings().getSaveFormData()) { + Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_enabled, Snackbar.LENGTH_SHORT).show(); + } else { + Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_disabled, Snackbar.LENGTH_SHORT).show(); + } - // Enable Clear Form Data is there is any. This can be removed once the minimum API >= 26. - if (Build.VERSION.SDK_INT < 26) { - // Get the WebView database. - WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this); + // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. + updatePrivacyIcons(true); - // Enable the clear form data menu item if there is anything to clear. - clearFormDataMenuItem.setEnabled(webViewDatabase.hasFormData()); - } + // Reload the current WebView. + currentWebView.reload(); + return true; - // Enable Clear Data if any of the submenu items are enabled. - clearDataMenuItem.setEnabled(clearCookiesMenuItem.isEnabled() || clearDOMStorageMenuItem.isEnabled() || clearFormDataMenuItem.isEnabled()); + case R.id.clear_cookies: + Snackbar.make(findViewById(R.id.webviewpager), R.string.cookies_deleted, Snackbar.LENGTH_LONG) + .setAction(R.string.undo, v -> { + // Do nothing because everything will be handled by `onDismissed()` below. + }) + .addCallback(new Snackbar.Callback() { + @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`. + @Override + public void onDismissed(Snackbar snackbar, int event) { + if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) { // The snackbar was dismissed without the undo button being pushed. + // Delete the cookies, which command varies by SDK. + if (Build.VERSION.SDK_INT < 21) { + cookieManager.removeAllCookie(); + } else { + cookieManager.removeAllCookies(null); + } + } + } + }) + .show(); + return true; - // Disable Fanboy's Social Blocking List menu item if Fanboy's Annoyance List is checked. - fanboysSocialBlockingListMenuItem.setEnabled(!fanboysAnnoyanceListMenuItem.isChecked()); + case R.id.clear_dom_storage: + Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_deleted, Snackbar.LENGTH_LONG) + .setAction(R.string.undo, v -> { + // Do nothing because everything will be handled by `onDismissed()` below. + }) + .addCallback(new Snackbar.Callback() { + @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`. + @Override + public void onDismissed(Snackbar snackbar, int event) { + if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) { // The snackbar was dismissed without the undo button being pushed. + // Delete the DOM Storage. + WebStorage webStorage = WebStorage.getInstance(); + webStorage.deleteAllData(); - // 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); - } + // Initialize a handler to manually delete the DOM storage files and directories. + Handler deleteDomStorageHandler = new Handler(); + + // Setup a runnable to manually delete the DOM storage files and directories. + Runnable deleteDomStorageRunnable = () -> { + try { + // Get a handle for the runtime. + Runtime runtime = Runtime.getRuntime(); + + // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`, + // which links to `/data/data/com.stoutner.privacybrowser.standard`. + String privateDataDirectoryString = getApplicationInfo().dataDir; + + // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly. + Process deleteLocalStorageProcess = runtime.exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"}); + + // Multiple commands must be used because `Runtime.exec()` does not like `*`. + Process deleteIndexProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB"); + Process deleteQuotaManagerProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager"); + Process deleteQuotaManagerJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal"); + Process deleteDatabasesProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases"); + + // Wait for the processes to finish. + deleteLocalStorageProcess.waitFor(); + deleteIndexProcess.waitFor(); + deleteQuotaManagerProcess.waitFor(); + deleteQuotaManagerJournalProcess.waitFor(); + deleteDatabasesProcess.waitFor(); + } catch (Exception exception) { + // Do nothing if an error is thrown. + } + }; - // Instantiate the font size title and the selected font size menu item. - String fontSizeTitle; - MenuItem selectedFontSizeMenuItem; + // Manually delete the DOM storage files after 200 milliseconds. + deleteDomStorageHandler.postDelayed(deleteDomStorageRunnable, 200); + } + } + }) + .show(); + return true; - // Prepare the font size title and current size menu item. - switch (fontSize) { - case 25: - fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.twenty_five_percent); - selectedFontSizeMenuItem = menu.findItem(R.id.font_size_twenty_five_percent); - break; + // Form data can be remove once the minimum API >= 26. + case R.id.clear_form_data: + Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_deleted, Snackbar.LENGTH_LONG) + .setAction(R.string.undo, v -> { + // Do nothing because everything will be handled by `onDismissed()` below. + }) + .addCallback(new Snackbar.Callback() { + @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`. + @Override + public void onDismissed(Snackbar snackbar, int event) { + if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) { // The snackbar was dismissed without the undo button being pushed. + // Delete the form data. + WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(getApplicationContext()); + mainWebViewDatabase.clearFormData(); + } + } + }) + .show(); + return true; - case 50: - fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.fifty_percent); - selectedFontSizeMenuItem = menu.findItem(R.id.font_size_fifty_percent); - break; + case R.id.easylist: + // Toggle the EasyList status. + currentWebView.enableBlocklist(NestedScrollWebView.EASY_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST)); - 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; + // Update the menu checkbox. + menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST)); - case 100: - fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent); - selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent); - break; + // Reload the current WebView. + currentWebView.reload(); + return true; - case 125: - fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_twenty_five_percent); - selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_twenty_five_percent); - break; + case R.id.easyprivacy: + // Toggle the EasyPrivacy status. + currentWebView.enableBlocklist(NestedScrollWebView.EASY_PRIVACY, !currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY)); - 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; + // Update the menu checkbox. + menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY)); - case 175: - fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_seventy_five_percent); - selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_seventy_five_percent); - break; + // Reload the current WebView. + currentWebView.reload(); + return true; - case 200: - fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.two_hundred_percent); - selectedFontSizeMenuItem = menu.findItem(R.id.font_size_two_hundred_percent); - break; + case R.id.fanboys_annoyance_list: + // Toggle Fanboy's Annoyance List status. + currentWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST)); - default: - fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent); - selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent); - break; - } + // Update the menu checkbox. + menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST)); - // Set the font size title and select the current size menu item. - fontSizeMenuItem.setTitle(fontSizeTitle); - selectedFontSizeMenuItem.setChecked(true); + // Update the staus of Fanboy's Social Blocking List. + MenuItem fanboysSocialBlockingListMenuItem = optionsMenu.findItem(R.id.fanboys_social_blocking_list); + fanboysSocialBlockingListMenuItem.setEnabled(!currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST)); - // Run all the other default commands. - super.onPrepareOptionsMenu(menu); + // Reload the current WebView. + currentWebView.reload(); + return true; - // Display the menu. - return true; - } + case R.id.fanboys_social_blocking_list: + // Toggle Fanboy's Social Blocking List status. + currentWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST)); - @Override - // Remove Android Studio's warning about the dangers of using SetJavaScriptEnabled. - @SuppressLint("SetJavaScriptEnabled") - public boolean onOptionsItemSelected(MenuItem menuItem) { - // Reenter full screen browsing mode if it was interrupted by the options menu. - if (inFullScreenBrowsingMode) { - // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen. - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + // Update the menu checkbox. + menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST)); - FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout); + // Reload the current WebView. + currentWebView.reload(); + 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); - } + case R.id.ultraprivacy: + // Toggle the UltraPrivacy status. + currentWebView.enableBlocklist(NestedScrollWebView.ULTRA_PRIVACY, !currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY)); - // Get the selected menu item ID. - int menuItemId = menuItem.getItemId(); + // Update the menu checkbox. + menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY)); - // Get a handle for the shared preferences. - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + // Reload the current WebView. + currentWebView.reload(); + return true; - // Get a handle for the cookie manager. - CookieManager cookieManager = CookieManager.getInstance(); + case R.id.block_all_third_party_requests: + //Toggle the third-party requests blocker status. + currentWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS, !currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS)); - // Run the commands that correlate to the selected menu item. - switch (menuItemId) { - case R.id.toggle_javascript: - // Toggle the JavaScript status. - currentWebView.getSettings().setJavaScriptEnabled(!currentWebView.getSettings().getJavaScriptEnabled()); + // Update the menu checkbox. + menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS)); - // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. - updatePrivacyIcons(true); + // Reload the current WebView. + currentWebView.reload(); + return true; - // Display a `Snackbar`. - if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScrip is enabled. - Snackbar.make(findViewById(R.id.webviewpager), R.string.javascript_enabled, Snackbar.LENGTH_SHORT).show(); - } else if (cookieManager.acceptCookie()) { // JavaScript is disabled, but first-party cookies are enabled. - Snackbar.make(findViewById(R.id.webviewpager), R.string.javascript_disabled, Snackbar.LENGTH_SHORT).show(); - } else { // Privacy mode. - Snackbar.make(findViewById(R.id.webviewpager), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show(); - } + case R.id.user_agent_privacy_browser: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[0]); // Reload the current WebView. currentWebView.reload(); return true; - case R.id.add_or_edit_domain: - if (currentWebView.getDomainSettingsApplied()) { // Edit the current domain settings. - // Reapply the domain settings on returning to `MainWebViewActivity`. - reapplyDomainSettingsOnRestart = true; + case R.id.user_agent_webview_default: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(""); - // Create an intent to launch the domains activity. - Intent domainsIntent = new Intent(this, DomainsActivity.class); + // Reload the current WebView. + currentWebView.reload(); + return true; - // Add the extra information to the intent. - domainsIntent.putExtra("load_domain", currentWebView.getDomainSettingsDatabaseId()); - domainsIntent.putExtra("close_on_back", true); - domainsIntent.putExtra("current_url", currentWebView.getUrl()); + case R.id.user_agent_firefox_on_android: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[2]); - // Get the current certificate. - SslCertificate sslCertificate = currentWebView.getCertificate(); + // Reload the current WebView. + currentWebView.reload(); + return true; - // Check to see if the SSL certificate is populated. - if (sslCertificate != null) { - // Extract the certificate to strings. - String issuedToCName = sslCertificate.getIssuedTo().getCName(); - String issuedToOName = sslCertificate.getIssuedTo().getOName(); - String issuedToUName = sslCertificate.getIssuedTo().getUName(); - String issuedByCName = sslCertificate.getIssuedBy().getCName(); - String issuedByOName = sslCertificate.getIssuedBy().getOName(); - String issuedByUName = sslCertificate.getIssuedBy().getUName(); - long startDateLong = sslCertificate.getValidNotBeforeDate().getTime(); - long endDateLong = sslCertificate.getValidNotAfterDate().getTime(); + case R.id.user_agent_chrome_on_android: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[3]); - // Add the certificate to the intent. - domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName); - domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName); - domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName); - domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName); - domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName); - domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName); - domainsIntent.putExtra("ssl_start_date", startDateLong); - domainsIntent.putExtra("ssl_end_date", endDateLong); - } + // Reload the current WebView. + currentWebView.reload(); + return true; - // Check to see if the current IP addresses have been received. - if (currentWebView.hasCurrentIpAddresses()) { - // Add the current IP addresses to the intent. - domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses()); - } + case R.id.user_agent_safari_on_ios: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[4]); - // Make it so. - startActivity(domainsIntent); - } else { // Add a new domain. - // Apply the new domain settings on returning to `MainWebViewActivity`. - reapplyDomainSettingsOnRestart = true; + // Reload the current WebView. + currentWebView.reload(); + return true; - // Get the current domain - Uri currentUri = Uri.parse(currentWebView.getUrl()); - String currentDomain = currentUri.getHost(); + case R.id.user_agent_firefox_on_linux: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[5]); - // 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); + // Reload the current WebView. + currentWebView.reload(); + return true; - // Create the domain and store the database ID. - int newDomainDatabaseId = domainsDatabaseHelper.addDomain(currentDomain); + case R.id.user_agent_chromium_on_linux: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[6]); - // Create an intent to launch the domains activity. - Intent domainsIntent = new Intent(this, DomainsActivity.class); + // Reload the current WebView. + currentWebView.reload(); + return true; - // Add the extra information to the intent. - domainsIntent.putExtra("load_domain", newDomainDatabaseId); - domainsIntent.putExtra("close_on_back", true); - domainsIntent.putExtra("current_url", currentWebView.getUrl()); + case R.id.user_agent_firefox_on_windows: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[7]); - // Get the current certificate. - SslCertificate sslCertificate = currentWebView.getCertificate(); + // Reload the current WebView. + currentWebView.reload(); + return true; - // Check to see if the SSL certificate is populated. - if (sslCertificate != null) { - // Extract the certificate to strings. - String issuedToCName = sslCertificate.getIssuedTo().getCName(); - String issuedToOName = sslCertificate.getIssuedTo().getOName(); - String issuedToUName = sslCertificate.getIssuedTo().getUName(); - String issuedByCName = sslCertificate.getIssuedBy().getCName(); - String issuedByOName = sslCertificate.getIssuedBy().getOName(); - String issuedByUName = sslCertificate.getIssuedBy().getUName(); - long startDateLong = sslCertificate.getValidNotBeforeDate().getTime(); - long endDateLong = sslCertificate.getValidNotAfterDate().getTime(); + case R.id.user_agent_chrome_on_windows: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[8]); - // Add the certificate to the intent. - domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName); - domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName); - domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName); - domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName); - domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName); - domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName); - domainsIntent.putExtra("ssl_start_date", startDateLong); - domainsIntent.putExtra("ssl_end_date", endDateLong); - } + // Reload the current WebView. + currentWebView.reload(); + return true; - // Check to see if the current IP addresses have been received. - if (currentWebView.hasCurrentIpAddresses()) { - // Add the current IP addresses to the intent. - domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses()); - } + case R.id.user_agent_edge_on_windows: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[9]); - // Make it so. - startActivity(domainsIntent); - } + // Reload the current WebView. + currentWebView.reload(); return true; - case R.id.toggle_first_party_cookies: - // Switch the first-party cookie status. - cookieManager.setAcceptCookie(!cookieManager.acceptCookie()); + case R.id.user_agent_internet_explorer_on_windows: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[10]); - // Store the first-party cookie status. - currentWebView.setAcceptFirstPartyCookies(cookieManager.acceptCookie()); + // Reload the current WebView. + currentWebView.reload(); + return true; - // Update the menu checkbox. - menuItem.setChecked(cookieManager.acceptCookie()); + case R.id.user_agent_safari_on_macos: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[11]); - // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. - updatePrivacyIcons(true); + // Reload the current WebView. + currentWebView.reload(); + return true; - // Display a snackbar. - if (cookieManager.acceptCookie()) { // First-party cookies are enabled. - Snackbar.make(findViewById(R.id.webviewpager), R.string.first_party_cookies_enabled, Snackbar.LENGTH_SHORT).show(); - } else if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScript is still enabled. - Snackbar.make(findViewById(R.id.webviewpager), R.string.first_party_cookies_disabled, Snackbar.LENGTH_SHORT).show(); - } else { // Privacy mode. - Snackbar.make(findViewById(R.id.webviewpager), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show(); - } + case R.id.user_agent_custom: + // Update the user agent. + currentWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value))); // Reload the current WebView. currentWebView.reload(); return true; - case R.id.toggle_third_party_cookies: - if (Build.VERSION.SDK_INT >= 21) { - // Switch the status of thirdPartyCookiesEnabled. - cookieManager.setAcceptThirdPartyCookies(currentWebView, !cookieManager.acceptThirdPartyCookies(currentWebView)); + case R.id.font_size_twenty_five_percent: + currentWebView.getSettings().setTextZoom(25); + return true; - // Update the menu checkbox. - menuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView)); + case R.id.font_size_fifty_percent: + currentWebView.getSettings().setTextZoom(50); + return true; - // Display a snackbar. - if (cookieManager.acceptThirdPartyCookies(currentWebView)) { - Snackbar.make(findViewById(R.id.webviewpager), R.string.third_party_cookies_enabled, Snackbar.LENGTH_SHORT).show(); - } else { - Snackbar.make(findViewById(R.id.webviewpager), R.string.third_party_cookies_disabled, Snackbar.LENGTH_SHORT).show(); - } + case R.id.font_size_seventy_five_percent: + currentWebView.getSettings().setTextZoom(75); + return true; - // Reload the current WebView. - currentWebView.reload(); - } // Else do nothing because SDK < 21. + case R.id.font_size_one_hundred_percent: + currentWebView.getSettings().setTextZoom(100); return true; - case R.id.toggle_dom_storage: - // Toggle the status of domStorageEnabled. - currentWebView.getSettings().setDomStorageEnabled(!currentWebView.getSettings().getDomStorageEnabled()); + case R.id.font_size_one_hundred_twenty_five_percent: + currentWebView.getSettings().setTextZoom(125); + return true; - // Update the menu checkbox. - menuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled()); + case R.id.font_size_one_hundred_fifty_percent: + currentWebView.getSettings().setTextZoom(150); + return true; - // Update the privacy icon. `true` refreshes the app bar icons. - updatePrivacyIcons(true); + case R.id.font_size_one_hundred_seventy_five_percent: + currentWebView.getSettings().setTextZoom(175); + return true; - // Display a snackbar. - if (currentWebView.getSettings().getDomStorageEnabled()) { - Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_enabled, Snackbar.LENGTH_SHORT).show(); - } else { - Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_disabled, Snackbar.LENGTH_SHORT).show(); + case R.id.font_size_two_hundred_percent: + currentWebView.getSettings().setTextZoom(200); + return true; + + case R.id.swipe_to_refresh: + // Toggle the stored status of swipe to refresh. + currentWebView.setSwipeToRefresh(!currentWebView.getSwipeToRefresh()); + + // Get a handle for the swipe refresh layout. + SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout); + + // Update the swipe refresh layout. + if (currentWebView.getSwipeToRefresh()) { // Swipe to refresh is enabled. + // Only enable the swipe refresh layout if the WebView is scrolled to the top. It is updated every time the scroll changes. + swipeRefreshLayout.setEnabled(currentWebView.getY() == 0); + } else { // Swipe to refresh is disabled. + // Disable the swipe refresh layout. + swipeRefreshLayout.setEnabled(false); } + return true; - // Reload the current WebView. - currentWebView.reload(); + case R.id.wide_viewport: + // Toggle the viewport. + currentWebView.getSettings().setUseWideViewPort(!currentWebView.getSettings().getUseWideViewPort()); return true; - // Form data can be removed once the minimum API >= 26. - case R.id.toggle_save_form_data: - // Switch the status of saveFormDataEnabled. - currentWebView.getSettings().setSaveFormData(!currentWebView.getSettings().getSaveFormData()); + case R.id.display_images: + if (currentWebView.getSettings().getLoadsImagesAutomatically()) { // Images are currently loaded automatically. + // Disable loading of images. + currentWebView.getSettings().setLoadsImagesAutomatically(false); - // Update the menu checkbox. - menuItem.setChecked(currentWebView.getSettings().getSaveFormData()); + // 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; - // Display a snackbar. - if (currentWebView.getSettings().getSaveFormData()) { - Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_enabled, Snackbar.LENGTH_SHORT).show(); - } else { - Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_disabled, Snackbar.LENGTH_SHORT).show(); + case R.id.night_mode: + // Toggle night mode. + currentWebView.setNightMode(!currentWebView.getNightMode()); + + // Enable or disable JavaScript according to night mode, the global preference, and any domain settings. + if (currentWebView.getNightMode()) { // Night mode is enabled, which requires JavaScript. + // Enable JavaScript. + currentWebView.getSettings().setJavaScriptEnabled(true); + } else if (currentWebView.getDomainSettingsApplied()) { // Night mode is disabled and domain settings are applied. Set JavaScript according to the domain settings. + // Apply the JavaScript preference that was stored the last time domain settings were loaded. + currentWebView.getSettings().setJavaScriptEnabled(currentWebView.getDomainSettingsJavaScriptEnabled()); + } else { // Night mode is disabled and domain settings are not applied. Set JavaScript according to the global preference. + // Apply the JavaScript preference. + currentWebView.getSettings().setJavaScriptEnabled(sharedPreferences.getBoolean("javascript", false)); } - // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. - updatePrivacyIcons(true); + // Update the privacy icons. + updatePrivacyIcons(false); - // Reload the current WebView. + // Reload the website. 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) { - if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) { // The snackbar was dismissed without the undo button being pushed. - // Delete the cookies, which command varies by SDK. - if (Build.VERSION.SDK_INT < 21) { - cookieManager.removeAllCookie(); - } else { - cookieManager.removeAllCookies(null); - } - } - } - }) - .show(); - return true; + case R.id.find_on_page: + // Get a handle for the views. + Toolbar toolbar = findViewById(R.id.toolbar); + LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout); + EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext); - case R.id.clear_dom_storage: - Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_deleted, Snackbar.LENGTH_LONG) - .setAction(R.string.undo, v -> { - // Do nothing because everything will be handled by `onDismissed()` below. - }) - .addCallback(new Snackbar.Callback() { - @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`. - @Override - public void onDismissed(Snackbar snackbar, int event) { - if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) { // The snackbar was dismissed without the undo button being pushed. - // Delete the DOM Storage. - WebStorage webStorage = WebStorage.getInstance(); - webStorage.deleteAllData(); + // Set the minimum height of the find on page linear layout to match the toolbar. + findOnPageLinearLayout.setMinimumHeight(toolbar.getHeight()); + + // Hide the toolbar. + toolbar.setVisibility(View.GONE); + + // Show the find on page linear layout. + findOnPageLinearLayout.setVisibility(View.VISIBLE); + + // Display the keyboard. The app must wait 200 ms before running the command to work around a bug in Android. + // http://stackoverflow.com/questions/5520085/android-show-softkeyboard-with-showsoftinput-is-not-working + findOnPageEditText.postDelayed(() -> { + // Set the focus on `findOnPageEditText`. + findOnPageEditText.requestFocus(); - // Initialize a handler to manually delete the DOM storage files and directories. - Handler deleteDomStorageHandler = new Handler(); + // Get a handle for the input method manager. + InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - // Setup a runnable to manually delete the DOM storage files and directories. - Runnable deleteDomStorageRunnable = () -> { - try { - // Get a handle for the runtime. - Runtime runtime = Runtime.getRuntime(); + // Remove the lint warning below that the input method manager might be null. + assert inputMethodManager != null; - // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`, - // which links to `/data/data/com.stoutner.privacybrowser.standard`. - String privateDataDirectoryString = getApplicationInfo().dataDir; + // Display the keyboard. `0` sets no input flags. + inputMethodManager.showSoftInput(findOnPageEditText, 0); + }, 200); + return true; - // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly. - Process deleteLocalStorageProcess = runtime.exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"}); + case R.id.print: + // Get a print manager instance. + PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE); - // Multiple commands must be used because `Runtime.exec()` does not like `*`. - Process deleteIndexProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB"); - Process deleteQuotaManagerProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager"); - Process deleteQuotaManagerJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal"); - Process deleteDatabasesProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases"); + // Remove the lint error below that print manager might be null. + assert printManager != null; - // 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. - } - }; + // Create a print document adapter from the current WebView. + PrintDocumentAdapter printDocumentAdapter = currentWebView.createPrintDocumentAdapter(); - // Manually delete the DOM storage files after 200 milliseconds. - deleteDomStorageHandler.postDelayed(deleteDomStorageRunnable, 200); - } - } - }) - .show(); + // Print the document. + printManager.print(getString(R.string.privacy_browser_web_page), printDocumentAdapter, null); return 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) { - if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) { // The snackbar was dismissed without the undo button being pushed. - // Delete the form data. - WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(getApplicationContext()); - mainWebViewDatabase.clearFormData(); - } - } - }) - .show(); + case R.id.add_to_homescreen: + // Instantiate the create home screen shortcut dialog. + DialogFragment createHomeScreenShortcutDialogFragment = CreateHomeScreenShortcutDialog.createDialog(currentWebView.getTitle(), currentWebView.getUrl(), + currentWebView.getFavoriteOrDefaultIcon()); + + // Show the create home screen shortcut dialog. + createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getString(R.string.create_shortcut)); return true; - case R.id.easylist: - // Toggle the EasyList status. - currentWebView.enableBlocklist(NestedScrollWebView.EASY_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST)); + case R.id.view_source: + // Create an intent to launch the view source activity. + Intent viewSourceIntent = new Intent(this, ViewSourceActivity.class); - // Update the menu checkbox. - menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST)); + // Add the variables to the intent. + viewSourceIntent.putExtra("user_agent", currentWebView.getSettings().getUserAgentString()); + viewSourceIntent.putExtra("current_url", currentWebView.getUrl()); - // Reload the current WebView. - currentWebView.reload(); + // Make it so. + startActivity(viewSourceIntent); return true; - case R.id.easyprivacy: - // Toggle the EasyPrivacy status. - currentWebView.enableBlocklist(NestedScrollWebView.EASY_PRIVACY, !currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY)); + case R.id.share_url: + // Setup the share string. + String shareString = currentWebView.getTitle() + " – " + currentWebView.getUrl(); - // Update the menu checkbox. - menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY)); + // Create the share intent. + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.putExtra(Intent.EXTRA_TEXT, shareString); + shareIntent.setType("text/plain"); - // Reload the current WebView. - currentWebView.reload(); + // Make it so. + startActivity(Intent.createChooser(shareIntent, getString(R.string.share_url))); return true; - case R.id.fanboys_annoyance_list: - // Toggle Fanboy's Annoyance List status. - currentWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST)); + case R.id.open_with_app: + openWithApp(currentWebView.getUrl()); + return true; - // Update the menu checkbox. - menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST)); + case R.id.open_with_browser: + openWithBrowser(currentWebView.getUrl()); + return true; - // Update the staus of Fanboy's Social Blocking List. - MenuItem fanboysSocialBlockingListMenuItem = optionsMenu.findItem(R.id.fanboys_social_blocking_list); - fanboysSocialBlockingListMenuItem.setEnabled(!currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST)); + case R.id.proxy_through_orbot: + // Toggle the proxy through Orbot variable. + proxyThroughOrbot = !proxyThroughOrbot; - // Reload the current WebView. - currentWebView.reload(); + // Apply the proxy through Orbot settings. + applyProxyThroughOrbot(true); return true; - case R.id.fanboys_social_blocking_list: - // Toggle Fanboy's Social Blocking List status. - currentWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST)); - - // Update the menu checkbox. - menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST)); + 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; - // Reload the current WebView. - currentWebView.reload(); + case R.id.ad_consent: + // Display the ad consent dialog. + DialogFragment adConsentDialogFragment = new AdConsentDialog(); + adConsentDialogFragment.show(getSupportFragmentManager(), getString(R.string.ad_consent)); return true; - case R.id.ultraprivacy: - // Toggle the UltraPrivacy status. - currentWebView.enableBlocklist(NestedScrollWebView.ULTRA_PRIVACY, !currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY)); + default: + // Don't consume the event. + return super.onOptionsItemSelected(menuItem); + } + } - // Update the menu checkbox. - menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY)); + // removeAllCookies is deprecated, but it is required for API < 21. + @Override + public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) { + // Get the menu item ID. + int menuItemId = menuItem.getItemId(); - // Reload the current WebView. - currentWebView.reload(); - return true; + // Get a handle for the shared preferences. + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - case R.id.block_all_third_party_requests: - //Toggle the third-party requests blocker status. - currentWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS, !currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS)); + // Run the commands that correspond to the selected menu item. + switch (menuItemId) { + case R.id.clear_and_exit: + // Clear and exit Privacy Browser. + clearAndExit(); + break; - // Update the menu checkbox. - menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS)); + case R.id.home: + // Select the homepage based on the proxy through Orbot status. + if (proxyThroughOrbot) { + // Load the Tor homepage. + loadUrl(sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value))); + } else { + // Load the normal homepage. + loadUrl(sharedPreferences.getString("homepage", getString(R.string.homepage_default_value))); + } + break; - // Reload the current WebView. - currentWebView.reload(); - return true; + case R.id.back: + if (currentWebView.canGoBack()) { + // Reset the current domain name so that navigation works if third-party requests are blocked. + currentWebView.resetCurrentDomainName(); - case R.id.user_agent_privacy_browser: - // Update the user agent. - currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[0]); + // Set navigating history so that the domain settings are applied when the new URL is loaded. + currentWebView.setNavigatingHistory(true); - // Reload the current WebView. - currentWebView.reload(); - return true; + // Load the previous website in the history. + currentWebView.goBack(); + } + break; - case R.id.user_agent_webview_default: - // Update the user agent. - currentWebView.getSettings().setUserAgentString(""); + case R.id.forward: + if (currentWebView.canGoForward()) { + // Reset the current domain name so that navigation works if third-party requests are blocked. + currentWebView.resetCurrentDomainName(); - // Reload the current WebView. - currentWebView.reload(); - return true; + // Set navigating history so that the domain settings are applied when the new URL is loaded. + currentWebView.setNavigatingHistory(true); + + // Load the next website in the history. + currentWebView.goForward(); + } + break; + + case R.id.history: + // Instantiate the URL history dialog. + DialogFragment urlHistoryDialogFragment = UrlHistoryDialog.loadBackForwardList(currentWebView.getWebViewFragmentId()); + + // Show the URL history dialog. + urlHistoryDialogFragment.show(getSupportFragmentManager(), getString(R.string.history)); + break; + + case R.id.requests: + // Populate the resource requests. + RequestsActivity.resourceRequests = currentWebView.getResourceRequests(); + + // Create an intent to launch the Requests activity. + Intent requestsIntent = new Intent(this, RequestsActivity.class); + + // Add the block third-party requests status to the intent. + requestsIntent.putExtra("block_all_third_party_requests", currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS)); + + // Make it so. + startActivity(requestsIntent); + break; + + case R.id.downloads: + // 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); - case R.id.user_agent_firefox_on_android: - // Update the user agent. - currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[2]); + startActivity(downloadManagerIntent); + break; - // Reload the current WebView. - currentWebView.reload(); - return true; + case R.id.domains: + // Set the flag to reapply the domain settings on restart when returning from Domain Settings. + reapplyDomainSettingsOnRestart = true; - case R.id.user_agent_chrome_on_android: - // Update the user agent. - currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[3]); + // Launch the domains activity. + Intent domainsIntent = new Intent(this, DomainsActivity.class); - // Reload the current WebView. - currentWebView.reload(); - return true; + // Add the extra information to the intent. + domainsIntent.putExtra("current_url", currentWebView.getUrl()); - case R.id.user_agent_safari_on_ios: - // Update the user agent. - currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[4]); + // Get the current certificate. + SslCertificate sslCertificate = currentWebView.getCertificate(); - // Reload the current WebView. - currentWebView.reload(); - return true; + // Check to see if the SSL certificate is populated. + if (sslCertificate != null) { + // Extract the certificate to strings. + String issuedToCName = sslCertificate.getIssuedTo().getCName(); + String issuedToOName = sslCertificate.getIssuedTo().getOName(); + String issuedToUName = sslCertificate.getIssuedTo().getUName(); + String issuedByCName = sslCertificate.getIssuedBy().getCName(); + String issuedByOName = sslCertificate.getIssuedBy().getOName(); + String issuedByUName = sslCertificate.getIssuedBy().getUName(); + long startDateLong = sslCertificate.getValidNotBeforeDate().getTime(); + long endDateLong = sslCertificate.getValidNotAfterDate().getTime(); - case R.id.user_agent_firefox_on_linux: - // Update the user agent. - currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[5]); + // Add the certificate to the intent. + domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName); + domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName); + domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName); + domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName); + domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName); + domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName); + domainsIntent.putExtra("ssl_start_date", startDateLong); + domainsIntent.putExtra("ssl_end_date", endDateLong); + } - // Reload the current WebView. - currentWebView.reload(); - return true; + // Check to see if the current IP addresses have been received. + if (currentWebView.hasCurrentIpAddresses()) { + // Add the current IP addresses to the intent. + domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses()); + } - case R.id.user_agent_chromium_on_linux: - // Update the user agent. - currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[6]); + // Make it so. + startActivity(domainsIntent); + break; - // Reload the current WebView. - currentWebView.reload(); - return true; + case R.id.settings: + // Set the flag to reapply app settings on restart when returning from Settings. + reapplyAppSettingsOnRestart = true; - case R.id.user_agent_firefox_on_windows: - // Update the user agent. - currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[7]); + // Set the flag to reapply the domain settings on restart when returning from Settings. + reapplyDomainSettingsOnRestart = true; - // Reload the current WebView. - currentWebView.reload(); - return true; + // Launch the settings activity. + Intent settingsIntent = new Intent(this, SettingsActivity.class); + startActivity(settingsIntent); + break; - case R.id.user_agent_chrome_on_windows: - // Update the user agent. - currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[8]); + case R.id.import_export: + // Launch the import/export activity. + Intent importExportIntent = new Intent (this, ImportExportActivity.class); + startActivity(importExportIntent); + break; - // Reload the current WebView. - currentWebView.reload(); - return true; + case R.id.logcat: + // Launch the logcat activity. + Intent logcatIntent = new Intent(this, LogcatActivity.class); + startActivity(logcatIntent); + break; - case R.id.user_agent_edge_on_windows: - // Update the user agent. - currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[9]); + case R.id.guide: + // Launch `GuideActivity`. + Intent guideIntent = new Intent(this, GuideActivity.class); + startActivity(guideIntent); + break; - // Reload the current WebView. - currentWebView.reload(); - return true; + case R.id.about: + // Create an intent to launch the about activity. + Intent aboutIntent = new Intent(this, AboutActivity.class); - case R.id.user_agent_internet_explorer_on_windows: - // Update the user agent. - currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[10]); + // Create a string array for the blocklist versions. + String[] blocklistVersions = new String[] {easyList.get(0).get(0)[0], easyPrivacy.get(0).get(0)[0], fanboysAnnoyanceList.get(0).get(0)[0], fanboysSocialList.get(0).get(0)[0], + ultraPrivacy.get(0).get(0)[0]}; - // Reload the current WebView. - currentWebView.reload(); - return true; + // Add the blocklist versions to the intent. + aboutIntent.putExtra("blocklist_versions", blocklistVersions); - case R.id.user_agent_safari_on_macos: - // Update the user agent. - currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[11]); + // Make it so. + startActivity(aboutIntent); + break; + } - // Reload the current WebView. - currentWebView.reload(); - return true; + // Get a handle for the drawer layout. + DrawerLayout drawerLayout = findViewById(R.id.drawerlayout); - case R.id.user_agent_custom: - // Update the user agent. - currentWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value))); + // Close the navigation drawer. + drawerLayout.closeDrawer(GravityCompat.START); + return true; + } - // Reload the current WebView. - currentWebView.reload(); - return true; + @Override + public void onPostCreate(Bundle savedInstanceState) { + // Run the default commands. + super.onPostCreate(savedInstanceState); - case R.id.font_size_twenty_five_percent: - currentWebView.getSettings().setTextZoom(25); - return true; + // Sync the state of the DrawerToggle after the default `onRestoreInstanceState()` has finished. This creates the navigation drawer icon. + actionBarDrawerToggle.syncState(); + } - case R.id.font_size_fifty_percent: - currentWebView.getSettings().setTextZoom(50); - return true; + @Override + public void onConfigurationChanged(Configuration newConfig) { + // Run the default commands. + super.onConfigurationChanged(newConfig); - case R.id.font_size_seventy_five_percent: - currentWebView.getSettings().setTextZoom(75); - return true; + // Get the status bar pixel size. + int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android"); + int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId); - case R.id.font_size_one_hundred_percent: - currentWebView.getSettings().setTextZoom(100); - return true; + // Get the resource density. + float screenDensity = getResources().getDisplayMetrics().density; - case R.id.font_size_one_hundred_twenty_five_percent: - currentWebView.getSettings().setTextZoom(125); - return true; + // Recalculate the drawer header padding. + drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity); + drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity); + drawerHeaderPaddingBottom = (int) (8 * screenDensity); - case R.id.font_size_one_hundred_fifty_percent: - currentWebView.getSettings().setTextZoom(150); - return true; + // Reload the ad for the free flavor if not in full screen mode. + if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) { + // Reload the ad. The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations. + AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id)); + } - case R.id.font_size_one_hundred_seventy_five_percent: - currentWebView.getSettings().setTextZoom(175); - return true; + // `invalidateOptionsMenu` should recalculate the number of action buttons from the menu to display on the app bar, but it doesn't because of the this bug: + // https://code.google.com/p/android/issues/detail?id=20493#c8 + // ActivityCompat.invalidateOptionsMenu(this); + } - case R.id.font_size_two_hundred_percent: - currentWebView.getSettings().setTextZoom(200); - return true; + @Override + public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) { + // Store the hit test result. + final WebView.HitTestResult hitTestResult = currentWebView.getHitTestResult(); - case R.id.swipe_to_refresh: - // Toggle the stored status of swipe to refresh. - currentWebView.setSwipeToRefresh(!currentWebView.getSwipeToRefresh()); + // Create the URL strings. + final String imageUrl; + final String linkUrl; - // Get a handle for the swipe refresh layout. - SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout); + // Get handles for the system managers. + final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); + FragmentManager fragmentManager = getSupportFragmentManager(); + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - // Update the swipe refresh layout. - if (currentWebView.getSwipeToRefresh()) { // Swipe to refresh is enabled. - // Only enable the swipe refresh layout if the WebView is scrolled to the top. It is updated every time the scroll changes. - swipeRefreshLayout.setEnabled(currentWebView.getY() == 0); - } else { // Swipe to refresh is disabled. - // Disable the swipe refresh layout. - swipeRefreshLayout.setEnabled(false); - } - return true; + // Remove the lint errors below that the clipboard manager might be null. + assert clipboardManager != null; - case R.id.wide_viewport: - // Toggle the viewport. - currentWebView.getSettings().setUseWideViewPort(!currentWebView.getSettings().getUseWideViewPort()); - return true; + // Process the link according to the type. + switch (hitTestResult.getType()) { + // `SRC_ANCHOR_TYPE` is a link. + case WebView.HitTestResult.SRC_ANCHOR_TYPE: + // Get the target URL. + linkUrl = hitTestResult.getExtra(); - case R.id.display_images: - if (currentWebView.getSettings().getLoadsImagesAutomatically()) { // Images are currently loaded automatically. - // Disable loading of images. - currentWebView.getSettings().setLoadsImagesAutomatically(false); + // Set the target URL as the title of the `ContextMenu`. + menu.setHeaderTitle(linkUrl); - // 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; + // Add an Open in New Tab entry. + menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> { + // Load the link URL in a new tab. + addNewTab(linkUrl); + return false; + }); - case R.id.night_mode: - // Toggle night mode. - currentWebView.setNightMode(!currentWebView.getNightMode()); + // Add an Open with App entry. + menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> { + openWithApp(linkUrl); + return false; + }); - // Enable or disable JavaScript according to night mode, the global preference, and any domain settings. - if (currentWebView.getNightMode()) { // Night mode is enabled, which requires JavaScript. - // Enable JavaScript. - currentWebView.getSettings().setJavaScriptEnabled(true); - } else if (currentWebView.getDomainSettingsApplied()) { // Night mode is disabled and domain settings are applied. Set JavaScript according to the domain settings. - // Apply the JavaScript preference that was stored the last time domain settings were loaded. - currentWebView.getSettings().setJavaScriptEnabled(currentWebView.getDomainSettingsJavaScriptEnabled()); - } else { // Night mode is disabled and domain settings are not applied. Set JavaScript according to the global preference. - // Apply the JavaScript preference. - currentWebView.getSettings().setJavaScriptEnabled(sharedPreferences.getBoolean("javascript", false)); - } + // Add an Open with Browser entry. + menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> { + openWithBrowser(linkUrl); + return false; + }); - // Update the privacy icons. - updatePrivacyIcons(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); - // Reload the website. - currentWebView.reload(); - return true; + // Set the `ClipData` as the clipboard's primary clip. + clipboardManager.setPrimaryClip(srcAnchorTypeClipData); + return false; + }); - case R.id.find_on_page: - // Get a handle for the views. - Toolbar toolbar = findViewById(R.id.toolbar); - LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout); - EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext); + // Add a Download URL entry. + menu.add(R.string.download_url).setOnMenuItemClickListener((MenuItem item) -> { + // Check if the download should be processed by an external app. + if (sharedPreferences.getBoolean("download_with_external_app", false)) { // Download with an external app. + openUrlWithExternalApp(linkUrl); + } else { // Download with Android's download manager. + // Check to see if the storage permission has already been granted. + if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested. + // Store the variables for future use by `onRequestPermissionsResult()`. + downloadUrl = linkUrl; + downloadContentDisposition = "none"; + downloadContentLength = -1; - // Set the minimum height of the find on page linear layout to match the toolbar. - findOnPageLinearLayout.setMinimumHeight(toolbar.getHeight()); + // 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); - // Hide the toolbar. - toolbar.setVisibility(View.GONE); + // 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); - // Show the find on page linear layout. - findOnPageLinearLayout.setVisibility(View.VISIBLE); + // Show the download file alert dialog. + downloadFileDialogFragment.show(fragmentManager, getString(R.string.download)); + } + } + return 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(); + // Add a Cancel entry, which by default closes the context menu. + menu.add(R.string.cancel); + break; - // Get a handle for the input method manager. - InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + case WebView.HitTestResult.EMAIL_TYPE: + // Get the target URL. + linkUrl = hitTestResult.getExtra(); - // Remove the lint warning below that the input method manager might be null. - assert inputMethodManager != null; + // Set the target URL as the title of the `ContextMenu`. + menu.setHeaderTitle(linkUrl); - // Display the keyboard. `0` sets no input flags. - inputMethodManager.showSoftInput(findOnPageEditText, 0); - }, 200); - return true; + // Add a Write Email entry. + menu.add(R.string.write_email).setOnMenuItemClickListener(item -> { + // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched. + Intent emailIntent = new Intent(Intent.ACTION_SENDTO); - case R.id.print: - // Get a print manager instance. - PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE); + // Parse the url and set it as the data for the `Intent`. + emailIntent.setData(Uri.parse("mailto:" + linkUrl)); - // Remove the lint error below that print manager might be null. - assert printManager != 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); - // Create a print document adapter from the current WebView. - PrintDocumentAdapter printDocumentAdapter = currentWebView.createPrintDocumentAdapter(); + // Make it so. + startActivity(emailIntent); + return false; + }); - // Print the document. - printManager.print(getString(R.string.privacy_browser_web_page), printDocumentAdapter, null); - return true; + // Add a Copy Email Address entry. + menu.add(R.string.copy_email_address).setOnMenuItemClickListener(item -> { + // Save the email address in a `ClipData`. + ClipData srcEmailTypeClipData = ClipData.newPlainText(getString(R.string.email_address), linkUrl); - case R.id.add_to_homescreen: - // Instantiate the create home screen shortcut dialog. - DialogFragment createHomeScreenShortcutDialogFragment = CreateHomeScreenShortcutDialog.createDialog(currentWebView.getTitle(), currentWebView.getUrl(), - currentWebView.getFavoriteOrDefaultIcon()); + // Set the `ClipData` as the clipboard's primary clip. + clipboardManager.setPrimaryClip(srcEmailTypeClipData); + return false; + }); - // Show the create home screen shortcut dialog. - createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getString(R.string.create_shortcut)); - return true; + // Add a `Cancel` entry, which by default closes the `ContextMenu`. + menu.add(R.string.cancel); + break; - case R.id.view_source: - // Create an intent to launch the view source activity. - Intent viewSourceIntent = new Intent(this, ViewSourceActivity.class); + // `IMAGE_TYPE` is an image. `SRC_IMAGE_ANCHOR_TYPE` is an image that is also a link. Privacy Browser processes them the same. + case WebView.HitTestResult.IMAGE_TYPE: + case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE: + // Get the image URL. + imageUrl = hitTestResult.getExtra(); - // Add the variables to the intent. - viewSourceIntent.putExtra("user_agent", currentWebView.getSettings().getUserAgentString()); - viewSourceIntent.putExtra("current_url", currentWebView.getUrl()); + // Set the image URL as the title of the context menu. + menu.setHeaderTitle(imageUrl); - // Make it so. - startActivity(viewSourceIntent); - return true; + // Add an Open in New Tab entry. + menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> { + // Load the image URL in a new tab. + addNewTab(imageUrl); + return false; + }); - case R.id.share_url: - // Setup the share string. - String shareString = currentWebView.getTitle() + " – " + currentWebView.getUrl(); + // Add a View Image entry. + menu.add(R.string.view_image).setOnMenuItemClickListener(item -> { + loadUrl(imageUrl); + return false; + }); - // Create the share intent. - Intent shareIntent = new Intent(Intent.ACTION_SEND); - shareIntent.putExtra(Intent.EXTRA_TEXT, shareString); - shareIntent.setType("text/plain"); + // Add a `Download Image` entry. + menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> { + // Check if the download should be processed by an external app. + if (sharedPreferences.getBoolean("download_with_external_app", false)) { // Download with an external app. + openUrlWithExternalApp(imageUrl); + } else { // Download with Android's download manager. + // Check to see if the storage permission has already been granted. + if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested. + // Store the image URL for use by `onRequestPermissionResult()`. + downloadImageUrl = imageUrl; - // Make it so. - startActivity(Intent.createChooser(shareIntent, getString(R.string.share_url))); - return true; + // Show a dialog if the user has previously denied the permission. + if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first. + // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE. + DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE); - case R.id.open_with_app: - openWithApp(currentWebView.getUrl()); - return true; + // 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); - case R.id.open_with_browser: - openWithBrowser(currentWebView.getUrl()); - return true; + // Show the download image alert dialog. + downloadImageDialogFragment.show(fragmentManager, getString(R.string.download)); + } + } + return false; + }); - case R.id.proxy_through_orbot: - // Toggle the proxy through Orbot variable. - proxyThroughOrbot = !proxyThroughOrbot; + // 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); - // Apply the proxy through Orbot settings. - applyProxyThroughOrbot(true); - return true; + // Set the `ClipData` as the clipboard's primary clip. + clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData); + return false; + }); - 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; + // Add an Open with App entry. + menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> { + openWithApp(imageUrl); + return false; + }); - case R.id.ad_consent: - // Display the ad consent dialog. - DialogFragment adConsentDialogFragment = new AdConsentDialog(); - adConsentDialogFragment.show(getSupportFragmentManager(), getString(R.string.ad_consent)); - return true; + // Add an Open with Browser entry. + menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> { + openWithBrowser(imageUrl); + return false; + }); - default: - // Don't consume the event. - return super.onOptionsItemSelected(menuItem); + // Add a `Cancel` entry, which by default closes the `ContextMenu`. + menu.add(R.string.cancel); + break; } } - // removeAllCookies is deprecated, but it is required for API < 21. @Override - public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) { - // Get the menu item ID. - int menuItemId = menuItem.getItemId(); - - // Get a handle for the shared preferences. - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - - // Run the commands that correspond to the selected menu item. - switch (menuItemId) { - case R.id.clear_and_exit: - // Clear and exit Privacy Browser. - clearAndExit(); - break; + public void onCreateBookmark(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) { + // Get a handle for the bookmarks list view. + ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview); - case R.id.home: - // Select the homepage based on the proxy through Orbot status. - if (proxyThroughOrbot) { - // Load the Tor homepage. - loadUrl(sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value))); - } else { - // Load the normal homepage. - loadUrl(sharedPreferences.getString("homepage", getString(R.string.homepage_default_value))); - } - break; + // 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); - case R.id.back: - if (currentWebView.canGoBack()) { - // Reset the current domain name so that navigation works if third-party requests are blocked. - currentWebView.resetCurrentDomainName(); + // Extract the strings from the edit texts. + String bookmarkNameString = createBookmarkNameEditText.getText().toString(); + String bookmarkUrlString = createBookmarkUrlEditText.getText().toString(); - // Set navigating history so that the domain settings are applied when the new URL is loaded. - currentWebView.setNavigatingHistory(true); + // Create a favorite icon byte array output stream. + ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream(); - // Load the previous website in the history. - currentWebView.goBack(); - } - break; + // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG). + favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream); - case R.id.forward: - if (currentWebView.canGoForward()) { - // Reset the current domain name so that navigation works if third-party requests are blocked. - currentWebView.resetCurrentDomainName(); + // Convert the favorite icon byte array stream to a byte array. + byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray(); - // Set navigating history so that the domain settings are applied when the new URL is loaded. - currentWebView.setNavigatingHistory(true); + // Display the new bookmark below the current items in the (0 indexed) list. + int newBookmarkDisplayOrder = bookmarksListView.getCount(); - // Load the next website in the history. - currentWebView.goForward(); - } - break; + // Create the bookmark. + bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolder, newBookmarkDisplayOrder, favoriteIconByteArray); - case R.id.history: - // Instantiate the URL history dialog. - DialogFragment urlHistoryDialogFragment = UrlHistoryDialog.loadBackForwardList(currentWebView.getWebViewFragmentId()); + // Update the bookmarks cursor with the current contents of this folder. + bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder); - // Show the URL history dialog. - urlHistoryDialogFragment.show(getSupportFragmentManager(), getString(R.string.history)); - break; + // Update the list view. + bookmarksCursorAdapter.changeCursor(bookmarksCursor); - case R.id.requests: - // Populate the resource requests. - RequestsActivity.resourceRequests = currentWebView.getResourceRequests(); + // Scroll to the new bookmark. + bookmarksListView.setSelection(newBookmarkDisplayOrder); + } - // Create an intent to launch the Requests activity. - Intent requestsIntent = new Intent(this, RequestsActivity.class); + @Override + public void onCreateBookmarkFolder(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) { + // Get a handle for the bookmarks list view. + ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview); - // Add the block third-party requests status to the intent. - requestsIntent.putExtra("block_all_third_party_requests", currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS)); + // 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); - // Make it so. - startActivity(requestsIntent); - break; + // Get new folder name string. + String folderNameString = createFolderNameEditText.getText().toString(); - case R.id.downloads: - // Launch the system Download Manager. - Intent downloadManagerIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS); + // Create a folder icon bitmap. + Bitmap folderIconBitmap; - // 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); + // 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(); - startActivity(downloadManagerIntent); - break; + // Convert the folder icon drawable to a bitmap drawable. + BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable; - case R.id.domains: - // Set the flag to reapply the domain settings on restart when returning from Domain Settings. - reapplyDomainSettingsOnRestart = true; + // Convert the folder icon bitmap drawable to a bitmap. + folderIconBitmap = folderIconBitmapDrawable.getBitmap(); + } else { // Use the WebView favorite icon. + // Copy the favorite icon bitmap to the folder icon bitmap. + folderIconBitmap = favoriteIconBitmap; + } - // Launch the domains activity. - Intent domainsIntent = new Intent(this, DomainsActivity.class); + // Create a folder icon byte array output stream. + ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream(); - // Add the extra information to the intent. - domainsIntent.putExtra("current_url", currentWebView.getUrl()); + // 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); - // Get the current certificate. - SslCertificate sslCertificate = currentWebView.getCertificate(); + // Convert the folder icon byte array stream to a byte array. + byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray(); - // Check to see if the SSL certificate is populated. - if (sslCertificate != null) { - // Extract the certificate to strings. - String issuedToCName = sslCertificate.getIssuedTo().getCName(); - String issuedToOName = sslCertificate.getIssuedTo().getOName(); - String issuedToUName = sslCertificate.getIssuedTo().getUName(); - String issuedByCName = sslCertificate.getIssuedBy().getCName(); - String issuedByOName = sslCertificate.getIssuedBy().getOName(); - String issuedByUName = sslCertificate.getIssuedBy().getUName(); - long startDateLong = sslCertificate.getValidNotBeforeDate().getTime(); - long endDateLong = sslCertificate.getValidNotAfterDate().getTime(); + // 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); + } - // Add the certificate to the intent. - domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName); - domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName); - domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName); - domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName); - domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName); - domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName); - domainsIntent.putExtra("ssl_start_date", startDateLong); - domainsIntent.putExtra("ssl_end_date", endDateLong); - } + // Create the folder, which will be placed at the top of the `ListView`. + bookmarksDatabaseHelper.createFolder(folderNameString, currentBookmarksFolder, folderIconByteArray); - // Check to see if the current IP addresses have been received. - if (currentWebView.hasCurrentIpAddresses()) { - // Add the current IP addresses to the intent. - domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses()); - } + // Update the bookmarks cursor with the current contents of this folder. + bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder); - // Make it so. - startActivity(domainsIntent); - break; + // Update the `ListView`. + bookmarksCursorAdapter.changeCursor(bookmarksCursor); - case R.id.settings: - // Set the flag to reapply app settings on restart when returning from Settings. - reapplyAppSettingsOnRestart = true; + // Scroll to the new folder. + bookmarksListView.setSelection(0); + } - // Set the flag to reapply the domain settings on restart when returning from Settings. - reapplyDomainSettingsOnRestart = true; + @Override + public void onSaveBookmark(DialogFragment dialogFragment, int selectedBookmarkDatabaseId, Bitmap favoriteIconBitmap) { + // Get handles for the views from `dialogFragment`. + EditText editBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_name_edittext); + EditText editBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_url_edittext); + RadioButton currentBookmarkIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_current_icon_radiobutton); - // Launch the settings activity. - Intent settingsIntent = new Intent(this, SettingsActivity.class); - startActivity(settingsIntent); - break; + // Store the bookmark strings. + String bookmarkNameString = editBookmarkNameEditText.getText().toString(); + String bookmarkUrlString = editBookmarkUrlEditText.getText().toString(); - case R.id.import_export: - // Launch the import/export activity. - Intent importExportIntent = new Intent (this, ImportExportActivity.class); - startActivity(importExportIntent); - break; + // Update the bookmark. + if (currentBookmarkIconRadioButton.isChecked()) { // Update the bookmark without changing the favorite icon. + bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString); + } else { // Update the bookmark using the `WebView` favorite icon. + // Create a favorite icon byte array output stream. + ByteArrayOutputStream newFavoriteIconByteArrayOutputStream = new ByteArrayOutputStream(); - case R.id.logcat: - // Launch the logcat activity. - Intent logcatIntent = new Intent(this, LogcatActivity.class); - startActivity(logcatIntent); - break; + // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG). + favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFavoriteIconByteArrayOutputStream); - case R.id.guide: - // Launch `GuideActivity`. - Intent guideIntent = new Intent(this, GuideActivity.class); - startActivity(guideIntent); - break; + // Convert the favorite icon byte array stream to a byte array. + byte[] newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray(); - case R.id.about: - // Create an intent to launch the about activity. - Intent aboutIntent = new Intent(this, AboutActivity.class); + // Update the bookmark and the favorite icon. + bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, newFavoriteIconByteArray); + } - // Create a string array for the blocklist versions. - String[] blocklistVersions = new String[] {easyList.get(0).get(0)[0], easyPrivacy.get(0).get(0)[0], fanboysAnnoyanceList.get(0).get(0)[0], fanboysSocialList.get(0).get(0)[0], - ultraPrivacy.get(0).get(0)[0]}; + // Update the bookmarks cursor with the current contents of this folder. + bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder); - // Add the blocklist versions to the intent. - aboutIntent.putExtra("blocklist_versions", blocklistVersions); + // Update the list view. + bookmarksCursorAdapter.changeCursor(bookmarksCursor); + } - // Make it so. - startActivity(aboutIntent); - break; - } + @Override + public void onSaveBookmarkFolder(DialogFragment dialogFragment, int selectedFolderDatabaseId, Bitmap favoriteIconBitmap) { + // Get handles for the views from `dialogFragment`. + EditText editFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_folder_name_edittext); + RadioButton currentFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_current_icon_radiobutton); + RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_radiobutton); + ImageView defaultFolderIconImageView = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_imageview); - // Get a handle for the drawer layout. - DrawerLayout drawerLayout = findViewById(R.id.drawerlayout); + // Get the new folder name. + String newFolderNameString = editFolderNameEditText.getText().toString(); - // Close the navigation drawer. - drawerLayout.closeDrawer(GravityCompat.START); - 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; - @Override - public void onPostCreate(Bundle savedInstanceState) { - // Run the default commands. - super.onPostCreate(savedInstanceState); + // Populate the new folder icon bitmap. + if (defaultFolderIconRadioButton.isChecked()) { + // Get the default folder icon drawable. + Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable(); - // Sync the state of the DrawerToggle after the default `onRestoreInstanceState()` has finished. This creates the navigation drawer icon. - actionBarDrawerToggle.syncState(); - } + // Convert the folder icon drawable to a bitmap drawable. + BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable; - @Override - public void onConfigurationChanged(Configuration newConfig) { - // Run the default commands. - super.onConfigurationChanged(newConfig); + // Convert the folder icon bitmap drawable to a bitmap. + folderIconBitmap = folderIconBitmapDrawable.getBitmap(); + } else { // Use the `WebView` favorite icon. + // Copy the favorite icon bitmap to the folder icon bitmap. + folderIconBitmap = favoriteIconBitmap; + } - // Get the status bar pixel size. - int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android"); - int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId); + // Create a folder icon byte array output stream. + ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream(); - // Get the resource density. - float screenDensity = getResources().getDisplayMetrics().density; + // 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); - // Recalculate the drawer header padding. - drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity); - drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity); - drawerHeaderPaddingBottom = (int) (8 * screenDensity); + // Convert the folder icon byte array stream to a byte array. + byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray(); - // 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)); - } + // 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(); - // `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); - } + // Convert the folder icon drawable to a bitmap drawable. + BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable; - @Override - public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) { - // Store the hit test result. - final WebView.HitTestResult hitTestResult = currentWebView.getHitTestResult(); + // Convert the folder icon bitmap drawable to a bitmap. + folderIconBitmap = folderIconBitmapDrawable.getBitmap(); + } else { // Use the `WebView` favorite icon. + // Copy the favorite icon bitmap to the folder icon bitmap. + folderIconBitmap = favoriteIconBitmap; + } - // Create the URL strings. - final String imageUrl; - final String linkUrl; + // Create a folder icon byte array output stream. + ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream(); - // Get handles for the system managers. - final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); - FragmentManager fragmentManager = getSupportFragmentManager(); - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + // 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); - // Remove the lint errors below that the clipboard manager might be null. - assert clipboardManager != null; + // Convert the folder icon byte array stream to a byte array. + byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray(); - // Process the link according to the type. - switch (hitTestResult.getType()) { - // `SRC_ANCHOR_TYPE` is a link. - case WebView.HitTestResult.SRC_ANCHOR_TYPE: - // Get the target URL. - linkUrl = hitTestResult.getExtra(); + // Update the folder name and icon in the database. + bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, newFolderIconByteArray); + } - // Set the target URL as the title of the `ContextMenu`. - menu.setHeaderTitle(linkUrl); + // Update the bookmarks cursor with the current contents of this folder. + bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder); - // Add an Open in New Tab entry. - menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> { - // Load the link URL in a new tab. - addNewTab(linkUrl); - return false; - }); + // Update the `ListView`. + bookmarksCursorAdapter.changeCursor(bookmarksCursor); + } - // Add an Open with App entry. - menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> { - openWithApp(linkUrl); - return false; - }); + @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; - // Add an Open with Browser entry. - menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> { - openWithBrowser(linkUrl); - return false; - }); + 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; + } + } - // 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); + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + // Get a handle for the fragment manager. + FragmentManager fragmentManager = getSupportFragmentManager(); - // Set the `ClipData` as the clipboard's primary clip. - clipboardManager.setPrimaryClip(srcAnchorTypeClipData); - return false; - }); + 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); - // Add a Download URL entry. - menu.add(R.string.download_url).setOnMenuItemClickListener((MenuItem item) -> { - // Check if the download should be processed by an external app. - if (sharedPreferences.getBoolean("download_with_external_app", false)) { // Download with an external app. - openUrlWithExternalApp(linkUrl); - } else { // Download with Android's download manager. - // Check to see if the storage permission has already been granted. - if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested. - // Store the variables for future use by `onRequestPermissionsResult()`. - downloadUrl = linkUrl; - downloadContentDisposition = "none"; - downloadContentLength = -1; + // 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)); + } - // 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); + // Reset the download variables. + downloadUrl = ""; + downloadContentDisposition = ""; + downloadContentLength = 0; + break; - // 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); + 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); - // Show the download file alert dialog. - downloadFileDialogFragment.show(fragmentManager, getString(R.string.download)); - } - } - return 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)); + } - // Add a Cancel entry, which by default closes the context menu. - menu.add(R.string.cancel); + // Reset the image URL variable. + downloadImageUrl = ""; break; + } + } - case WebView.HitTestResult.EMAIL_TYPE: - // Get the target URL. - linkUrl = hitTestResult.getExtra(); + @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); - // Set the target URL as the title of the `ContextMenu`. - menu.setHeaderTitle(linkUrl); + // Parse `imageUrl`. + DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(imageUrl)); - // 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 a handle for the cookie manager. + CookieManager cookieManager = CookieManager.getInstance(); - // Parse the url and set it as the data for the `Intent`. - emailIntent.setData(Uri.parse("mailto:" + linkUrl)); + // Pass cookies to download manager if cookies are enabled. This is required to download images from websites that require a login. + // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner . + if (cookieManager.acceptCookie()) { + // Get the cookies for `imageUrl`. + String cookies = cookieManager.getCookie(imageUrl); - // `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); + // Add the cookies to `downloadRequest`. In the HTTP request header, cookies are named `Cookie`. + downloadRequest.addRequestHeader("Cookie", cookies); + } - // Make it so. - startActivity(emailIntent); - return false; - }); + // Get the file name from the dialog fragment. + EditText downloadImageNameEditText = dialogFragment.getDialog().findViewById(R.id.download_image_name); + String imageName = downloadImageNameEditText.getText().toString(); - // 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); + // Specify the download location. + if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // External write permission granted. + // Download to the public download directory. + downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, imageName); + } else { // External write permission denied. + // Download to the app's external download directory. + downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, imageName); + } + + // Allow `MediaScanner` to index the download if it is a media file. + downloadRequest.allowScanningByMediaScanner(); - // Set the `ClipData` as the clipboard's primary clip. - clipboardManager.setPrimaryClip(srcEmailTypeClipData); - return false; - }); + // Add the URL as the description for the download. + downloadRequest.setDescription(imageUrl); - // Add a `Cancel` entry, which by default closes the `ContextMenu`. - menu.add(R.string.cancel); - break; + // Show the download notification after the download is completed. + downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); - // `IMAGE_TYPE` is an image. `SRC_IMAGE_ANCHOR_TYPE` is an image that is also a link. Privacy Browser processes them the same. - case WebView.HitTestResult.IMAGE_TYPE: - case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE: - // Get the image URL. - imageUrl = hitTestResult.getExtra(); + // Remove the lint warning below that `downloadManager` might be `null`. + assert downloadManager != null; - // Set the image URL as the title of the context menu. - menu.setHeaderTitle(imageUrl); + // 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(); + } + } - // Add an Open in New Tab entry. - menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> { - // Load the image URL in a new tab. - addNewTab(imageUrl); - return false; - }); + @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); - // Add a View Image entry. - menu.add(R.string.view_image).setOnMenuItemClickListener(item -> { - loadUrl(imageUrl); - return false; - }); + // Parse `downloadUrl`. + DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(downloadUrl)); - // Add a `Download Image` entry. - menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> { - // Check if the download should be processed by an external app. - if (sharedPreferences.getBoolean("download_with_external_app", false)) { // Download with an external app. - openUrlWithExternalApp(imageUrl); - } else { // Download with Android's download manager. - // Check to see if the storage permission has already been granted. - if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested. - // Store the image URL for use by `onRequestPermissionResult()`. - downloadImageUrl = imageUrl; + // Get a handle for the cookie manager. + CookieManager cookieManager = CookieManager.getInstance(); - // 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); + // Pass cookies to download manager if cookies are enabled. This is required to download files from websites that require a login. + // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner . + if (cookieManager.acceptCookie()) { + // Get the cookies for `downloadUrl`. + String cookies = cookieManager.getCookie(downloadUrl); - // 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); + // Add the cookies to `downloadRequest`. In the HTTP request header, cookies are named `Cookie`. + downloadRequest.addRequestHeader("Cookie", cookies); + } - // Show the download image alert dialog. - downloadImageDialogFragment.show(fragmentManager, getString(R.string.download)); - } - } - return false; - }); + // Get the file name from the dialog fragment. + EditText downloadFileNameEditText = dialogFragment.getDialog().findViewById(R.id.download_file_name); + String fileName = downloadFileNameEditText.getText().toString(); - // 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); + // 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); + } - // Set the `ClipData` as the clipboard's primary clip. - clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData); - return false; - }); + // Allow `MediaScanner` to index the download if it is a media file. + downloadRequest.allowScanningByMediaScanner(); - // Add an Open with App entry. - menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> { - openWithApp(imageUrl); - return false; - }); + // Add the URL as the description for the download. + downloadRequest.setDescription(downloadUrl); - // Add an Open with Browser entry. - menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> { - openWithBrowser(imageUrl); - return false; - }); + // Show the download notification after the download is completed. + downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); - // Add a `Cancel` entry, which by default closes the `ContextMenu`. - menu.add(R.string.cancel); - break; + // Remove the lint warning below that `downloadManager` might be `null`. + assert downloadManager != null; + + // Initiate the download. + downloadManager.enqueue(downloadRequest); + } else { // The download is not an HTTP or HTTPS URI. + Snackbar.make(currentWebView, R.string.cannot_download_file, Snackbar.LENGTH_INDEFINITE).show(); } } + // Override `onBackPressed` to handle the navigation drawer and and the WebView. @Override - public void onCreateBookmark(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) { - // Get a handle for the bookmarks list view. - ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview); + public void onBackPressed() { + // Get a handle for the drawer layout and the tab layout. + DrawerLayout drawerLayout = findViewById(R.id.drawerlayout); + TabLayout tabLayout = findViewById(R.id.tablayout); - // 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); + 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); - // Extract the strings from the edit texts. - String bookmarkNameString = createBookmarkNameEditText.getText().toString(); - String bookmarkUrlString = createBookmarkUrlEditText.getText().toString(); + // Load the new folder. + loadBookmarksFolder(); + } + } else if (currentWebView.canGoBack()) { // There is at least one item in the current WebView history. + // Reset the current domain name so that navigation works if third-party requests are blocked. + currentWebView.resetCurrentDomainName(); - // Create a favorite icon byte array output stream. - ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream(); + // Set navigating history so that the domain settings are applied when the new URL is loaded. + currentWebView.setNavigatingHistory(true); - // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG). - favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream); + // Go back. + currentWebView.goBack(); + } else if (tabLayout.getTabCount() > 1) { // There are at least two tabs. + // Close the current tab. + closeCurrentTab(); + } else { // There isn't anything to do in Privacy Browser. + // Run the default commands. + super.onBackPressed(); - // Convert the favorite icon byte array stream to a byte array. - byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray(); + // Manually kill Privacy Browser. Otherwise, it is glitchy when restarted. + System.exit(0); + } + } - // Display the new bookmark below the current items in the (0 indexed) list. - int newBookmarkDisplayOrder = bookmarksListView.getCount(); + // 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)); + } + } - // Create the bookmark. - bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolder, newBookmarkDisplayOrder, favoriteIconByteArray); + private void loadUrlFromTextBox() { + // Get a handle for the URL edit text. + EditText urlEditText = findViewById(R.id.url_edittext); - // Update the bookmarks cursor with the current contents of this folder. - bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder); + // Get the text from urlTextBox and convert it to a string. trim() removes white spaces from the beginning and end of the string. + String unformattedUrlString = urlEditText.getText().toString().trim(); - // Update the list view. - bookmarksCursorAdapter.changeCursor(bookmarksCursor); + // Initialize the formatted URL string. + String url = ""; - // Scroll to the new bookmark. - bookmarksListView.setSelection(newBookmarkDisplayOrder); - } + // Check to see if `unformattedUrlString` is a valid URL. Otherwise, convert it into a search. + if (unformattedUrlString.startsWith("content://")) { // This is a Content URL. + // Load the entire content URL. + url = unformattedUrlString; + } else if (Patterns.WEB_URL.matcher(unformattedUrlString).matches() || unformattedUrlString.startsWith("http://") || unformattedUrlString.startsWith("https://") || + unformattedUrlString.startsWith("file://")) { // This is a standard URL. + // Add `https://` at the beginning if there is no protocol. Otherwise the app will segfault. + if (!unformattedUrlString.startsWith("http") && !unformattedUrlString.startsWith("file://") && !unformattedUrlString.startsWith("content://")) { + unformattedUrlString = "https://" + unformattedUrlString; + } - @Override - public void onCreateBookmarkFolder(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) { - // Get a handle for the bookmarks list view. - ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview); + // Initialize `unformattedUrl`. + URL unformattedUrl = null; - // 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); + // 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 new folder name string. - String folderNameString = createFolderNameEditText.getText().toString(); + // 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; - // Create a folder icon bitmap. - Bitmap folderIconBitmap; + // Build the URI. + Uri.Builder uri = new Uri.Builder(); + uri.scheme(scheme).authority(authority).path(path).query(query).fragment(fragment); - // 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(); + // Decode the URI as a UTF-8 string in. + try { + url = URLDecoder.decode(uri.build().toString(), "UTF-8"); + } catch (UnsupportedEncodingException exception) { + // Do nothing. The formatted URL string will remain blank. + } + } else if (!unformattedUrlString.isEmpty()){ // This is not a URL, but rather a search string. + // Create an encoded URL String. + String encodedUrlString; - // Convert the folder icon drawable to a bitmap drawable. - BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable; + // Sanitize the search input. + try { + encodedUrlString = URLEncoder.encode(unformattedUrlString, "UTF-8"); + } catch (UnsupportedEncodingException exception) { + encodedUrlString = ""; + } - // Convert the folder icon bitmap drawable to a bitmap. - folderIconBitmap = folderIconBitmapDrawable.getBitmap(); - } else { // Use the WebView favorite icon. - // Copy the favorite icon bitmap to the folder icon bitmap. - folderIconBitmap = favoriteIconBitmap; + // Add the base search URL. + url = searchURL + encodedUrlString; } - // Create a folder icon byte array output stream. - ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream(); - - // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG). - folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream); + // Clear the focus from the URL edit text. Otherwise, proximate typing in the box will retain the colorized formatting instead of being reset during refocus. + urlEditText.clearFocus(); - // Convert the folder icon byte array stream to a byte array. - byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray(); + // Make it so. + loadUrl(url); + } - // Move all the bookmarks down one in the display order. - for (int i = 0; i < bookmarksListView.getCount(); i++) { - int databaseId = (int) bookmarksListView.getItemIdAtPosition(i); - bookmarksDatabaseHelper.updateDisplayOrder(databaseId, i + 1); - } + private void loadUrl(String url) { + // Sanitize the URL. + url = sanitizeUrl(url); - // Create the folder, which will be placed at the top of the `ListView`. - bookmarksDatabaseHelper.createFolder(folderNameString, currentBookmarksFolder, folderIconByteArray); + // Apply the domain settings. + applyDomainSettings(currentWebView, url, true, false); - // Update the bookmarks cursor with the current contents of this folder. - bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder); + // Load the URL. + currentWebView.loadUrl(url, customHeaders); + } - // Update the `ListView`. - bookmarksCursorAdapter.changeCursor(bookmarksCursor); + public void findPreviousOnPage(View view) { + // Go to the previous highlighted phrase on the page. `false` goes backwards instead of forwards. + currentWebView.findNext(false); + } - // Scroll to the new folder. - bookmarksListView.setSelection(0); + public void findNextOnPage(View view) { + // Go to the next highlighted phrase on the page. `true` goes forwards instead of backwards. + currentWebView.findNext(true); } - @Override - public void onSaveBookmark(DialogFragment dialogFragment, int selectedBookmarkDatabaseId, Bitmap favoriteIconBitmap) { - // Get handles for the views from `dialogFragment`. - EditText editBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_name_edittext); - EditText editBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_url_edittext); - RadioButton currentBookmarkIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_current_icon_radiobutton); + public void closeFindOnPage(View view) { + // Get a handle for the views. + Toolbar toolbar = findViewById(R.id.toolbar); + LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout); + EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext); - // Store the bookmark strings. - String bookmarkNameString = editBookmarkNameEditText.getText().toString(); - String bookmarkUrlString = editBookmarkUrlEditText.getText().toString(); + // Delete the contents of `find_on_page_edittext`. + findOnPageEditText.setText(null); - // Update the bookmark. - if (currentBookmarkIconRadioButton.isChecked()) { // Update the bookmark without changing the favorite icon. - bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString); - } else { // Update the bookmark using the `WebView` favorite icon. - // Create a favorite icon byte array output stream. - ByteArrayOutputStream newFavoriteIconByteArrayOutputStream = new ByteArrayOutputStream(); + // Clear the highlighted phrases if the WebView is not null. + if (currentWebView != null) { + currentWebView.clearMatches(); + } - // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG). - favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFavoriteIconByteArrayOutputStream); + // Hide the find on page linear layout. + findOnPageLinearLayout.setVisibility(View.GONE); - // Convert the favorite icon byte array stream to a byte array. - byte[] newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray(); + // Show the toolbar. + toolbar.setVisibility(View.VISIBLE); - // Update the bookmark and the favorite icon. - bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, newFavoriteIconByteArray); - } + // Get a handle for the input method manager. + InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - // Update the bookmarks cursor with the current contents of this folder. - bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder); + // Remove the lint warning below that the input method manager might be null. + assert inputMethodManager != null; - // Update the list view. - bookmarksCursorAdapter.changeCursor(bookmarksCursor); + // Hide the keyboard. + inputMethodManager.hideSoftInputFromWindow(toolbar.getWindowToken(), 0); } - @Override - public void onSaveBookmarkFolder(DialogFragment dialogFragment, int selectedFolderDatabaseId, Bitmap favoriteIconBitmap) { - // Get handles for the views from `dialogFragment`. - EditText editFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_folder_name_edittext); - RadioButton currentFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_current_icon_radiobutton); - RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_radiobutton); - ImageView defaultFolderIconImageView = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_imageview); - - // Get the new folder name. - String newFolderNameString = editFolderNameEditText.getText().toString(); + private void applyAppSettings() { + // Initialize the app if this is the first run. This is done here instead of in `onCreate()` to shorten the time that an unthemed background is displayed on app startup. + if (webViewDefaultUserAgent == null) { + initializeApp(); + } - // 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 shared preferences. + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - // Populate the new folder icon bitmap. - if (defaultFolderIconRadioButton.isChecked()) { - // Get the default folder icon drawable. - Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable(); + // Store the values from the shared preferences in variables. + incognitoModeEnabled = sharedPreferences.getBoolean("incognito_mode", false); + boolean doNotTrackEnabled = sharedPreferences.getBoolean("do_not_track", false); + sanitizeGoogleAnalytics = sharedPreferences.getBoolean("google_analytics", true); + sanitizeFacebookClickIds = sharedPreferences.getBoolean("facebook_click_ids", true); + sanitizeTwitterAmpRedirects = sharedPreferences.getBoolean("twitter_amp_redirects", true); + proxyThroughOrbot = sharedPreferences.getBoolean("proxy_through_orbot", false); + fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean("full_screen_browsing_mode", false); + hideAppBar = sharedPreferences.getBoolean("hide_app_bar", true); + scrollAppBar = sharedPreferences.getBoolean("scroll_app_bar", true); - // Convert the folder icon drawable to a bitmap drawable. - BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable; + // Get handles for the views that need to be modified. + FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout); + AppBarLayout appBarLayout = findViewById(R.id.appbar_layout); + ActionBar actionBar = getSupportActionBar(); + Toolbar toolbar = findViewById(R.id.toolbar); + LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout); + LinearLayout tabsLinearLayout = findViewById(R.id.tabs_linearlayout); + SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout); - // Convert the folder icon bitmap drawable to a bitmap. - folderIconBitmap = folderIconBitmapDrawable.getBitmap(); - } else { // Use the `WebView` favorite icon. - // Copy the favorite icon bitmap to the folder icon bitmap. - folderIconBitmap = favoriteIconBitmap; - } + // Remove the incorrect lint warning below that the action bar might be null. + assert actionBar != null; - // Create a folder icon byte array output stream. - ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream(); + // Apply the proxy through Orbot settings. + applyProxyThroughOrbot(false); - // 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 Do Not Track status. + if (doNotTrackEnabled) { + customHeaders.put("DNT", "1"); + } else { + customHeaders.remove("DNT"); + } - // Convert the folder icon byte array stream to a byte array. - byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray(); + // Get the current layout parameters. Using coordinator layout parameters allows the `setBehavior()` command and using app bar layout parameters allows the `setScrollFlags()` command. + CoordinatorLayout.LayoutParams swipeRefreshLayoutParams = (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams(); + AppBarLayout.LayoutParams toolbarLayoutParams = (AppBarLayout.LayoutParams) toolbar.getLayoutParams(); + AppBarLayout.LayoutParams findOnPageLayoutParams = (AppBarLayout.LayoutParams) findOnPageLinearLayout.getLayoutParams(); + AppBarLayout.LayoutParams tabsLayoutParams = (AppBarLayout.LayoutParams) tabsLinearLayout.getLayoutParams(); - // 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(); + // Add the scrolling behavior to the layout parameters. + if (scrollAppBar) { + // Enable scrolling of the app bar. + swipeRefreshLayoutParams.setBehavior(new AppBarLayout.ScrollingViewBehavior()); + toolbarLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP); + findOnPageLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP); + tabsLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP); + } else { + // Disable scrolling of the app bar. + swipeRefreshLayoutParams.setBehavior(null); + toolbarLayoutParams.setScrollFlags(0); + findOnPageLayoutParams.setScrollFlags(0); + tabsLayoutParams.setScrollFlags(0); - // Convert the folder icon drawable to a bitmap drawable. - BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable; + // Expand the app bar if it is currently collapsed. + appBarLayout.setExpanded(true); + } - // Convert the folder icon bitmap drawable to a bitmap. - folderIconBitmap = folderIconBitmapDrawable.getBitmap(); - } else { // Use the `WebView` favorite icon. - // Copy the favorite icon bitmap to the folder icon bitmap. - folderIconBitmap = favoriteIconBitmap; - } + // Apply the modified layout parameters. + swipeRefreshLayout.setLayoutParams(swipeRefreshLayoutParams); + toolbar.setLayoutParams(toolbarLayoutParams); + findOnPageLinearLayout.setLayoutParams(findOnPageLayoutParams); + tabsLinearLayout.setLayoutParams(tabsLayoutParams); - // Create a folder icon byte array output stream. - ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream(); + // Set the app bar scrolling for each WebView. + for (int i = 0; i < webViewPagerAdapter.getCount(); i++) { + // Get the WebView tab fragment. + WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i); - // 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); + // Get the fragment view. + View fragmentView = webViewTabFragment.getView(); - // Convert the folder icon byte array stream to a byte array. - byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray(); + // Only modify the WebViews if they exist. + if (fragmentView != null) { + // Get the nested scroll WebView from the tab fragment. + NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview); - // Update the folder name and icon in the database. - bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, newFolderIconByteArray); + // Set the app bar scrolling. + nestedScrollWebView.setNestedScrollingEnabled(scrollAppBar); + } } - // Update the bookmarks cursor with the current contents of this folder. - bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder); - - // Update the `ListView`. - bookmarksCursorAdapter.changeCursor(bookmarksCursor); - } + // Update the full screen browsing mode settings. + if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode. + // Update the visibility of the app bar, which might have changed in the settings. + if (hideAppBar) { + // Hide the tab linear layout. + tabsLinearLayout.setVisibility(View.GONE); - @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; + // Hide the action bar. + actionBar.hide(); + } else { + // Show the tab linear layout. + tabsLinearLayout.setVisibility(View.VISIBLE); - 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; - } - } + // Show the action bar. + actionBar.show(); + } - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - // Get a handle for the fragment manager. - FragmentManager fragmentManager = getSupportFragmentManager(); + // Hide the banner ad in the free flavor. + if (BuildConfig.FLAVOR.contentEquals("free")) { + AdHelper.hideAd(findViewById(R.id.adview)); + } - 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); + // 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); - // 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)); - } + /* 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; - // Reset the download variables. - downloadUrl = ""; - downloadContentDisposition = ""; - downloadContentLength = 0; - break; + // Show the tab linear layout. + tabsLinearLayout.setVisibility(View.VISIBLE); - 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); + // Show the action bar. + actionBar.show(); - // 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)); - } + // Show the banner ad in the free flavor. + if (BuildConfig.FLAVOR.contentEquals("free")) { + // Initialize the ads. If this isn't the first run, `loadAd()` will be automatically called instead. + AdHelper.initializeAds(findViewById(R.id.adview), getApplicationContext(), getSupportFragmentManager(), getString(R.string.google_app_id), getString(R.string.ad_unit_id)); + } - // Reset the image URL variable. - downloadImageUrl = ""; - break; + // 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); } } - @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 initializeApp() { + // Get a handle for the shared preferences. + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - // Parse `imageUrl`. - DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(imageUrl)); + // Get the theme preference. + boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false); - // Get a handle for the cookie manager. - CookieManager cookieManager = CookieManager.getInstance(); + // Get a handle for the input method. + InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - // Pass cookies to download manager if cookies are enabled. This is required to download images from websites that require a login. - // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner . - if (cookieManager.acceptCookie()) { - // Get the cookies for `imageUrl`. - String cookies = cookieManager.getCookie(imageUrl); + // Remove the lint warning below that the input method manager might be null. + assert inputMethodManager != null; - // Add the cookies to `downloadRequest`. In the HTTP request header, cookies are named `Cookie`. - downloadRequest.addRequestHeader("Cookie", cookies); - } + // Initialize the foreground color spans for highlighting the URLs. We have to use the deprecated `getColor()` until API >= 23. + redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_a700)); + initialGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500)); + finalGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500)); - // Get the file name from the dialog fragment. - EditText downloadImageNameEditText = dialogFragment.getDialog().findViewById(R.id.download_image_name); - String imageName = downloadImageNameEditText.getText().toString(); + // Get handles for the URL views. + EditText urlEditText = findViewById(R.id.url_edittext); - // 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); + // Remove the formatting from the URL edit text when the user is editing the text. + urlEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> { + if (hasFocus) { // The user is editing the URL text box. + // Remove the highlighting. + urlEditText.getText().removeSpan(redColorSpan); + urlEditText.getText().removeSpan(initialGrayColorSpan); + urlEditText.getText().removeSpan(finalGrayColorSpan); + } else { // The user has stopped editing the URL text box. + // Move to the beginning of the string. + urlEditText.setSelection(0); + + // Reapply the highlighting. + highlightUrlText(); } + }); - // Allow `MediaScanner` to index the download if it is a media file. - downloadRequest.allowScanningByMediaScanner(); + // Set the go button on the keyboard to load the URL in `urlTextBox`. + urlEditText.setOnKeyListener((View v, int keyCode, KeyEvent event) -> { + // If the event is a key-down event on the `enter` button, load the URL. + if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { + // Load the URL into the mainWebView and consume the event. + loadUrlFromTextBox(); - // Add the URL as the description for the download. - downloadRequest.setDescription(imageUrl); + // If the enter key was pressed, consume the event. + return true; + } else { + // If any other key was pressed, do not consume the event. + return false; + } + }); - // Show the download notification after the download is completed. - downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); + // Initialize the Orbot status and the waiting for Orbot trackers. + orbotStatus = "unknown"; + waitingForOrbot = false; - // Remove the lint warning below that `downloadManager` might be `null`. - assert downloadManager != null; + // Create an Orbot status `BroadcastReceiver`. + orbotStatusBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + // Store the content of the status message in `orbotStatus`. + orbotStatus = intent.getStringExtra("org.torproject.android.intent.extra.STATUS"); - // 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(); - } - } + // If Privacy Browser is waiting on Orbot, load the website now that Orbot is connected. + if (orbotStatus.equals("ON") && waitingForOrbot) { + // Reset the waiting for Orbot status. + waitingForOrbot = false; - @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); + // Get the intent that started the app. + Intent launchingIntent = getIntent(); - // Parse `downloadUrl`. - DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(downloadUrl)); + // Get the information from the intent. + String launchingIntentAction = launchingIntent.getAction(); + Uri launchingIntentUriData = launchingIntent.getData(); - // Get a handle for the cookie manager. - CookieManager cookieManager = CookieManager.getInstance(); + // 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; - // Pass cookies to download manager if cookies are enabled. This is required to download files from websites that require a login. - // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner . - if (cookieManager.acceptCookie()) { - // Get the cookies for `downloadUrl`. - String cookies = cookieManager.getCookie(downloadUrl); + // Sanitize the search input and convert it to a search. + try { + encodedUrlString = URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8"); + } catch (UnsupportedEncodingException exception) { + encodedUrlString = ""; + } - // Add the cookies to `downloadRequest`. In the HTTP request header, cookies are named `Cookie`. - downloadRequest.addRequestHeader("Cookie", cookies); + // Load the completed search URL. + loadUrl(searchURL + encodedUrlString); + } else if (launchingIntentUriData != null){ // Check to see if the intent contains a new URL. + // Load the URL from the intent. + loadUrl(launchingIntentUriData.toString()); + } else { // The is no URL in the intent. + // Select the homepage based on the proxy through Orbot status. + if (proxyThroughOrbot) { + // Load the Tor homepage. + loadUrl(sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value))); + } else { + // Load the normal homepage. + loadUrl(sharedPreferences.getString("homepage", getString(R.string.homepage_default_value))); + } + } + } } + }; - // Get the file name from the dialog fragment. - EditText downloadFileNameEditText = dialogFragment.getDialog().findViewById(R.id.download_file_name); - String fileName = downloadFileNameEditText.getText().toString(); + // Register `orbotStatusBroadcastReceiver` on `this` context. + this.registerReceiver(orbotStatusBroadcastReceiver, new IntentFilter("org.torproject.android.intent.action.STATUS")); - // 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 handles for views that need to be modified. + DrawerLayout drawerLayout = findViewById(R.id.drawerlayout); + NavigationView navigationView = findViewById(R.id.navigationview); + TabLayout tabLayout = findViewById(R.id.tablayout); + SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout); + ViewPager webViewPager = findViewById(R.id.webviewpager); + ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview); + FloatingActionButton launchBookmarksActivityFab = findViewById(R.id.launch_bookmarks_activity_fab); + FloatingActionButton createBookmarkFolderFab = findViewById(R.id.create_bookmark_folder_fab); + FloatingActionButton createBookmarkFab = findViewById(R.id.create_bookmark_fab); + EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext); + + // Listen for touches on the navigation menu. + navigationView.setNavigationItemSelectedListener(this); + + // Get handles for the navigation menu and the back and forward menu items. The menu is zero-based. + Menu navigationMenu = navigationView.getMenu(); + MenuItem navigationBackMenuItem = navigationMenu.getItem(2); + MenuItem navigationForwardMenuItem = navigationMenu.getItem(3); + MenuItem navigationHistoryMenuItem = navigationMenu.getItem(4); + MenuItem navigationRequestsMenuItem = navigationMenu.getItem(5); + + // 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. } - // Allow `MediaScanner` to index the download if it is a media file. - downloadRequest.allowScanningByMediaScanner(); - - // Add the URL as the description for the download. - downloadRequest.setDescription(downloadUrl); + @Override + public void onPageSelected(int position) { + // Close the find on page bar if it is open. + closeFindOnPage(null); - // Show the download notification after the download is completed. - downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); + // Set the current WebView. + setCurrentWebView(position); - // Remove the lint warning below that `downloadManager` might be `null`. - assert downloadManager != null; + // Select the corresponding tab if it does not match the currently selected page. This will happen if the page was scrolled via swiping in the view pager or by creating a new tab. + if (tabLayout.getSelectedTabPosition() != position) { + // Create a handler to select the tab. + Handler selectTabHandler = new Handler(); - // 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(); - } - } + // Create a runnable to select the tab. + Runnable selectTabRunnable = () -> { + // Get a handle for the tab. + TabLayout.Tab tab = tabLayout.getTabAt(position); - // Override `onBackPressed` to handle the navigation drawer and and the WebView. - @Override - public void onBackPressed() { - // Get a handle for the drawer layout and the tab layout. - DrawerLayout drawerLayout = findViewById(R.id.drawerlayout); - TabLayout tabLayout = findViewById(R.id.tablayout); + // Assert that the tab is not null. + assert tab != null; - 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); + // Select the tab. + tab.select(); + }; - // Load the new folder. - loadBookmarksFolder(); + // Select the tab layout after 150 milliseconds, which leaves enough time for a new tab to be inflated. + selectTabHandler.postDelayed(selectTabRunnable, 150); + } } - } else if (currentWebView.canGoBack()) { // There is at least one item in the current WebView history. - // Reset the current domain name so that navigation works if third-party requests are blocked. - currentWebView.resetCurrentDomainName(); - // Set navigating history so that the domain settings are applied when the new URL is loaded. - currentWebView.setNavigatingHistory(true); + @Override + public void onPageScrollStateChanged(int state) { + // Do nothing. + } + }); - // Go back. - currentWebView.goBack(); - } else if (tabLayout.getTabCount() > 1) { // There are at least two tabs. - // Close the current tab. - closeCurrentTab(); - } else { // There isn't anything to do in Privacy Browser. - // Run the default commands. - super.onBackPressed(); + // 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()); + } - // Manually kill Privacy Browser. Otherwise, it is glitchy when restarted. - System.exit(0); - } - } + @Override + public void onTabUnselected(TabLayout.Tab tab) { + // Do nothing. + } - // 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)); - } - } + @Override + public void onTabReselected(TabLayout.Tab tab) { + // Instantiate the View SSL Certificate dialog. + DialogFragment viewSslCertificateDialogFragment = ViewSslCertificateDialog.displayDialog(currentWebView.getWebViewFragmentId()); - private void loadUrlFromTextBox() { - // Get a handle for the URL edit text. - EditText urlEditText = findViewById(R.id.url_edittext); + // Display the View SSL Certificate dialog. + viewSslCertificateDialogFragment.show(getSupportFragmentManager(), getString(R.string.view_ssl_certificate)); + } + }); - // Get the text from urlTextBox and convert it to a string. trim() removes white spaces from the beginning and end of the string. - String unformattedUrlString = urlEditText.getText().toString().trim(); + // Set the bookmarks drawer resources according to the theme. This can't be done in the layout due to compatibility issues with the `DrawerLayout` support widget. + // The deprecated `getResources().getDrawable()` must be used until the minimum API >= 21 and and `getResources().getColor()` must be used until the minimum API >= 23. + if (darkTheme) { + launchBookmarksActivityFab.setImageDrawable(getResources().getDrawable(R.drawable.bookmarks_dark)); + createBookmarkFolderFab.setImageDrawable(getResources().getDrawable(R.drawable.create_folder_dark)); + createBookmarkFab.setImageDrawable(getResources().getDrawable(R.drawable.create_bookmark_dark)); + bookmarksListView.setBackgroundColor(getResources().getColor(R.color.gray_850)); + } else { + launchBookmarksActivityFab.setImageDrawable(getResources().getDrawable(R.drawable.bookmarks_light)); + createBookmarkFolderFab.setImageDrawable(getResources().getDrawable(R.drawable.create_folder_light)); + createBookmarkFab.setImageDrawable(getResources().getDrawable(R.drawable.create_bookmark_light)); + bookmarksListView.setBackgroundColor(getResources().getColor(R.color.white)); + } - // Initialize the formatted URL string. - String url = ""; + // Set the launch bookmarks activity FAB to launch the bookmarks activity. + launchBookmarksActivityFab.setOnClickListener(v -> { + // Get a copy of the favorite icon bitmap. + Bitmap favoriteIconBitmap = currentWebView.getFavoriteOrDefaultIcon(); - // Check to see if `unformattedUrlString` is a valid URL. Otherwise, convert it into a search. - if (unformattedUrlString.startsWith("content://")) { // This is a Content URL. - // Load the entire content URL. - url = unformattedUrlString; - } else if (Patterns.WEB_URL.matcher(unformattedUrlString).matches() || unformattedUrlString.startsWith("http://") || unformattedUrlString.startsWith("https://") || - unformattedUrlString.startsWith("file://")) { // This is a standard URL. - // Add `https://` at the beginning if there is no protocol. Otherwise the app will segfault. - if (!unformattedUrlString.startsWith("http") && !unformattedUrlString.startsWith("file://") && !unformattedUrlString.startsWith("content://")) { - unformattedUrlString = "https://" + unformattedUrlString; - } + // Create a favorite icon byte array output stream. + ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream(); - // Initialize `unformattedUrl`. - URL unformattedUrl = null; + // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG). + favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream); - // Convert `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(); - } + // Convert the favorite icon byte array stream to a byte array. + byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray(); - // 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; + // Create an intent to launch the bookmarks activity. + Intent bookmarksIntent = new Intent(getApplicationContext(), BookmarksActivity.class); - // Build the URI. - Uri.Builder uri = new Uri.Builder(); - uri.scheme(scheme).authority(authority).path(path).query(query).fragment(fragment); + // Add the extra information to the intent. + bookmarksIntent.putExtra("current_url", currentWebView.getUrl()); + bookmarksIntent.putExtra("current_title", currentWebView.getTitle()); + bookmarksIntent.putExtra("current_folder", currentBookmarksFolder); + bookmarksIntent.putExtra("favorite_icon_byte_array", favoriteIconByteArray); - // Decode the URI as a UTF-8 string in. - try { - url = URLDecoder.decode(uri.build().toString(), "UTF-8"); - } catch (UnsupportedEncodingException exception) { - // Do nothing. The formatted URL string will remain blank. - } - } else if (!unformattedUrlString.isEmpty()){ // This is not a URL, but rather a search string. - // Create an encoded URL String. - String encodedUrlString; + // Make it so. + startActivity(bookmarksIntent); + }); - // Sanitize the search input. - try { - encodedUrlString = URLEncoder.encode(unformattedUrlString, "UTF-8"); - } catch (UnsupportedEncodingException exception) { - encodedUrlString = ""; - } + // Set the create new bookmark folder FAB to display an alert dialog. + createBookmarkFolderFab.setOnClickListener(v -> { + // Create a create bookmark folder dialog. + DialogFragment createBookmarkFolderDialog = CreateBookmarkFolderDialog.createBookmarkFolder(currentWebView.getFavoriteOrDefaultIcon()); - // Add the base search URL. - url = searchURL + encodedUrlString; - } + // Show the create bookmark folder dialog. + createBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.create_folder)); + }); - // Clear the focus from the URL edit text. Otherwise, proximate typing in the box will retain the colorized formatting instead of being reset during refocus. - urlEditText.clearFocus(); + // Set the create new bookmark FAB to display an alert dialog. + createBookmarkFab.setOnClickListener(view -> { + // Instantiate the create bookmark dialog. + DialogFragment createBookmarkDialog = CreateBookmarkDialog.createBookmark(currentWebView.getUrl(), currentWebView.getTitle(), currentWebView.getFavoriteOrDefaultIcon()); - // Make it so. - loadUrl(url); - } + // Display the create bookmark dialog. + createBookmarkDialog.show(getSupportFragmentManager(), getString(R.string.create_bookmark)); + }); - private void loadUrl(String url) { - // Sanitize the URL. - url = sanitizeUrl(url); + // Search for the string on the page whenever a character changes in the `findOnPageEditText`. + findOnPageEditText.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // Do nothing. + } - // Apply the domain settings. - applyDomainSettings(currentWebView, url, true, false); + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + // Do nothing. + } - // Load the URL. - currentWebView.loadUrl(url, customHeaders); - } + @Override + public void afterTextChanged(Editable s) { + // Search for the text in the WebView if it is not null. Sometimes on resume after a period of non-use the WebView will be null. + if (currentWebView != null) { + currentWebView.findAllAsync(findOnPageEditText.getText().toString()); + } + } + }); - public void findPreviousOnPage(View view) { - // Go to the previous highlighted phrase on the page. `false` goes backwards instead of forwards. - currentWebView.findNext(false); - } + // Set the `check mark` button for the `findOnPageEditText` keyboard to close the soft keyboard. + findOnPageEditText.setOnKeyListener((v, keyCode, event) -> { + if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { // The `enter` key was pressed. + // Hide the soft keyboard. + inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0); - public void findNextOnPage(View view) { - // Go to the next highlighted phrase on the page. `true` goes forwards instead of backwards. - currentWebView.findNext(true); - } + // Consume the event. + return true; + } else { // A different key was pressed. + // Do not consume the event. + return false; + } + }); - public void closeFindOnPage(View view) { - // Get a handle for the views. - Toolbar toolbar = findViewById(R.id.toolbar); - LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout); - EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext); + // Implement swipe to refresh. + swipeRefreshLayout.setOnRefreshListener(() -> currentWebView.reload()); - // Delete the contents of `find_on_page_edittext`. - findOnPageEditText.setText(null); + // Store the default progress view offsets for use later in `initializeWebView()`. + defaultProgressViewStartOffset = swipeRefreshLayout.getProgressViewStartOffset(); + defaultProgressViewEndOffset = swipeRefreshLayout.getProgressViewEndOffset(); - // Clear the highlighted phrases if the WebView is not null. - if (currentWebView != null) { - currentWebView.clearMatches(); + // Set the swipe to refresh color according to the theme. + if (darkTheme) { + swipeRefreshLayout.setColorSchemeResources(R.color.blue_800); + swipeRefreshLayout.setProgressBackgroundColorSchemeResource(R.color.gray_850); + } else { + swipeRefreshLayout.setColorSchemeResources(R.color.blue_500); } - // Hide the find on page linear layout. - findOnPageLinearLayout.setVisibility(View.GONE); + // `DrawerTitle` identifies the `DrawerLayouts` in accessibility mode. + drawerLayout.setDrawerTitle(GravityCompat.START, getString(R.string.navigation_drawer)); + drawerLayout.setDrawerTitle(GravityCompat.END, getString(R.string.bookmarks)); - // Show the toolbar. - toolbar.setVisibility(View.VISIBLE); + // 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); - // Get a handle for the input method manager. - InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + // Initialize `currentBookmarksFolder`. `""` is the home folder in the database. + currentBookmarksFolder = ""; - // Remove the lint warning below that the input method manager might be null. - assert inputMethodManager != null; + // Load the home folder, which is `""` in the database. + loadBookmarksFolder(); - // Hide the keyboard. - inputMethodManager.hideSoftInputFromWindow(toolbar.getWindowToken(), 0); - } + bookmarksListView.setOnItemClickListener((parent, view, position, id) -> { + // Convert the id from long to int to match the format of the bookmarks database. + int databaseID = (int) id; - private void applyAppSettings() { - // Get a handle for the shared preferences. - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + // Get the bookmark cursor for this ID and move it to the first row. + Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseID); + bookmarkCursor.moveToFirst(); - // Store the values from the shared preferences in variables. - incognitoModeEnabled = sharedPreferences.getBoolean("incognito_mode", false); - boolean doNotTrackEnabled = sharedPreferences.getBoolean("do_not_track", false); - sanitizeGoogleAnalytics = sharedPreferences.getBoolean("google_analytics", true); - sanitizeFacebookClickIds = sharedPreferences.getBoolean("facebook_click_ids", true); - sanitizeTwitterAmpRedirects = sharedPreferences.getBoolean("twitter_amp_redirects", true); - proxyThroughOrbot = sharedPreferences.getBoolean("proxy_through_orbot", false); - fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean("full_screen_browsing_mode", false); - hideAppBar = sharedPreferences.getBoolean("hide_app_bar", true); - scrollAppBar = sharedPreferences.getBoolean("scroll_app_bar", true); + // Act upon the bookmark according to the type. + if (bookmarkCursor.getInt(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) { // The selected bookmark is a folder. + // Store the new folder name in `currentBookmarksFolder`. + currentBookmarksFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME)); - // Get handles for the views that need to be modified. - FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout); - AppBarLayout appBarLayout = findViewById(R.id.appbar_layout); - ActionBar actionBar = getSupportActionBar(); - Toolbar toolbar = findViewById(R.id.toolbar); - LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout); - LinearLayout tabsLinearLayout = findViewById(R.id.tabs_linearlayout); - SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout); + // Load the new folder. + loadBookmarksFolder(); + } else { // The selected bookmark is not a folder. + // Load the bookmark URL. + loadUrl(bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_URL))); - // Remove the incorrect lint warning below that the action bar might be null. - assert actionBar != null; + // Close the bookmarks drawer. + drawerLayout.closeDrawer(GravityCompat.END); + } - // Apply the proxy through Orbot settings. - applyProxyThroughOrbot(false); + // Close the `Cursor`. + bookmarkCursor.close(); + }); - // Set Do Not Track status. - if (doNotTrackEnabled) { - customHeaders.put("DNT", "1"); - } else { - customHeaders.remove("DNT"); - } + bookmarksListView.setOnItemLongClickListener((parent, view, position, id) -> { + // Convert the database ID from `long` to `int`. + int databaseId = (int) id; - // Get the current layout parameters. Using coordinator layout parameters allows the `setBehavior()` command and using app bar layout parameters allows the `setScrollFlags()` command. - CoordinatorLayout.LayoutParams swipeRefreshLayoutParams = (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams(); - AppBarLayout.LayoutParams toolbarLayoutParams = (AppBarLayout.LayoutParams) toolbar.getLayoutParams(); - AppBarLayout.LayoutParams findOnPageLayoutParams = (AppBarLayout.LayoutParams) findOnPageLinearLayout.getLayoutParams(); - AppBarLayout.LayoutParams tabsLayoutParams = (AppBarLayout.LayoutParams) tabsLinearLayout.getLayoutParams(); + // Find out if the selected bookmark is a folder. + boolean isFolder = bookmarksDatabaseHelper.isFolder(databaseId); - // Add the scrolling behavior to the layout parameters. - if (scrollAppBar) { - // Enable scrolling of the app bar. - swipeRefreshLayoutParams.setBehavior(new AppBarLayout.ScrollingViewBehavior()); - toolbarLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP); - findOnPageLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP); - tabsLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP); - } else { - // Disable scrolling of the app bar. - swipeRefreshLayoutParams.setBehavior(null); - toolbarLayoutParams.setScrollFlags(0); - findOnPageLayoutParams.setScrollFlags(0); - tabsLayoutParams.setScrollFlags(0); + if (isFolder) { + // Save the current folder name, which is used in `onSaveEditBookmarkFolder()`. + oldFolderNameString = bookmarksCursor.getString(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME)); - // Expand the app bar if it is currently collapsed. - appBarLayout.setExpanded(true); - } + // Show the edit bookmark folder `AlertDialog` and name the instance `@string/edit_folder`. + DialogFragment editBookmarkFolderDialog = EditBookmarkFolderDialog.folderDatabaseId(databaseId, currentWebView.getFavoriteOrDefaultIcon()); + editBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.edit_folder)); + } else { + // Show the edit bookmark `AlertDialog` and name the instance `@string/edit_bookmark`. + DialogFragment editBookmarkDialog = EditBookmarkDialog.bookmarkDatabaseId(databaseId, currentWebView.getFavoriteOrDefaultIcon()); + editBookmarkDialog.show(getSupportFragmentManager(), getString(R.string.edit_bookmark)); + } - // Apply the modified layout parameters. - swipeRefreshLayout.setLayoutParams(swipeRefreshLayoutParams); - toolbar.setLayoutParams(toolbarLayoutParams); - findOnPageLinearLayout.setLayoutParams(findOnPageLayoutParams); - tabsLinearLayout.setLayoutParams(tabsLayoutParams); + // Consume the event. + return true; + }); - // Set the app bar scrolling for each WebView. - for (int i = 0; i < webViewPagerAdapter.getCount(); i++) { - // Get the WebView tab fragment. - WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i); + // Get the status bar pixel size. + int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android"); + int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId); - // Get the fragment view. - View fragmentView = webViewTabFragment.getView(); + // Get the resource density. + float screenDensity = getResources().getDisplayMetrics().density; - // Only modify the WebViews if they exist. - if (fragmentView != null) { - // Get the nested scroll WebView from the tab fragment. - NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview); + // Calculate the drawer header padding. This is used to move the text in the drawer headers below any cutouts. + drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity); + drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity); + drawerHeaderPaddingBottom = (int) (8 * screenDensity); - // Set the app bar scrolling. - nestedScrollWebView.setNestedScrollingEnabled(scrollAppBar); + // The drawer listener is used to update the navigation menu.` + drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() { + @Override + public void onDrawerSlide(@NonNull View drawerView, float slideOffset) { } - } - - // Update the full screen browsing mode settings. - if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode. - // Update the visibility of the app bar, which might have changed in the settings. - if (hideAppBar) { - // Hide the tab linear layout. - tabsLinearLayout.setVisibility(View.GONE); - - // Hide the action bar. - actionBar.hide(); - } else { - // Show the tab linear layout. - tabsLinearLayout.setVisibility(View.VISIBLE); - // Show the action bar. - actionBar.show(); + @Override + public void onDrawerOpened(@NonNull View drawerView) { } - // Hide the banner ad in the free flavor. - if (BuildConfig.FLAVOR.contentEquals("free")) { - AdHelper.hideAd(findViewById(R.id.adview)); + @Override + public void onDrawerClosed(@NonNull View drawerView) { } - // 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 onDrawerStateChanged(int newState) { + if ((newState == DrawerLayout.STATE_SETTLING) || (newState == DrawerLayout.STATE_DRAGGING)) { // A drawer is opening or closing. + // Get handles for the drawer headers. + TextView navigationHeaderTextView = findViewById(R.id.navigationText); + TextView bookmarksHeaderTextView = findViewById(R.id.bookmarks_title_textview); - /* 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; + // Apply the navigation header paddings if the view is not null (sometimes it is null if another activity has already started). This moves the text in the header below any cutouts. + if (navigationHeaderTextView != null) { + navigationHeaderTextView.setPadding(drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingTop, drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingBottom); + } - // Show the tab linear layout. - tabsLinearLayout.setVisibility(View.VISIBLE); + // Apply the bookmarks header paddings if the view is not null (sometimes it is null if another activity has already started). This moves the text in the header below any cutouts. + if (bookmarksHeaderTextView != null) { + bookmarksHeaderTextView.setPadding(drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingTop, drawerHeaderPaddingLeftAndRight, drawerHeaderPaddingBottom); + } - // Show the action bar. - actionBar.show(); + // Update the navigation menu items if the WebView is not null. + if (currentWebView != null) { + navigationBackMenuItem.setEnabled(currentWebView.canGoBack()); + navigationForwardMenuItem.setEnabled(currentWebView.canGoForward()); + navigationHistoryMenuItem.setEnabled((currentWebView.canGoBack() || currentWebView.canGoForward())); + navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); - // Show the banner ad in the free flavor. - if (BuildConfig.FLAVOR.contentEquals("free")) { - // Initialize the ads. If this isn't the first run, `loadAd()` will be automatically called instead. - AdHelper.initializeAds(findViewById(R.id.adview), getApplicationContext(), getSupportFragmentManager(), getString(R.string.google_app_id), getString(R.string.ad_unit_id)); + // Hide the keyboard (if displayed). + inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0); + } + + // Clear the focus from from the URL text box and the WebView. This removes any text selection markers and context menus, which otherwise draw above the open drawers. + urlEditText.clearFocus(); + currentWebView.clearFocus(); + } } + }); - // Remove the `SYSTEM_UI` flags from the root frame layout. - rootFrameLayout.setSystemUiVisibility(0); + // 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", ""); - // Add the translucent status flag. - getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); - } - } + // Inflate a bare WebView to get the default user agent. It is not used to render content on the screen. + @SuppressLint("InflateParams") View webViewLayout = getLayoutInflater().inflate(R.layout.bare_webview, null, false); + + // Get a handle for the WebView. + WebView bareWebView = webViewLayout.findViewById(R.id.bare_webview); + + // Store the default user agent. + webViewDefaultUserAgent = bareWebView.getSettings().getUserAgentString(); + // Destroy the bare WebView. + bareWebView.destroy(); + } // `reloadWebsite` is used if returning from the Domains activity. Otherwise JavaScript might not function correctly if it is newly enabled. @SuppressLint("SetJavaScriptEnabled") -- 2.45.2