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=800b3b044b5c618c3a96e4376c2bbea784e700c5;hp=971eaad1fa5c36082d82d5878f5faaf459e16568;hb=641d15ace34579762580ed8297f324133354499b;hpb=23de13bf1815ffc666157997ca3bff6930e1762c 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 971eaad1..800b3b04 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; @@ -84,6 +82,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 +101,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 +136,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,14 +144,17 @@ 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; @@ -176,8 +174,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 +201,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()`. @@ -315,11 +313,6 @@ 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; - // Declare the class views. private FrameLayout rootFrameLayout; private DrawerLayout drawerLayout; @@ -1756,30 +1749,29 @@ 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(), + new PrepareSaveDialog(this, this, getSupportFragmentManager(), SaveWebpageDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptFirstPartyCookies()).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. @@ -2240,7 +2232,7 @@ 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(), + new PrepareSaveDialog(this, this, getSupportFragmentManager(), SaveWebpageDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptFirstPartyCookies()).execute(linkUrl); // Consume the event. @@ -2307,7 +2299,7 @@ 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(), + new PrepareSaveDialog(this, this, getSupportFragmentManager(), SaveWebpageDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptFirstPartyCookies()).execute(imageUrl); // Consume the event. @@ -2407,7 +2399,7 @@ 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(), + new PrepareSaveDialog(this, this, getSupportFragmentManager(), SaveWebpageDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptFirstPartyCookies()).execute(imageUrl); // Consume the event. @@ -2429,7 +2421,7 @@ 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(), + new PrepareSaveDialog(this, this, getSupportFragmentManager(), SaveWebpageDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptFirstPartyCookies()).execute(linkUrl); // Consume the event. @@ -2544,20 +2536,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 +2601,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 +2644,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. @@ -2799,76 +2791,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,206 +3008,149 @@ 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) { + 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.getAcceptFirstPartyCookies()).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; } } @@ -3595,6 +3520,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook @Override public void onDrawerClosed(@NonNull View drawerView) { + // Reset the drawer icon when the drawer is closed. Otherwise, it is an arrow if the drawer is open when the app is restarted. + actionBarDrawerToggle.syncState(); } @Override @@ -3786,7 +3713,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); @@ -4415,7 +4342,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) { @@ -4868,51 +4795,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook if (webViewPagerAdapter.deletePage(currentTabNumber, webViewPager)) { setCurrentWebView(currentTabNumber); } - - // Expand the app bar if it is currently collapsed. - appBarLayout.setExpanded(true); - } - - private void saveWebpageArchive(String filePath) { - // Save the webpage archive. - currentWebView.saveWebArchive(filePath); - - // Display a snackbar. - Snackbar saveWebpageArchiveSnackbar = Snackbar.make(currentWebView, getString(R.string.file_saved) + " " + filePath, Snackbar.LENGTH_SHORT); - - // 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); - - // Declare a file URI variable. - Uri fileUri; - - // 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); - } - - // Get a handle for the content resolver. - ContentResolver contentResolver = getContentResolver(); - - // Create an open intent with `ACTION_VIEW`. - Intent openIntent = new Intent(Intent.ACTION_VIEW); - - // Set the URI and the MIME type. - openIntent.setDataAndType(fileUri, contentResolver.getType(fileUri)); - - // Allow the app to read the file URI. - openIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - - // Show the chooser. - startActivity(Intent.createChooser(openIntent, getString(R.string.open))); - }); - - // Show the snackbar. - saveWebpageArchiveSnackbar.show(); } private void clearAndExit() { @@ -5299,6 +5181,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. @@ -5429,8 +5314,19 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Get the file name from the content disposition. String fileNameString = PrepareSaveDialog.getFileNameFromHeaders(this, contentDisposition, mimetype, downloadUrl); + // 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. + } + } + // Instantiate the save dialog. - DialogFragment saveDialogFragment = SaveWebpageDialog.saveWebpage(StoragePermissionDialog.SAVE_URL, downloadUrl, formattedFileSizeString, fileNameString, userAgent, + DialogFragment saveDialogFragment = SaveWebpageDialog.saveWebpage(SaveWebpageDialog.SAVE_URL, downloadUrl, formattedFileSizeString, fileNameString, userAgent, nestedScrollWebView.getAcceptFirstPartyCookies()); // Show the save dialog. It must be named `save_dialog` so that the file picker can update the file name. @@ -6430,6 +6326,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());