import com.stoutner.privacybrowser.adapters.WebViewPagerAdapter;
import com.stoutner.privacybrowser.asynctasks.GetHostIpAddresses;
import com.stoutner.privacybrowser.asynctasks.PopulateBlocklists;
+import com.stoutner.privacybrowser.asynctasks.SaveUrl;
import com.stoutner.privacybrowser.asynctasks.SaveWebpageImage;
import com.stoutner.privacybrowser.dialogs.AdConsentDialog;
import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog;
private final int PERMISSION_OPEN_REQUEST_CODE = 2;
private final int PERMISSION_SAVE_WEBPAGE_ARCHIVE_REQUEST_CODE = 3;
private final int PERMISSION_SAVE_WEBPAGE_IMAGE_REQUEST_CODE = 4;
+ private final int PERMISSION_SAVE_WEBPAGE_RAW_REQUEST_CODE = 5;
// The current WebView is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`,
// `findNextOnPage()`, `closeFindOnPage()`, `loadUrlFromTextBox()`, `onSslMismatchBack()`, `applyProxy()`, and `applyDomainSettings()`.
// The file path strings are used in `onSaveWebpageImage()` and `onRequestPermissionResult()`
private String openFilePath;
+ private String saveWebpageUrl;
private String saveWebpageFilePath;
@Override
// Select the current user agent menu item. A switch statement cannot be used because the user agents are not compile time constants.
if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[0])) { // Privacy Browser.
// Update the user agent menu item title.
- userAgentMenuItem.setTitle(getString(R.string.user_agent) + " - " + getString(R.string.user_agent_privacy_browser));
+ userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_privacy_browser));
// Select the Privacy Browser radio box.
menu.findItem(R.id.user_agent_privacy_browser).setChecked(true);
} else if (currentUserAgent.equals(webViewDefaultUserAgent)) { // WebView Default.
// Update the user agent menu item title.
- userAgentMenuItem.setTitle(getString(R.string.user_agent) + " - " + getString(R.string.user_agent_webview_default));
+ userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_webview_default));
// Select the WebView Default radio box.
menu.findItem(R.id.user_agent_webview_default).setChecked(true);
} else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[2])) { // Firefox on Android.
// Update the user agent menu item title.
- userAgentMenuItem.setTitle(getString(R.string.user_agent) + " - " + getString(R.string.user_agent_firefox_on_android));
+ userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_firefox_on_android));
// Select the Firefox on Android radio box.
menu.findItem(R.id.user_agent_firefox_on_android).setChecked(true);
} else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[3])) { // Chrome on Android.
// Update the user agent menu item title.
- userAgentMenuItem.setTitle(getString(R.string.user_agent) + " - " + getString(R.string.user_agent_chrome_on_android));
+ userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_chrome_on_android));
// Select the Chrome on Android radio box.
menu.findItem(R.id.user_agent_chrome_on_android).setChecked(true);
} else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[4])) { // Safari on iOS.
// Update the user agent menu item title.
- userAgentMenuItem.setTitle(getString(R.string.user_agent) + " - " + getString(R.string.user_agent_safari_on_ios));
+ userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_safari_on_ios));
// Select the Safari on iOS radio box.
menu.findItem(R.id.user_agent_safari_on_ios).setChecked(true);
} else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[5])) { // Firefox on Linux.
// Update the user agent menu item title.
- userAgentMenuItem.setTitle(getString(R.string.user_agent) + " - " + getString(R.string.user_agent_firefox_on_linux));
+ userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_firefox_on_linux));
// Select the Firefox on Linux radio box.
menu.findItem(R.id.user_agent_firefox_on_linux).setChecked(true);
} else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[6])) { // Chromium on Linux.
// Update the user agent menu item title.
- userAgentMenuItem.setTitle(getString(R.string.user_agent) + " - " + getString(R.string.user_agent_chromium_on_linux));
+ userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_chromium_on_linux));
// Select the Chromium on Linux radio box.
menu.findItem(R.id.user_agent_chromium_on_linux).setChecked(true);
} else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[7])) { // Firefox on Windows.
// Update the user agent menu item title.
- userAgentMenuItem.setTitle(getString(R.string.user_agent) + " - " + getString(R.string.user_agent_firefox_on_windows));
+ userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_firefox_on_windows));
// Select the Firefox on Windows radio box.
menu.findItem(R.id.user_agent_firefox_on_windows).setChecked(true);
} else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[8])) { // Chrome on Windows.
// Update the user agent menu item title.
- userAgentMenuItem.setTitle(getString(R.string.user_agent) + " - " + getString(R.string.user_agent_chrome_on_windows));
+ userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_chrome_on_windows));
// Select the Chrome on Windows radio box.
menu.findItem(R.id.user_agent_chrome_on_windows).setChecked(true);
} else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[9])) { // Edge on Windows.
// Update the user agent menu item title.
- userAgentMenuItem.setTitle(getString(R.string.user_agent) + " - " + getString(R.string.user_agent_edge_on_windows));
+ userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_edge_on_windows));
// Select the Edge on Windows radio box.
menu.findItem(R.id.user_agent_edge_on_windows).setChecked(true);
} else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[10])) { // Internet Explorer on Windows.
// Update the user agent menu item title.
- userAgentMenuItem.setTitle(getString(R.string.user_agent) + " - " + getString(R.string.user_agent_internet_explorer_on_windows));
+ userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_internet_explorer_on_windows));
// Select the Internet on Windows radio box.
menu.findItem(R.id.user_agent_internet_explorer_on_windows).setChecked(true);
} else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[11])) { // Safari on macOS.
// Update the user agent menu item title.
- userAgentMenuItem.setTitle(getString(R.string.user_agent) + " - " + getString(R.string.user_agent_safari_on_macos));
+ userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_safari_on_macos));
// Select the Safari on macOS radio box.
menu.findItem(R.id.user_agent_safari_on_macos).setChecked(true);
} else { // Custom user agent.
// Update the user agent menu item title.
- userAgentMenuItem.setTitle(getString(R.string.user_agent) + " - " + getString(R.string.user_agent_custom));
+ userAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_custom));
// Select the Custom radio box.
menu.findItem(R.id.user_agent_custom).setChecked(true);
// Consume the event.
return true;
+ case R.id.save_url:
+ // Instantiate the save dialog.
+ DialogFragment saveDialogFragment = SaveWebpageDialog.saveUrl(StoragePermissionDialog.SAVE, currentWebView.getCurrentUrl());
+
+ // Show the save dialog. It must be named `save_dialog` so that the file picked can update the file name.
+ saveDialogFragment.show(getSupportFragmentManager(), getString(R.string.save_dialog));
+
+ // Consume the event.
+ return true;
+
case R.id.save_as_archive:
// Instantiate the save webpage archive dialog.
- DialogFragment saveWebpageArchiveDialogFragment = SaveWebpageDialog.saveWebpage(StoragePermissionDialog.SAVE_ARCHIVE);
+ DialogFragment saveWebpageArchiveDialogFragment = SaveWebpageDialog.saveUrl(StoragePermissionDialog.SAVE_AS_ARCHIVE, currentWebView.getCurrentUrl());
- // Show the save webpage archive dialog.
- saveWebpageArchiveDialogFragment.show(getSupportFragmentManager(), getString(R.string.save_webpage));
+ // Show the save webpage archive dialog. It must be named `save_dialog` so that the file picked can update the file name.
+ saveWebpageArchiveDialogFragment.show(getSupportFragmentManager(), getString(R.string.save_dialog));
// Consume the event.
return true;
case R.id.save_as_image:
- // Instantiate the save webpage image dialog.
- DialogFragment saveWebpageImageDialogFragment = SaveWebpageDialog.saveWebpage(StoragePermissionDialog.SAVE_IMAGE);
+ // Instantiate the save webpage image dialog. It must be named `save_webpage` so that the file picked can update the file name.
+ DialogFragment saveWebpageImageDialogFragment = SaveWebpageDialog.saveUrl(StoragePermissionDialog.SAVE_AS_IMAGE, currentWebView.getCurrentUrl());
- // Show the save webpage image dialog.
- saveWebpageImageDialogFragment.show(getSupportFragmentManager(), getString(R.string.save_webpage));
+ // Show the save webpage image dialog. It must be named `save_dialog` so that the file picked can update the file name.
+ saveWebpageImageDialogFragment.show(getSupportFragmentManager(), getString(R.string.save_dialog));
// Consume the event.
return true;
// Get handles for the system managers.
final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
- FragmentManager fragmentManager = getSupportFragmentManager();
- SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
// Remove the lint errors below that the clipboard manager might be null.
assert clipboardManager != null;
return true;
});
- // Add a Download URL entry.
- menu.add(R.string.download_url).setOnMenuItemClickListener((MenuItem item) -> {
- // Check if the download should be processed by an external app.
- if (sharedPreferences.getBoolean("download_with_external_app", false)) { // Download with an external app.
- openUrlWithExternalApp(linkUrl);
- } else { // Download with Android's download manager.
- // Check to see if the storage permission has already been granted.
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
- // Store the variables for future use by `onRequestPermissionsResult()`.
- downloadUrl = linkUrl;
- downloadContentDisposition = "none";
- downloadContentLength = -1;
-
- // Show a dialog if the user has previously denied the permission.
- if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
- // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
- DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE);
-
- // Show the download location permission alert dialog. The permission will be requested when the the dialog is closed.
- downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
- } else { // Show the permission request directly.
- // Request the permission. The download dialog will be launched by `onRequestPermissionResult()`.
- ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_DOWNLOAD_FILE_REQUEST_CODE);
- }
- } else { // The storage permission has already been granted.
- // Get a handle for the download file alert dialog.
- DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(linkUrl, "none", -1);
+ // Add a Save URL entry.
+ menu.add(R.string.save_url).setOnMenuItemClickListener((MenuItem item) -> {
+ // Instantiate the save dialog.
+ DialogFragment saveDialogFragment = SaveWebpageDialog.saveUrl(StoragePermissionDialog.SAVE, linkUrl);
- // Show the download file alert dialog.
- downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
- }
- }
+ // Show the save dialog. It must be named `save_dialog` so that the file picked can update the file name.
+ saveDialogFragment.show(getSupportFragmentManager(), getString(R.string.save_dialog));
// Consume the event.
return true;
return true;
});
- // Add a Download Image entry.
- menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
- // Check if the download should be processed by an external app.
- if (sharedPreferences.getBoolean("download_with_external_app", false)) { // Download with an external app.
- openUrlWithExternalApp(imageUrl);
- } else { // Download with Android's download manager.
- // Check to see if the storage permission has already been granted.
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
- // Store the image URL for use by `onRequestPermissionResult()`.
- downloadImageUrl = imageUrl;
-
- // Show a dialog if the user has previously denied the permission.
- if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
- // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
- DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
-
- // Show the download location permission alert dialog. The permission will be requested when the dialog is closed.
- downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
- } else { // Show the permission request directly.
- // Request the permission. The download dialog will be launched by `onRequestPermissionResult()`.
- ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_DOWNLOAD_IMAGE_REQUEST_CODE);
- }
- } else { // The storage permission has already been granted.
- // Get a handle for the download image alert dialog.
- DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
+ // Add a Save Image entry.
+ menu.add(R.string.save_image).setOnMenuItemClickListener((MenuItem item) -> {
+ // Instantiate the save dialog.
+ DialogFragment saveDialogFragment = SaveWebpageDialog.saveUrl(StoragePermissionDialog.SAVE, imageUrl);
- // Show the download image alert dialog.
- downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
- }
- }
+ // Show the save dialog. It must be named `save_dialog` so that the file picked can update the file name.
+ saveDialogFragment.show(getSupportFragmentManager(), getString(R.string.save_dialog));
// Consume the event.
return true;
return true;
});
- // Add a Download Image entry.
- menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
- // Check if the download should be processed by an external app.
- if (sharedPreferences.getBoolean("download_with_external_app", false)) { // Download with an external app.
- openUrlWithExternalApp(imageUrl);
- } else { // Download with Android's download manager.
- // Check to see if the storage permission has already been granted.
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested.
- // Store the image URL for use by `onRequestPermissionResult()`.
- downloadImageUrl = imageUrl;
-
- // Show a dialog if the user has previously denied the permission.
- if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
- // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
- DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
-
- // Show the download location permission alert dialog. The permission will be requested when the dialog is closed.
- downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
- } else { // Show the permission request directly.
- // Request the permission. The download dialog will be launched by `onRequestPermissionResult()`.
- ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_DOWNLOAD_IMAGE_REQUEST_CODE);
- }
- } else { // The storage permission has already been granted.
- // Get a handle for the download image alert dialog.
- DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
-
- // Show the download image alert dialog.
- downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
- }
- }
-
- // Consume the event.
- return true;
- });
-
// Add a Copy URL entry.
menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
// Save the link URL in a clip data.
return true;
});
+ menu.add(R.string.save_image).setOnMenuItemClickListener((MenuItem item) -> {
+ // Instantiate the save dialog.
+ DialogFragment saveDialogFragment = SaveWebpageDialog.saveUrl(StoragePermissionDialog.SAVE, imageUrl);
+
+ // Show the save raw dialog. It must be named `save_dialog` so that the file picked can update the file name.
+ saveDialogFragment.show(getSupportFragmentManager(), getString(R.string.save_dialog));
+
+ // Consume the event.
+ return true;
+ });
+
+ menu.add(R.string.save_url).setOnMenuItemClickListener((MenuItem item) -> {
+ // Instantiate the save dialog.
+ DialogFragment saveDialogFragment = SaveWebpageDialog.saveUrl(StoragePermissionDialog.SAVE, linkUrl);
+
+ // Show the save raw dialog. It must be named `save_dialog` so that the file picked can update the file name.
+ saveDialogFragment.show(getSupportFragmentManager(), getString(R.string.save_dialog));
+
+ // Consume the event.
+ return true;
+ });
+
// Add an Open with App entry.
menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
// Open the link URL with an external app.
// 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_webpage));
+ DialogFragment saveWebpageDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.save_dialog));
// Only update the file name if the dialog still exists.
if (saveWebpageDialogFragment != null) {
// 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 a handle for the edit texts.
+ EditText urlEditText = dialog.findViewById(R.id.url_edittext);
EditText fileNameEditText = dialog.findViewById(R.id.file_name_edittext);
- // Get the file path string.
+ // Get the strings from the edit texts.
+ saveWebpageUrl = urlEditText.getText().toString();
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_ARCHIVE:
+ case StoragePermissionDialog.SAVE:
+ // Save the URL.
+ new SaveUrl(this, saveWebpageFilePath, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptFirstPartyCookies()).execute(saveWebpageUrl);
+ break;
+
+ case StoragePermissionDialog.SAVE_AS_ARCHIVE:
// Save the webpage archive.
currentWebView.saveWebArchive(saveWebpageFilePath);
break;
- case StoragePermissionDialog.SAVE_IMAGE:
+ case StoragePermissionDialog.SAVE_AS_IMAGE:
// Save the webpage image.
new SaveWebpageImage(this, currentWebView).execute(saveWebpageFilePath);
break;
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_ARCHIVE:
+ case StoragePermissionDialog.SAVE:
+ // Save the URL.
+ new SaveUrl(this, saveWebpageFilePath, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptFirstPartyCookies()).execute(saveWebpageUrl);
+ break;
+
+ case StoragePermissionDialog.SAVE_AS_ARCHIVE:
// Save the webpage archive.
currentWebView.saveWebArchive(saveWebpageFilePath);
break;
- case StoragePermissionDialog.SAVE_IMAGE:
+ case StoragePermissionDialog.SAVE_AS_IMAGE:
// Save the webpage image.
new SaveWebpageImage(this, currentWebView).execute(saveWebpageFilePath);
break;
storagePermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.storage_permission));
} else { // Show the permission request directly.
switch (saveType) {
- case StoragePermissionDialog.SAVE_ARCHIVE:
+ case StoragePermissionDialog.SAVE:
+ // Request the write external storage permission. The URL will be saved when it finishes.
+ ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_WEBPAGE_RAW_REQUEST_CODE);
+
+ case StoragePermissionDialog.SAVE_AS_ARCHIVE:
// Request the write external storage permission. The webpage archive will be saved when it finishes.
ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_WEBPAGE_ARCHIVE_REQUEST_CODE);
break;
- case StoragePermissionDialog.SAVE_IMAGE:
+ case StoragePermissionDialog.SAVE_AS_IMAGE:
// Request the write external storage permission. The webpage image will be saved when it finishes.
ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_WEBPAGE_IMAGE_REQUEST_CODE);
break;
ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_OPEN_REQUEST_CODE);
break;
- case StoragePermissionDialog.SAVE_ARCHIVE:
+ case StoragePermissionDialog.SAVE:
+ // Request the write external storage permission. The URL will be saved when it finishes.
+ ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_WEBPAGE_RAW_REQUEST_CODE);
+ break;
+
+ case StoragePermissionDialog.SAVE_AS_ARCHIVE:
// Request the write external storage permission. The webpage archive will be saved when it finishes.
ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_WEBPAGE_ARCHIVE_REQUEST_CODE);
break;
- case StoragePermissionDialog.SAVE_IMAGE:
+ case StoragePermissionDialog.SAVE_AS_IMAGE:
// Request the write external storage permission. The webpage image will be saved when it finishes.
ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_WEBPAGE_IMAGE_REQUEST_CODE);
break;
// Reset the save webpage file path.
saveWebpageFilePath = "";
break;
+
+ case PERMISSION_SAVE_WEBPAGE_RAW_REQUEST_CODE:
+ // Check to see if the storage permission was granted. If the dialog was canceled the grant results will be empty.
+ if ((grantResults.length > 0) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) { // The storage permission was granted.
+ // Save the raw URL.
+ new SaveUrl(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();
+ }
+
+ // Reset the save strings.
+ saveWebpageUrl = "";
+ saveWebpageFilePath = "";
+ break;
}
}
}
}
- private void openUrlWithExternalApp(String url) {
- // Create a download intent. Not specifying the action type will display the maximum number of options.
- Intent downloadIntent = new Intent();
-
- // Set the URI and the MIME type. Specifying `text/html` displays a good number of options.
- 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.open_with)));
- }
-
private void highlightUrlText() {
// Get a handle for the URL edit text.
EditText urlEditText = findViewById(R.id.url_edittext);
/*
- * Copyright © 2017-2019 Soren Stoutner <soren@stoutner.com>.
+ * Copyright © 2017-2020 Soren Stoutner <soren@stoutner.com>.
*
* This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
*
// Only process the cookies if they are not null.
if (cookiesString != null) {
- // Set the `Cookie` header property.
+ // Add the cookies to the header property.
httpUrlConnection.setRequestProperty("Cookie", cookiesString);
- // Add the `Cookie` header to the string builder and format the text.
+ // Add the cookie header to the string builder and format the text.
requestHeadersBuilder.append(System.getProperty("line.separator"));
if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
requestHeadersBuilder.append("Cookie", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] conversionBufferByteArray = new byte[1024];
- // Instantiate the variable to track the buffer length.
+ // Define the buffer length variable.
int bufferLength;
try {
- // Attempt to read data from the input stream and store it in the conversion buffer byte array. Also store the amount of data transferred in the buffer length variable.
+ // Attempt to read data from the input stream and store it in the conversion buffer byte array. Also store the amount of data read in the buffer length variable.
while ((bufferLength = inputStream.read(conversionBufferByteArray)) > 0) { // Proceed while the amount of data stored in the buffer is > 0.
// Write the contents of the conversion buffer to the byte array output stream.
byteArrayOutputStream.write(conversionBufferByteArray, 0, bufferLength);
}
- } catch (IOException e) {
- e.printStackTrace();
+ } catch (IOException exception) {
+ // Do nothing.
}
// Close the input stream.
// Populate the response body string with the contents of the byte array output stream.
responseBodyBuilder.append(byteArrayOutputStream.toString());
} finally {
- // Disconnect `httpUrlConnection`.
+ // Disconnect HTTP URL connection.
httpUrlConnection.disconnect();
}
- } catch (IOException e) {
- e.printStackTrace();
+ } catch (IOException exception) {
+ exception.printStackTrace();
}
// Return the response body string as the result.
--- /dev/null
+/*
+ * Copyright © 2020 Soren Stoutner <soren@stoutner.com>.
+ *
+ * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
+ *
+ * Privacy Browser is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Privacy Browser is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Privacy Browser. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.stoutner.privacybrowser.asynctasks;
+
+import android.app.Activity;
+import android.os.AsyncTask;
+import android.webkit.CookieManager;
+
+import com.google.android.material.snackbar.Snackbar;
+import com.stoutner.privacybrowser.R;
+import com.stoutner.privacybrowser.views.NoSwipeViewPager;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.ref.WeakReference;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+public class SaveUrl extends AsyncTask<String, Void, String> {
+ // Define a weak reference to the calling activity.
+ private WeakReference<Activity> activityWeakReference;
+
+ // Define a success string constant.
+ private final String SUCCESS = "Success";
+
+ // Define the class variables.
+ private String filePathString;
+ private String userAgent;
+ private boolean cookiesEnabled;
+ private Snackbar savingFileSnackbar;
+
+ // The public constructor.
+ public SaveUrl(Activity activity, String filePathString, String userAgent, boolean cookiesEnabled) {
+ // Populate the weak reference to the calling activity.
+ activityWeakReference = new WeakReference<>(activity);
+
+ // Store the class variables.
+ this.filePathString = filePathString;
+ this.userAgent = userAgent;
+ this.cookiesEnabled = cookiesEnabled;
+ }
+
+ // `onPreExecute()` operates on the UI thread.
+ @Override
+ protected void onPreExecute() {
+ // Get a handle for the activity.
+ Activity activity = activityWeakReference.get();
+
+ // Abort if the activity is gone.
+ if ((activity==null) || activity.isFinishing()) {
+ return;
+ }
+
+ // Get a handle for the no swipe view pager.
+ NoSwipeViewPager noSwipeViewPager = activity.findViewById(R.id.webviewpager);
+
+ // Create a saving file snackbar.
+ savingFileSnackbar = Snackbar.make(noSwipeViewPager, R.string.saving_file, Snackbar.LENGTH_INDEFINITE);
+
+ // Display the saving file snackbar.
+ savingFileSnackbar.show();
+ }
+
+ @Override
+ protected String doInBackground(String... urlToSave) {
+ // Get a handle for the activity.
+ Activity activity = activityWeakReference.get();
+
+ // Abort if the activity is gone.
+ if ((activity == null) || activity.isFinishing()) {
+ return null;
+ }
+
+ // Define a save disposition string.
+ String saveDisposition = SUCCESS;
+
+ // Because everything relating to requesting data from a webserver can throw errors, the entire section must catch `IOExceptions`.
+ try {
+ // Get the URL from the main activity.
+ URL url = new URL(urlToSave[0]);
+
+ // Open a connection to the URL. No data is actually sent at this point.
+ HttpURLConnection httpUrlConnection = (HttpURLConnection) url.openConnection();
+
+ // Add the user agent to the header property.
+ httpUrlConnection.setRequestProperty("User-Agent", userAgent);
+
+ // Add the cookies if they are enabled.
+ if (cookiesEnabled) {
+ // Get the cookies for the current domain.
+ String cookiesString = CookieManager.getInstance().getCookie(url.toString());
+
+ // Only add the cookies if they are not null.
+ if (cookiesString != null) {
+ // Add the cookies to the header property.
+ httpUrlConnection.setRequestProperty("Cookie", cookiesString);
+ }
+ }
+
+ // The actual network request is in a `try` bracket so that `disconnect()` is run in the `finally` section even if an error is encountered in the main block.
+ try {
+ // Get the response code, which causes the connection to the server to be made.
+ httpUrlConnection.getResponseCode();
+
+ // Get the response body stream.
+ InputStream inputStream = new BufferedInputStream(httpUrlConnection.getInputStream());
+
+ // Get the file.
+ File file = new File(filePathString);
+
+ // Delete the file if it exists.
+ if (file.exists()) {
+ //noinspection ResultOfMethodCallIgnored
+ file.delete();
+ }
+
+ // Create a new file.
+ //noinspection ResultOfMethodCallIgnored
+ file.createNewFile();
+
+ // Create an output file stream.
+ OutputStream outputStream = new FileOutputStream(file);
+
+ // Initialize the conversion buffer byte array.
+ byte[] conversionBufferByteArray = new byte[1024];
+
+ // Define the buffer length variable.
+ int bufferLength;
+
+ // Attempt to read data from the input stream and store it in the output stream. Also store the amount of data read in the buffer length variable.
+ while ((bufferLength = inputStream.read(conversionBufferByteArray)) > 0) { // Proceed while the amount of data stored in the buffer in > 0.
+ // Write the contents of the conversion buffer to the output stream.
+ outputStream.write(conversionBufferByteArray, 0, bufferLength);
+ }
+
+ // Close the input stream.
+ inputStream.close();
+
+ // Close the output stream.
+ outputStream.close();
+ } finally {
+ // Disconnect the HTTP URL connection.
+ httpUrlConnection.disconnect();
+ }
+ } catch (IOException exception) {
+ // Store the error in the save disposition string.
+ saveDisposition = exception.toString();
+ }
+
+ // Return the save disposition string.
+ return saveDisposition;
+ }
+
+ // `onPostExecute()` operates on the UI thread.
+ @Override
+ protected void onPostExecute(String saveDisposition) {
+ // Get a handle for the activity.
+ Activity activity = activityWeakReference.get();
+
+ // Abort if the activity is gone.
+ if ((activity == null) || activity.isFinishing()) {
+ return;
+ }
+
+ // Get a handle for the no swipe view pager.
+ NoSwipeViewPager noSwipeViewPager = activity.findViewById(R.id.webviewpager);
+
+ // Dismiss the saving file snackbar.
+ savingFileSnackbar.dismiss();
+
+ // Display a save disposition snackbar.
+ if (saveDisposition.equals(SUCCESS)) {
+ Snackbar.make(noSwipeViewPager, R.string.file_saved, Snackbar.LENGTH_SHORT).show();
+ } else {
+ Snackbar.make(noSwipeViewPager, activity.getString(R.string.error_saving_file) + " " + saveDisposition, Snackbar.LENGTH_INDEFINITE).show();
+ }
+ }
+}
\ No newline at end of file
}
// Set the view. The parent view is null because it will be assigned by the alert dialog.
- dialogBuilder.setView(activity.getLayoutInflater().inflate(R.layout.save_dialog, null));
+ dialogBuilder.setView(activity.getLayoutInflater().inflate(R.layout.save_logcat_dialog, null));
// Set the cancel button listener.
dialogBuilder.setNegativeButton(R.string.cancel, (DialogInterface dialog, int which) -> {
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
+import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
saveWebpageListener = (SaveWebpageListener) context;
}
- public static SaveWebpageDialog saveWebpage(int saveType) {
+ public static SaveWebpageDialog saveUrl(int saveType, String url) {
// Create an arguments bundle.
Bundle argumentsBundle = new Bundle();
- // Store the save type in the bundle.
+ // Store the arguments in the bundle.
argumentsBundle.putInt("save_type", saveType);
+ argumentsBundle.putString("url", url);
// Create a new instance of the save webpage dialog.
SaveWebpageDialog saveWebpageDialog = new SaveWebpageDialog();
// Remove the incorrect lint warning that the arguments might be null.
assert arguments != null;
- // Get the save type.
+ // Get the arguments from the bundle.
int saveType = arguments.getInt("save_type");
+ String url = arguments.getString("url");
// Get a handle for the activity and the context.
Activity activity = getActivity();
// Set the icon according to the save type.
switch (saveType) {
- case StoragePermissionDialog.SAVE_ARCHIVE:
+ case StoragePermissionDialog.SAVE:
+ dialogBuilder.setIcon(R.drawable.copy_enabled_dark);
+ break;
+
+ case StoragePermissionDialog.SAVE_AS_ARCHIVE:
dialogBuilder.setIcon(R.drawable.dom_storage_cleared_dark);
break;
- case StoragePermissionDialog.SAVE_IMAGE:
+ case StoragePermissionDialog.SAVE_AS_IMAGE:
dialogBuilder.setIcon(R.drawable.images_enabled_dark);
break;
}
// Set the icon according to the save type.
switch (saveType) {
- case StoragePermissionDialog.SAVE_ARCHIVE:
+ case StoragePermissionDialog.SAVE:
+ dialogBuilder.setIcon(R.drawable.copy_enabled_light);
+ break;
+
+ case StoragePermissionDialog.SAVE_AS_ARCHIVE:
dialogBuilder.setIcon(R.drawable.dom_storage_cleared_light);
break;
- case StoragePermissionDialog.SAVE_IMAGE:
+ case StoragePermissionDialog.SAVE_AS_IMAGE:
dialogBuilder.setIcon(R.drawable.images_enabled_light);
break;
}
// Set the title according to the type.
switch (saveType) {
- case StoragePermissionDialog.SAVE_ARCHIVE:
+ case StoragePermissionDialog.SAVE:
+ dialogBuilder.setTitle(R.string.save);
+ break;
+
+ case StoragePermissionDialog.SAVE_AS_ARCHIVE:
dialogBuilder.setTitle(R.string.save_archive);
break;
- case StoragePermissionDialog.SAVE_IMAGE:
+ case StoragePermissionDialog.SAVE_AS_IMAGE:
dialogBuilder.setTitle(R.string.save_image);
break;
}
// Set the view. The parent view is null because it will be assigned by the alert dialog.
- dialogBuilder.setView(activity.getLayoutInflater().inflate(R.layout.save_dialog, null));
+ dialogBuilder.setView(activity.getLayoutInflater().inflate(R.layout.save_webpage_dialog, null));
// Set the cancel button listener. Using `null` as the listener closes the dialog without doing anything else.
dialogBuilder.setNegativeButton(R.string.cancel, null);
alertDialog.show();
// Get handles for the layout items.
+ EditText urlEditText = alertDialog.findViewById(R.id.url_edittext);
EditText fileNameEditText = alertDialog.findViewById(R.id.file_name_edittext);
Button browseButton = alertDialog.findViewById(R.id.browse_button);
TextView fileExistsWarningTextView = alertDialog.findViewById(R.id.file_exists_warning_textview);
TextView storagePermissionTextView = alertDialog.findViewById(R.id.storage_permission_textview);
Button saveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
+ // Update the status of the save button whe the URL changes.
+ urlEditText.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+ // Do nothing.
+ }
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+ // Enable the save button if the URL and file name are populated.
+ saveButton.setEnabled(!urlEditText.getText().toString().isEmpty() && !fileNameEditText.getText().toString().isEmpty());
+ }
+ });
+
// Update the status of the save button when the file name changes.
fileNameEditText.addTextChangedListener(new TextWatcher() {
@Override
}
// Enable the save button if the file name is populated.
- saveButton.setEnabled(!fileNameString.isEmpty());
+ saveButton.setEnabled(!fileNameString.isEmpty() && !urlEditText.getText().toString().isEmpty());
}
});
- // Create a default file name string.
- String defaultFileName = "";
+ // Create a file name string.
+ String fileName = "";
- // Set the default file name according to the type.
+ // Set the file name according to the type.
switch (saveType) {
- case StoragePermissionDialog.SAVE_ARCHIVE:
- defaultFileName = getString(R.string.webpage_mht);
+ case StoragePermissionDialog.SAVE:
+ // Convert the URL to a URI.
+ Uri uri = Uri.parse(url);
+
+ // Get the last path segment.
+ String lastPathSegment = uri.getLastPathSegment();
+
+ // Use a default file name if the last path segment is null.
+ if (lastPathSegment == null) {
+ lastPathSegment = getString(R.string.file);
+ }
+
+ // Use the last path segment as the file name.
+ fileName = lastPathSegment;
break;
- case StoragePermissionDialog.SAVE_IMAGE:
- defaultFileName = getString(R.string.webpage_png);
+ case StoragePermissionDialog.SAVE_AS_ARCHIVE:
+ fileName = getString(R.string.webpage_mht);
+ break;
+
+ case StoragePermissionDialog.SAVE_AS_IMAGE:
+ fileName = getString(R.string.webpage_png);
break;
}
+ // Save the file name as the default file name. This must be final to be used in the lambda below.
+ final String defaultFileName = fileName;
+
// Create a string for the default file path.
String defaultFilePath;
defaultFilePath = context.getExternalFilesDir(null) + "/" + defaultFileName;
}
- // Display the default file path.
+ // Populate the edit texts.
+ urlEditText.setText(url);
fileNameEditText.setText(defaultFilePath);
// Move the cursor to the end of the default file path.
browseIntent.setType("*/*");
// Set the initial file name according to the type.
- switch (saveType) {
- case StoragePermissionDialog.SAVE_ARCHIVE:
- browseIntent.putExtra(Intent.EXTRA_TITLE, getString(R.string.webpage_mht));
- break;
-
- case StoragePermissionDialog.OPEN:
- browseIntent.putExtra(Intent.EXTRA_TITLE, getString(R.string.webpage_png));
- break;
- }
+ browseIntent.putExtra(Intent.EXTRA_TITLE, defaultFileName);
// Set the initial directory if the minimum API >= 26.
if (Build.VERSION.SDK_INT >= 26) {
/*
- * Copyright © 2018-2019 Soren Stoutner <soren@stoutner.com>.
+ * Copyright © 2018-2020 Soren Stoutner <soren@stoutner.com>.
*
* This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
*
public class StoragePermissionDialog extends DialogFragment {
// Define the save type constants.
public static final int OPEN = 0;
- public static final int SAVE_ARCHIVE = 1;
- public static final int SAVE_IMAGE = 2;
+ public static final int SAVE = 1;
+ public static final int SAVE_AS_ARCHIVE = 2;
+ public static final int SAVE_AS_IMAGE = 3;
// The listener is used in `onAttach()` and `onCreateDialog()`.
private StoragePermissionDialogListener storagePermissionDialogListener;
/*
- * Copyright © 2019 Soren Stoutner <soren@stoutner.com>.
+ * Copyright © 2019-2020 Soren Stoutner <soren@stoutner.com>.
*
* This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
*
String fileNameContentPath = rawFileNamePath.substring(0, rawFileNamePath.indexOf(":"));
String fileNameFinalPath = rawFileNamePath.substring(rawFileNamePath.indexOf(":") + 1);
- // Check to see if the current file name final patch is a complete, valid path
+ // Check to see if the current file name final patch is a complete, valid path.
if (fileNameFinalPath.startsWith("/storage/emulated/")) { // The existing file name final path is a complete, valid path.
// Use the provided file name path as is.
fileNamePath = fileNameFinalPath;
-<!-- `copy_dark.xml` comes from the Android Material icon set, where it is called `file_copy`. It is released under the Apache License 2.0. -->
+<!-- This file comes from the Android Material icon set, where it is called `file_copy`. It is released under the Apache License 2.0. -->
<!-- `tools:ignore="VectorRaster"` removes the lint warning about `android:autoMirrored="true"` not applying to API < 21. -->
<vector
--- /dev/null
+<!-- This file comes from the Android Material icon set, where it is called `file_copy`. It is released under the Apache License 2.0. -->
+
+<!-- `tools:ignore="VectorRaster"` removes the lint warning about `android:autoMirrored="true"` not applying to API < 21. -->
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:height="24dp"
+ android:width="24dp"
+ android:viewportHeight="24"
+ android:viewportWidth="24"
+ android:autoMirrored="true"
+ tools:ignore="VectorRaster" >
+
+ <!-- A hard coded color must be used until the minimum API >= 21. Then `@color` may be used. -->
+ <path
+ android:fillColor="#FF1E88E5"
+ android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM15,5l6,6v10c0,1.1 -0.9,2 -2,2L7.99,23C6.89,23 6,22.1 6,21l0.01,-14c0,-1.1 0.89,-2 1.99,-2h7zM14,12h5.5L14,6.5L14,12z"/>
+</vector>
\ No newline at end of file
--- /dev/null
+<!-- This file comes from the Android Material icon set, where it is called `file_copy`. It is released under the Apache License 2.0. -->
+
+<!-- `tools:ignore="VectorRaster"` removes the lint warning about `android:autoMirrored="true"` not applying to API < 21. -->
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:height="24dp"
+ android:width="24dp"
+ android:viewportHeight="24"
+ android:viewportWidth="24"
+ android:autoMirrored="true"
+ tools:ignore="VectorRaster" >
+
+ <!-- A hard coded color must be used until the minimum API >= 21. Then `@color` may be used. -->
+ <path
+ android:fillColor="#FF1565C0"
+ android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM15,5l6,6v10c0,1.1 -0.9,2 -2,2L7.99,23C6.89,23 6,22.1 6,21l0.01,-14c0,-1.1 0.89,-2 1.99,-2h7zM14,12h5.5L14,6.5L14,12z"/>
+</vector>
\ No newline at end of file
-<!-- `copy_light.xml` comes from the Android Material icon set, where it is called `file_copy`. It is released under the Apache License 2.0. -->
+<!-- This file comes from the Android Material icon set, where it is called `file_copy`. It is released under the Apache License 2.0. -->
<!-- `tools:ignore="VectorRaster"` removes the lint warning about `android:autoMirrored="true"` not applying to API < 21. -->
<vector
-<!-- `custom_user_agent_enabled_dark.xml` comes from the Android Material icon set, where it is called `important_devices_off`. It is released under the Apache License 2.0. -->
+<!-- This file comes from the Android Material icon set, where it is called `important_devices_off`. It is released under the Apache License 2.0. -->
<!-- `tools:ignore="VectorRaster"` removes the lint warning about `android:autoMirrored="true"` not applying to API < 21. -->
<vector
android:viewportWidth="24.0"
tools:ignore="VectorRaster">
- <!-- We have to use a hard coded color until API >= 21. Then we can use `@color`. -->
+ <!-- A hard coded color must be used until the minimum API >= 21. Then `@color` may be used. -->
<path
android:fillColor="#FF1E88E5"
android:pathData="M23,11.01L18,11c-0.55,0 -1,0.45 -1,1v9c0,0.55 0.45,1 1,1h5c0.55,0 1,-0.45 1,-1v-9c0,-0.55 -0.45,-0.99 -1,-0.99zM23,20h-5v-7h5v7zM20,2L2,2C0.89,2 0,2.89 0,4v12c0,1.1 0.89,2 2,2h7v2L7,20v2h8v-2h-2v-2h2v-2L2,16L2,4h18v5h2L22,4c0,-1.11 -0.9,-2 -2,-2zM11.97,9L11,6l-0.97,3L7,9l2.47,1.76 -0.94,2.91 2.47,-1.8 2.47,1.8 -0.94,-2.91L15,9h-3.03z"/>
-<!-- `custom_user_agent_enabled_light.xml` comes from the Android Material icon set, where it is called `important_devices_off`. It is released under the Apache License 2.0. -->
+<!-- This file comes from the Android Material icon set, where it is called `important_devices_off`. It is released under the Apache License 2.0. -->
<!-- `tools:ignore="VectorRaster"` removes the lint warning about `android:autoMirrored="true"` not applying to API < 21. -->
<vector
android:viewportWidth="24.0"
tools:ignore="VectorRaster">
- <!-- We have to use a hard coded color until API >= 21. Then we can use `@color`. -->
+ <!-- A hard coded color must be used until the minimum API >= 21. Then `@color` may be used. -->
<path
android:fillColor="#FF1565C0"
android:pathData="M23,11.01L18,11c-0.55,0 -1,0.45 -1,1v9c0,0.55 0.45,1 1,1h5c0.55,0 1,-0.45 1,-1v-9c0,-0.55 -0.45,-0.99 -1,-0.99zM23,20h-5v-7h5v7zM20,2L2,2C0.89,2 0,2.89 0,4v12c0,1.1 0.89,2 2,2h7v2L7,20v2h8v-2h-2v-2h2v-2L2,16L2,4h18v5h2L22,4c0,-1.11 -0.9,-2 -2,-2zM11.97,9L11,6l-0.97,3L7,9l2.47,1.76 -0.94,2.91 2.47,-1.8 2.47,1.8 -0.94,-2.91L15,9h-3.03z"/>
-<!-- `custom_user_agent_ghosted_dark.xml` comes from the Android Material icon set, where it is called `important_devices_off`. It is released under the Apache License 2.0. -->
+<!-- This file comes from the Android Material icon set, where it is called `important_devices_off`. It is released under the Apache License 2.0. -->
<!-- `tools:ignore="VectorRaster"` removes the lint warning about `android:autoMirrored="true"` not applying to API < 21. -->
<vector
android:viewportWidth="24.0"
tools:ignore="VectorRaster">
- <!-- We have to use a hard coded color until API >= 21. Then we can use `@color`. -->
+ <!-- A hard coded color must be used until the minimum API >= 21. Then `@color` may be used. -->
<path
android:fillColor="#FF616161"
android:pathData="M23,11.01L18,11c-0.55,0 -1,0.45 -1,1v9c0,0.55 0.45,1 1,1h5c0.55,0 1,-0.45 1,-1v-9c0,-0.55 -0.45,-0.99 -1,-0.99zM23,20h-5v-7h5v7zM20,2L2,2C0.89,2 0,2.89 0,4v12c0,1.1 0.89,2 2,2h7v2L7,20v2h8v-2h-2v-2h2v-2L2,16L2,4h18v5h2L22,4c0,-1.11 -0.9,-2 -2,-2zM11.97,9L11,6l-0.97,3L7,9l2.47,1.76 -0.94,2.91 2.47,-1.8 2.47,1.8 -0.94,-2.91L15,9h-3.03z"/>
-<!-- `custom_user_agent_ghosted_light.xml` comes from the Android Material icon set, where it is called `important_devices_off`. It is released under the Apache License 2.0. -->
+<!-- This file comes from the Android Material icon set, where it is called `important_devices_off`. It is released under the Apache License 2.0. -->
<!-- `tools:ignore="VectorRaster"` removes the lint warning about `android:autoMirrored="true"` not applying to API < 21. -->
<vector
android:viewportWidth="24.0"
tools:ignore="VectorRaster">
- <!-- We have to use a hard coded color until API >= 21. Then we can use `@color`. -->
+ <!-- A hard coded color must be used until the minimum API >= 21. Then `@color` may be used. -->
<path
android:fillColor="#FFB7B7B7"
android:pathData="M23,11.01L18,11c-0.55,0 -1,0.45 -1,1v9c0,0.55 0.45,1 1,1h5c0.55,0 1,-0.45 1,-1v-9c0,-0.55 -0.45,-0.99 -1,-0.99zM23,20h-5v-7h5v7zM20,2L2,2C0.89,2 0,2.89 0,4v12c0,1.1 0.89,2 2,2h7v2L7,20v2h8v-2h-2v-2h2v-2L2,16L2,4h18v5h2L22,4c0,-1.11 -0.9,-2 -2,-2zM11.97,9L11,6l-0.97,3L7,9l2.47,1.76 -0.94,2.91 2.47,-1.8 2.47,1.8 -0.94,-2.91L15,9h-3.03z"/>
+++ /dev/null
-<?xml version="1.0" encoding="utf-8"?>
-
-<!--
- Copyright © 2019-2020 Soren Stoutner <soren@stoutner.com>.
-
- This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
-
- Privacy Browser is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- Privacy Browser is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with Privacy Browser. If not, see <http://www.gnu.org/licenses/>. -->
-
-<ScrollView
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_height="wrap_content"
- android:layout_width="match_parent" >
-
- <LinearLayout
- android:layout_height="wrap_content"
- android:layout_width="match_parent"
- android:orientation="vertical"
- android:layout_marginTop="10dp"
- android:layout_marginStart="10dp"
- android:layout_marginEnd="10dp" >
-
- <!-- Align the edit text and the select file button horizontally. -->
- <LinearLayout
- android:layout_height="wrap_content"
- android:layout_width="match_parent"
- android:orientation="horizontal" >
-
- <!-- The text input layout makes the `android:hint` float above the edit text. -->
- <com.google.android.material.textfield.TextInputLayout
- android:layout_height="wrap_content"
- android:layout_width="0dp"
- android:layout_weight="1" >
-
- <!-- `android:inputType="textUri"` disables spell check and places an `/` on the main keyboard. -->
- <com.google.android.material.textfield.TextInputEditText
- android:id="@+id/file_name_edittext"
- android:layout_height="wrap_content"
- android:layout_width="match_parent"
- android:hint="@string/file_name"
- android:inputType="textMultiLine|textUri" />
- </com.google.android.material.textfield.TextInputLayout>
-
- <Button
- android:id="@+id/browse_button"
- android:layout_height="wrap_content"
- android:layout_width="wrap_content"
- android:layout_gravity="center_vertical"
- android:text="@string/browse" />
- </LinearLayout>
-
- <!-- File already exists warning. -->
- <TextView
- android:id="@+id/file_exists_warning_textview"
- android:layout_height="wrap_content"
- android:layout_width="wrap_content"
- android:layout_gravity="center_horizontal"
- android:layout_margin="5dp"
- android:text="@string/file_exists_warning"
- android:textColor="?attr/redText"
- android:textAlignment="center" />
-
- <!-- Storage permission explanation. -->
- <TextView
- android:id="@+id/storage_permission_textview"
- android:layout_height="wrap_content"
- android:layout_width="wrap_content"
- android:layout_gravity="center_horizontal"
- android:text="@string/storage_permission_explanation"
- android:textColor="?android:textColorPrimary"
- android:textAlignment="center" />
- </LinearLayout>
-</ScrollView>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ Copyright © 2019-2020 Soren Stoutner <soren@stoutner.com>.
+
+ This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
+
+ Privacy Browser is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Privacy Browser is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Privacy Browser. If not, see <http://www.gnu.org/licenses/>. -->
+
+<ScrollView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent" >
+
+ <LinearLayout
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:orientation="vertical"
+ android:layout_marginTop="10dp"
+ android:layout_marginStart="10dp"
+ android:layout_marginEnd="10dp" >
+
+ <!-- Align the edit text and the select file button horizontally. -->
+ <LinearLayout
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:orientation="horizontal" >
+
+ <!-- The text input layout makes the `android:hint` float above the edit text. -->
+ <com.google.android.material.textfield.TextInputLayout
+ android:layout_height="wrap_content"
+ android:layout_width="0dp"
+ android:layout_weight="1" >
+
+ <!-- `android:inputType="textUri"` disables spell check and places an `/` on the main keyboard. -->
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/file_name_edittext"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:hint="@string/file_name"
+ android:inputType="textMultiLine|textUri" />
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <Button
+ android:id="@+id/browse_button"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:text="@string/browse" />
+ </LinearLayout>
+
+ <!-- File already exists warning. -->
+ <TextView
+ android:id="@+id/file_exists_warning_textview"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:layout_margin="5dp"
+ android:text="@string/file_exists_warning"
+ android:textColor="?attr/redText"
+ android:textAlignment="center" />
+
+ <!-- Storage permission explanation. -->
+ <TextView
+ android:id="@+id/storage_permission_textview"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:text="@string/storage_permission_explanation"
+ android:textColor="?android:textColorPrimary"
+ android:textAlignment="center" />
+ </LinearLayout>
+</ScrollView>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ Copyright © 2019-2020 Soren Stoutner <soren@stoutner.com>.
+
+ This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
+
+ Privacy Browser is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Privacy Browser is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Privacy Browser. If not, see <http://www.gnu.org/licenses/>. -->
+
+<ScrollView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent" >
+
+ <LinearLayout
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:orientation="vertical"
+ android:layout_marginTop="10dp"
+ android:layout_marginStart="10dp"
+ android:layout_marginEnd="10dp" >
+
+ <!-- The text input layout makes the `android:hint` float above the edit text. -->
+ <com.google.android.material.textfield.TextInputLayout
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent">
+
+ <!-- `android:inputType="TextUri"` disables spell check and places an `/` on the main keyboard. -->
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/url_edittext"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:hint="@string/url"
+ android:inputType="textMultiLine|textUri" />
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <!-- Align the edit text and the select file button horizontally. -->
+ <LinearLayout
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:orientation="horizontal"
+ android:layout_marginTop="5dp">
+
+ <!-- The text input layout makes the `android:hint` float above the edit text. -->
+ <com.google.android.material.textfield.TextInputLayout
+ android:layout_height="wrap_content"
+ android:layout_width="0dp"
+ android:layout_weight="1" >
+
+ <!-- `android:inputType="textUri"` disables spell check and places an `/` on the main keyboard. -->
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/file_name_edittext"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:hint="@string/file_name"
+ android:inputType="textMultiLine|textUri" />
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <Button
+ android:id="@+id/browse_button"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:text="@string/browse" />
+ </LinearLayout>
+
+ <!-- File already exists warning. -->
+ <TextView
+ android:id="@+id/file_exists_warning_textview"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:layout_margin="5dp"
+ android:text="@string/file_exists_warning"
+ android:textColor="?attr/redText"
+ android:textAlignment="center" />
+
+ <!-- Storage permission explanation. -->
+ <TextView
+ android:id="@+id/storage_permission_textview"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:text="@string/storage_permission_explanation"
+ android:textColor="?android:textColorPrimary"
+ android:textAlignment="center" />
+ </LinearLayout>
+</ScrollView>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<!--
- Copyright © 2015-2019 Soren Stoutner <soren@stoutner.com>.
+ Copyright © 2015-2020 Soren Stoutner <soren@stoutner.com>.
This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
app:showAsAction="never" >
<menu>
+ <item
+ android:id="@+id/save_url"
+ android:title="@string/save_url"
+ android:orderInCategory="1101"
+ app:showAsAction="never" />
<item
android:id="@+id/save_as_archive"
android:title="@string/save_as_archive"
- android:orderInCategory="1101"
+ android:orderInCategory="1102"
app:showAsAction="never" />
<item
android:id="@+id/save_as_image"
android:title="@string/save_as_image"
- android:orderInCategory="1102"
+ android:orderInCategory="1103"
app:showAsAction="never" />
</menu>
</item>
<?xml version="1.0" encoding="utf-8"?>
<!--
- Copyright © 2016-2019 Soren Stoutner <soren@stoutner.com>.
+ Copyright © 2016-2020 Soren Stoutner <soren@stoutner.com>.
- Translation 2019 Bernhard G. Keller. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
+ Translation 2019-2020 Bernhard G. Keller. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
Translation 2018 Stefan Erhardt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
<item>OpenPGP</item>
</string-array>
<string name="kitkat_password_encryption_message">Passwort-Verschlüsselung ist mit Android KitKat nicht möglich.</string>
+ <string name="file_does_not_exist">Die Datei existiert nicht.</string>
+ <string name="file_exists_warning">Die Datei existiert bereits. Wenn Sie fortfahren, wird sie überschrieben.</string>
<string name="openkeychain_required">Für die OpenPGP-Verschlüsselung muss OpenKeychain installiert sein.</string>
<string name="openkeychain_import_instructions">Die unverschlüsselte Datei muss in einem weiteren Schritt importiert werden, nachdem sie entschlüsselt wurde.</string>
<string name="settings_pbs">Einstellungen.pbs</string>
<?xml version="1.0" encoding="utf-8"?>
<!--
- Copyright © 2016-2019 Soren Stoutner <soren@stoutner.com>.
+ Copyright © 2016-2020 Soren Stoutner <soren@stoutner.com>.
- Translation 2017-2019 Jose A. León Becerra. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
+ Translation 2017-2020 Jose A. León Becerra. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
<item>OpenPGP</item>
</string-array>
<string name="kitkat_password_encryption_message">El cifrado de contraseñas no funciona en Android KitKat.</string>
+ <string name="file_does_not_exist">El archivo no existe.</string>
+ <string name="file_exists_warning">El archivo ya existe. Si procede, se sobrescribirá.</string>
<string name="openkeychain_required">El cifrado OpenPGP requiere que esté instalado OpenKeychain.</string>
<string name="openkeychain_import_instructions">El archivo sin cifrar tendrá que ser importado en un paso separado después de ser descifrado.</string>
<string name="settings_pbs">Configuración.pbs</string>
<?xml version="1.0" encoding="utf-8"?>
<!--
- Copyright © 2015-2019 Soren Stoutner <soren@stoutner.com>.
+ Copyright © 2015-2020 Soren Stoutner <soren@stoutner.com>.
- Translation 2019 Kévin LE FLOHIC <kevinliste@framalistes.org>. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
+ Translation 2019-2020 Kévin LE FLOHIC <kevinliste@framalistes.org>. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
<item>OpenPGP</item>
</string-array>
<string name="kitkat_password_encryption_message">Le chiffrement par mot de passe ne fonctionne pas sous Android KitKat.</string>
+ <string name="file_does_not_exist">Le fichier n\'existe pas.</string>
+ <string name="file_exists_warning">Le fichier existe déjà. Si vous continuez, il sera écrasé.</string>
<string name="openkeychain_required">Le chiffrement OpenPGP nécessite l\'installation d\'OpenKeychain.</string>
<string name="openkeychain_import_instructions">Le fichier non-chiffré devra être importé dans un deuxième temps, après son déchiffrement.</string>
<string name="settings_pbs">Settings.pbs</string>
<string name="import_failed">L\'import a échoué :</string>
<string name="storage_permission">Permission de stockage</string>
<string name="storage_permission_message">Privacy Browser nécessite les droits d\'accès au stockage pour accéder aux dossiers publics.
- Si cela est refusé, les dossiers internes à l\'application peut néanmoins être utilisé.</string>
+ Si cela est refusé, les dossiers internes à l\'application peut néanmoins être utilisé.</string>
<string name="storage_permission_explanation">Accéder à des fichiers dans des dossiers publics nécessite des droits de lecture/écriture.
Autrement, seuls les dossiers internes à l\'application ne pourront être utilisés.</string>
<string name="cannot_use_location">Ce dossier ne peut pas être utilisé car les droits d\'accès au stockage n\'ont pas été autorisés.</string>
<?xml version="1.0" encoding="utf-8"?>
<!--
- Copyright © 2017-2019 Soren Stoutner <soren@stoutner.com>.
+ Copyright © 2017-2020 Soren Stoutner <soren@stoutner.com>.
- Translation 2017-2019 Francesco Buratti. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
+ Translation 2017-2020 Francesco Buratti. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
<item>OpenPGP</item>
</string-array>
<string name="kitkat_password_encryption_message">La cifratura delle Password non funziona su Android KitKat.</string>
+ <string name="file_does_not_exist">Il file non esiste.</string>
+ <string name="file_exists_warning">Il file è già esistente. Se si decide di procedere sarà sovrascritto.</string>
<string name="openkeychain_required">La cifratura OpenPGP richiede l\'installazione di OpenKeychain.</string>
<string name="openkeychain_import_instructions">Il file non cifrato deve essere importato in un secondo momento dopo che è stato decriptato.</string>
<string name="settings_pbs">Impostazioni.pbs</string>
<?xml version="1.0" encoding="utf-8"?>
<!--
- Copyright © 2015-2019 Soren Stoutner <soren@stoutner.com>.
+ Copyright © 2015-2020 Soren Stoutner <soren@stoutner.com>.
This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
<item>OpenPGP</item>
</string-array>
<string name="kitkat_password_encryption_message">Шифрование паролем не работает на Android KitKat.</string>
+ <string name="file_does_not_exist">Файл не существует.</string>
+ <string name="file_exists_warning">Файл уже существует. Если вы продолжите, он будет перезаписан.</string>
<string name="openkeychain_required">Для использования шифрования OpenPGP необходимо приложение OpenKeychain.</string>
<string name="openkeychain_import_instructions">Незашифрованный файл должен быть импортирован на отдельном шаге после его дешифрования.</string>
<string name="settings_pbs">Настройки.pbs</string>
<string name="print">Print</string>
<string name="privacy_browser_web_page">Privacy Browser Web Page</string>
<string name="save">Save</string>
+ <string name="save_url">Save URL</string>
<string name="save_as_archive">Save as Archive</string>
<string name="save_as_image">Save as Image</string>
<string name="add_to_home_screen">Add to Home Screen</string>
<string name="next">Next</string>
<!-- Save Webpage. -->
- <string name="save_webpage" translatable="false">Save Webpage</string> <!-- This string is used to tag the save dialog. It is never displayed to the user. -->
+ <string name="save_dialog" translatable="false">Save Dialog</string> <!-- This string is used to tag the save dialog. It is never displayed to the user. -->
<string name="save_archive">Save Archive</string>
<string name="save_image">Save Image</string>
<string name="webpage_mht">Webpage.mht</string>
<string name="webpage_png">Webpage.png</string>
+ <string name="file">File</string>
+ <string name="saving_file">Saving file…</string>
<string name="saving_image">Saving image…</string>
+ <string name="file_saved">File saved.</string>
<string name="image_saved">Image saved.</string>
+ <string name="error_saving_file">Error saving file:</string>
<string name="error_saving_image">Error saving image:</string>
<!-- View Source. -->