Implement saving a webpage as an image. https://redmine.stoutner.com/issues/187
authorSoren Stoutner <soren@stoutner.com>
Wed, 26 Jun 2019 00:17:48 +0000 (17:17 -0700)
committerSoren Stoutner <soren@stoutner.com>
Wed, 26 Jun 2019 00:17:48 +0000 (17:17 -0700)
23 files changed:
app/src/main/java/com/stoutner/privacybrowser/activities/ImportExportActivity.java
app/src/main/java/com/stoutner/privacybrowser/activities/LogcatActivity.java
app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java
app/src/main/java/com/stoutner/privacybrowser/asynctasks/PopulateBlocklists.java
app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveWebpageImage.java [new file with mode: 0644]
app/src/main/java/com/stoutner/privacybrowser/dialogs/CreateHomeScreenShortcutDialog.java
app/src/main/java/com/stoutner/privacybrowser/dialogs/DownloadFileDialog.java
app/src/main/java/com/stoutner/privacybrowser/dialogs/DownloadImageDialog.java
app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveLogcatDialog.java
app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveWebpageImageDialog.java [new file with mode: 0644]
app/src/main/java/com/stoutner/privacybrowser/helpers/FileNameHelper.java [new file with mode: 0644]
app/src/main/res/drawable/images_enabled_dark.xml
app/src/main/res/drawable/images_enabled_light.xml
app/src/main/res/drawable/save_dialog_dark.xml
app/src/main/res/drawable/save_dialog_light.xml
app/src/main/res/layout/save_dialog.xml [new file with mode: 0644]
app/src/main/res/layout/save_logcat_dialog.xml [deleted file]
app/src/main/res/values-de/strings.xml
app/src/main/res/values-es/strings.xml
app/src/main/res/values-it/strings.xml
app/src/main/res/values-ru/strings.xml
app/src/main/res/values-tr/strings.xml
app/src/main/res/values/strings.xml

index c8ff219..bea58d3 100644 (file)
@@ -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;
 
index 46c0a88..f270cd2 100644 (file)
@@ -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);
             }
         }
     }
index 657a270..8ae5e7a 100644 (file)
@@ -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;
             }
index 8816084..7a7fc4d 100644 (file)
@@ -43,13 +43,14 @@ public class PopulateBlocklists extends AsyncTask<Void, String, ArrayList<ArrayL
         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);
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 (file)
index 0000000..aeb9298
--- /dev/null
@@ -0,0 +1,152 @@
+/*
+ * 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
index dc4b2f8..93e0ec8 100644 (file)
@@ -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();
index 0553f80..eb2b0eb 100644 (file)
@@ -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) -> {
index 06d765c..f200d95 100644 (file)
@@ -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) -> {
index ec1ef33..8178a8f 100644 (file)
@@ -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 (file)
index 0000000..11127d9
--- /dev/null
@@ -0,0 +1,213 @@
+/*
+ * 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
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 (file)
index 0000000..5eb1948
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * 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
index cb45f47..4187484 100644 (file)
@@ -1,4 +1,4 @@
-<!-- `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
index 6241eb9..a118988 100644 (file)
@@ -1,4 +1,4 @@
-<!-- `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
index f49b8ab..d20d248 100644 (file)
@@ -1,4 +1,4 @@
-<!-- `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
index e4592c8..21aca67 100644 (file)
@@ -1,4 +1,4 @@
-<!-- `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
diff --git a/app/src/main/res/layout/save_dialog.xml b/app/src/main/res/layout/save_dialog.xml
new file mode 100644 (file)
index 0000000..973055b
--- /dev/null
@@ -0,0 +1,72 @@
+<?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
diff --git a/app/src/main/res/layout/save_logcat_dialog.xml b/app/src/main/res/layout/save_logcat_dialog.xml
deleted file mode 100644 (file)
index 973055b..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-<?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
index 135d030..9fc59d1 100644 (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>
index d94d478..fd14e0c 100644 (file)
     <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>
index c79bee3..444ee8a 100644 (file)
     <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>
index a591478..35e3a4b 100644 (file)
     <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>
index 88f473c..ebb1189 100644 (file)
     <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>
index a64d221..1f89968 100644 (file)
     <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>