From: Soren Stoutner Date: Wed, 26 Jun 2019 00:17:48 +0000 (-0700) Subject: Implement saving a webpage as an image. https://redmine.stoutner.com/issues/187 X-Git-Tag: v3.2~14 X-Git-Url: https://gitweb.stoutner.com/?a=commitdiff_plain;h=c1c9a0bf83ecef671356d554bb6e4927392b1cc8;p=PrivacyBrowserAndroid.git Implement saving a webpage as an image. https://redmine.stoutner.com/issues/187 --- diff --git a/app/src/main/java/com/stoutner/privacybrowser/activities/ImportExportActivity.java b/app/src/main/java/com/stoutner/privacybrowser/activities/ImportExportActivity.java index c8ff219b..bea58d3f 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/ImportExportActivity.java +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/ImportExportActivity.java @@ -25,7 +25,6 @@ import android.content.Intent; 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; @@ -60,6 +59,7 @@ import com.google.android.material.textfield.TextInputLayout; 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; @@ -566,56 +566,14 @@ public class ImportExportActivity extends AppCompatActivity implements StoragePe // 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; diff --git a/app/src/main/java/com/stoutner/privacybrowser/activities/LogcatActivity.java b/app/src/main/java/com/stoutner/privacybrowser/activities/LogcatActivity.java index 46c0a88a..f270cd24 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/LogcatActivity.java +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/LogcatActivity.java @@ -28,10 +28,8 @@ import android.content.Intent; 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; @@ -49,9 +47,11 @@ import androidx.fragment.app.DialogFragment; 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; @@ -166,7 +166,7 @@ public class LogcatActivity extends AppCompatActivity implements SaveLogcatDialo 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. @@ -233,7 +233,7 @@ public class LogcatActivity extends AppCompatActivity implements SaveLogcatDialo // 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); } @@ -325,63 +325,14 @@ public class LogcatActivity extends AppCompatActivity implements SaveLogcatDialo // 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); } } } diff --git a/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java b/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java index 657a2706..8ae5e7ad 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java @@ -24,6 +24,7 @@ package com.stoutner.privacybrowser.activities; 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; @@ -39,7 +40,6 @@ import android.content.res.Configuration; 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; @@ -117,6 +117,7 @@ import com.stoutner.privacybrowser.R; 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; @@ -127,7 +128,9 @@ import com.stoutner.privacybrowser.dialogs.DownloadLocationPermissionDialog; 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; @@ -136,13 +139,13 @@ import com.stoutner.privacybrowser.helpers.BlocklistHelper; 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; @@ -160,7 +163,8 @@ import java.util.Set; // 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; @@ -187,6 +191,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook public final static int DOMAINS_WEBVIEW_DEFAULT_USER_AGENT = 2; public final static int DOMAINS_CUSTOM_USER_AGENT = 13; + // Start activity for result request codes. + 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()`, @@ -295,9 +302,14 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // `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`. @@ -963,6 +975,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Reload the current WebView. currentWebView.reload(); + + // Consume the event. return true; case R.id.add_or_edit_domain: @@ -1069,6 +1083,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Make it so. startActivity(domainsIntent); } + + // Consume the event. return true; case R.id.toggle_first_party_cookies: @@ -1095,6 +1111,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Reload the current WebView. currentWebView.reload(); + + // Consume the event. return true; case R.id.toggle_third_party_cookies: @@ -1115,6 +1133,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Reload the current WebView. currentWebView.reload(); } // Else do nothing because SDK < 21. + + // Consume the event. return true; case R.id.toggle_dom_storage: @@ -1136,6 +1156,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Reload the current WebView. currentWebView.reload(); + + // Consume the event. return true; // Form data can be removed once the minimum API >= 26. @@ -1158,6 +1180,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Reload the current WebView. currentWebView.reload(); + + // Consume the event. return true; case R.id.clear_cookies: @@ -1180,6 +1204,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook } }) .show(); + + // Consume the event. return true; case R.id.clear_dom_storage: @@ -1235,6 +1261,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook } }) .show(); + + // Consume the event. return true; // Form data can be remove once the minimum API >= 26. @@ -1255,6 +1283,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook } }) .show(); + + // Consume the event. return true; case R.id.easylist: @@ -1266,6 +1296,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Reload the current WebView. currentWebView.reload(); + + // Consume the event. return true; case R.id.easyprivacy: @@ -1277,6 +1309,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Reload the current WebView. currentWebView.reload(); + + // Consume the event. return true; case R.id.fanboys_annoyance_list: @@ -1292,6 +1326,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Reload the current WebView. currentWebView.reload(); + + // Consume the event. return true; case R.id.fanboys_social_blocking_list: @@ -1303,6 +1339,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Reload the current WebView. currentWebView.reload(); + + // Consume the event. return true; case R.id.ultraprivacy: @@ -1314,6 +1352,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Reload the current WebView. currentWebView.reload(); + + // Consume the event. return true; case R.id.block_all_third_party_requests: @@ -1325,6 +1365,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Reload the current WebView. currentWebView.reload(); + + // Consume the event. return true; case R.id.user_agent_privacy_browser: @@ -1333,6 +1375,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Reload the current WebView. currentWebView.reload(); + + // Consume the event. return true; case R.id.user_agent_webview_default: @@ -1341,6 +1385,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Reload the current WebView. currentWebView.reload(); + + // Consume the event. return true; case R.id.user_agent_firefox_on_android: @@ -1349,6 +1395,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Reload the current WebView. currentWebView.reload(); + + // Consume the event. return true; case R.id.user_agent_chrome_on_android: @@ -1357,6 +1405,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Reload the current WebView. currentWebView.reload(); + + // Consume the event. return true; case R.id.user_agent_safari_on_ios: @@ -1365,6 +1415,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Reload the current WebView. currentWebView.reload(); + + // Consume the event. return true; case R.id.user_agent_firefox_on_linux: @@ -1373,6 +1425,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Reload the current WebView. currentWebView.reload(); + + // Consume the event. return true; case R.id.user_agent_chromium_on_linux: @@ -1381,6 +1435,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Reload the current WebView. currentWebView.reload(); + + // Consume the event. return true; case R.id.user_agent_firefox_on_windows: @@ -1389,6 +1445,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Reload the current WebView. currentWebView.reload(); + + // Consume the event. return true; case R.id.user_agent_chrome_on_windows: @@ -1397,6 +1455,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Reload the current WebView. currentWebView.reload(); + + // Consume the event. return true; case R.id.user_agent_edge_on_windows: @@ -1405,6 +1465,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Reload the current WebView. currentWebView.reload(); + + // Consume the event. return true; case R.id.user_agent_internet_explorer_on_windows: @@ -1413,6 +1475,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Reload the current WebView. currentWebView.reload(); + + // Consume the event. return true; case R.id.user_agent_safari_on_macos: @@ -1421,6 +1485,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Reload the current WebView. currentWebView.reload(); + + // Consume the event. return true; case R.id.user_agent_custom: @@ -1429,38 +1495,64 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // 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: @@ -1478,11 +1570,15 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // 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: @@ -1496,6 +1592,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // 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: @@ -1519,6 +1617,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Reload the website. currentWebView.reload(); + + // Consume the event. return true; case R.id.find_on_page: @@ -1551,6 +1651,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Display the keyboard. `0` sets no input flags. inputMethodManager.showSoftInput(findOnPageEditText, 0); }, 200); + + // Consume the event. return true; case R.id.print: @@ -1565,42 +1667,18 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // 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: @@ -1610,6 +1688,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // 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: @@ -1622,6 +1702,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Make it so. startActivity(viewSourceIntent); + + // Consume the event. return true; case R.id.share_url: @@ -1635,14 +1717,22 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // 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: @@ -1651,6 +1741,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Apply the proxy through Orbot settings. applyProxyThroughOrbot(true); + + // Consume the event. return true; case R.id.refresh: @@ -1661,12 +1753,18 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // 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: @@ -2378,6 +2476,20 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // 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; } } @@ -2533,13 +2645,44 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook } } - // 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; } } @@ -2664,6 +2807,54 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook 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) { @@ -4903,8 +5094,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // 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; } diff --git a/app/src/main/java/com/stoutner/privacybrowser/asynctasks/PopulateBlocklists.java b/app/src/main/java/com/stoutner/privacybrowser/asynctasks/PopulateBlocklists.java index 88160849..7a7fc4da 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/asynctasks/PopulateBlocklists.java +++ b/app/src/main/java/com/stoutner/privacybrowser/asynctasks/PopulateBlocklists.java @@ -43,13 +43,14 @@ public class PopulateBlocklists extends AsyncTask>> 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 contextWeakReference; private WeakReference activityWeakReference; + // The public constructor. public PopulateBlocklists(Context context, Activity activity) { // Populate the weak reference to the context. contextWeakReference = new WeakReference<>(context); diff --git a/app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveWebpageImage.java b/app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveWebpageImage.java new file mode 100644 index 00000000..aeb92988 --- /dev/null +++ b/app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveWebpageImage.java @@ -0,0 +1,152 @@ +/* + * Copyright © 2019 Soren Stoutner . + * + * This file is part of 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 . + */ + +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 { + // Define the weak references. + private WeakReference activityWeakReference; + private WeakReference 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 diff --git a/app/src/main/java/com/stoutner/privacybrowser/dialogs/CreateHomeScreenShortcutDialog.java b/app/src/main/java/com/stoutner/privacybrowser/dialogs/CreateHomeScreenShortcutDialog.java index dc4b2f8f..93e0ec86 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/dialogs/CreateHomeScreenShortcutDialog.java +++ b/app/src/main/java/com/stoutner/privacybrowser/dialogs/CreateHomeScreenShortcutDialog.java @@ -61,6 +61,7 @@ public class CreateHomeScreenShortcutDialog extends DialogFragment { 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(); diff --git a/app/src/main/java/com/stoutner/privacybrowser/dialogs/DownloadFileDialog.java b/app/src/main/java/com/stoutner/privacybrowser/dialogs/DownloadFileDialog.java index 0553f804..eb2b0ebf 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/dialogs/DownloadFileDialog.java +++ b/app/src/main/java/com/stoutner/privacybrowser/dialogs/DownloadFileDialog.java @@ -29,7 +29,6 @@ import android.net.Uri; 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; @@ -67,14 +66,17 @@ public class DownloadFileDialog extends DialogFragment { // 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(); @@ -114,15 +116,6 @@ public class DownloadFileDialog extends DialogFragment { 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()); @@ -130,6 +123,9 @@ public class DownloadFileDialog extends DialogFragment { 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); @@ -140,8 +136,18 @@ public class DownloadFileDialog extends DialogFragment { // 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) -> { diff --git a/app/src/main/java/com/stoutner/privacybrowser/dialogs/DownloadImageDialog.java b/app/src/main/java/com/stoutner/privacybrowser/dialogs/DownloadImageDialog.java index 06d765c2..f200d95a 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/dialogs/DownloadImageDialog.java +++ b/app/src/main/java/com/stoutner/privacybrowser/dialogs/DownloadImageDialog.java @@ -29,7 +29,6 @@ import android.net.Uri; 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; @@ -94,12 +93,6 @@ public class DownloadImageDialog extends DialogFragment { // 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()); @@ -107,6 +100,9 @@ public class DownloadImageDialog extends DialogFragment { 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); @@ -117,8 +113,18 @@ public class DownloadImageDialog extends DialogFragment { // 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) -> { diff --git a/app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveLogcatDialog.java b/app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveLogcatDialog.java index ec1ef33c..8178a8f2 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveLogcatDialog.java +++ b/app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveLogcatDialog.java @@ -21,6 +21,7 @@ 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; @@ -48,34 +49,28 @@ import androidx.fragment.app.DialogFragment; // The AndroidX dialog fragment is 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()); @@ -83,22 +78,25 @@ public class SaveLogcatDialog extends DialogFragment { 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); @@ -106,6 +104,9 @@ public class SaveLogcatDialog extends DialogFragment { 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. @@ -120,7 +121,7 @@ public class SaveLogcatDialog extends DialogFragment { // 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. @@ -140,13 +141,19 @@ public class SaveLogcatDialog extends DialogFragment { // 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. @@ -190,8 +197,8 @@ public class SaveLogcatDialog extends DialogFragment { // 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. diff --git a/app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveWebpageImageDialog.java b/app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveWebpageImageDialog.java new file mode 100644 index 00000000..11127d92 --- /dev/null +++ b/app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveWebpageImageDialog.java @@ -0,0 +1,213 @@ +/* + * Copyright © 2019 Soren Stoutner . + * + * This file is part of 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 . + */ + +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 diff --git a/app/src/main/java/com/stoutner/privacybrowser/helpers/FileNameHelper.java b/app/src/main/java/com/stoutner/privacybrowser/helpers/FileNameHelper.java new file mode 100644 index 00000000..5eb19484 --- /dev/null +++ b/app/src/main/java/com/stoutner/privacybrowser/helpers/FileNameHelper.java @@ -0,0 +1,73 @@ +/* + * Copyright © 2019 Soren Stoutner . + * + * This file is part of 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 . + */ + +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 diff --git a/app/src/main/res/drawable/images_enabled_dark.xml b/app/src/main/res/drawable/images_enabled_dark.xml index cb45f476..41874845 100644 --- a/app/src/main/res/drawable/images_enabled_dark.xml +++ b/app/src/main/res/drawable/images_enabled_dark.xml @@ -1,4 +1,4 @@ - + + + + + + + + + + + + + + + + + + + + + +