import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.media.MediaScannerConnection;
-import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import com.stoutner.privacybrowser.R;
import com.stoutner.privacybrowser.dialogs.StoragePermissionDialog;
+import com.stoutner.privacybrowser.helpers.FileNameHelper;
import com.stoutner.privacybrowser.helpers.ImportExportDatabaseHelper;
import java.io.File;
// Get a handle for the file name edit text.
EditText fileNameEditText = findViewById(R.id.file_name_edittext);
- // Get the file name URI.
- Uri fileNameUri = data.getData();
-
- // Remove the lint warning that the file name URI might be null.
- assert fileNameUri != null;
-
- // Get the raw file name path.
- String rawFileNamePath = fileNameUri.getPath();
-
- // Remove the incorrect lint warning that the file name path might be null.
- assert rawFileNamePath != null;
-
- // Check to see if the file name Path includes a valid storage location.
- if (rawFileNamePath.contains(":")) { // The path is valid.
- // Split the path into the initial content uri and the final path information.
- String fileNameContentPath = rawFileNamePath.substring(0, rawFileNamePath.indexOf(":"));
- String fileNameFinalPath = rawFileNamePath.substring(rawFileNamePath.indexOf(":") + 1);
-
- // Create the file name path string.
- String fileNamePath;
-
- // 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;
- } else { // The existing file name final path is not a complete, valid path.
- // Construct the file name path.
- switch (fileNameContentPath) {
- // The documents home has a special content path.
- case "/document/home":
- fileNamePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) + "/" + fileNameFinalPath;
- break;
-
- // Everything else for the primary user should be in `/document/primary`.
- case "/document/primary":
- fileNamePath = Environment.getExternalStorageDirectory() + "/" + fileNameFinalPath;
- break;
-
- // Just in case, catch everything else and place it in the external storage directory.
- default:
- fileNamePath = Environment.getExternalStorageDirectory() + "/" + fileNameFinalPath;
- break;
- }
- }
+ // Instantiate the file name helper.
+ FileNameHelper fileNameHelper = new FileNameHelper();
- // Set the file name path as the text of the file name edit text.
- fileNameEditText.setText(fileNamePath);
- } else { // The path is invalid.
- Snackbar.make(fileNameEditText, rawFileNamePath + " " + getString(R.string.invalid_location), Snackbar.LENGTH_INDEFINITE).show();
- }
+ // Convert the file name URI to a file name path.
+ String fileNamePath = fileNameHelper.convertUriToFileNamePath(data.getData());
+
+ // Set the file name path as the text of the file name edit text.
+ fileNameEditText.setText(fileNamePath);
}
break;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.media.MediaScannerConnection;
-import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
-import android.os.Environment;
import android.preference.PreferenceManager;
import android.view.Menu;
import android.view.MenuItem;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.google.android.material.snackbar.Snackbar;
+
import com.stoutner.privacybrowser.R;
import com.stoutner.privacybrowser.dialogs.StoragePermissionDialog;
import com.stoutner.privacybrowser.dialogs.SaveLogcatDialog;
+import com.stoutner.privacybrowser.helpers.FileNameHelper;
import java.io.BufferedReader;
import java.io.BufferedWriter;
return true;
case R.id.save:
- // Get a handle for the save alert dialog.
+ // Instantiate the save alert dialog.
DialogFragment saveDialogFragment = new SaveLogcatDialog();
// Show the save alert dialog.
// 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 storage permission. The logcat will be saved when it finishes.
+ // Request the write external storage permission. The logcat will be saved when it finishes.
ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
}
// Get a handle for the file name edit text.
EditText fileNameEditText = saveDialog.findViewById(R.id.file_name_edittext);
- // Get the file name URI.
- Uri fileNameUri = data.getData();
-
- // Remove the incorrect lint warning that the file name URI might be null.
- assert fileNameUri != null;
-
- // Get the raw file name path.
- String rawFileNamePath = fileNameUri.getPath();
-
- // Remove the incorrect lint warning that the file name path might be null.
- assert rawFileNamePath != null;
-
- // Check to see if the file name Path includes a valid storage location.
- if (rawFileNamePath.contains(":")) { // The path is valid.
- // Split the path into the initial content uri and the final path information.
- String fileNameContentPath = rawFileNamePath.substring(0, rawFileNamePath.indexOf(":"));
- String fileNameFinalPath = rawFileNamePath.substring(rawFileNamePath.indexOf(":") + 1);
-
- // Create the file name path string.
- String fileNamePath;
-
- // 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;
- } else { // The existing file name final path is not a complete, valid path.
- // Construct the file name path.
- switch (fileNameContentPath) {
- // The documents home has a special content path.
- case "/document/home":
- fileNamePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) + "/" + fileNameFinalPath;
- break;
-
- // Everything else for the primary user should be in `/document/primary`.
- case "/document/primary":
- fileNamePath = Environment.getExternalStorageDirectory() + "/" + fileNameFinalPath;
- break;
-
- // Just in case, catch everything else and place it in the external storage directory.
- default:
- fileNamePath = Environment.getExternalStorageDirectory() + "/" + fileNameFinalPath;
- break;
- }
- }
-
- // Set the file name path as the text of the file name edit text.
- fileNameEditText.setText(fileNamePath);
- } else { // The path is invalid.
- // Close the alert dialog.
- saveDialog.dismiss();
-
- // Get a handle for the logcat text view.
- TextView logcatTextView = findViewById(R.id.logcat_textview);
-
- // Display a snackbar with the error message.
- Snackbar.make(logcatTextView, rawFileNamePath + " " + getString(R.string.invalid_location), Snackbar.LENGTH_INDEFINITE).show();
- }
+ // Instantiate the file name helper.
+ FileNameHelper fileNameHelper = new FileNameHelper();
+
+ // Convert the file name URI to a file name path.
+ String fileNamePath = fileNameHelper.convertUriToFileNamePath(data.getData());
+
+ // Set the file name path as the text of the file name edit text.
+ fileNameEditText.setText(fileNamePath);
}
}
}
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
+import android.app.Dialog;
import android.app.DownloadManager;
import android.app.SearchManager;
import android.content.ActivityNotFoundException;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
-import android.graphics.Canvas;
import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import com.stoutner.privacybrowser.adapters.WebViewPagerAdapter;
import com.stoutner.privacybrowser.asynctasks.GetHostIpAddresses;
import com.stoutner.privacybrowser.asynctasks.PopulateBlocklists;
+import com.stoutner.privacybrowser.asynctasks.SaveWebpageImage;
import com.stoutner.privacybrowser.dialogs.AdConsentDialog;
import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog;
import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog;
import com.stoutner.privacybrowser.dialogs.EditBookmarkDialog;
import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDialog;
import com.stoutner.privacybrowser.dialogs.HttpAuthenticationDialog;
+import com.stoutner.privacybrowser.dialogs.SaveWebpageImageDialog;
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.fragments.WebViewTabFragment;
import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper;
import com.stoutner.privacybrowser.helpers.CheckPinnedMismatchHelper;
import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper;
+import com.stoutner.privacybrowser.helpers.FileNameHelper;
import com.stoutner.privacybrowser.helpers.OrbotProxyHelper;
import com.stoutner.privacybrowser.views.NestedScrollWebView;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
-import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
// AppCompatActivity from android.support.v7.app.AppCompatActivity must be used to have access to the SupportActionBar until the minimum API is >= 21.
public class MainWebViewActivity extends AppCompatActivity implements CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener,
DownloadFileDialog.DownloadFileListener, DownloadImageDialog.DownloadImageListener, DownloadLocationPermissionDialog.DownloadLocationPermissionDialogListener, EditBookmarkDialog.EditBookmarkListener,
- EditBookmarkFolderDialog.EditBookmarkFolderListener, NavigationView.OnNavigationItemSelectedListener, PopulateBlocklists.PopulateBlocklistsListener, WebViewTabFragment.NewTabListener {
+ EditBookmarkFolderDialog.EditBookmarkFolderListener, NavigationView.OnNavigationItemSelectedListener, PopulateBlocklists.PopulateBlocklistsListener, SaveWebpageImageDialog.SaveWebpageImageListener,
+ StoragePermissionDialog.StoragePermissionDialogListener, WebViewTabFragment.NewTabListener {
// `orbotStatus` is public static so it can be accessed from `OrbotProxyHelper`. It is also used in `onCreate()`, `onResume()`, and `applyProxyThroughOrbot()`.
public static String orbotStatus;
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.
+ private final int FILE_UPLOAD_REQUEST_CODE = 0;
+ public final static int BROWSE_SAVE_WEBPAGE_IMAGE_REQUEST_CODE = 1;
// The current WebView is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`,
// `downloadImageUrl` is used in `onCreateContextMenu()` and `onRequestPermissionResult()`.
private String downloadImageUrl;
- // The request codes are used in `onCreate()`, `onCreateContextMenu()`, `onCloseDownloadLocationPermissionDialog()`, `onRequestPermissionResult()`, and `initializeWebView()`.
+ // The save website image file path string is used in `onSaveWebpageImage()` and `onRequestPermissionResult()`
+ private String saveWebsiteImageFilePath;
+
+ // The permission result request codes are used in `onCreateContextMenu()`, `onCloseDownloadLocationPermissionDialog()`, `onRequestPermissionResult()`, `onSaveWebpageImage()`,
+ // `onCloseStoragePermissionDialog()`, and `initializeWebView()`.
private final int DOWNLOAD_FILE_REQUEST_CODE = 1;
private final int DOWNLOAD_IMAGE_REQUEST_CODE = 2;
+ private final int SAVE_WEBPAGE_IMAGE_REQUEST_CODE = 3;
@Override
// Remove the warning about needing to override `performClick()` when using an `OnTouchListener` with `WebView`.
// Reload the current WebView.
currentWebView.reload();
+
+ // Consume the event.
return true;
case R.id.add_or_edit_domain:
// Make it so.
startActivity(domainsIntent);
}
+
+ // Consume the event.
return true;
case R.id.toggle_first_party_cookies:
// Reload the current WebView.
currentWebView.reload();
+
+ // Consume the event.
return true;
case R.id.toggle_third_party_cookies:
// Reload the current WebView.
currentWebView.reload();
} // Else do nothing because SDK < 21.
+
+ // Consume the event.
return true;
case R.id.toggle_dom_storage:
// Reload the current WebView.
currentWebView.reload();
+
+ // Consume the event.
return true;
// Form data can be removed once the minimum API >= 26.
// Reload the current WebView.
currentWebView.reload();
+
+ // Consume the event.
return true;
case R.id.clear_cookies:
}
})
.show();
+
+ // Consume the event.
return true;
case R.id.clear_dom_storage:
}
})
.show();
+
+ // Consume the event.
return true;
// Form data can be remove once the minimum API >= 26.
}
})
.show();
+
+ // Consume the event.
return true;
case R.id.easylist:
// Reload the current WebView.
currentWebView.reload();
+
+ // Consume the event.
return true;
case R.id.easyprivacy:
// Reload the current WebView.
currentWebView.reload();
+
+ // Consume the event.
return true;
case R.id.fanboys_annoyance_list:
// Reload the current WebView.
currentWebView.reload();
+
+ // Consume the event.
return true;
case R.id.fanboys_social_blocking_list:
// Reload the current WebView.
currentWebView.reload();
+
+ // Consume the event.
return true;
case R.id.ultraprivacy:
// Reload the current WebView.
currentWebView.reload();
+
+ // Consume the event.
return true;
case R.id.block_all_third_party_requests:
// Reload the current WebView.
currentWebView.reload();
+
+ // Consume the event.
return true;
case R.id.user_agent_privacy_browser:
// Reload the current WebView.
currentWebView.reload();
+
+ // Consume the event.
return true;
case R.id.user_agent_webview_default:
// Reload the current WebView.
currentWebView.reload();
+
+ // Consume the event.
return true;
case R.id.user_agent_firefox_on_android:
// Reload the current WebView.
currentWebView.reload();
+
+ // Consume the event.
return true;
case R.id.user_agent_chrome_on_android:
// Reload the current WebView.
currentWebView.reload();
+
+ // Consume the event.
return true;
case R.id.user_agent_safari_on_ios:
// Reload the current WebView.
currentWebView.reload();
+
+ // Consume the event.
return true;
case R.id.user_agent_firefox_on_linux:
// Reload the current WebView.
currentWebView.reload();
+
+ // Consume the event.
return true;
case R.id.user_agent_chromium_on_linux:
// Reload the current WebView.
currentWebView.reload();
+
+ // Consume the event.
return true;
case R.id.user_agent_firefox_on_windows:
// Reload the current WebView.
currentWebView.reload();
+
+ // Consume the event.
return true;
case R.id.user_agent_chrome_on_windows:
// Reload the current WebView.
currentWebView.reload();
+
+ // Consume the event.
return true;
case R.id.user_agent_edge_on_windows:
// Reload the current WebView.
currentWebView.reload();
+
+ // Consume the event.
return true;
case R.id.user_agent_internet_explorer_on_windows:
// Reload the current WebView.
currentWebView.reload();
+
+ // Consume the event.
return true;
case R.id.user_agent_safari_on_macos:
// Reload the current WebView.
currentWebView.reload();
+
+ // Consume the event.
return true;
case R.id.user_agent_custom:
// Reload the current WebView.
currentWebView.reload();
+
+ // Consume the event.
return true;
case R.id.font_size_twenty_five_percent:
+ // Set the font size.
currentWebView.getSettings().setTextZoom(25);
+
+ // Consume the event.
return true;
case R.id.font_size_fifty_percent:
+ // Set the font size.
currentWebView.getSettings().setTextZoom(50);
+
+ // Consume the event.
return true;
case R.id.font_size_seventy_five_percent:
+ // Set the font size.
currentWebView.getSettings().setTextZoom(75);
+
+ // Consume the event.
return true;
case R.id.font_size_one_hundred_percent:
+ // Set the font size.
currentWebView.getSettings().setTextZoom(100);
+
+ // Consume the event.
return true;
case R.id.font_size_one_hundred_twenty_five_percent:
+ // Set the font size.
currentWebView.getSettings().setTextZoom(125);
+
+ // Consume the event.
return true;
case R.id.font_size_one_hundred_fifty_percent:
+ // Set the font size.
currentWebView.getSettings().setTextZoom(150);
+
+ // Consume the event.
return true;
case R.id.font_size_one_hundred_seventy_five_percent:
+ // Set the font size.
currentWebView.getSettings().setTextZoom(175);
+
+ // Consume the event.
return true;
case R.id.font_size_two_hundred_percent:
+ // Set the font size.
currentWebView.getSettings().setTextZoom(200);
+
+ // Consume the event.
return true;
case R.id.swipe_to_refresh:
// Disable the swipe refresh layout.
swipeRefreshLayout.setEnabled(false);
}
+
+ // Consume the event.
return true;
case R.id.wide_viewport:
// Toggle the viewport.
currentWebView.getSettings().setUseWideViewPort(!currentWebView.getSettings().getUseWideViewPort());
+
+ // Consume the event.
return true;
case R.id.display_images:
// Enable loading of images. Missing images will be loaded without the need for a reload.
currentWebView.getSettings().setLoadsImagesAutomatically(true);
}
+
+ // Consume the event.
return true;
case R.id.night_mode:
// Reload the website.
currentWebView.reload();
+
+ // Consume the event.
return true;
case R.id.find_on_page:
// Display the keyboard. `0` sets no input flags.
inputMethodManager.showSoftInput(findOnPageEditText, 0);
}, 200);
+
+ // Consume the event.
return true;
case R.id.print:
// Print the document.
printManager.print(getString(R.string.privacy_browser_web_page), printDocumentAdapter, null);
+
+ // Consume the event.
return true;
case R.id.save_as_image:
- // Create a webpage bitmap. Once the Minimum API >= 26 Bitmap.Config.RBGA_F16 can be used instead of ARGB_8888.
- Bitmap webpageBitmap = Bitmap.createBitmap(currentWebView.getHorizontalScrollRange(), currentWebView.getVerticalScrollRange(), Bitmap.Config.ARGB_8888);
-
- // Create a canvas.
- Canvas webpageCanvas = new Canvas(webpageBitmap);
-
- // Draw the current webpage onto the bitmap.
- currentWebView.draw(webpageCanvas);
-
- // Create a webpage PNG byte array output stream.
- ByteArrayOutputStream webpageByteArrayOutputStream = new ByteArrayOutputStream();
+ // Instantiate the save webpage image dialog.
+ DialogFragment saveWebpageImageDialogFragment = new SaveWebpageImageDialog();
- // Convert the bitmap to a PNG. `0` is for lossless compression (the only option for a PNG).
- webpageBitmap.compress(Bitmap.CompressFormat.PNG, 0, webpageByteArrayOutputStream);
+ // Show the save webpage image dialog.
+ saveWebpageImageDialogFragment.show(getSupportFragmentManager(), getString(R.string.save_as_image));
- // Get a file for the image.
- File imageFile = new File("/storage/emulated/0/webpage.png");
-
- // Delete the current file if it exists.
- if (imageFile.exists()) {
- //noinspection ResultOfMethodCallIgnored
- imageFile.delete();
- }
-
- try {
- // Create an image file output stream.
- FileOutputStream imageFileOutputStream = new FileOutputStream(imageFile);
-
- // Write the webpage image to the image file.
- webpageByteArrayOutputStream.writeTo(imageFileOutputStream);
- } catch (Exception exception) {
- // Add a snackbar.
- }
+ // Consume the event.
return true;
case R.id.add_to_homescreen:
// Show the create home screen shortcut dialog.
createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getString(R.string.create_shortcut));
+
+ // Consume the event.
return true;
case R.id.view_source:
// Make it so.
startActivity(viewSourceIntent);
+
+ // Consume the event.
return true;
case R.id.share_url:
// Make it so.
startActivity(Intent.createChooser(shareIntent, getString(R.string.share_url)));
+
+ // Consume the event.
return true;
case R.id.open_with_app:
+ // Open the URL with an outside app.
openWithApp(currentWebView.getUrl());
+
+ // Consume the event.
return true;
case R.id.open_with_browser:
+ // Open the URL with an outside browser.
openWithBrowser(currentWebView.getUrl());
+
+ // Consume the event.
return true;
case R.id.proxy_through_orbot:
// Apply the proxy through Orbot settings.
applyProxyThroughOrbot(true);
+
+ // Consume the event.
return true;
case R.id.refresh:
// Stop the loading of the WebView.
currentWebView.stopLoading();
}
+
+ // Consume the event.
return true;
case R.id.ad_consent:
- // Display the ad consent dialog.
+ // Instantiate the ad consent dialog.
DialogFragment adConsentDialogFragment = new AdConsentDialog();
+
+ // Display the ad consent dialog.
adConsentDialogFragment.show(getSupportFragmentManager(), getString(R.string.ad_consent));
+
+ // Consume the event.
return true;
default:
// Reset the image URL variable.
downloadImageUrl = "";
break;
+
+ case SAVE_WEBPAGE_IMAGE_REQUEST_CODE:
+ // Check to see if the storage permission was granted. If the dialog was canceled the grant result will be empty.
+ if ((grantResults.length > 0) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) { // The storage permission was granted.
+ // Save the webpage image.
+ new SaveWebpageImage(this, currentWebView).execute(saveWebsiteImageFilePath);
+ } 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 website image file path.
+ saveWebsiteImageFilePath = "";
+ break;
}
}
}
}
- // Process the results of an upload file chooser. Currently there is only one `startActivityForResult` in this activity, so the request code, used to differentiate them, is ignored.
+ // Process the results of a file browse.
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
- // File uploads only work on API >= 21.
- if (Build.VERSION.SDK_INT >= 21) {
- // Pass the file to the WebView.
- fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, data));
+ // Run the commands that correlate to the specified request code.
+ switch (requestCode) {
+ case FILE_UPLOAD_REQUEST_CODE:
+ // File uploads only work on API >= 21.
+ if (Build.VERSION.SDK_INT >= 21) {
+ // Pass the file to the WebView.
+ fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, data));
+ }
+ break;
+
+ case BROWSE_SAVE_WEBPAGE_IMAGE_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 saveWebpageImageDialogFragment= (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.save_as_image));
+
+ // Only update the file name if the dialog still exists.
+ if (saveWebpageImageDialogFragment != null) {
+ // Get a handle for the save webpage image dialog.
+ Dialog saveWebpageImageDialog = saveWebpageImageDialogFragment.getDialog();
+
+ // Get a handle for the file name edit text.
+ EditText fileNameEditText = saveWebpageImageDialog.findViewById(R.id.file_name_edittext);
+
+ // Instantiate the file name helper.
+ FileNameHelper fileNameHelper = new FileNameHelper();
+
+ // Convert the file name URI to a file name path.
+ String fileNamePath = fileNameHelper.convertUriToFileNamePath(data.getData());
+
+ // Set the file name path as the text of the file name edit text.
+ fileNameEditText.setText(fileNamePath);
+ }
+ }
+ break;
}
}
inputMethodManager.hideSoftInputFromWindow(toolbar.getWindowToken(), 0);
}
+ @Override
+ public void onSaveWebpageImage(DialogFragment dialogFragment) {
+ // Get a handle for the file name edit text.
+ EditText fileNameEditText = dialogFragment.getDialog().findViewById(R.id.file_name_edittext);
+
+ // Get the file path string.
+ saveWebsiteImageFilePath = 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 image.
+ new SaveWebpageImage(this, currentWebView).execute(saveWebsiteImageFilePath);
+ } 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 (saveWebsiteImageFilePath.startsWith(externalPrivateDirectory)) { // The file path is in the external private directory.
+ // Save the webpage image.
+ new SaveWebpageImage(this, currentWebView).execute(saveWebsiteImageFilePath);
+ } 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 = new StoragePermissionDialog();
+
+ // 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. The webpage image will be saved when it finishes.
+ ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, SAVE_WEBPAGE_IMAGE_REQUEST_CODE);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onCloseStoragePermissionDialog() {
+ // 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}, SAVE_WEBPAGE_IMAGE_REQUEST_CODE);
+ }
+
private void applyAppSettings() {
// Initialize the app if this is the first run. This is done here instead of in `onCreate()` to shorten the time that an unthemed background is displayed on app startup.
if (webViewDefaultUserAgent == null) {
// Create an intent to open a chooser based ont the file chooser parameters.
Intent fileChooserIntent = fileChooserParams.createIntent();
- // Open the file chooser. Currently only one `startActivityForResult` exists in this activity, so the request code, used to differentiate them, is simply `0`.
- startActivityForResult(fileChooserIntent, 0);
+ // Open the file chooser.
+ startActivityForResult(fileChooserIntent, FILE_UPLOAD_REQUEST_CODE);
}
return true;
}
void finishedPopulatingBlocklists(ArrayList<ArrayList<List<String[]>>> combinedBlocklists);
}
- // Declare a populate blocklists listener.
+ // Define a populate blocklists listener.
private PopulateBlocklistsListener populateBlocklistsListener;
- // Declare weak references for the activity and context.
+ // Define weak references for the activity and context.
private WeakReference<Context> contextWeakReference;
private WeakReference<Activity> activityWeakReference;
+ // The public constructor.
public PopulateBlocklists(Context context, Activity activity) {
// Populate the weak reference to the context.
contextWeakReference = new WeakReference<>(context);
--- /dev/null
+/*
+ * Copyright © 2019 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.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.os.AsyncTask;
+
+import com.google.android.material.snackbar.Snackbar;
+
+import com.stoutner.privacybrowser.R;
+import com.stoutner.privacybrowser.views.NestedScrollWebView;
+import com.stoutner.privacybrowser.views.NoSwipeViewPager;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.lang.ref.WeakReference;
+
+public class SaveWebpageImage extends AsyncTask<String, Void, String> {
+ // Define the weak references.
+ private WeakReference<Activity> activityWeakReference;
+ private WeakReference<NestedScrollWebView> nestedScrollWebViewWeakReference;
+
+ // Define a success string constant.
+ private final String SUCCESS = "Success";
+
+ // Define the saving image snackbar and the webpage bitmap.
+ private Snackbar savingImageSnackbar;
+ private Bitmap webpageBitmap;
+
+ // The public constructor.
+ public SaveWebpageImage(Activity activity, NestedScrollWebView nestedScrollWebView) {
+ // Populate the weak references.
+ activityWeakReference = new WeakReference<>(activity);
+ nestedScrollWebViewWeakReference = new WeakReference<>(nestedScrollWebView);
+ }
+
+ // `onPreExecute()` operates on the UI thread.
+ @Override
+ protected void onPreExecute() {
+ // Get a handle for the activity and the nested scroll WebView.
+ Activity activity = activityWeakReference.get();
+ NestedScrollWebView nestedScrollWebView = nestedScrollWebViewWeakReference.get();
+
+ // Abort if the activity or the nested scroll WebView is gone.
+ if ((activity == null) || activity.isFinishing() || nestedScrollWebView == null) {
+ return;
+ }
+
+ // Create a saving image snackbar.
+ savingImageSnackbar = Snackbar.make(nestedScrollWebView, R.string.saving_image, Snackbar.LENGTH_INDEFINITE);
+
+ // Display the saving image snackbar.
+ savingImageSnackbar.show();
+
+ // Create a webpage bitmap. Once the Minimum API >= 26 Bitmap.Config.RBGA_F16 can be used instead of ARGB_8888. The nested scroll WebView commands must be run on the UI thread.
+ webpageBitmap = Bitmap.createBitmap(nestedScrollWebView.getHorizontalScrollRange(), nestedScrollWebView.getVerticalScrollRange(), Bitmap.Config.ARGB_8888);
+
+ // Create a canvas.
+ Canvas webpageCanvas = new Canvas(webpageBitmap);
+
+ // Draw the current webpage onto the bitmap. The nested scroll WebView commands must be run on the UI thread.
+ nestedScrollWebView.draw(webpageCanvas);
+ }
+
+ @Override
+ protected String doInBackground(String... fileName) {
+ // Get a handle for the activity.
+ Activity activity = activityWeakReference.get();
+
+ // Abort if the activity is gone.
+ if ((activity == null) || activity.isFinishing()) {
+ return "";
+ }
+
+ // Create a webpage PNG byte array output stream.
+ ByteArrayOutputStream webpageByteArrayOutputStream = new ByteArrayOutputStream();
+
+ // Convert the bitmap to a PNG. `0` is for lossless compression (the only option for a PNG). This compression takes a long time.
+ webpageBitmap.compress(Bitmap.CompressFormat.PNG, 0, webpageByteArrayOutputStream);
+
+ // Get a file for the image.
+ File imageFile = new File(fileName[0]);
+
+ // Delete the current file if it exists.
+ if (imageFile.exists()) {
+ //noinspection ResultOfMethodCallIgnored
+ imageFile.delete();
+ }
+
+ // Create a file creation disposition string.
+ String fileCreationDisposition = SUCCESS;
+
+ try {
+ // Create an image file output stream.
+ FileOutputStream imageFileOutputStream = new FileOutputStream(imageFile);
+
+ // Write the webpage image to the image file.
+ webpageByteArrayOutputStream.writeTo(imageFileOutputStream);
+ } catch (Exception exception) {
+ // Store the error in the file creation disposition string.
+ fileCreationDisposition = exception.toString();
+ }
+
+ // Return the file creation disposition string.
+ return fileCreationDisposition;
+ }
+
+ // `onPostExecute()` operates on the UI thread.
+ @Override
+ protected void onPostExecute(String fileCreationDisposition) {
+ // 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 image snackbar.
+ savingImageSnackbar.dismiss();
+
+ // Display a file creation disposition snackbar.
+ if (fileCreationDisposition.equals(SUCCESS)) {
+ Snackbar.make(noSwipeViewPager, R.string.image_saved, Snackbar.LENGTH_SHORT).show();
+ } else {
+ Snackbar.make(noSwipeViewPager, activity.getString(R.string.error_saving_image) + " " + fileCreationDisposition, Snackbar.LENGTH_INDEFINITE).show();
+ }
+ }
+}
\ No newline at end of file
private EditText urlEditText;
private RadioButton openWithPrivacyBrowserRadioButton;
+ // The public constructor.
public static CreateHomeScreenShortcutDialog createDialog(String shortcutName, String urlString, Bitmap favoriteIconBitmap) {
// Create a favorite icon byte array output stream.
ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.view.KeyEvent;
-import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.widget.EditText;
// Create a variable for the file name string.
String fileNameString;
+ // Get the index of the end of `filename=` from the file name string.
+ int fileNameIndex = contentDisposition.indexOf("filename=") + 9;
+
// Parse the filename from `contentDisposition`.
if (contentDisposition.contains("filename=\"")) { // The file name is contained in a string surrounded by `""`.
fileNameString = contentDisposition.substring(contentDisposition.indexOf("filename=\"") + 10, contentDisposition.indexOf("\"", contentDisposition.indexOf("filename=\"") + 10));
- } else if (contentDisposition.contains("filename=") && ((contentDisposition.indexOf(";", contentDisposition.indexOf("filename=") + 9)) > 0 )) {
+ } else if (contentDisposition.contains("filename=") && ((contentDisposition.indexOf(";", fileNameIndex)) > 0 )) {
// The file name is contained in a string beginning with `filename=` and ending with `;`.
- fileNameString = contentDisposition.substring(contentDisposition.indexOf("filename=") + 9, contentDisposition.indexOf(";", contentDisposition.indexOf("filename=") + 9));
+ fileNameString = contentDisposition.substring(fileNameIndex, contentDisposition.indexOf(";", fileNameIndex));
} else if (contentDisposition.contains("filename=")) { // The file name is contained in a string beginning with `filename=` and proceeding to the end of `contentDisposition`.
- fileNameString = contentDisposition.substring(contentDisposition.indexOf("filename=") + 9);
+ fileNameString = contentDisposition.substring(fileNameIndex);
} else { // `contentDisposition` does not contain the filename, so use the last path segment of the URL.
Uri downloadUri = Uri.parse(urlString);
fileNameString = downloadUri.getLastPathSegment();
fileSize = String.format(Locale.getDefault(), "%.3g", (float) fileSizeLong / 1048576) + " MB";
}
- // Remove the warning below that `getActivity()` might be null;
- assert getActivity() != null;
-
- // Get the activity's layout inflater.
- LayoutInflater layoutInflater = getActivity().getLayoutInflater();
-
- // Use an alert dialog builder to create the alert dialog.
- AlertDialog.Builder dialogBuilder;
-
// Get a handle for the shared preferences.
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext());
boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
+ // Use an alert dialog builder to create the alert dialog.
+ AlertDialog.Builder dialogBuilder;
+
// Set the style according to the theme.
if (darkTheme) {
dialogBuilder = new AlertDialog.Builder(getActivity(), R.style.PrivacyBrowserAlertDialogDark);
// Set the title.
dialogBuilder.setTitle(R.string.save_as);
+ // Set the icon according to the theme.
+ if (darkTheme) {
+ dialogBuilder.setIcon(R.drawable.save_dialog_dark);
+ } else {
+ dialogBuilder.setIcon(R.drawable.save_dialog_light);
+ }
+
+ // Remove the warning below that `getActivity()` might be null;
+ assert getActivity() != null;
+
// Set the view. The parent view is `null` because it will be assigned by `AlertDialog`.
- dialogBuilder.setView(layoutInflater.inflate(R.layout.download_file_dialog, null));
+ dialogBuilder.setView(getActivity().getLayoutInflater().inflate(R.layout.download_file_dialog, null));
// Set an listener on the negative button.
dialogBuilder.setNegativeButton(R.string.cancel, (DialogInterface dialog, int which) -> {
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.view.KeyEvent;
-import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.widget.EditText;
// Remove the warning below that `.getActivity()` might be null.
assert getActivity() != null;
- // Get the activity's layout inflater.
- LayoutInflater layoutInflater = getActivity().getLayoutInflater();
-
- // Use and alert dialog builder to create the alert dialog.
- AlertDialog.Builder dialogBuilder;
-
// Get a handle for the shared preferences.
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext());
boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
+ // Use and alert dialog builder to create the alert dialog.
+ AlertDialog.Builder dialogBuilder;
+
// Set the style according to the theme.
if (darkTheme) {
dialogBuilder = new AlertDialog.Builder(getActivity(), R.style.PrivacyBrowserAlertDialogDark);
// Set the title.
dialogBuilder.setTitle(R.string.save_image_as);
+ // Set the icon according to the theme.
+ if (darkTheme) {
+ dialogBuilder.setIcon(R.drawable.images_enabled_dark);
+ } else {
+ dialogBuilder.setIcon(R.drawable.images_enabled_light);
+ }
+
+ // Remove the incorrect lint warning below that `getActivity() might be null.
+ assert getActivity() != null;
+
// Set the view. The parent view is `null` because it will be assigned by `AlertDialog`.
- dialogBuilder.setView(layoutInflater.inflate(R.layout.download_image_dialog, null));
+ dialogBuilder.setView(getActivity().getLayoutInflater().inflate(R.layout.download_image_dialog, null));
// Set an listener on the negative button.
dialogBuilder.setNegativeButton(R.string.cancel, (DialogInterface dialog, int which) -> {
import android.Manifest;
import android.annotation.SuppressLint;
+import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import com.stoutner.privacybrowser.R;
public class SaveLogcatDialog extends DialogFragment {
- // Instantiate the class variables.
+ // Define the save logcat listener.
private SaveLogcatListener saveLogcatListener;
- private Context parentContext;
// The public interface is used to send information back to the parent activity.
public interface SaveLogcatListener {
void onSaveLogcat(DialogFragment dialogFragment);
}
+ @Override
public void onAttach(Context context) {
// Run the default commands.
super.onAttach(context);
- // Store a handle for the context.
- parentContext = context;
-
- // Get a handle for `SaveLogcatListener` from the launching context.
+ // Get a handle for save logcat listener from the launching context.
saveLogcatListener = (SaveLogcatListener) context;
}
- // `@SuppressLing("InflateParams")` removes the warning about using `null` as the parent view group when inflating the `AlertDialog`.
+ // `@SuppressLing("InflateParams")` removes the warning about using null as the parent view group when inflating the alert dialog.
@SuppressLint("InflateParams")
@Override
@NonNull
public Dialog onCreateDialog(Bundle savedInstanceState) {
- // Use an alert dialog builder to create the alert dialog.
- AlertDialog.Builder dialogBuilder;
-
// Get a handle for the shared preferences.
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext());
boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
+ // Use an alert dialog builder to create the alert dialog.
+ AlertDialog.Builder dialogBuilder;
+
+ // Get a handle for the activity.
+ Activity activity = getActivity();
+
+ // Remove the incorrect lint warning below that the activity might be null.
+ assert activity != null;
+
// Set the style according to the theme.
if (darkTheme) {
- dialogBuilder = new AlertDialog.Builder(getActivity(), R.style.PrivacyBrowserAlertDialogDark);
+ dialogBuilder = new AlertDialog.Builder(activity, R.style.PrivacyBrowserAlertDialogDark);
} else {
- dialogBuilder = new AlertDialog.Builder(getActivity(), R.style.PrivacyBrowserAlertDialogLight);
+ dialogBuilder = new AlertDialog.Builder(activity, R.style.PrivacyBrowserAlertDialogLight);
}
// Set the title.
dialogBuilder.setTitle(R.string.save_logcat);
- // Remove the incorrect lint warning that `getActivity().getLayoutInflater()` might be null.
- assert getActivity() != null;
-
- // Set the view. The parent view is null because it will be assigned by the alert dialog.
- dialogBuilder.setView(getActivity().getLayoutInflater().inflate(R.layout.save_logcat_dialog, null));
-
// Set the icon according to the theme.
if (darkTheme) {
dialogBuilder.setIcon(R.drawable.save_dialog_dark);
dialogBuilder.setIcon(R.drawable.save_dialog_light);
}
+ // 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));
+
// Set the cancel button listener.
dialogBuilder.setNegativeButton(R.string.cancel, (DialogInterface dialog, int which) -> {
// Do nothing. The alert dialog will close automatically.
// Create an alert dialog from the builder.
AlertDialog alertDialog = dialogBuilder.create();
- // Remove the incorrect lint warning below that `getWindow().addFlags()` might be null.
+ // Remove the incorrect lint warning below that `getWindow()` might be null.
assert alertDialog.getWindow() != null;
// Disable screenshots if not allowed.
// Create a string for the default file path.
String defaultFilePath;
+ // Get a handle for the context.
+ Context context = getContext();
+
+ // Remove the incorrect lint warning below that context might be null.
+ assert context != null;
+
// Set the default file path according to the storage permission state.
- if (ContextCompat.checkSelfPermission(parentContext, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // The storage permission has been granted.
+ if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // The storage permission has been granted.
// Set the default file path to use the external public directory.
defaultFilePath = Environment.getExternalStorageDirectory() + "/" + getString(R.string.privacy_browser_logcat_txt);
} else { // The storage permission has not been granted.
// Set the default file path to use the external private directory.
- defaultFilePath = parentContext.getExternalFilesDir(null) + "/" + getString(R.string.privacy_browser_logcat_txt);
+ defaultFilePath = context.getExternalFilesDir(null) + "/" + getString(R.string.privacy_browser_logcat_txt);
}
// Display the default file path.
// Request a file that can be opened.
browseIntent.addCategory(Intent.CATEGORY_OPENABLE);
- // Launch the file picker. There is only one `startActivityForResult()`, so the request code is simply set to 0.
- startActivityForResult(browseIntent, 0);
+ // Launch the file picker. There is only one `startActivityForResult()`, so the request code is simply set to 0, but it must be run under `activity` so the request code is correct.
+ activity.startActivityForResult(browseIntent, 0);
});
// Hide the storage permission text view on API < 23 as permissions on older devices are automatically granted.
--- /dev/null
+/*
+ * Copyright © 2019 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.dialogs;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.preference.PreferenceManager;
+import android.provider.DocumentsContract;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.DialogFragment;
+
+import com.stoutner.privacybrowser.R;
+import com.stoutner.privacybrowser.activities.MainWebViewActivity;
+
+public class SaveWebpageImageDialog extends DialogFragment {
+ // Define the save webpage image listener.
+ private SaveWebpageImageListener saveWebpageImageListener;
+
+ // The public interface is used to send information back to the parent activity.
+ public interface SaveWebpageImageListener {
+ void onSaveWebpageImage(DialogFragment dialogFragment);
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ // Run the default commands.
+ super.onAttach(context);
+
+ // Get a handle for the save webpage image listener from the launching context.
+ saveWebpageImageListener = (SaveWebpageImageListener) context;
+ }
+
+ // `@SuppressLing("InflateParams")` removes the warning about using null as the parent view group when inflating the alert dialog.
+ @SuppressLint("InflateParams")
+ @Override
+ @NonNull
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ // Get a handle for the shared preferences.
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext());
+
+ // Get the screenshot and theme preferences.
+ boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
+ boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
+
+ // Use an alert dialog builder to create the alert dialog.
+ AlertDialog.Builder dialogBuilder;
+
+ // Get a handle for the activity.
+ Activity activity = getActivity();
+
+ // Remove the incorrect lint warning below that the activity might be null.
+ assert activity != null;
+
+ // Set the style according to the theme.
+ if (darkTheme) {
+ dialogBuilder = new AlertDialog.Builder(activity, R.style.PrivacyBrowserAlertDialogDark);
+ } else {
+ dialogBuilder = new AlertDialog.Builder(activity, R.style.PrivacyBrowserAlertDialogLight);
+ }
+
+ // Set the title.
+ dialogBuilder.setTitle(R.string.save_image);
+
+ // Set the icon according to the theme.
+ if (darkTheme) {
+ dialogBuilder.setIcon(R.drawable.images_enabled_dark);
+ } else {
+ dialogBuilder.setIcon(R.drawable.images_enabled_light);
+ }
+
+ // 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));
+
+ // Set the cancel button listener.
+ dialogBuilder.setNegativeButton(R.string.cancel, (DialogInterface dialog, int which) -> {
+ // Do nothing. The alert dialog will close automatically.
+ });
+
+ // Set the save button listener.
+ dialogBuilder.setPositiveButton(R.string.save, (DialogInterface dialog, int which) -> {
+ // Return the dialog fragment to the parent activity.
+ saveWebpageImageListener.onSaveWebpageImage(this);
+ });
+
+ // Create an alert dialog from the builder.
+ AlertDialog alertDialog = dialogBuilder.create();
+
+ // Remove the incorrect lint warning below that `getWindows()` might be null.
+ assert alertDialog.getWindow() != null;
+
+ // Disable screenshots if not allowed.
+ if (!allowScreenshots) {
+ alertDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
+ }
+
+ // The alert dialog must be shown before items in the layout can be modified.
+ alertDialog.show();
+
+ // Get handles for the layout items.
+ EditText fileNameEditText = alertDialog.findViewById(R.id.file_name_edittext);
+ Button browseButton = alertDialog.findViewById(R.id.browse_button);
+ TextView storagePermissionTextView = alertDialog.findViewById(R.id.storage_permission_textview);
+ Button saveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
+
+ // Create a string for the default file path.
+ String defaultFilePath;
+
+ // Get a handle for the context.
+ Context context = getContext();
+
+ // Remove the incorrect lint warning that context might be null.
+ assert context != null;
+
+ // Set the default file path according to the storage permission state.
+ if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // The storage permission has been granted.
+ // Set the default file path to use the external public directory.
+ defaultFilePath = Environment.getExternalStorageDirectory() + "/" + getString(R.string.webpage_png);
+ } else { // The storage permission has not been granted.
+ // Set the default file path to use the external private directory.
+ defaultFilePath = context.getExternalFilesDir(null) + "/" + getString(R.string.webpage_png);
+ }
+
+ // Display the default file path.
+ fileNameEditText.setText(defaultFilePath);
+
+ // Update the status of the save button when the file name changes.
+ fileNameEditText.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ // Do nothing.
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ // // Enable the save button if a file name exists.
+ saveButton.setEnabled(!fileNameEditText.getText().toString().isEmpty());
+ }
+ });
+
+ // Handle clicks on the browse button.
+ browseButton.setOnClickListener((View view) -> {
+ // Create the file picker intent.
+ Intent browseIntent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
+
+ // Set the intent MIME type to include all files so that everything is visible.
+ browseIntent.setType("*/*");
+
+ // Set the initial file name.
+ browseIntent.putExtra(Intent.EXTRA_TITLE, getString(R.string.webpage_png));
+
+ // Set the initial directory if the minimum API >= 26.
+ if (Build.VERSION.SDK_INT >= 26) {
+ browseIntent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Environment.getExternalStorageDirectory());
+ }
+
+ // Request a file that can be opened.
+ browseIntent.addCategory(Intent.CATEGORY_OPENABLE);
+
+ // Start the file picker. This must be started under `activity` so that the request code is returned correctly.
+ activity.startActivityForResult(browseIntent, MainWebViewActivity.BROWSE_SAVE_WEBPAGE_IMAGE_REQUEST_CODE);
+ });
+
+ // Hide the storage permission text view on API < 23 as permissions on older devices are automatically granted.
+ if (Build.VERSION.SDK_INT < 23) {
+ storagePermissionTextView.setVisibility(View.GONE);
+ }
+
+ // Return the alert dialog.
+ return alertDialog;
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright © 2019 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.helpers;
+
+import android.net.Uri;
+import android.os.Environment;
+
+public class FileNameHelper {
+ public String convertUriToFileNamePath(Uri Uri) {
+ // Initialize a file name path string.
+ String fileNamePath = "";
+
+ // Convert the URI to a raw file name path.
+ String rawFileNamePath = Uri.getPath();
+
+ // Only process the raw file name path if it is not null.
+ if (rawFileNamePath != null) {
+ // Check to see if the file name Path includes a valid storage location.
+ if (rawFileNamePath.contains(":")) { // The path contains a `:`.
+ // Split the path into the initial content uri and the final path information.
+ 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
+ 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;
+ } else { // The existing file name final path is not a complete, valid path.
+ // Construct the file name path.
+ switch (fileNameContentPath) {
+ // The documents home has a special content path.
+ case "/document/home":
+ fileNamePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) + "/" + fileNameFinalPath;
+ break;
+
+ // Everything else for the primary user should be in `/document/primary`.
+ case "/document/primary":
+ fileNamePath = Environment.getExternalStorageDirectory() + "/" + fileNameFinalPath;
+ break;
+
+ // Just in case, catch everything else and place it in the external storage directory.
+ default:
+ fileNamePath = Environment.getExternalStorageDirectory() + "/" + fileNameFinalPath;
+ break;
+ }
+ }
+ } else { // The path does not contain a `:`.
+ // Use the raw file name path.
+ fileNamePath = rawFileNamePath;
+ }
+ }
+
+ // Return the file name path string.
+ return fileNamePath;
+ }
+}
\ No newline at end of file
-<!-- `images_enabled_dark.xml` comes from the Android Material icon set, where it is called `image`. It is released under the Apache License 2.0. -->
+<!-- This drawable comes from the Android Material icon set, where it is called `image`. 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
-<!-- `images_enabled_light.xml` comes from the Android Material icon set, where it is called `image`. It is released under the Apache License 2.0. -->
+<!-- This drawable comes from the Android Material icon set, where it is called `image`. 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
-<!-- `save_dialog_dark.xml` comes from the Android Material icon set, where it is called `save`. It is released under the Apache License 2.0. -->
+<!-- This drawable comes from the Android Material icon set, where it is called `save`. 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
-<!-- `save_dialog_light.xml` comes from the Android Material icon set, where it is called `save`. It is released under the Apache License 2.0. -->
+<!-- This drawable comes from the Android Material icon set, where it is called `save`. 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
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ Copyright © 2019 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 EditText and the select file button horizontally. -->
+ <LinearLayout
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:orientation="horizontal" >
+
+ <!-- The `TextInputLayout` makes the `android:hint` float above the `EditText`. -->
+ <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>
+
+ <TextView
+ android:id="@+id/storage_permission_textview"
+ android:layout_width="wrap_content"
+ android:layout_height="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 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 EditText and the select file button horizontally. -->
- <LinearLayout
- android:layout_height="wrap_content"
- android:layout_width="match_parent"
- android:orientation="horizontal" >
-
- <!-- The `TextInputLayout` makes the `android:hint` float above the `EditText`. -->
- <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>
-
- <TextView
- android:id="@+id/storage_permission_textview"
- android:layout_width="wrap_content"
- android:layout_height="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
<string name="export_successful">Export erfolgreich.</string>
<string name="export_failed">Export fehlgeschlagen:</string>
<string name="import_failed">Import fehlgeschlagen:</string>
- <string name="invalid_location">ist kein gültiger Ordner.</string>
<string name="storage_permission">Speicher-Berechtigung</string>
<string name="storage_permission_message">Privacy Browser benötigt die Speicher-Berechtigung, um auf öffentliche Ordner zuzugreifen.
Wenn diese verweigert wird, können die Ordner der Anwendung trotzdem verwendet werden.</string>
<!-- Orbot. -->
<string name="orbot_proxy_not_installed">Orbot-Proxy wird nicht funktionieren, solange Orbot nicht installiert ist.</string>
- <string name="waiting_for_orbot">Warte, bis sich Orbot verbindet...</string>
+ <string name="waiting_for_orbot">Warte, bis sich Orbot verbindet…</string>
<!-- About Activity. -->
<string name="about_privacy_browser">Über Privacy Browser</string>
<string name="export_successful">Exportación exitosa.</string>
<string name="export_failed">Exportación fallida:</string>
<string name="import_failed">Importación fallida:</string>
- <string name="invalid_location">no es una ubicación válida.</string>
<string name="storage_permission">Permiso de almacenamiento</string>
<string name="storage_permission_message">Navegador Privado necesita el permiso de almacenamiento para acceder a los directorios públicos.
Si se deniega, los directorios de la aplicación pueden seguir utilizándose.</string>
<!-- Orbot. -->
<string name="orbot_proxy_not_installed">Enviar a través de Orbot no funcionará a menos que se instale Orbot.</string>
- <string name="waiting_for_orbot">Esperando a Orbot para conectar...</string>
+ <string name="waiting_for_orbot">Esperando a Orbot para conectar…</string>
<!-- About Activity. -->
<string name="about_privacy_browser">Acerca de Navegador Privado</string>
<string name="export_successful">Esportazione riuscita</string>
<string name="export_failed">Esportazione fallita:</string>
<string name="import_failed">Importazione fallita:</string>
- <string name="invalid_location">non è una cartella valida.</string>
<string name="storage_permission">Permesso di accesso alla memoria</string>
<string name="storage_permission_message">Privacy Browser necessita del permesso di accesso alla memoria per poter accedere alle cartelle pubbliche.
Se questo permesso è negato possono comunque essere utilizzate le cartelle dell\'applicazione.</string>
<!-- Orbot. -->
<string name="orbot_proxy_not_installed">Il Proxy con Orbot funziona solo se è installato Orbot.</string>
- <string name="waiting_for_orbot">In attesa della connessione di Orbot...</string>
+ <string name="waiting_for_orbot">In attesa della connessione di Orbot…</string>
<!-- About Activity. -->
<string name="about_privacy_browser">Informazioni su Privacy Browser</string>
<string name="export_successful">Экспорт выполнен.</string>
<string name="export_failed">Сбой при экспорте:</string>
<string name="import_failed">Сбой при импорте:</string>
- <string name="invalid_location">- недопустимое расположение.</string>
<string name="storage_permission">Доступ к хранилищу</string>
<string name="storage_permission_message">Privacy Browser необходимо разрешение на доступ к внешним папкам. Если доступ предоставлен не будет, можно использовать локальную папку приложения.</string>
<string name="storage_permission_explanation">Для доступа к файлам во внешних папках требуется соответствующее разрешение. В противном случае будут работать только локальные папки.</string>
<!-- Orbot. -->
<string name="orbot_proxy_not_installed">Проксирование Orbot работает только с установленным Orbot.</string>
- <string name="waiting_for_orbot">Ожидание Orbot для подключения...</string>
+ <string name="waiting_for_orbot">Ожидание Orbot для подключения…</string>
<!-- About Activity. -->
<string name="about_privacy_browser">О Privacy Browser</string>
<string name="export_successful">Dışa aktarım başarılı</string>
<string name="export_failed">Dışa aktarım başarısız:</string>
<string name="import_failed">İçe aktarım başarısız:</string>
- <string name="invalid_location">geçerli bir konum değildir</string>
<string name="storage_permission">Depolama İzni</string>
<string name="storage_permission_message">Privacy Browser, genel dizinlere erişmek için depolama iznine ihtiyaç duymaktadır. Reddedildiği takdirde, uygulamanın dizinleri hala kullanılabilir.</string>
<string name="storage_permission_explanation">Genel dizinlerdeki dosyalara erişim icin depolama izni gerekmektedir. Aksi takdirde, sadece uygulamanın dizinleri çalışacaktır.</string>
<!-- Orbot. -->
<string name="orbot_proxy_not_installed">Orbot yüklenmeden Orbot vekil sunucusu çalışmayacaktır.</string>
- <string name="waiting_for_orbot">Orbot\'un bağlanması bekleniyor...</string>
+ <string name="waiting_for_orbot">Orbot\'un bağlanması bekleniyor…</string>
<!-- About Activity. -->
<string name="about_privacy_browser">Privacy Browser Hakkında</string>
<string name="previous">Previous</string>
<string name="next">Next</string>
+ <!-- Save Webpage as Image. -->
+ <string name="save_image">Save Image</string>
+ <string name="webpage_png">Webpage.png</string>
+ <string name="saving_image">Saving image…</string>
+ <string name="image_saved">Image saved.</string>
+ <string name="error_saving_image">Error saving image:</string>
+
<!-- View Source. -->
<string name="request_headers">Request Headers</string>
<string name="response_message">Response Message</string>
<string name="export_successful">Export successful.</string>
<string name="export_failed">Export failed:</string>
<string name="import_failed">Import failed:</string>
- <string name="invalid_location">is not a valid location.</string>
<string name="storage_permission">Storage Permission</string>
<string name="storage_permission_message">Privacy Browser needs the storage permission to access public directories. If it is denied, the app’s directories can still be used.</string>
<string name="storage_permission_explanation">Accessing files in public directories requires the storage permission. Otherwise, only app directories will work.</string>
<!-- Orbot. -->
<string name="orbot_proxy_not_installed">Orbot proxy will not work unless Orbot is installed.</string>
- <string name="waiting_for_orbot">Waiting for Orbot to connect...</string>
+ <string name="waiting_for_orbot">Waiting for Orbot to connect…</string>
<!-- About Activity. -->
<string name="about_privacy_browser">About Privacy Browser</string>
<item>WebView default user agent</item> <!-- This item must not be translated into other languages because it is referenced in code. It is never displayed on the screen. -->
<item>Mozilla/5.0 (Android 9; Mobile; rv:67.0) Gecko/67.0 Firefox/67.0</item>
<item>Mozilla/5.0 (Linux; Android 9; Pixel 2 XL) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.157 Mobile Safari/537.36</item>
- <item>Mozilla/5.0 (iPhone; CPU iPhone OS 12_1_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1</item>
+ <item>Mozilla/5.0 (iPhone; CPU iPhone OS 12_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1</item>
<item>Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0</item>
<item>Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36</item>
<item>Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:67.0) Gecko/20100101 Firefox/67.0</item>
<item>Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36</item>
<item>Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/18.17763</item>
<item>Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko</item>
- <item>Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0.2 Safari/605.1.15</item>
+ <item>Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0.3 Safari/605.1.15</item>
<item>Custom user agent</item> <!-- This item must not be translated into other languages because it is referenced in code. It is never displayed on the screen. -->
</string-array>
<string name="custom_user_agent">Custom user agent</string>