Add an option to save a raw URL. https://redmine.stoutner.com/issues/463
authorSoren Stoutner <soren@stoutner.com>
Wed, 12 Feb 2020 18:37:42 +0000 (11:37 -0700)
committerSoren Stoutner <soren@stoutner.com>
Wed, 12 Feb 2020 18:37:42 +0000 (11:37 -0700)
25 files changed:
app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java
app/src/main/java/com/stoutner/privacybrowser/asynctasks/GetSource.java
app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveUrl.java [new file with mode: 0644]
app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveLogcatDialog.java
app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveWebpageDialog.java
app/src/main/java/com/stoutner/privacybrowser/dialogs/StoragePermissionDialog.java
app/src/main/java/com/stoutner/privacybrowser/helpers/FileNameHelper.java
app/src/main/res/drawable/copy_dark.xml
app/src/main/res/drawable/copy_enabled_dark.xml [new file with mode: 0644]
app/src/main/res/drawable/copy_enabled_light.xml [new file with mode: 0644]
app/src/main/res/drawable/copy_light.xml
app/src/main/res/drawable/custom_user_agent_enabled_dark.xml
app/src/main/res/drawable/custom_user_agent_enabled_light.xml
app/src/main/res/drawable/custom_user_agent_ghosted_dark.xml
app/src/main/res/drawable/custom_user_agent_ghosted_light.xml
app/src/main/res/layout/save_dialog.xml [deleted file]
app/src/main/res/layout/save_logcat_dialog.xml [new file with mode: 0644]
app/src/main/res/layout/save_webpage_dialog.xml [new file with mode: 0644]
app/src/main/res/menu/webview_options_menu.xml
app/src/main/res/values-de/strings.xml
app/src/main/res/values-es/strings.xml
app/src/main/res/values-fr/strings.xml
app/src/main/res/values-it/strings.xml
app/src/main/res/values-ru/strings.xml
app/src/main/res/values/strings.xml

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