X-Git-Url: https://gitweb.stoutner.com/?p=PrivacyBrowserAndroid.git;a=blobdiff_plain;f=app%2Fsrc%2Fmain%2Fjava%2Fcom%2Fstoutner%2Fprivacybrowser%2Factivities%2FMainWebViewActivity.java;h=52d5f69998a21541177f67a634083bbd7795f0ec;hp=6eeeb253b25a1492d0b689133b7220167886bd1a;hb=32847917d4847968feaff721118cdd8bbc12661d;hpb=044e469923b042c6f019083166d8a543aa5edee3 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 6eeeb253..52d5f699 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java @@ -21,7 +21,6 @@ package com.stoutner.privacybrowser.activities; -import android.Manifest; import android.annotation.SuppressLint; import android.app.Activity; import android.app.Dialog; @@ -31,7 +30,6 @@ import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; import android.content.ClipData; import android.content.ClipboardManager; -import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -50,11 +48,13 @@ import android.net.http.SslError; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; +import android.os.Environment; import android.os.Handler; import android.os.Message; import android.preference.PreferenceManager; import android.print.PrintDocumentAdapter; import android.print.PrintManager; +import android.provider.DocumentsContract; import android.text.Editable; import android.text.Spanned; import android.text.TextWatcher; @@ -84,6 +84,7 @@ import android.webkit.WebView; import android.webkit.WebViewClient; import android.webkit.WebViewDatabase; import android.widget.ArrayAdapter; +import android.widget.CheckBox; import android.widget.CursorAdapter; import android.widget.EditText; import android.widget.FrameLayout; @@ -102,9 +103,6 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatDelegate; import androidx.appcompat.widget.Toolbar; import androidx.coordinatorlayout.widget.CoordinatorLayout; -import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; -import androidx.core.content.FileProvider; import androidx.core.content.res.ResourcesCompat; import androidx.core.view.GravityCompat; import androidx.drawerlayout.widget.DrawerLayout; @@ -140,7 +138,6 @@ import com.stoutner.privacybrowser.dialogs.ProxyNotInstalledDialog; import com.stoutner.privacybrowser.dialogs.PinnedMismatchDialog; import com.stoutner.privacybrowser.dialogs.SaveWebpageDialog; import com.stoutner.privacybrowser.dialogs.SslCertificateErrorDialog; -import com.stoutner.privacybrowser.dialogs.StoragePermissionDialog; import com.stoutner.privacybrowser.dialogs.UrlHistoryDialog; import com.stoutner.privacybrowser.dialogs.ViewSslCertificateDialog; import com.stoutner.privacybrowser.dialogs.WaitingForProxyDialog; @@ -149,21 +146,28 @@ import com.stoutner.privacybrowser.helpers.AdHelper; import com.stoutner.privacybrowser.helpers.BlocklistHelper; import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper; import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper; -import com.stoutner.privacybrowser.helpers.FileNameHelper; import com.stoutner.privacybrowser.helpers.ProxyHelper; import com.stoutner.privacybrowser.views.NestedScrollWebView; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.io.UnsupportedEncodingException; + import java.net.MalformedURLException; import java.net.URL; import java.net.URLDecoder; import java.net.URLEncoder; + import java.text.NumberFormat; + import java.util.ArrayList; +import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -176,8 +180,8 @@ import java.util.concurrent.Executors; public class MainWebViewActivity extends AppCompatActivity implements CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener, EditBookmarkFolderDialog.EditBookmarkFolderListener, FontSizeDialog.UpdateFontSizeListener, NavigationView.OnNavigationItemSelectedListener, OpenDialog.OpenListener, - PinnedMismatchDialog.PinnedMismatchListener, PopulateBlocklists.PopulateBlocklistsListener, SaveWebpageDialog.SaveWebpageListener, StoragePermissionDialog.StoragePermissionDialogListener, - UrlHistoryDialog.NavigateHistoryListener, WebViewTabFragment.NewTabListener { + PinnedMismatchDialog.PinnedMismatchListener, PopulateBlocklists.PopulateBlocklistsListener, SaveWebpageDialog.SaveWebpageListener, UrlHistoryDialog.NavigateHistoryListener, + WebViewTabFragment.NewTabListener { // The executor service handles background tasks. It is accessed from `ViewSourceActivity`. public static ExecutorService executorService = Executors.newFixedThreadPool(4); @@ -203,10 +207,10 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook public final static int DOMAINS_WEBVIEW_DEFAULT_USER_AGENT = 2; public final static int DOMAINS_CUSTOM_USER_AGENT = 13; - // Start activity for result request codes. The public static entries are accessed from `OpenDialog()` and `SaveWebpageDialog()`. - public final static int BROWSE_OPEN_REQUEST_CODE = 0; - public final static int BROWSE_SAVE_WEBPAGE_REQUEST_CODE = 1; - private final int BROWSE_FILE_UPLOAD_REQUEST_CODE = 2; + // Define the start activity for result request codes. The public static entries are accessed from `OpenDialog()` and `SaveWebpageDialog()`. + private final int BROWSE_FILE_UPLOAD_REQUEST_CODE = 0; + public final static int BROWSE_OPEN_REQUEST_CODE = 1; + public final static int BROWSE_SAVE_WEBPAGE_REQUEST_CODE = 2; // The proxy mode is public static so it can be accessed from `ProxyHelper()`. // It is also used in `onRestart()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `applyAppSettings()`, and `applyProxy()`. @@ -247,39 +251,21 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook private ArrayList> ultraList; private ArrayList> ultraPrivacy; - // `webViewDefaultUserAgent` is used in `onCreate()` and `onPrepareOptionsMenu()`. + // Declare the class variables + private BroadcastReceiver orbotStatusBroadcastReceiver; private String webViewDefaultUserAgent; - - // The incognito mode is set in `applyAppSettings()` and used in `initializeWebView()`. private boolean incognitoModeEnabled; - - // The full screen browsing mode tracker is set it `applyAppSettings()` and used in `initializeWebView()`. private boolean fullScreenBrowsingModeEnabled; - - // `inFullScreenBrowsingMode` is used in `onCreate()`, `onConfigurationChanged()`, and `applyAppSettings()`. private boolean inFullScreenBrowsingMode; - - // The app bar trackers are set in `applyAppSettings()` and used in `initializeWebView()`. + private boolean downloadWithExternalApp; private boolean hideAppBar; private boolean scrollAppBar; - - // The loading new intent tracker is set in `onNewIntent()` and used in `setCurrentWebView()`. + private boolean bottomAppBar; private boolean loadingNewIntent; - - // `reapplyDomainSettingsOnRestart` is used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, and `onAddDomain()`, . private boolean reapplyDomainSettingsOnRestart; - - // `reapplyAppSettingsOnRestart` is used in `onNavigationItemSelected()` and `onRestart()`. private boolean reapplyAppSettingsOnRestart; - - // `displayingFullScreenVideo` is used in `onCreate()` and `onResume()`. private boolean displayingFullScreenVideo; - - // `orbotStatusBroadcastReceiver` is used in `onCreate()` and `onDestroy()`. - private BroadcastReceiver orbotStatusBroadcastReceiver; - - // The waiting for proxy boolean is used in `onResume()`, `initializeApp()` and `applyProxy()`. - private boolean waitingForProxy = false; + private boolean waitingForProxy; // The action bar drawer toggle is initialized in `onCreate()` and used in `onResume()`. private ActionBarDrawerToggle actionBarDrawerToggle; @@ -315,10 +301,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook private boolean sanitizeFacebookClickIds; private boolean sanitizeTwitterAmpRedirects; - // The file path strings are used in `onSaveWebpage()` and `onRequestPermissionResult()` - private String openFilePath; - private String saveWebpageUrl; - private String saveWebpageFilePath; + // Define the class variables. + private long lastScrollUpdate = 0; // Declare the class views. private FrameLayout rootFrameLayout; @@ -346,8 +330,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook private MenuItem navigationRequestsMenuItem; private MenuItem optionsPrivacyMenuItem; private MenuItem optionsRefreshMenuItem; - private MenuItem optionsFirstPartyCookiesMenuItem; - private MenuItem optionsThirdPartyCookiesMenuItem; + private MenuItem optionsCookiesMenuItem; private MenuItem optionsDomStorageMenuItem; private MenuItem optionsSaveFormDataMenuItem; private MenuItem optionsClearDataMenuItem; @@ -410,9 +393,10 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Get a handle for the shared preferences. SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - // Get the screenshot preference. + // Get the preferences. String appTheme = sharedPreferences.getString("app_theme", getString(R.string.app_theme_default_value)); boolean allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false); + bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false); // Get the theme entry values string array. String[] appThemeEntryValuesStringArray = getResources().getStringArray(R.array.app_theme_entry_values); @@ -448,7 +432,11 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook setTheme(R.style.PrivacyBrowser); // Set the content view. - setContentView(R.layout.main_framelayout); + if (bottomAppBar) { + setContentView(R.layout.main_framelayout_bottom_appbar); + } else { + setContentView(R.layout.main_framelayout_top_appbar); + } // Get handles for the views. rootFrameLayout = findViewById(R.id.root_framelayout); @@ -543,6 +531,15 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // 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 || intentStringExtra != null || isWebSearch) { + // Exit the full screen video if it is displayed. + if (displayingFullScreenVideo) { + // Exit full screen video mode. + exitFullScreenVideo(); + + // Reload the current WebView. Otherwise, it can display entirely black. + currentWebView.reload(); + } + // Get the shared preferences. SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); @@ -827,8 +824,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Get handles for the class menu items. optionsPrivacyMenuItem = menu.findItem(R.id.javascript); optionsRefreshMenuItem = menu.findItem(R.id.refresh); - optionsFirstPartyCookiesMenuItem = menu.findItem(R.id.first_party_cookies); - optionsThirdPartyCookiesMenuItem = menu.findItem(R.id.third_party_cookies); + optionsCookiesMenuItem = menu.findItem(R.id.cookies); optionsDomStorageMenuItem = menu.findItem(R.id.dom_storage); optionsSaveFormDataMenuItem = menu.findItem(R.id.save_form_data); // Form data can be removed once the minimum API >= 26. optionsClearDataMenuItem = menu.findItem(R.id.clear_data); @@ -876,9 +872,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Set the initial status of the privacy icons. `false` does not call `invalidateOptionsMenu` as the last step. updatePrivacyIcons(false); - // Only display third-party cookies if API >= 21 - optionsThirdPartyCookiesMenuItem.setVisible(Build.VERSION.SDK_INT >= 21); - // Only display the form data menu items if the API < 26. optionsSaveFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26); optionsClearFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26); @@ -902,11 +895,11 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook if (displayAdditionalAppBarIcons) { optionsRefreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); bookmarksMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); - optionsFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); + optionsCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); } else { //Do not display the additional icons. optionsRefreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); bookmarksMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); - optionsFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + optionsCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); } // Replace Refresh with Stop if a URL is already loading. @@ -980,15 +973,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook optionsUltraPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.ULTRAPRIVACY) + " - " + getString(R.string.ultraprivacy)); optionsBlockAllThirdPartyRequestsMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " + getString(R.string.block_all_third_party_requests)); - // Only modify third-party cookies if the API >= 21. - if (Build.VERSION.SDK_INT >= 21) { - // Set the status of the third-party cookies checkbox. - optionsThirdPartyCookiesMenuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView)); - - // Enable third-party cookies if first-party cookies are enabled. - optionsThirdPartyCookiesMenuItem.setEnabled(cookieManager.acceptCookie()); - } - // Enable DOM Storage if JavaScript is enabled. optionsDomStorageMenuItem.setEnabled(currentWebView.getSettings().getJavaScriptEnabled()); @@ -998,8 +982,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook } } - // Set the checked status of the first party cookies menu item. - optionsFirstPartyCookiesMenuItem.setChecked(cookieManager.acceptCookie()); + // Set the cookies menu item checked status. + optionsCookiesMenuItem.setChecked(cookieManager.acceptCookie()); // Enable Clear Cookies if there are any. optionsClearCookiesMenuItem.setEnabled(cookieManager.hasCookies()); @@ -1218,12 +1202,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Consume the event. return true; - } else if (menuItemId == R.id.first_party_cookies) { // First-party cookies. + } else if (menuItemId == R.id.cookies) { // Cookies. // Switch the first-party cookie status. cookieManager.setAcceptCookie(!cookieManager.acceptCookie()); - // Store the first-party cookie status. - currentWebView.setAcceptFirstPartyCookies(cookieManager.acceptCookie()); + // Store the cookie status. + currentWebView.setAcceptCookies(cookieManager.acceptCookie()); // Update the menu checkbox. menuItem.setChecked(cookieManager.acceptCookie()); @@ -1232,10 +1216,10 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook updatePrivacyIcons(true); // Display a snackbar. - if (cookieManager.acceptCookie()) { // First-party cookies are enabled. - Snackbar.make(webViewPager, R.string.first_party_cookies_enabled, Snackbar.LENGTH_SHORT).show(); + if (cookieManager.acceptCookie()) { // Cookies are enabled. + Snackbar.make(webViewPager, R.string.cookies_enabled, Snackbar.LENGTH_SHORT).show(); } else if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScript is still enabled. - Snackbar.make(webViewPager, R.string.first_party_cookies_disabled, Snackbar.LENGTH_SHORT).show(); + Snackbar.make(webViewPager, R.string.cookies_disabled, Snackbar.LENGTH_SHORT).show(); } else { // Privacy mode. Snackbar.make(webViewPager, R.string.privacy_mode, Snackbar.LENGTH_SHORT).show(); } @@ -1243,28 +1227,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Reload the current WebView. currentWebView.reload(); - // Consume the event. - return true; - } else if (menuItemId == R.id.third_party_cookies) { // Third-party cookies. - // Only act if the API >= 21. Otherwise, there are no third-party cookie controls. - if (Build.VERSION.SDK_INT >= 21) { - // Toggle the status of thirdPartyCookiesEnabled. - cookieManager.setAcceptThirdPartyCookies(currentWebView, !cookieManager.acceptThirdPartyCookies(currentWebView)); - - // Update the menu checkbox. - menuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView)); - - // Display a snackbar. - if (cookieManager.acceptThirdPartyCookies(currentWebView)) { - Snackbar.make(webViewPager, R.string.third_party_cookies_enabled, Snackbar.LENGTH_SHORT).show(); - } else { - Snackbar.make(webViewPager, R.string.third_party_cookies_disabled, Snackbar.LENGTH_SHORT).show(); - } - - // Reload the current WebView. - currentWebView.reload(); - } - // Consume the event. return true; } else if (menuItemId == R.id.dom_storage) { // DOM storage. @@ -1443,7 +1405,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Update the menu checkbox. menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST)); - // Update the staus of Fanboy's Social Blocking List. + // Update the status of Fanboy's Social Blocking List. optionsFanboysSocialBlockingListMenuItem.setEnabled(!currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST)); // Reload the current WebView. @@ -1756,30 +1718,34 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook PrintDocumentAdapter printDocumentAdapter = currentWebView.createPrintDocumentAdapter(); // Print the document. - printManager.print(getString(R.string.privacy_browser_web_page), printDocumentAdapter, null); + printManager.print(getString(R.string.privacy_browser_webpage), printDocumentAdapter, null); // Consume the event. return true; } else if (menuItemId == R.id.save_url) { // Save URL. - // Prepare the save dialog. The dialog will be displayed once the file size and the content disposition have been acquired. - new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(), - currentWebView.getAcceptFirstPartyCookies()).execute(currentWebView.getCurrentUrl()); + // Check the download preference. + if (downloadWithExternalApp) { // Download with an external app. + downloadUrlWithExternalApp(currentWebView.getCurrentUrl()); + } else { // Handle the download inside of Privacy Browser. + // Prepare the save dialog. The dialog will be displayed once the file size and the content disposition have been acquired. + new PrepareSaveDialog(this, this, getSupportFragmentManager(), SaveWebpageDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(), + currentWebView.getAcceptCookies()).execute(currentWebView.getCurrentUrl()); + } // Consume the event. return true; - } else if (menuItemId == R.id.save_archive) { // Save archive. + } else if (menuItemId == R.id.save_archive) { // Instantiate the save dialog. - DialogFragment saveArchiveFragment = SaveWebpageDialog.saveWebpage(StoragePermissionDialog.SAVE_ARCHIVE, null, null, getString(R.string.webpage_mht), null, + DialogFragment saveArchiveFragment = SaveWebpageDialog.saveWebpage(SaveWebpageDialog.SAVE_ARCHIVE, currentWebView.getCurrentUrl(), null, null, null, false); // Show the save dialog. It must be named `save_dialog` so that the file picker can update the file name. saveArchiveFragment.show(getSupportFragmentManager(), getString(R.string.save_dialog)); - // Consume the event. return true; } else if (menuItemId == R.id.save_image) { // Save image. // Instantiate the save dialog. - DialogFragment saveImageFragment = SaveWebpageDialog.saveWebpage(StoragePermissionDialog.SAVE_IMAGE, null, null, getString(R.string.webpage_png), null, + DialogFragment saveImageFragment = SaveWebpageDialog.saveWebpage(SaveWebpageDialog.SAVE_IMAGE, currentWebView.getCurrentUrl(), null, null, null, false); // Show the save dialog. It must be named `save_dialog` so that the file picker can update the file name. @@ -2037,14 +2003,50 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Make it so. startActivity(requestsIntent); } else if (menuItemId == R.id.downloads) { // Downloads. - // Launch the system Download Manager. - Intent downloadManagerIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS); + // Try the default system download manager. + try { + // Launch the default system Download Manager. + Intent defaultDownloadManagerIntent = 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); + // Launch as a new task so that the download manager and Privacy Browser show as separate windows in the recent tasks list. + defaultDownloadManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - // Make it so. - startActivity(downloadManagerIntent); + // Make it so. + startActivity(defaultDownloadManagerIntent); + } catch (Exception defaultDownloadManagerException) { + // Try a generic file manager. + try { + // Create a generic file manager intent. + Intent genericFileManagerIntent = new Intent(Intent.ACTION_VIEW); + + // Open the download directory. + genericFileManagerIntent.setDataAndType(Uri.parse(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString()), DocumentsContract.Document.MIME_TYPE_DIR); + + // Launch as a new task so that the file manager and Privacy Browser show as separate windows in the recent tasks list. + genericFileManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + // Make it so. + startActivity(genericFileManagerIntent); + } catch (Exception genericFileManagerException) { + // Try an alternate file manager. + try { + // Create an alternate file manager intent. + Intent alternateFileManagerIntent = new Intent(Intent.ACTION_VIEW); + + // Open the download directory. + alternateFileManagerIntent.setDataAndType(Uri.parse(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString()), "resource/folder"); + + // Launch as a new task so that the file manager and Privacy Browser show as separate windows in the recent tasks list. + alternateFileManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + // Open the alternate file manager. + startActivity(alternateFileManagerIntent); + } catch (Exception alternateFileManagerException) { + // Display a snackbar. + Snackbar.make(currentWebView, R.string.no_file_manager_detected, Snackbar.LENGTH_INDEFINITE).show(); + } + } + } } else if (menuItemId == R.id.domains) { // Domains. // Set the flag to reapply the domain settings on restart when returning from Domain Settings. reapplyDomainSettingsOnRestart = true; @@ -2126,7 +2128,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook ultraList.get(0).get(0)[0], ultraPrivacy.get(0).get(0)[0]}; // Add the blocklist versions to the intent. - aboutIntent.putExtra("blocklist_versions", blocklistVersions); + aboutIntent.putExtra(AboutActivity.BLOCKLIST_VERSIONS, blocklistVersions); // Make it so. startActivity(aboutIntent); @@ -2239,9 +2241,14 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Add a Save URL entry. menu.add(R.string.save_url).setOnMenuItemClickListener((MenuItem item) -> { - // Prepare the save dialog. The dialog will be displayed once the file size and the content disposition have been acquired. - new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(), - currentWebView.getAcceptFirstPartyCookies()).execute(linkUrl); + // Check the download preference. + if (downloadWithExternalApp) { // Download with an external app. + downloadUrlWithExternalApp(linkUrl); + } else { // Handle the download inside of Privacy Browser. + // Prepare the save dialog. The dialog will be displayed once the file size and the content disposition have been acquired. + new PrepareSaveDialog(this, this, getSupportFragmentManager(), SaveWebpageDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(), + currentWebView.getAcceptCookies()).execute(linkUrl); + } // Consume the event. return true; @@ -2306,9 +2313,14 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Add a Save Image entry. menu.add(R.string.save_image).setOnMenuItemClickListener((MenuItem item) -> { - // Prepare the save dialog. The dialog will be displayed once the file size and the content disposition have been acquired. - new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(), - currentWebView.getAcceptFirstPartyCookies()).execute(imageUrl); + // Check the download preference. + if (downloadWithExternalApp) { // Download with an external app. + downloadUrlWithExternalApp(imageUrl); + } else { // Handle the download inside of Privacy Browser. + // Prepare the save dialog. The dialog will be displayed once the file size and the content disposition have been acquired. + new PrepareSaveDialog(this, this, getSupportFragmentManager(), SaveWebpageDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(), + currentWebView.getAcceptCookies()).execute(imageUrl); + } // Consume the event. return true; @@ -2406,9 +2418,14 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Add a Save Image entry. menu.add(R.string.save_image).setOnMenuItemClickListener((MenuItem item) -> { - // Prepare the save dialog. The dialog will be displayed once the file size and the content disposition have been acquired. - new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(), - currentWebView.getAcceptFirstPartyCookies()).execute(imageUrl); + // Check the download preference. + if (downloadWithExternalApp) { // Download with an external app. + downloadUrlWithExternalApp(imageUrl); + } else { // Handle the download inside of Privacy Browser. + // Prepare the save dialog. The dialog will be displayed once the file size and the content disposition have been acquired. + new PrepareSaveDialog(this, this, getSupportFragmentManager(), SaveWebpageDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(), + currentWebView.getAcceptCookies()).execute(imageUrl); + } // Consume the event. return true; @@ -2428,9 +2445,14 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Add a Save URL entry. menu.add(R.string.save_url).setOnMenuItemClickListener((MenuItem item) -> { - // Prepare the save dialog. The dialog will be displayed once the file size and the content disposition have been acquired. - new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(), - currentWebView.getAcceptFirstPartyCookies()).execute(linkUrl); + // Check the download preference. + if (downloadWithExternalApp) { // Download with an external app. + downloadUrlWithExternalApp(linkUrl); + } else { // Handle the download inside of Privacy Browser. + // Prepare the save dialog. The dialog will be displayed once the file size and the content disposition have been acquired. + new PrepareSaveDialog(this, this, getSupportFragmentManager(), SaveWebpageDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(), + currentWebView.getAcceptCookies()).execute(linkUrl); + } // Consume the event. return true; @@ -2544,20 +2566,20 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook assert dialog != null; // Get handles for the views in the dialog fragment. - EditText createFolderNameEditText = dialog.findViewById(R.id.create_folder_name_edittext); - RadioButton defaultFolderIconRadioButton = dialog.findViewById(R.id.create_folder_default_icon_radiobutton); - ImageView folderIconImageView = dialog.findViewById(R.id.create_folder_default_icon); + EditText folderNameEditText = dialog.findViewById(R.id.folder_name_edittext); + RadioButton defaultIconRadioButton = dialog.findViewById(R.id.default_icon_radiobutton); + ImageView defaultIconImageView = dialog.findViewById(R.id.default_icon_imageview); // Get new folder name string. - String folderNameString = createFolderNameEditText.getText().toString(); + String folderNameString = folderNameEditText.getText().toString(); // Create a folder icon bitmap. Bitmap folderIconBitmap; // Set the folder icon bitmap according to the dialog. - if (defaultFolderIconRadioButton.isChecked()) { // Use the default folder icon. + if (defaultIconRadioButton.isChecked()) { // Use the default folder icon. // Get the default folder icon drawable. - Drawable folderIconDrawable = folderIconImageView.getDrawable(); + Drawable folderIconDrawable = defaultIconImageView.getDrawable(); // Convert the folder icon drawable to a bitmap drawable. BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable; @@ -2609,10 +2631,10 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook assert dialog != null; // Get handles for the views from the dialog. - EditText editFolderNameEditText = dialog.findViewById(R.id.edit_folder_name_edittext); - RadioButton currentFolderIconRadioButton = dialog.findViewById(R.id.edit_folder_current_icon_radiobutton); - RadioButton defaultFolderIconRadioButton = dialog.findViewById(R.id.edit_folder_default_icon_radiobutton); - ImageView defaultFolderIconImageView = dialog.findViewById(R.id.edit_folder_default_icon_imageview); + RadioButton currentFolderIconRadioButton = dialog.findViewById(R.id.current_icon_radiobutton); + RadioButton defaultFolderIconRadioButton = dialog.findViewById(R.id.default_icon_radiobutton); + ImageView defaultFolderIconImageView = dialog.findViewById(R.id.default_icon_imageview); + EditText editFolderNameEditText = dialog.findViewById(R.id.folder_name_edittext); // Get the new folder name. String newFolderNameString = editFolderNameEditText.getText().toString(); @@ -2652,7 +2674,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // 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`. + // Get the new folder icon bitmap. Bitmap folderIconBitmap; if (defaultFolderIconRadioButton.isChecked()) { // Get the default folder icon drawable. @@ -2699,62 +2721,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // close the bookmarks drawer. drawerLayout.closeDrawer(GravityCompat.END); } else if (displayingFullScreenVideo) { // A full screen video is shown. - // Re-enable the screen timeout. - fullScreenVideoFrameLayout.setKeepScreenOn(false); - - // Unset the full screen video flag. - displayingFullScreenVideo = false; - - // Remove all the views from the full screen video frame layout. - fullScreenVideoFrameLayout.removeAllViews(); - - // Hide the full screen video frame layout. - fullScreenVideoFrameLayout.setVisibility(View.GONE); - - // Enable the sliding drawers. - drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED); - - // Show the main content relative layout. - mainContentRelativeLayout.setVisibility(View.VISIBLE); - - // Apply the appropriate full screen mode flags. - if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode. - // Hide the banner ad in the free flavor. - if (BuildConfig.FLAVOR.contentEquals("free")) { - // Get a handle for the ad view. This cannot be a class variable because it changes with each ad load. - View adView = findViewById(R.id.adview); - - // Hide the banner ad. - AdHelper.hideAd(adView); - } - - /* 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); - - // Reload the website if the app bar is hidden. Otherwise, there is some bug in Android that causes the WebView to be entirely black. - if (hideAppBar) { - // Reload the WebView. - currentWebView.reload(); - } - } else { // Switch to normal viewing mode. - // Remove the `SYSTEM_UI` flags from the root frame layout. - rootFrameLayout.setSystemUiVisibility(0); - } - - // Reload the ad for the free flavor if not in full screen mode. - if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) { - // Get a handle for the ad view. This cannot be a class variable because it changes with each ad load. - View adView = findViewById(R.id.adview); - - // Reload the ad. `getContext()` can be used instead of `getActivity.getApplicationContext()` once the minimum API >= 23. - AdHelper.loadAd(adView, getApplicationContext(), this, getString(R.string.ad_unit_id)); - } + // Exit the full screen video. + exitFullScreenVideo(); } else if (currentWebView.canGoBack()) { // There is at least one item in the current WebView history. // Get the current web back forward list. WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList(); @@ -2799,76 +2767,66 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook } break; - case BROWSE_SAVE_WEBPAGE_REQUEST_CODE: + case BROWSE_OPEN_REQUEST_CODE: // Don't do anything if the user pressed back from the file picker. if (resultCode == Activity.RESULT_OK) { - // Get a handle for the save dialog fragment. - DialogFragment saveWebpageDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.save_dialog)); + // Get a handle for the open dialog fragment. + DialogFragment openDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.open)); // Only update the file name if the dialog still exists. - if (saveWebpageDialogFragment != null) { - // Get a handle for the save webpage dialog. - Dialog saveWebpageDialog = saveWebpageDialogFragment.getDialog(); + if (openDialogFragment != null) { + // Get a handle for the open dialog. + Dialog openDialog = openDialogFragment.getDialog(); // Remove the incorrect lint warning below that the dialog might be null. - assert saveWebpageDialog != null; + assert openDialog != null; // Get a handle for the file name edit text. - EditText fileNameEditText = saveWebpageDialog.findViewById(R.id.file_name_edittext); - TextView fileExistsWarningTextView = saveWebpageDialog.findViewById(R.id.file_exists_warning_textview); - - // Instantiate the file name helper. - FileNameHelper fileNameHelper = new FileNameHelper(); + EditText fileNameEditText = openDialog.findViewById(R.id.file_name_edittext); - // Get the file path if it isn't null. - if (returnedIntent.getData() != null) { - // Convert the file name URI to a file name path. - String fileNamePath = fileNameHelper.convertUriToFileNamePath(returnedIntent.getData()); + // Get the file name URI from the intent. + Uri fileNameUri = returnedIntent.getData(); - // Set the file name path as the text of the file name edit text. - fileNameEditText.setText(fileNamePath); + // Get the file name string from the URI. + String fileNameString = fileNameUri.toString(); - // Move the cursor to the end of the file name edit text. - fileNameEditText.setSelection(fileNamePath.length()); + // Set the file name text. + fileNameEditText.setText(fileNameString); - // Hide the file exists warning. - fileExistsWarningTextView.setVisibility(View.GONE); - } + // Move the cursor to the end of the file name edit text. + fileNameEditText.setSelection(fileNameString.length()); } } break; - case BROWSE_OPEN_REQUEST_CODE: + case BROWSE_SAVE_WEBPAGE_REQUEST_CODE: // Don't do anything if the user pressed back from the file picker. if (resultCode == Activity.RESULT_OK) { - // Get a handle for the open dialog fragment. - DialogFragment openDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.open)); + // Get a handle for the save dialog fragment. + DialogFragment saveWebpageDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.save_dialog)); // Only update the file name if the dialog still exists. - if (openDialogFragment != null) { - // Get a handle for the open dialog. - Dialog openDialog = openDialogFragment.getDialog(); + if (saveWebpageDialogFragment != null) { + // Get a handle for the save webpage dialog. + Dialog saveWebpageDialog = saveWebpageDialogFragment.getDialog(); // Remove the incorrect lint warning below that the dialog might be null. - assert openDialog != null; + assert saveWebpageDialog != null; // Get a handle for the file name edit text. - EditText fileNameEditText = openDialog.findViewById(R.id.file_name_edittext); + EditText fileNameEditText = saveWebpageDialog.findViewById(R.id.file_name_edittext); - // Instantiate the file name helper. - FileNameHelper fileNameHelper = new FileNameHelper(); + // Get the file name URI from the intent. + Uri fileNameUri = returnedIntent.getData(); - // Get the file path if it isn't null. - if (returnedIntent.getData() != null) { - // Convert the file name URI to a file name path. - String fileNamePath = fileNameHelper.convertUriToFileNamePath(returnedIntent.getData()); + // Get the file name string from the URI. + String fileNameString = fileNameUri.toString(); - // Set the file name path as the text of the file name edit text. - fileNameEditText.setText(fileNamePath); + // Set the file name text. + fileNameEditText.setText(fileNameString); - // Move the cursor to the end of the file name edit text. - fileNameEditText.setSelection(fileNamePath.length()); - } + // Move the cursor to the end of the file name edit text. + fileNameEditText.setSelection(fileNameString.length()); } } break; @@ -3026,209 +2984,167 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Remove the incorrect lint warning below that the dialog might be null. assert dialog != null; - // Get a handle for the file name edit text. + // Get handles for the views. EditText fileNameEditText = dialog.findViewById(R.id.file_name_edittext); + CheckBox mhtCheckBox = dialog.findViewById(R.id.mht_checkbox); // Get the file path string. - openFilePath = fileNameEditText.getText().toString(); + String openFilePath = fileNameEditText.getText().toString(); // Apply the domain settings. This resets the favorite icon and removes any domain settings. - applyDomainSettings(currentWebView, "file://" + openFilePath, true, false, false); + applyDomainSettings(currentWebView, openFilePath, true, false, false); - // Check to see if the storage permission is needed. - if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // The storage permission has been granted. - // Open the file. - currentWebView.loadUrl("file://" + openFilePath); - } else { // The storage permission has not been granted. - // Get the external private directory file. - File externalPrivateDirectoryFile = getExternalFilesDir(null); - - // Remove the incorrect lint error below that the file might be null. - assert externalPrivateDirectoryFile != null; - - // Get the external private directory string. - String externalPrivateDirectory = externalPrivateDirectoryFile.toString(); - - // Check to see if the file path is in the external private directory. - if (openFilePath.startsWith(externalPrivateDirectory)) { // the file path is in the external private directory. - // Open the file. - currentWebView.loadUrl("file://" + openFilePath); - } else { // The file path is in a public directory. - // Check if the user has previously denied the storage permission. - if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first. - // Instantiate the storage permission alert dialog. - DialogFragment storagePermissionDialogFragment = StoragePermissionDialog.displayDialog(StoragePermissionDialog.OPEN); - - // Show the storage permission alert dialog. The permission will be requested the the dialog is closed. - storagePermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.storage_permission)); - } else { // Show the permission request directly. - // Request the write external storage permission. The file will be opened when it finishes. - ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, StoragePermissionDialog.OPEN); + // Open the file according to the type. + if (mhtCheckBox.isChecked()) { // Force opening of an MHT file. + try { + // Get the MHT file input stream. + InputStream mhtFileInputStream = getContentResolver().openInputStream(Uri.parse(openFilePath)); + + // Create a temporary MHT file. + File temporaryMhtFile = File.createTempFile("temporary_mht_file", ".mht", getCacheDir()); + + // Get a file output stream for the temporary MHT file. + FileOutputStream temporaryMhtFileOutputStream = new FileOutputStream(temporaryMhtFile); + + // Create a transfer byte array. + byte[] transferByteArray = new byte[1024]; + + // Create an integer to track the number of bytes read. + int bytesRead; + + // Copy the temporary MHT file input stream to the MHT output stream. + while ((bytesRead = mhtFileInputStream.read(transferByteArray)) > 0) { + temporaryMhtFileOutputStream.write(transferByteArray, 0, bytesRead); } + + // Flush the temporary MHT file output stream. + temporaryMhtFileOutputStream.flush(); + + // Close the streams. + temporaryMhtFileOutputStream.close(); + mhtFileInputStream.close(); + + // Load the temporary MHT file. + currentWebView.loadUrl(temporaryMhtFile.toString()); + } catch (Exception exception) { + // Display a snackbar. + Snackbar.make(currentWebView, getString(R.string.error) + " " + exception.toString(), Snackbar.LENGTH_INDEFINITE).show(); } + } else { // Let the WebView handle opening of the file. + // Open the file. + currentWebView.loadUrl(openFilePath); } } - @Override - public void onSaveWebpage(int saveType, String originalUrlString, DialogFragment dialogFragment) { + private void downloadUrlWithExternalApp(String url) { + // Create a download intent. Not specifying the action type will display the maximum number of options. + Intent downloadIntent = new Intent(); + + // Set the URI and the mime type. + downloadIntent.setDataAndType(Uri.parse(url), "text/html"); + + // Flag the intent to open in a new task. + downloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + // Show the chooser. + startActivity(Intent.createChooser(downloadIntent, getString(R.string.download_with_external_app))); + } + + public void onSaveWebpage(int saveType, @NonNull String originalUrlString, DialogFragment dialogFragment) { // Get the dialog. Dialog dialog = dialogFragment.getDialog(); // Remove the incorrect lint warning below that the dialog might be null. assert dialog != null; - // Get a handle for the edit texts. - EditText dialogUrlEditText = dialog.findViewById(R.id.url_edittext); + // Get a handle for the file name edit text. EditText fileNameEditText = dialog.findViewById(R.id.file_name_edittext); - // Store the URL. - if ((originalUrlString != null) && originalUrlString.startsWith("data:")) { - // Save the original URL. - saveWebpageUrl = originalUrlString; - } else { - // Get the URL from the edit text, which may have been modified. - saveWebpageUrl = dialogUrlEditText.getText().toString(); - } - // Get the file path from the edit text. - saveWebpageFilePath = fileNameEditText.getText().toString(); - - // Check to see if the storage permission is needed. - if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // The storage permission has been granted. - //Save the webpage according to the save type. - switch (saveType) { - case StoragePermissionDialog.SAVE_URL: - // Save the URL. - new SaveUrl(this, this, saveWebpageFilePath, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptFirstPartyCookies()).execute(saveWebpageUrl); - break; - - case StoragePermissionDialog.SAVE_ARCHIVE: - // Save the webpage archive. - saveWebpageArchive(saveWebpageFilePath); - break; - - case StoragePermissionDialog.SAVE_IMAGE: - // Save the webpage image. - new SaveWebpageImage(this, this, saveWebpageFilePath, currentWebView).execute(); - break; - } + String saveWebpageFilePath = fileNameEditText.getText().toString(); - // Reset the strings. - saveWebpageUrl = ""; - saveWebpageFilePath = ""; - } else { // The storage permission has not been granted. - // Get the external private directory file. - File externalPrivateDirectoryFile = getExternalFilesDir(null); - - // Remove the incorrect lint error below that the file might be null. - assert externalPrivateDirectoryFile != null; - - // Get the external private directory string. - String externalPrivateDirectory = externalPrivateDirectoryFile.toString(); - - // Check to see if the file path is in the external private directory. - if (saveWebpageFilePath.startsWith(externalPrivateDirectory)) { // The file path is in the external private directory. - // Save the webpage according to the save type. - switch (saveType) { - case StoragePermissionDialog.SAVE_URL: - // Save the URL. - new SaveUrl(this, this, saveWebpageFilePath, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptFirstPartyCookies()).execute(saveWebpageUrl); - break; + //Save the webpage according to the save type. + switch (saveType) { + case SaveWebpageDialog.SAVE_URL: + // Get a handle for the dialog URL edit text. + EditText dialogUrlEditText = dialog.findViewById(R.id.url_edittext); - case StoragePermissionDialog.SAVE_ARCHIVE: - // Save the webpage archive. - saveWebpageArchive(saveWebpageFilePath); - break; + // Define the save webpage URL. + String saveWebpageUrl; - case StoragePermissionDialog.SAVE_IMAGE: - // Save the webpage image. - new SaveWebpageImage(this, this, saveWebpageFilePath, currentWebView).execute(); - break; + // Store the URL. + if (originalUrlString.startsWith("data:")) { + // Save the original URL. + saveWebpageUrl = originalUrlString; + } else { + // Get the URL from the edit text, which may have been modified. + saveWebpageUrl = dialogUrlEditText.getText().toString(); } - // Reset the strings. - saveWebpageUrl = ""; - saveWebpageFilePath = ""; - } else { // The file path is in a public directory. - // Check if the user has previously denied the storage permission. - if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first. - // Instantiate the storage permission alert dialog. - DialogFragment storagePermissionDialogFragment = StoragePermissionDialog.displayDialog(saveType); - - // Show the storage permission alert dialog. The permission will be requested when the dialog is closed. - storagePermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.storage_permission)); - } else { // Show the permission request directly. - // Request the write external storage permission according to the save type. The URL will be saved when it finishes. - ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, saveType); - } - } - } - } + // Save the URL. + new SaveUrl(this, this, saveWebpageFilePath, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptCookies()).execute(saveWebpageUrl); + break; - @Override - public void onCloseStoragePermissionDialog(int requestType) { - // Request the write external storage permission according to the request type. The file will be opened when it finishes. - ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestType); + case SaveWebpageDialog.SAVE_ARCHIVE: + try { + // Create a temporary MHT file. + File temporaryMhtFile = File.createTempFile("temporary_mht_file", ".mht", getCacheDir()); - } + // Save the temporary MHT file. + currentWebView.saveWebArchive(temporaryMhtFile.toString(), false, callbackValue -> { + if (callbackValue != null) { // The temporary MHT file was saved successfully. + try { + // Create a temporary MHT file input stream. + FileInputStream temporaryMhtFileInputStream = new FileInputStream(temporaryMhtFile); - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - //Only process the results if they exist (this method is triggered when a dialog is presented the first time for an app, but no grant results are included). - if (grantResults.length > 0) { - switch (requestCode) { - case StoragePermissionDialog.OPEN: - // Check to see if the storage permission was granted. If the dialog was canceled the grant results will be empty. - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { // The storage permission was granted. - // Load the file. - currentWebView.loadUrl("file://" + openFilePath); - } else { // The storage permission was not granted. - // Display an error snackbar. - Snackbar.make(currentWebView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show(); - } - break; - - case StoragePermissionDialog.SAVE_URL: - // Check to see if the storage permission was granted. If the dialog was canceled the grant results will be empty. - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { // The storage permission was granted. - // Save the raw URL. - new SaveUrl(this, this, saveWebpageFilePath, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptFirstPartyCookies()).execute(saveWebpageUrl); - } else { // The storage permission was not granted. - // Display an error snackbar. - Snackbar.make(currentWebView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show(); - } - break; - - case StoragePermissionDialog.SAVE_ARCHIVE: - // Check to see if the storage permission was granted. If the dialog was canceled the grant results will be empty. - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { // The storage permission was granted. - // Save the webpage archive. - saveWebpageArchive(saveWebpageFilePath); - } else { // The storage permission was not granted. - // Display an error snackbar. - Snackbar.make(currentWebView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show(); - } - break; - - case StoragePermissionDialog.SAVE_IMAGE: - // Check to see if the storage permission was granted. If the dialog was canceled the grant results will be empty. - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { // The storage permission was granted. - // Save the webpage image. - new SaveWebpageImage(this, this, saveWebpageFilePath, currentWebView).execute(); - } else { // The storage permission was not granted. - // Display an error snackbar. - Snackbar.make(currentWebView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show(); - } - break; - } + // Get an output stream for the save webpage file path. + OutputStream mhtOutputStream = getContentResolver().openOutputStream(Uri.parse(saveWebpageFilePath)); + + // Create a transfer byte array. + byte[] transferByteArray = new byte[1024]; + + // Create an integer to track the number of bytes read. + int bytesRead; + + // Copy the temporary MHT file input stream to the MHT output stream. + while ((bytesRead = temporaryMhtFileInputStream.read(transferByteArray)) > 0) { + mhtOutputStream.write(transferByteArray, 0, bytesRead); + } + + // Close the streams. + mhtOutputStream.close(); + temporaryMhtFileInputStream.close(); + + // Display a snackbar. + Snackbar.make(currentWebView, getString(R.string.file_saved) + " " + currentWebView.getCurrentUrl(), Snackbar.LENGTH_SHORT).show(); + } catch (Exception exception) { + // Display a snackbar with the exception. + Snackbar.make(currentWebView, getString(R.string.error_saving_file) + " " + exception.toString(), Snackbar.LENGTH_INDEFINITE).show(); + } finally { + // Delete the temporary MHT file. + //noinspection ResultOfMethodCallIgnored + temporaryMhtFile.delete(); + } + } else { // There was an unspecified error while saving the temporary MHT file. + // Display an error snackbar. + Snackbar.make(currentWebView, getString(R.string.error_saving_file), Snackbar.LENGTH_INDEFINITE).show(); + } + }); + } catch (IOException ioException) { + // Display a snackbar with the IO exception. + Snackbar.make(currentWebView, getString(R.string.error_saving_file) + " " + ioException.toString(), Snackbar.LENGTH_INDEFINITE).show(); + } + break; - // Reset the strings. - openFilePath = ""; - saveWebpageUrl = ""; - saveWebpageFilePath = ""; + case SaveWebpageDialog.SAVE_IMAGE: + // Save the webpage image. + new SaveWebpageImage(this, saveWebpageFilePath, currentWebView).execute(); + break; } } - + + // Remove the warning that `OnTouchListener()` needs to override `performClick()`, as the only purpose of setting the `OnTouchListener()` is to make it do nothing. + @SuppressLint("ClickableViewAccessibility") private void initializeApp() { // Get a handle for the input method. InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); @@ -3338,6 +3254,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook this.registerReceiver(orbotStatusBroadcastReceiver, new IntentFilter("org.torproject.android.intent.action.STATUS")); // Get handles for views that need to be modified. + LinearLayout bookmarksHeaderLinearLayout = findViewById(R.id.bookmarks_header_linearlayout); 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); @@ -3404,6 +3321,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook } }); + // Set a touch listener on the bookmarks header linear layout so that touches don't pass through to the button underneath. + bookmarksHeaderLinearLayout.setOnTouchListener((view, motionEvent) -> { + // Consume the touch. + return true; + }); + // Set the launch bookmarks activity FAB to launch the bookmarks activity. launchBookmarksActivityFab.setOnClickListener(v -> { // Get a copy of the favorite icon bitmap. @@ -3485,7 +3408,19 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook }); // Implement swipe to refresh. - swipeRefreshLayout.setOnRefreshListener(() -> currentWebView.reload()); + swipeRefreshLayout.setOnRefreshListener(() -> { + // Check the visibility of the bottom app bar. Sometimes it is hidden if the WebView is the same size as the visible screen. + if (bottomAppBar && scrollAppBar && (appBarLayout.getVisibility() == View.GONE)) { // The bottom app bar is currently hidden. + // Show the app bar. + appBarLayout.setVisibility(View.VISIBLE); + + // Disable the refreshing animation. + swipeRefreshLayout.setRefreshing(false); + } else { // A bottom app bar is not currently hidden. + // Reload the website. + currentWebView.reload(); + } + }); // Store the default progress view offsets for use later in `initializeWebView()`. defaultProgressViewStartOffset = swipeRefreshLayout.getProgressViewStartOffset(); @@ -3559,7 +3494,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Find out if the selected bookmark is a folder. boolean isFolder = bookmarksDatabaseHelper.isFolder(databaseId); - if (isFolder) { + // Check to see if the bookmark is a folder. + if (isFolder) { // The bookmark is a folder. // Save the current folder name, which is used in `onSaveEditBookmarkFolder()`. oldFolderNameString = bookmarksCursor.getString(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME)); @@ -3568,7 +3504,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Show the edit folder bookmark dialog. editBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.edit_folder)); - } else { + } else { // The bookmark is not a folder. // Get the bookmark cursor for this ID. Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseId); @@ -3577,6 +3513,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Load the bookmark in a new tab but do not switch to the tab or close the drawer. addNewTab(bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_URL)), false); + + // Display a snackbar. + Snackbar.make(currentWebView, R.string.bookmark_opened_in_background, Snackbar.LENGTH_SHORT).show(); } // Consume the event. @@ -3642,12 +3581,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // 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); proxyMode = sharedPreferences.getString("proxy", getString(R.string.proxy_default_value)); fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean("full_screen_browsing_mode", false); + downloadWithExternalApp = sharedPreferences.getBoolean(getString(R.string.download_with_external_app_key), false); hideAppBar = sharedPreferences.getBoolean("hide_app_bar", true); scrollAppBar = sharedPreferences.getBoolean("scroll_app_bar", true); @@ -3673,58 +3612,48 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Apply the proxy. applyProxy(false); - // Set Do Not Track status. - if (doNotTrackEnabled) { - customHeaders.put("DNT", "1"); - } else { - customHeaders.remove("DNT"); - } - - // Get the current layout parameters. Using coordinator layout parameters allows the `setBehavior()` command and using app bar layout parameters allows the `setScrollFlags()` command. - CoordinatorLayout.LayoutParams swipeRefreshLayoutParams = (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams(); - AppBarLayout.LayoutParams toolbarLayoutParams = (AppBarLayout.LayoutParams) toolbar.getLayoutParams(); - AppBarLayout.LayoutParams findOnPageLayoutParams = (AppBarLayout.LayoutParams) findOnPageLinearLayout.getLayoutParams(); - AppBarLayout.LayoutParams tabsLayoutParams = (AppBarLayout.LayoutParams) tabsLinearLayout.getLayoutParams(); - - // Add the scrolling behavior to the layout parameters. - if (scrollAppBar) { - // Enable scrolling of the app bar. - swipeRefreshLayoutParams.setBehavior(new AppBarLayout.ScrollingViewBehavior()); - toolbarLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP); - findOnPageLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP); - tabsLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP); - } else { - // Disable scrolling of the app bar. - swipeRefreshLayoutParams.setBehavior(null); - toolbarLayoutParams.setScrollFlags(0); - findOnPageLayoutParams.setScrollFlags(0); - tabsLayoutParams.setScrollFlags(0); - - // Expand the app bar if it is currently collapsed. - appBarLayout.setExpanded(true); - } - - // Apply the modified layout parameters. - swipeRefreshLayout.setLayoutParams(swipeRefreshLayoutParams); - toolbar.setLayoutParams(toolbarLayoutParams); - findOnPageLinearLayout.setLayoutParams(findOnPageLayoutParams); - tabsLinearLayout.setLayoutParams(tabsLayoutParams); + // Adjust the layout and scrolling parameters if the app bar is at the top of the screen. + if (!bottomAppBar) { + // Get the current layout parameters. Using coordinator layout parameters allows the `setBehavior()` command and using app bar layout parameters allows the `setScrollFlags()` command. + CoordinatorLayout.LayoutParams swipeRefreshLayoutParams = (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams(); + AppBarLayout.LayoutParams toolbarLayoutParams = (AppBarLayout.LayoutParams) toolbar.getLayoutParams(); + AppBarLayout.LayoutParams findOnPageLayoutParams = (AppBarLayout.LayoutParams) findOnPageLinearLayout.getLayoutParams(); + AppBarLayout.LayoutParams tabsLayoutParams = (AppBarLayout.LayoutParams) tabsLinearLayout.getLayoutParams(); + + // Add the scrolling behavior to the layout parameters. + if (scrollAppBar) { + // Enable scrolling of the app bar. + swipeRefreshLayoutParams.setBehavior(new AppBarLayout.ScrollingViewBehavior()); + toolbarLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP); + findOnPageLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP); + tabsLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP); + } else { + // Disable scrolling of the app bar. + swipeRefreshLayoutParams.setBehavior(null); + toolbarLayoutParams.setScrollFlags(0); + findOnPageLayoutParams.setScrollFlags(0); + tabsLayoutParams.setScrollFlags(0); + + // Expand the app bar if it is currently collapsed. + appBarLayout.setExpanded(true); + } - // 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); + // Set the app bar scrolling for each WebView. + for (int i = 0; i < webViewPagerAdapter.getCount(); i++) { + // Get the WebView tab fragment. + WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i); - // Get the fragment view. - View fragmentView = webViewTabFragment.getView(); + // Get the fragment view. + View fragmentView = webViewTabFragment.getView(); - // Only modify the WebViews if they exist. - if (fragmentView != null) { - // Get the nested scroll WebView from the tab fragment. - NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview); + // Only modify the WebViews if they exist. + if (fragmentView != null) { + // Get the nested scroll WebView from the tab fragment. + NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview); - // Set the app bar scrolling. - nestedScrollWebView.setNestedScrollingEnabled(scrollAppBar); + // Set the app bar scrolling. + nestedScrollWebView.setNestedScrollingEnabled(scrollAppBar); + } } } @@ -3788,7 +3717,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook } @Override - public void navigateHistory(String url, int steps) { + public void navigateHistory(@NonNull String url, int steps) { // Apply the domain settings. applyDomainSettings(currentWebView, url, false, false, false); @@ -3954,8 +3883,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Get the settings from the cursor. nestedScrollWebView.setDomainSettingsDatabaseId(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper._ID))); nestedScrollWebView.getSettings().setJavaScriptEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)) == 1); - nestedScrollWebView.setAcceptFirstPartyCookies(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FIRST_PARTY_COOKIES)) == 1); - boolean domainThirdPartyCookiesEnabled = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_THIRD_PARTY_COOKIES)) == 1); + nestedScrollWebView.setAcceptCookies(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.COOKIES)) == 1); nestedScrollWebView.getSettings().setDomStorageEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_DOM_STORAGE)) == 1); // Form data can be removed once the minimum API >= 26. boolean saveFormData = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FORM_DATA)) == 1); @@ -4025,12 +3953,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook } // Apply the cookie domain settings. - cookieManager.setAcceptCookie(nestedScrollWebView.getAcceptFirstPartyCookies()); - - // Set third-party cookies status if API >= 21. - if (Build.VERSION.SDK_INT >= 21) { - cookieManager.setAcceptThirdPartyCookies(nestedScrollWebView, domainThirdPartyCookiesEnabled); - } + cookieManager.setAcceptCookie(nestedScrollWebView.getAcceptCookies()); // Apply the form data setting if the API < 26. if (Build.VERSION.SDK_INT < 26) { @@ -4209,8 +4132,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook } else { // The new URL does not have custom domain settings. Load the defaults. // Store the values from the shared preferences. nestedScrollWebView.getSettings().setJavaScriptEnabled(sharedPreferences.getBoolean("javascript", false)); - nestedScrollWebView.setAcceptFirstPartyCookies(sharedPreferences.getBoolean("first_party_cookies", false)); - boolean defaultThirdPartyCookiesEnabled = sharedPreferences.getBoolean("third_party_cookies", false); + nestedScrollWebView.setAcceptCookies(sharedPreferences.getBoolean(getString(R.string.cookies_key), false)); nestedScrollWebView.getSettings().setDomStorageEnabled(sharedPreferences.getBoolean("dom_storage", false)); boolean saveFormData = sharedPreferences.getBoolean("save_form_data", false); // Form data can be removed once the minimum API >= 26. nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASYLIST, sharedPreferences.getBoolean("easylist", true)); @@ -4221,8 +4143,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRAPRIVACY, sharedPreferences.getBoolean("ultraprivacy", true)); nestedScrollWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS, sharedPreferences.getBoolean("block_all_third_party_requests", false)); - // Apply the default first-party cookie setting. - cookieManager.setAcceptCookie(nestedScrollWebView.getAcceptFirstPartyCookies()); + // Apply the default cookie setting. + cookieManager.setAcceptCookie(nestedScrollWebView.getAcceptCookies()); // Apply the default font size setting. try { @@ -4253,11 +4175,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Reset the pinned variables. nestedScrollWebView.setDomainSettingsDatabaseId(-1); - // Set third-party cookies status if API >= 21. - if (Build.VERSION.SDK_INT >= 21) { - cookieManager.setAcceptThirdPartyCookies(nestedScrollWebView, defaultThirdPartyCookiesEnabled); - } - // Get the array position of the user agent name. int userAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName); @@ -4417,7 +4334,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook PackageManager packageManager = getPackageManager(); // Check to see if I2P is in the list. This will throw an error and drop to the catch section if it isn't installed. - packageManager.getPackageInfo("org.torproject.android", 0); + packageManager.getPackageInfo("net.i2p.android.router", 0); } catch (PackageManager.NameNotFoundException exception) { // I2P is not installed. // Sow the I2P not installed dialog if it is not already displayed. if (getSupportFragmentManager().findFragmentByTag(getString(R.string.proxy_not_installed_dialog)) == null) { @@ -4468,7 +4385,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Update the privacy icon. if (currentWebView.getSettings().getJavaScriptEnabled()) { // JavaScript is enabled. optionsPrivacyMenuItem.setIcon(R.drawable.javascript_enabled); - } else if (currentWebView.getAcceptFirstPartyCookies()) { // JavaScript is disabled but cookies are enabled. + } else if (currentWebView.getAcceptCookies()) { // JavaScript is disabled but cookies are enabled. optionsPrivacyMenuItem.setIcon(R.drawable.warning); } else { // All the dangerous features are disabled. optionsPrivacyMenuItem.setIcon(R.drawable.privacy_mode); @@ -4477,14 +4394,14 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Get the current theme status. int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; - // Update the first-party cookies icon. - if (currentWebView.getAcceptFirstPartyCookies()) { // First-party cookies are enabled. - optionsFirstPartyCookiesMenuItem.setIcon(R.drawable.cookies_enabled); - } else { // First-party cookies are disabled. + // Update the cookies icon. + if (currentWebView.getAcceptCookies()) { // Cookies are enabled. + optionsCookiesMenuItem.setIcon(R.drawable.cookies_enabled); + } else { // Cookies are disabled. if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { - optionsFirstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_day); + optionsCookiesMenuItem.setIcon(R.drawable.cookies_disabled_day); } else { - optionsFirstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_night); + optionsCookiesMenuItem.setIcon(R.drawable.cookies_disabled_night); } } @@ -4574,11 +4491,11 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Update the bookmarks cursor with the contents of the bookmarks database for the current folder. bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder); - // Populate the bookmarks cursor adapter. `this` specifies the `Context`. `false` disables `autoRequery`. + // Populate the bookmarks cursor adapter. bookmarksCursorAdapter = new CursorAdapter(this, bookmarksCursor, false) { @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { - // Inflate the individual item layout. `false` does not attach it to the root. + // Inflate the individual item layout. return getLayoutInflater().inflate(R.layout.bookmarks_drawer_item_linearlayout, parent, false); } @@ -4872,46 +4789,66 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook } } - private void saveWebpageArchive(String filePath) { - // Save the webpage archive. - currentWebView.saveWebArchive(filePath); + private void exitFullScreenVideo() { + // Re-enable the screen timeout. + fullScreenVideoFrameLayout.setKeepScreenOn(false); - // Display a snackbar. - Snackbar saveWebpageArchiveSnackbar = Snackbar.make(currentWebView, getString(R.string.file_saved) + " " + filePath, Snackbar.LENGTH_SHORT); + // Unset the full screen video flag. + displayingFullScreenVideo = false; - // Add an open option to the snackbar. - saveWebpageArchiveSnackbar.setAction(R.string.open, (View view) -> { - // Get a file for the file name string. - File file = new File(filePath); + // Remove all the views from the full screen video frame layout. + fullScreenVideoFrameLayout.removeAllViews(); - // Declare a file URI variable. - Uri fileUri; + // Hide the full screen video frame layout. + fullScreenVideoFrameLayout.setVisibility(View.GONE); - // Get the URI for the file according to the Android version. - if (Build.VERSION.SDK_INT >= 24) { // Use a file provider. - fileUri = FileProvider.getUriForFile(this, getString(R.string.file_provider), file); - } else { // Get the raw file path URI. - fileUri = Uri.fromFile(file); - } + // Enable the sliding drawers. + drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED); - // Get a handle for the content resolver. - ContentResolver contentResolver = getContentResolver(); + // Show the main content relative layout. + mainContentRelativeLayout.setVisibility(View.VISIBLE); - // Create an open intent with `ACTION_VIEW`. - Intent openIntent = new Intent(Intent.ACTION_VIEW); + // Apply the appropriate full screen mode flags. + if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode. + // Hide the app bar if specified. + if (hideAppBar) { + // Hide the tab linear layout. + tabsLinearLayout.setVisibility(View.GONE); - // Set the URI and the MIME type. - openIntent.setDataAndType(fileUri, contentResolver.getType(fileUri)); + // Hide the action bar. + actionBar.hide(); + } - // Allow the app to read the file URI. - openIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + // Hide the banner ad in the free flavor. + if (BuildConfig.FLAVOR.contentEquals("free")) { + // Get a handle for the ad view. This cannot be a class variable because it changes with each ad load. + View adView = findViewById(R.id.adview); - // Show the chooser. - startActivity(Intent.createChooser(openIntent, getString(R.string.open))); - }); + // Hide the banner ad. + AdHelper.hideAd(adView); + } + + /* Hide the system bars. + * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen. + * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar. + * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen. + * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown. + */ + rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + } else { // Switch to normal viewing mode. + // Remove the `SYSTEM_UI` flags from the root frame layout. + rootFrameLayout.setSystemUiVisibility(0); + } + + // Reload the ad for the free flavor if not in full screen mode. + if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) { + // Get a handle for the ad view. This cannot be a class variable because it changes with each ad load. + View adView = findViewById(R.id.adview); - // Show the snackbar. - saveWebpageArchiveSnackbar.show(); + // Reload the ad. + AdHelper.loadAd(adView, this, this, getString(R.string.ad_unit_id)); + } } private void clearAndExit() { @@ -5147,8 +5084,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Get a handle for the cookie manager. CookieManager cookieManager = CookieManager.getInstance(); - // Set the first-party cookie status. - cookieManager.setAcceptCookie(currentWebView.getAcceptFirstPartyCookies()); + // Set the cookie status. + cookieManager.setAcceptCookie(currentWebView.getAcceptCookies()); // Update the privacy icons. `true` redraws the icons in the app bar. updatePrivacyIcons(true); @@ -5298,6 +5235,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Explicitly disable geolocation. nestedScrollWebView.getSettings().setGeolocationEnabled(false); + // Allow loading of file:// URLs. This is necessary for opening MHT web archives, which are copies into a temporary cache location. + nestedScrollWebView.getSettings().setAllowFileAccess(true); + // Create a double-tap gesture detector to toggle full-screen mode. GestureDetector doubleTapGestureDetector = new GestureDetector(getApplicationContext(), new GestureDetector.SimpleOnGestureListener() { // Override `onDoubleTap()`. All other events are handled using the default settings. @@ -5320,19 +5260,22 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Hide the action bar. actionBar.hide(); - // Check to see if the app bar is normally scrolled. - if (scrollAppBar) { // The app bar is scrolled when it is displayed. - // Get the swipe refresh layout parameters. - CoordinatorLayout.LayoutParams swipeRefreshLayoutParams = (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams(); - - // Remove the off-screen scrolling layout. - swipeRefreshLayoutParams.setBehavior(null); - } else { // The app bar is not scrolled when it is displayed. - // Remove the padding from the top of the swipe refresh layout. - swipeRefreshLayout.setPadding(0, 0, 0, 0); - - // The swipe refresh circle must be moved above the now removed status bar location. - swipeRefreshLayout.setProgressViewOffset(false, -200, defaultProgressViewEndOffset); + // Set layout and scrolling parameters if the app bar is at the top of the screen. + if (!bottomAppBar) { + // Check to see if the app bar is normally scrolled. + if (scrollAppBar) { // The app bar is scrolled when it is displayed. + // Get the swipe refresh layout parameters. + CoordinatorLayout.LayoutParams swipeRefreshLayoutParams = (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams(); + + // Remove the off-screen scrolling layout. + swipeRefreshLayoutParams.setBehavior(null); + } else { // The app bar is not scrolled when it is displayed. + // Remove the padding from the top of the swipe refresh layout. + swipeRefreshLayout.setPadding(0, 0, 0, 0); + + // The swipe refresh circle must be moved above the now removed status bar location. + swipeRefreshLayout.setProgressViewOffset(false, -200, defaultProgressViewEndOffset); + } } } @@ -5362,19 +5305,22 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Show the action bar. actionBar.show(); - // Check to see if the app bar is normally scrolled. - if (scrollAppBar) { // The app bar is scrolled when it is displayed. - // Get the swipe refresh layout parameters. - CoordinatorLayout.LayoutParams swipeRefreshLayoutParams = (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams(); - - // Add the off-screen scrolling layout. - swipeRefreshLayoutParams.setBehavior(new AppBarLayout.ScrollingViewBehavior()); - } else { // The app bar is not scrolled when it is displayed. - // The swipe refresh layout must be manually moved below the app bar layout. - swipeRefreshLayout.setPadding(0, appBarHeight, 0, 0); - - // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels. - swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10 + appBarHeight, defaultProgressViewEndOffset + appBarHeight); + // Set layout and scrolling parameters if the app bar is at the top of the screen. + if (!bottomAppBar) { + // Check to see if the app bar is normally scrolled. + if (scrollAppBar) { // The app bar is scrolled when it is displayed. + // Get the swipe refresh layout parameters. + CoordinatorLayout.LayoutParams swipeRefreshLayoutParams = (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams(); + + // Add the off-screen scrolling layout. + swipeRefreshLayoutParams.setBehavior(new AppBarLayout.ScrollingViewBehavior()); + } else { // The app bar is not scrolled when it is displayed. + // The swipe refresh layout must be manually moved below the app bar layout. + swipeRefreshLayout.setPadding(0, appBarHeight, 0, 0); + + // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels. + swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10 + appBarHeight, defaultProgressViewEndOffset + appBarHeight); + } } } @@ -5413,27 +5359,43 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Allow the downloading of files. nestedScrollWebView.setDownloadListener((String downloadUrl, String userAgent, String contentDisposition, String mimetype, long contentLength) -> { - // Define a formatted file size string. - String formattedFileSizeString; - - // Process the content length if it contains data. - if (contentLength > 0) { // The content length is greater than 0. - // Format the content length as a string. - formattedFileSizeString = NumberFormat.getInstance().format(contentLength) + " " + getString(R.string.bytes); - } else { // The content length is not greater than 0. - // Set the formatted file size string to be `unknown size`. - formattedFileSizeString = getString(R.string.unknown_size); - } + // Check the download preference. + if (downloadWithExternalApp) { // Download with an external app. + downloadUrlWithExternalApp(downloadUrl); + } else { // Handle the download inside of Privacy Browser. + // Define a formatted file size string. + String formattedFileSizeString; + + // Process the content length if it contains data. + if (contentLength > 0) { // The content length is greater than 0. + // Format the content length as a string. + formattedFileSizeString = NumberFormat.getInstance().format(contentLength) + " " + getString(R.string.bytes); + } else { // The content length is not greater than 0. + // Set the formatted file size string to be `unknown size`. + formattedFileSizeString = getString(R.string.unknown_size); + } - // Get the file name from the content disposition. - String fileNameString = PrepareSaveDialog.getFileNameFromHeaders(this, contentDisposition, mimetype, downloadUrl); + // Get the file name from the content disposition. + String fileNameString = PrepareSaveDialog.getFileNameFromHeaders(this, contentDisposition, mimetype, downloadUrl); - // Instantiate the save dialog. - DialogFragment saveDialogFragment = SaveWebpageDialog.saveWebpage(StoragePermissionDialog.SAVE_URL, downloadUrl, formattedFileSizeString, fileNameString, userAgent, - nestedScrollWebView.getAcceptFirstPartyCookies()); + // Prevent the dialog from displaying if the app window is not visible. + // The download listener continues to function even when the WebView is paused. Attempting to display a dialog in that state leads to a crash. + while (!activity.getWindow().isActive()) { + try { + // The window is not active. Wait 1 second. + wait(1000); + } catch (InterruptedException e) { + // Do nothing. + } + } - // Show the save dialog. It must be named `save_dialog` so that the file picker can update the file name. - saveDialogFragment.show(getSupportFragmentManager(), getString(R.string.save_dialog)); + // Instantiate the save dialog. + DialogFragment saveDialogFragment = SaveWebpageDialog.saveWebpage(SaveWebpageDialog.SAVE_URL, downloadUrl, formattedFileSizeString, fileNameString, userAgent, + nestedScrollWebView.getAcceptCookies()); + + // Show the save dialog. It must be named `save_dialog` so that the file picker can update the file name. + saveDialogFragment.show(getSupportFragmentManager(), getString(R.string.save_dialog)); + } }); // Update the find on page count. @@ -5462,7 +5424,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Update the status of swipe to refresh based on the scroll position of the nested scroll WebView. Also reinforce full screen browsing mode. // On API < 23, `getViewTreeObserver().addOnScrollChangedListener()` must be used, but it is a little bit buggy and appears to get garbage collected from time to time. if (Build.VERSION.SDK_INT >= 23) { - nestedScrollWebView.setOnScrollChangeListener((view, i, i1, i2, i3) -> { + nestedScrollWebView.setOnScrollChangeListener((view, scrollX, scrollY, oldScrollX, oldScrollY) -> { + // Set the swipe to refresh status. if (nestedScrollWebView.getSwipeToRefresh()) { // Only enable swipe to refresh if the WebView is scrolled to the top. swipeRefreshLayout.setEnabled(nestedScrollWebView.getScrollY() == 0); @@ -5471,6 +5434,18 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook swipeRefreshLayout.setEnabled(false); } + // Set the visibility of the bottom app bar. + if (bottomAppBar && scrollAppBar && (Calendar.getInstance().getTimeInMillis() - lastScrollUpdate > 100)) { + if (scrollY - oldScrollY > 25) { // The WebView was scrolled down. + appBarLayout.setVisibility(View.GONE); + } else if (scrollY - oldScrollY < -25) { // The WebView was scrolled up. + appBarLayout.setVisibility(View.VISIBLE); + } + + // Update the last scroll update variable. This prevents the app bar from flashing on and off at the bottom of the screen. + lastScrollUpdate = Calendar.getInstance().getTimeInMillis(); + } + // Reinforce the system UI visibility flags if in full screen browsing mode. // This hides the status and navigation bars, which are displayed if other elements are shown, like dialog boxes, the options menu, or the keyboard. if (inFullScreenBrowsingMode) { @@ -5494,7 +5469,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook swipeRefreshLayout.setEnabled(false); } - // Reinforce the system UI visibility flags if in full screen browsing mode. // This hides the status and navigation bars, which are displayed if other elements are shown, like dialog boxes, the options menu, or the keyboard. if (inFullScreenBrowsingMode) { @@ -5642,65 +5616,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Exit full screen video. @Override public void onHideCustomView() { - // Re-enable the screen timeout. - fullScreenVideoFrameLayout.setKeepScreenOn(false); - - // Unset the full screen video flag. - displayingFullScreenVideo = false; - - // Remove all the views from the full screen video frame layout. - fullScreenVideoFrameLayout.removeAllViews(); - - // Hide the full screen video frame layout. - fullScreenVideoFrameLayout.setVisibility(View.GONE); - - // Enable the sliding drawers. - drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED); - - // Show the main content relative layout. - mainContentRelativeLayout.setVisibility(View.VISIBLE); - - // Apply the appropriate full screen mode flags. - if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode. - // Hide the app bar if specified. - if (hideAppBar) { - // Hide the tab linear layout. - tabsLinearLayout.setVisibility(View.GONE); - - // Hide the action bar. - actionBar.hide(); - } - - // Hide the banner ad in the free flavor. - if (BuildConfig.FLAVOR.contentEquals("free")) { - // Get a handle for the ad view. This cannot be a class variable because it changes with each ad load. - View adView = findViewById(R.id.adview); - - // Hide the banner ad. - AdHelper.hideAd(adView); - } - - /* Hide the system bars. - * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen. - * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar. - * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen. - * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown. - */ - rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); - } else { // Switch to normal viewing mode. - // Remove the `SYSTEM_UI` flags from the root frame layout. - rootFrameLayout.setSystemUiVisibility(0); - } - - // Reload the ad for the free flavor if not in full screen mode. - if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) { - // Get a handle for the ad view. This cannot be a class variable because it changes with each ad load. - View adView = findViewById(R.id.adview); - - // Reload the ad. `getContext()` can be used instead of `getActivity.getApplicationContext()` once the minimum API >= 23. - AdHelper.loadAd(adView, getApplicationContext(), activity, getString(R.string.ad_unit_id)); - } + // Exit the full screen video. + exitFullScreenVideo(); } // Upload files. @@ -5929,7 +5846,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook String[] ultraListResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, ultraList); // Process the UltraList results. - if (ultraListResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) { // The resource request matched UltraLists's blacklist. + if (ultraListResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) { // The resource request matched UltraList's blacklist. // Add the result to the resource requests. nestedScrollWebView.addResourceRequest(new String[] {ultraListResults[0], ultraListResults[1], ultraListResults[2], ultraListResults[3], ultraListResults[4], ultraListResults[5]}); @@ -6183,25 +6100,25 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { - // Get the preferences. - boolean scrollAppBar = sharedPreferences.getBoolean("scroll_app_bar", true); - - // Set the top padding of the swipe refresh layout according to the app bar scrolling preference. This can't be done in `appAppSettings()` because the app bar is not yet populated there. - if (scrollAppBar || (inFullScreenBrowsingMode && hideAppBar)) { - // No padding is needed because it will automatically be placed below the app bar layout due to the scrolling layout behavior. - swipeRefreshLayout.setPadding(0, 0, 0, 0); - - // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels. - swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10, defaultProgressViewEndOffset); - } else { - // Get the app bar layout height. This can't be done in `applyAppSettings()` because the app bar is not yet populated there. - appBarHeight = appBarLayout.getHeight(); - - // The swipe refresh layout must be manually moved below the app bar layout. - swipeRefreshLayout.setPadding(0, appBarHeight, 0, 0); - - // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels. - swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10 + appBarHeight, defaultProgressViewEndOffset + appBarHeight); + // Set the padding and layout settings if the app bar is at the top of the screen. + if (!bottomAppBar) { + // Set the top padding of the swipe refresh layout according to the app bar scrolling preference. This can't be done in `appAppSettings()` because the app bar is not yet populated there. + if (scrollAppBar || (inFullScreenBrowsingMode && hideAppBar)) { + // No padding is needed because it will automatically be placed below the app bar layout due to the scrolling layout behavior. + swipeRefreshLayout.setPadding(0, 0, 0, 0); + + // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels. + swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10, defaultProgressViewEndOffset); + } else { + // Get the app bar layout height. This can't be done in `applyAppSettings()` because the app bar is not yet populated there. + appBarHeight = appBarLayout.getHeight(); + + // The swipe refresh layout must be manually moved below the app bar layout. + swipeRefreshLayout.setPadding(0, appBarHeight, 0, 0); + + // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels. + swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10 + appBarHeight, defaultProgressViewEndOffset + appBarHeight); + } } // Reset the list of resource requests. @@ -6218,7 +6135,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Display the formatted URL text. urlEditText.setText(url); - // Apply text highlighting to `urlTextBox`. + // Apply text highlighting to the URL text box. highlightUrlText(); // Hide the keyboard. @@ -6260,7 +6177,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook @Override public void onPageFinished(WebView view, String url) { // Flush any cookies to persistent storage. The cookie manager has become very lazy about flushing cookies in recent versions. - if (nestedScrollWebView.getAcceptFirstPartyCookies() && Build.VERSION.SDK_INT >= 21) { + if (nestedScrollWebView.getAcceptCookies() && Build.VERSION.SDK_INT >= 21) { CookieManager.getInstance().flush(); } @@ -6429,6 +6346,17 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Store the SSL error handler. nestedScrollWebView.setSslErrorHandler(handler); + // Prevent the dialog from displaying if the app window is not visible. + // The SSL error handler continues to function even when the WebView is paused. Attempting to display a dialog in that state leads to a crash. + while (!activity.getWindow().isActive()) { + try { + // The window is not active. Wait 1 second. + wait(1000); + } catch (InterruptedException e) { + // Do nothing. + } + } + // Instantiate an SSL certificate error alert dialog. DialogFragment sslCertificateErrorDialogFragment = SslCertificateErrorDialog.displayDialog(error, nestedScrollWebView.getWebViewFragmentId());