Implement opening of local files. https://redmine.stoutner.com/issues/513
authorSoren Stoutner <soren@stoutner.com>
Tue, 31 Dec 2019 21:14:49 +0000 (14:14 -0700)
committerSoren Stoutner <soren@stoutner.com>
Tue, 31 Dec 2019 21:14:49 +0000 (14:14 -0700)
app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java
app/src/main/java/com/stoutner/privacybrowser/dialogs/CreateBookmarkDialog.java
app/src/main/java/com/stoutner/privacybrowser/dialogs/OpenDialog.java [new file with mode: 0644]
app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveWebpageDialog.java
app/src/main/java/com/stoutner/privacybrowser/dialogs/StoragePermissionDialog.java
app/src/main/res/layout/open_dialog.xml [new file with mode: 0644]
app/src/main/res/layout/save_dialog.xml
app/src/main/res/menu/webview_navigation_menu.xml
app/src/main/res/values/strings.xml

index efa66ac9516ef9970b83216dbe23fb2e6f52a19f..83bd942d518e6e1fc82bacfbc50c069998529bfc 100644 (file)
@@ -131,6 +131,7 @@ import com.stoutner.privacybrowser.dialogs.EditBookmarkDialog;
 import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDialog;
 import com.stoutner.privacybrowser.dialogs.FontSizeDialog;
 import com.stoutner.privacybrowser.dialogs.HttpAuthenticationDialog;
+import com.stoutner.privacybrowser.dialogs.OpenDialog;
 import com.stoutner.privacybrowser.dialogs.ProxyNotInstalledDialog;
 import com.stoutner.privacybrowser.dialogs.PinnedMismatchDialog;
 import com.stoutner.privacybrowser.dialogs.SaveWebpageDialog;
@@ -170,9 +171,9 @@ 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, FontSizeDialog.UpdateFontSizeListener, NavigationView.OnNavigationItemSelectedListener, PinnedMismatchDialog.PinnedMismatchListener,
-        PopulateBlocklists.PopulateBlocklistsListener, SaveWebpageDialog.SaveWebpageListener, StoragePermissionDialog.StoragePermissionDialogListener, UrlHistoryDialog.NavigateHistoryListener,
-        WebViewTabFragment.NewTabListener {
+        EditBookmarkFolderDialog.EditBookmarkFolderListener, FontSizeDialog.UpdateFontSizeListener, NavigationView.OnNavigationItemSelectedListener, OpenDialog.OpenListener,
+        PinnedMismatchDialog.PinnedMismatchListener, PopulateBlocklists.PopulateBlocklistsListener, SaveWebpageDialog.SaveWebpageListener, StoragePermissionDialog.StoragePermissionDialogListener,
+        UrlHistoryDialog.NavigateHistoryListener, WebViewTabFragment.NewTabListener {
 
     // `orbotStatus` is public static so it can be accessed from `OrbotProxyHelper`.  It is also used in `onCreate()`, `onResume()`, and `applyProxy()`.
     public static String orbotStatus = "unknown";
@@ -199,11 +200,20 @@ 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_REQUEST_CODE = 1;
+    // Start activity for result request codes.  The public static entries are accessed from `OpenDialog()` and `SaveWebpageDialog()`.
+    public static final int BROWSE_OPEN_REQUEST_CODE = 0;
+    public static final int BROWSE_SAVE_WEBPAGE_REQUEST_CODE = 1;
+    private final int BROWSE_FILE_UPLOAD_REQUEST_CODE = 2;
 
 
+    // The permission result request codes are used in `onCreateContextMenu()`, `onCloseDownloadLocationPermissionDialog()`, `onRequestPermissionResult()`, `onSaveWebpage()`,
+    // `onCloseStoragePermissionDialog()`, and `initializeWebView()`.
+    private final int PERMISSION_DOWNLOAD_FILE_REQUEST_CODE = 0;
+    private final int PERMISSION_DOWNLOAD_IMAGE_REQUEST_CODE = 1;
+    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;
+
     // The current WebView is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`,
     // `findNextOnPage()`, `closeFindOnPage()`, `loadUrlFromTextBox()`, `onSslMismatchBack()`, `applyProxy()`, and `applyDomainSettings()`.
     private NestedScrollWebView currentWebView;
@@ -312,16 +322,10 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     // `downloadImageUrl` is used in `onCreateContextMenu()` and `onRequestPermissionResult()`.
     private String downloadImageUrl;
 
-    // The save webpage file path string is used in `onSaveWebpageImage()` and `onRequestPermissionResult()`
+    // The file path strings are used in `onSaveWebpageImage()` and `onRequestPermissionResult()`
+    private String openFilePath;
     private String saveWebpageFilePath;
 
-    // The permission result request codes are used in `onCreateContextMenu()`, `onCloseDownloadLocationPermissionDialog()`, `onRequestPermissionResult()`, `onSaveWebpageImage()`,
-    // `onCloseStoragePermissionDialog()`, and `initializeWebView()`.
-    private final int DOWNLOAD_FILE_REQUEST_CODE = 0;
-    private final int DOWNLOAD_IMAGE_REQUEST_CODE = 1;
-    private final int SAVE_WEBPAGE_ARCHIVE_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`.
     @SuppressLint("ClickableViewAccessibility")
@@ -1702,7 +1706,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
             case R.id.save_as_archive:
                 // Instantiate the save webpage archive dialog.
-                DialogFragment saveWebpageArchiveDialogFragment = SaveWebpageDialog.saveWebpage(SaveWebpageDialog.ARCHIVE);
+                DialogFragment saveWebpageArchiveDialogFragment = SaveWebpageDialog.saveWebpage(StoragePermissionDialog.SAVE_ARCHIVE);
 
                 // Show the save webpage archive dialog.
                 saveWebpageArchiveDialogFragment.show(getSupportFragmentManager(), getString(R.string.save_webpage));
@@ -1712,7 +1716,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
             case R.id.save_as_image:
                 // Instantiate the save webpage image dialog.
-                DialogFragment saveWebpageImageDialogFragment = SaveWebpageDialog.saveWebpage(SaveWebpageDialog.IMAGE);
+                DialogFragment saveWebpageImageDialogFragment = SaveWebpageDialog.saveWebpage(StoragePermissionDialog.SAVE_IMAGE);
 
                 // Show the save webpage image dialog.
                 saveWebpageImageDialogFragment.show(getSupportFragmentManager(), getString(R.string.save_webpage));
@@ -1863,6 +1867,14 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 urlHistoryDialogFragment.show(getSupportFragmentManager(), getString(R.string.history));
                 break;
 
+            case R.id.open:
+                // Instantiate the open file dialog.
+                DialogFragment openDialogFragment = new OpenDialog();
+
+                // Show the open file dialog.
+                openDialogFragment.show(getSupportFragmentManager(), getString(R.string.open));
+                break;
+
             case R.id.requests:
                 // Populate the resource requests.
                 RequestsActivity.resourceRequests = currentWebView.getResourceRequests();
@@ -2119,7 +2131,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                                 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}, DOWNLOAD_FILE_REQUEST_CODE);
+                                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.
@@ -2225,7 +2237,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                                 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}, DOWNLOAD_IMAGE_REQUEST_CODE);
+                                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.
@@ -2350,7 +2362,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                                 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}, DOWNLOAD_IMAGE_REQUEST_CODE);
+                                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.
@@ -2644,80 +2656,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         switch (downloadType) {
             case DownloadLocationPermissionDialog.DOWNLOAD_FILE:
                 // Request the WRITE_EXTERNAL_STORAGE permission with a file request code.
-                ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
+                ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_DOWNLOAD_FILE_REQUEST_CODE);
                 break;
 
             case DownloadLocationPermissionDialog.DOWNLOAD_IMAGE:
                 // Request the WRITE_EXTERNAL_STORAGE permission with an image request code.
-                ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
-                break;
-        }
-    }
-
-    @Override
-    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
-        // Get a handle for the fragment manager.
-        FragmentManager fragmentManager = getSupportFragmentManager();
-
-        switch (requestCode) {
-            case DOWNLOAD_FILE_REQUEST_CODE:
-                // Show the download file alert dialog.  When the dialog closes, the correct command will be used based on the permission status.
-                DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(downloadUrl, downloadContentDisposition, downloadContentLength);
-
-                // On API 23, displaying the fragment must be delayed or the app will crash.
-                if (Build.VERSION.SDK_INT == 23) {
-                    new Handler().postDelayed(() -> downloadFileDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
-                } else {
-                    downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
-                }
-
-                // Reset the download variables.
-                downloadUrl = "";
-                downloadContentDisposition = "";
-                downloadContentLength = 0;
-                break;
-
-            case DOWNLOAD_IMAGE_REQUEST_CODE:
-                // Show the download image alert dialog.  When the dialog closes, the correct command will be used based on the permission status.
-                DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(downloadImageUrl);
-
-                // On API 23, displaying the fragment must be delayed or the app will crash.
-                if (Build.VERSION.SDK_INT == 23) {
-                    new Handler().postDelayed(() -> downloadImageDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
-                } else {
-                    downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
-                }
-
-                // Reset the image URL variable.
-                downloadImageUrl = "";
-                break;
-
-            case SAVE_WEBPAGE_ARCHIVE_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 webpage archive.
-                    currentWebView.saveWebArchive(saveWebpageFilePath);
-                } else {
-                    // Display an error snackbar.
-                    Snackbar.make(currentWebView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show();
-                }
-
-                // Reset the save webpage file path.
-                saveWebpageFilePath = "";
-                break;
-
-            case SAVE_WEBPAGE_IMAGE_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 webpage image.
-                    new SaveWebpageImage(this, currentWebView).execute(saveWebpageFilePath);
-                } 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 webpage file path.
-                saveWebpageFilePath = "";
+                ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_DOWNLOAD_IMAGE_REQUEST_CODE);
                 break;
         }
     }
@@ -2966,17 +2910,17 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
     // Process the results of a file browse.
     @Override
-    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+    public void onActivityResult(int requestCode, int resultCode, Intent returnedIntent) {
         // Run the default commands.
-        super.onActivityResult(requestCode, resultCode, data);
+        super.onActivityResult(requestCode, resultCode, returnedIntent);
 
         // Run the commands that correlate to the specified request code.
         switch (requestCode) {
-            case FILE_UPLOAD_REQUEST_CODE:
+            case BROWSE_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));
+                    fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, returnedIntent));
                 }
                 break;
 
@@ -2984,7 +2928,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_webpage));
 
                     // Only update the file name if the dialog still exists.
                     if (saveWebpageDialogFragment != null) {
@@ -3001,12 +2945,50 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                         FileNameHelper fileNameHelper = new FileNameHelper();
 
                         // Get the file path if it isn't null.
-                        if (data.getData() != null) {
+                        if (returnedIntent.getData() != null) {
                             // Convert the file name URI to a file name path.
-                            String fileNamePath = fileNameHelper.convertUriToFileNamePath(data.getData());
+                            String fileNamePath = fileNameHelper.convertUriToFileNamePath(returnedIntent.getData());
 
                             // Set the file name path as the text of the file name edit text.
                             fileNameEditText.setText(fileNamePath);
+
+                            // Move the cursor to the end of the file name edit text.
+                            fileNameEditText.setSelection(fileNamePath.length());
+                        }
+                    }
+                }
+                break;
+
+            case BROWSE_OPEN_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 open dialog fragment.
+                    DialogFragment openDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.open));
+
+                    // Only update the file name if the dialog still exists.
+                    if (openDialogFragment != null) {
+                        // Get a handle for the open dialog.
+                        Dialog openDialog = openDialogFragment.getDialog();
+
+                        // Remove the incorrect lint warning below tha tth edialog might be null.
+                        assert openDialog != null;
+
+                        // Get a handle for the file name edit text.
+                        EditText fileNameEditText = openDialog.findViewById(R.id.file_name_edittext);
+
+                        // Instantiate the file name helper.
+                        FileNameHelper fileNameHelper = new FileNameHelper();
+
+                        // Get the file path if it isn't null.
+                        if (returnedIntent.getData() != null) {
+                            // Convert the file name URI to a file name path.
+                            String fileNamePath = fileNameHelper.convertUriToFileNamePath(returnedIntent.getData());
+
+                            // Set the file name path as the text of the file name edit text.
+                            fileNameEditText.setText(fileNamePath);
+
+                            // Move the cursor to the end of the file name edit text.
+                            fileNameEditText.setSelection(fileNamePath.length());
                         }
                     }
                 }
@@ -3160,6 +3142,54 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         currentWebView.getSettings().setTextZoom(newFontSize);
     }
 
+    @Override
+    public void onOpen(DialogFragment dialogFragment) {
+        // Get the dialog.
+        Dialog dialog = dialogFragment.getDialog();
+
+        // Remove the incorrect lint warning below that the dialog might be null.
+        assert dialog != null;
+
+        // Get a handle for the file name edit text.
+        EditText fileNameEditText = dialog.findViewById(R.id.file_name_edittext);
+
+        // Get the file path string.
+        openFilePath = 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.
+            // Open the file.
+            currentWebView.loadUrl("file://" + openFilePath);
+        } 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 (openFilePath.startsWith(externalPrivateDirectory)) {  // the file path is in the external private directory.
+                // Open the file.
+                currentWebView.loadUrl("file://" + openFilePath);
+            } 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 = StoragePermissionDialog.displayDialog(StoragePermissionDialog.OPEN);
+
+                    // Show the storage permission alert dialog.  The permission will be requested the 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 file will be opened when it finishes.
+                    ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_OPEN_REQUEST_CODE);
+                }
+            }
+        }
+    }
+
     @Override
     public void onSaveWebpage(int saveType, DialogFragment dialogFragment) {
         // Get the dialog.
@@ -3178,18 +3208,18 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         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 SaveWebpageDialog.ARCHIVE:
+                case StoragePermissionDialog.SAVE_ARCHIVE:
                     // Save the webpage archive.
                     currentWebView.saveWebArchive(saveWebpageFilePath);
                     break;
 
-                case SaveWebpageDialog.IMAGE:
+                case StoragePermissionDialog.SAVE_IMAGE:
                     // Save the webpage image.
                     new SaveWebpageImage(this, currentWebView).execute(saveWebpageFilePath);
                     break;
             }
         } else {  // The storage permission has not been granted.
-            // Get the external private directory `File`.
+            // Get the external private directory file.
             File externalPrivateDirectoryFile = getExternalFilesDir(null);
 
             // Remove the incorrect lint error below that the file might be null.
@@ -3200,14 +3230,14 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
             // Check to see if the file path is in the external private directory.
             if (saveWebpageFilePath.startsWith(externalPrivateDirectory)) {  // The file path is in the external private directory.
-                //Save the webpage according to the save type.
+                // Save the webpage according to the save type.
                 switch (saveType) {
-                    case SaveWebpageDialog.ARCHIVE:
+                    case StoragePermissionDialog.SAVE_ARCHIVE:
                         // Save the webpage archive.
                         currentWebView.saveWebArchive(saveWebpageFilePath);
                         break;
 
-                    case SaveWebpageDialog.IMAGE:
+                    case StoragePermissionDialog.SAVE_IMAGE:
                         // Save the webpage image.
                         new SaveWebpageImage(this, currentWebView).execute(saveWebpageFilePath);
                         break;
@@ -3222,14 +3252,14 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     storagePermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.storage_permission));
                 } else {  // Show the permission request directly.
                     switch (saveType) {
-                        case SaveWebpageDialog.ARCHIVE:
+                        case StoragePermissionDialog.SAVE_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}, SAVE_WEBPAGE_ARCHIVE_REQUEST_CODE);
+                            ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_WEBPAGE_ARCHIVE_REQUEST_CODE);
                             break;
 
-                        case SaveWebpageDialog.IMAGE:
+                        case StoragePermissionDialog.SAVE_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}, SAVE_WEBPAGE_IMAGE_REQUEST_CODE);
+                            ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_WEBPAGE_IMAGE_REQUEST_CODE);
                             break;
                     }
                 }
@@ -3238,16 +3268,103 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     }
 
     @Override
-    public void onCloseStoragePermissionDialog(int saveType) {
-        switch (saveType) {
-            case SaveWebpageDialog.ARCHIVE:
+    public void onCloseStoragePermissionDialog(int requestType) {
+        switch (requestType) {
+            case StoragePermissionDialog.OPEN:
+                // Request the write external storage permission.  The file will be opened when it finishes.
+                ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_OPEN_REQUEST_CODE);
+                break;
+
+            case StoragePermissionDialog.SAVE_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}, SAVE_WEBPAGE_ARCHIVE_REQUEST_CODE);
+                ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_WEBPAGE_ARCHIVE_REQUEST_CODE);
                 break;
 
-            case SaveWebpageDialog.IMAGE:
+            case StoragePermissionDialog.SAVE_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}, SAVE_WEBPAGE_IMAGE_REQUEST_CODE);
+                ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_WEBPAGE_IMAGE_REQUEST_CODE);
+                break;
+        }
+    }
+
+    @Override
+    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+        // Get a handle for the fragment manager.
+        FragmentManager fragmentManager = getSupportFragmentManager();
+
+        switch (requestCode) {
+            case PERMISSION_DOWNLOAD_FILE_REQUEST_CODE:
+                // Show the download file alert dialog.  When the dialog closes, the correct command will be used based on the permission status.
+                DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(downloadUrl, downloadContentDisposition, downloadContentLength);
+
+                // On API 23, displaying the fragment must be delayed or the app will crash.
+                if (Build.VERSION.SDK_INT == 23) {
+                    new Handler().postDelayed(() -> downloadFileDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
+                } else {
+                    downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
+                }
+
+                // Reset the download variables.
+                downloadUrl = "";
+                downloadContentDisposition = "";
+                downloadContentLength = 0;
+                break;
+
+            case PERMISSION_DOWNLOAD_IMAGE_REQUEST_CODE:
+                // Show the download image alert dialog.  When the dialog closes, the correct command will be used based on the permission status.
+                DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(downloadImageUrl);
+
+                // On API 23, displaying the fragment must be delayed or the app will crash.
+                if (Build.VERSION.SDK_INT == 23) {
+                    new Handler().postDelayed(() -> downloadImageDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
+                } else {
+                    downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
+                }
+
+                // Reset the image URL variable.
+                downloadImageUrl = "";
+                break;
+
+            case PERMISSION_OPEN_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.
+                    // Load the file.
+                    currentWebView.loadUrl("file://" + openFilePath);
+                } 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 open file path.
+                openFilePath = "";
+                break;
+
+            case PERMISSION_SAVE_WEBPAGE_ARCHIVE_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 webpage archive.
+                    currentWebView.saveWebArchive(saveWebpageFilePath);
+                } 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 webpage file path.
+                saveWebpageFilePath = "";
+                break;
+
+            case PERMISSION_SAVE_WEBPAGE_IMAGE_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 webpage image.
+                    new SaveWebpageImage(this, currentWebView).execute(saveWebpageFilePath);
+                } 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 webpage file path.
+                saveWebpageFilePath = "";
                 break;
         }
     }
@@ -3529,12 +3646,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Listen for touches on the navigation menu.
         navigationView.setNavigationItemSelectedListener(this);
 
-        // Get handles for the navigation menu and the back and forward menu items.  The menu is zero-based.
+        // Get handles for the navigation menu and the back and forward menu items.  The menu is based.
         Menu navigationMenu = navigationView.getMenu();
         MenuItem navigationBackMenuItem = navigationMenu.getItem(2);
         MenuItem navigationForwardMenuItem = navigationMenu.getItem(3);
         MenuItem navigationHistoryMenuItem = navigationMenu.getItem(4);
-        MenuItem navigationRequestsMenuItem = navigationMenu.getItem(5);
+        MenuItem navigationRequestsMenuItem = navigationMenu.getItem(6);
 
         // Update the web view pager every time a tab is modified.
         webViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@@ -3796,7 +3913,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity);
         drawerHeaderPaddingBottom = (int) (8 * screenDensity);
 
-        // The drawer listener is used to update the navigation menu.`
+        // The drawer listener is used to update the navigation menu.
         drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() {
             @Override
             public void onDrawerSlide(@NonNull View drawerView, float slideOffset) {
@@ -4587,12 +4704,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             String urlString = urlEditText.getText().toString();
 
             // Highlight the URL according to the protocol.
-            if (urlString.startsWith("file://")) {  // This is a file URL.
-                // De-emphasize only the protocol.
-                urlEditText.getText().setSpan(initialGrayColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-            } else if (urlString.startsWith("content://")) {
-                // De-emphasize only the protocol.
-                urlEditText.getText().setSpan(initialGrayColorSpan, 0, 10, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+            if (urlString.startsWith("file://") || urlString.startsWith("content://")) {  // This is a file or content URL.
+                // De-emphasize everything before the file name.
+                urlEditText.getText().setSpan(initialGrayColorSpan, 0, urlString.lastIndexOf("/") + 1,Spanned.SPAN_INCLUSIVE_INCLUSIVE);
             } else {  // This is a web URL.
                 // Get the index of the `/` immediately after the domain name.
                 int endOfDomainName = urlString.indexOf("/", (urlString.indexOf("//") + 2));
@@ -5330,7 +5444,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                         downloadLocationPermissionDialogFragment.show(getSupportFragmentManager(), 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}, DOWNLOAD_FILE_REQUEST_CODE);
+                        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.
@@ -5627,7 +5741,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     Intent fileChooserIntent = fileChooserParams.createIntent();
 
                     // Open the file chooser.
-                    startActivityForResult(fileChooserIntent, FILE_UPLOAD_REQUEST_CODE);
+                    startActivityForResult(fileChooserIntent, BROWSE_FILE_UPLOAD_REQUEST_CODE);
                 }
                 return true;
             }
@@ -5743,7 +5857,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 Menu navigationMenu = navigationView.getMenu();
 
                 // Get a handle for the navigation requests menu item.  The menu is 0 based.
-                MenuItem navigationRequestsMenuItem = navigationMenu.getItem(5);
+                MenuItem navigationRequestsMenuItem = navigationMenu.getItem(6);
 
                 // Create an empty web resource response to be used if the resource request is blocked.
                 WebResourceResponse emptyWebResourceResponse = new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes()));
index 4da0f14eb73c0e13094dd17c1a42c01e1161840f..f89649bc542d8f9910be6c37f74630a252b01f85 100644 (file)
@@ -176,40 +176,42 @@ public class CreateBookmarkDialog extends DialogFragment {
         // Set the current `WebView` title as the text for `create_bookmark_name_edittext`.
         createBookmarkNameEditText.setText(title);
 
-        // Allow the `enter` key on the keyboard to create the bookmark from `create_bookmark_name_edittext`.
-        createBookmarkNameEditText.setOnKeyListener((View view, int keyCode, KeyEvent event) -> {
-            // If the event is a key-down on the `enter` key, select the `PositiveButton` `Create`.
-            if ((keyCode == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN)) {
-                // Trigger `createBookmarkListener` and return the `DialogFragment` to the parent activity.
+        // Allow the `enter` key on the keyboard to create the bookmark from the create bookmark name edittext`.
+        createBookmarkNameEditText.setOnKeyListener((View view, int keyCode, KeyEvent keyEvent) -> {
+            // If the event is a key-down on the `enter` key, select the create button.
+            if ((keyCode == KeyEvent.KEYCODE_ENTER) && (keyEvent.getAction() == KeyEvent.ACTION_DOWN)) {
+                // Trigger the create bookmark listener and return the dialog fragment and the favorite icon bitmap to the parent activity.
                 createBookmarkListener.onCreateBookmark(this, favoriteIconBitmap);
 
-                // Manually dismiss the `AlertDialog`.
+                // Manually dismiss the alert dialog.
                 alertDialog.dismiss();
 
                 // Consume the event.
                 return true;
-            } else {  // If any other key was pressed, do not consume the event.
+            } else {  // Some other key was pressed.
+                // Do not consume the event.
                 return false;
             }
         });
 
-        // Set the formattedUrlString as the initial text of `create_bookmark_url_edittext`.
+        // Set the formatted URL string as the initial text of the create bookmark URL edit text.
         EditText createBookmarkUrlEditText = alertDialog.findViewById(R.id.create_bookmark_url_edittext);
         createBookmarkUrlEditText.setText(url);
 
-        // Allow the `enter` key on the keyboard to create the bookmark from `create_bookmark_url_edittext`.
-        createBookmarkUrlEditText.setOnKeyListener((View v, int keyCode, KeyEvent event) -> {
-            // If the event is a key-down on the "enter" key, select the PositiveButton "Create".
-            if ((keyCode == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN)) {
-                // Trigger `createBookmarkListener` and return the DialogFragment to the parent activity.
+        // Allow the enter key on the keyboard to create the bookmark from create bookmark URL edit text.
+        createBookmarkUrlEditText.setOnKeyListener((View v, int keyCode, KeyEvent keyEvent) -> {
+            // If the event is a key-down on the `enter` key, select the create button.
+            if ((keyCode == KeyEvent.KEYCODE_ENTER) && (keyEvent.getAction() == KeyEvent.ACTION_DOWN)) {
+                // Trigger the create bookmark listener and return the dialog fragment and the favorite icon bitmap to the parent activity.
                 createBookmarkListener.onCreateBookmark(this, favoriteIconBitmap);
 
-                // Manually dismiss the `AlertDialog`.
+                // Manually dismiss the alert dialog.
                 alertDialog.dismiss();
 
                 // Consume the event.
                 return true;
-            } else { // If any other key was pressed, do not consume the event.
+            } else { // Some other key was pressed.
+                // Do not consume the event.
                 return false;
             }
         });
diff --git a/app/src/main/java/com/stoutner/privacybrowser/dialogs/OpenDialog.java b/app/src/main/java/com/stoutner/privacybrowser/dialogs/OpenDialog.java
new file mode 100644 (file)
index 0000000..b478d72
--- /dev/null
@@ -0,0 +1,205 @@
+/*
+ * 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.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 com.stoutner.privacybrowser.R;
+import com.stoutner.privacybrowser.activities.MainWebViewActivity;
+
+import androidx.annotation.NonNull;
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.DialogFragment;
+import androidx.preference.PreferenceManager;
+
+public class OpenDialog extends DialogFragment {
+    // Define the open listener.
+    private OpenListener openListener;
+
+    // The public interface is used to send information back to the parent activity.
+    public interface OpenListener {
+        void onOpen(DialogFragment dialogFragment);
+    }
+
+    @Override
+    public void onAttach(@NonNull Context context) {
+        // Run the default commands.
+        super.onAttach(context);
+
+        // Get a handle for the open listener from the launching context.
+        openListener = (OpenListener) context;
+    }
+
+    // `@SuppressLint("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 activity and the context.
+        Activity activity = getActivity();
+        Context context = getContext();
+
+        // Remove the incorrect lint warnings below that the activity and the context might be null.
+        assert activity != null;
+        assert context != null;
+
+        // Get a handle for the shared preferences.
+        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+
+        // 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;
+
+        // Set the style and icon according to the theme.
+        if (darkTheme) {
+            // Set the style.
+            dialogBuilder = new AlertDialog.Builder(activity, R.style.PrivacyBrowserAlertDialogDark);
+
+            // Set the icon.
+            dialogBuilder.setIcon(R.drawable.proxy_enabled_dark);
+        } else {
+            // Set the style.
+            dialogBuilder = new AlertDialog.Builder(activity, R.style.PrivacyBrowserAlertDialogLight);
+
+            // Set the icon.
+            dialogBuilder.setIcon(R.drawable.proxy_enabled_light);
+        }
+
+        // Set the title.
+        dialogBuilder.setTitle(R.string.open);
+
+        // Set the view.  The parent view is null because it will be assigned by the alert dialog.
+        dialogBuilder.setView(activity.getLayoutInflater().inflate(R.layout.open_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);
+
+        // Set the open button listener.
+        dialogBuilder.setPositiveButton(R.string.open, (DialogInterface dialog, int which) -> {
+            // Return the dialog fragment to the parent activity.
+            openListener.onOpen(this);
+        });
+
+        // Create an alert dialog from the builder.
+        AlertDialog alertDialog = dialogBuilder.create();
+
+        // Remove the incorrect lint warning below that the window 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 openButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
+
+        // Create a string for the default file path.
+        String defaultFilePath;
+
+        // 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() + "/";
+        } else {  // The storage permission has not been granted.
+            // Set the default file path to use the external private directory.
+            defaultFilePath = context.getExternalFilesDir(null) + "/";
+        }
+
+        // Display the default file path.
+        fileNameEditText.setText(defaultFilePath);
+
+        // Move the cursor to the end of the default file path.
+        fileNameEditText.setSelection(defaultFilePath.length());
+
+        // Update the status of the open button when the file name changes.
+        fileNameEditText.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 open button if a file name exists.
+                openButton.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_OPEN_DOCUMENT);
+
+            // Set the intent MIME type to include all files so that everything is visible.
+            browseIntent.setType("*/*");
+
+            // Set the initial directory if the minimum API >= 26.
+            if (Build.VERSION.SDK_INT >= 26) {
+                browseIntent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Environment.getExternalStorageDirectory());
+            }
+
+            // Start the file picker.  This must be started under `activity` to that the request code is returned correctly.
+            activity.startActivityForResult(browseIntent, MainWebViewActivity.BROWSE_OPEN_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 || (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED)) {
+            storagePermissionTextView.setVisibility(View.GONE);
+        }
+
+        // Return the alert dialog.
+        return alertDialog;
+    }
+}
index 1c24e0bedc6ca95f4414c74ae3c92bd92001c205..72d0f20a6725cf7d65c7ca584243662f470fa4dc 100644 (file)
@@ -50,10 +50,6 @@ import com.stoutner.privacybrowser.R;
 import com.stoutner.privacybrowser.activities.MainWebViewActivity;
 
 public class SaveWebpageDialog extends DialogFragment {
-    // Define the save type constants.
-    public static final int ARCHIVE = 0;
-    public static final int IMAGE = 1;
-
     // Define the save webpage listener.
     private SaveWebpageListener saveWebpageListener;
 
@@ -88,7 +84,7 @@ public class SaveWebpageDialog extends DialogFragment {
         return saveWebpageDialog;
     }
 
-    // `@SuppressLing("InflateParams")` removes the warning about using null as the parent view group when inflating the alert dialog.
+    // `@SuppressLint("InflateParams")` removes the warning about using null as the parent view group when inflating the alert dialog.
     @SuppressLint("InflateParams")
     @Override
     @NonNull
@@ -127,11 +123,11 @@ public class SaveWebpageDialog extends DialogFragment {
 
             // Set the icon according to the save type.
             switch (saveType) {
-                case ARCHIVE:
+                case StoragePermissionDialog.SAVE_ARCHIVE:
                     dialogBuilder.setIcon(R.drawable.dom_storage_cleared_dark);
                     break;
 
-                case IMAGE:
+                case StoragePermissionDialog.SAVE_IMAGE:
                     dialogBuilder.setIcon(R.drawable.images_enabled_dark);
                     break;
             }
@@ -141,11 +137,11 @@ public class SaveWebpageDialog extends DialogFragment {
 
             // Set the icon according to the save type.
             switch (saveType) {
-                case ARCHIVE:
+                case StoragePermissionDialog.SAVE_ARCHIVE:
                     dialogBuilder.setIcon(R.drawable.dom_storage_cleared_light);
                     break;
 
-                case IMAGE:
+                case StoragePermissionDialog.SAVE_IMAGE:
                     dialogBuilder.setIcon(R.drawable.images_enabled_light);
                     break;
             }
@@ -153,11 +149,11 @@ public class SaveWebpageDialog extends DialogFragment {
 
         // Set the title according to the type.
         switch (saveType) {
-            case ARCHIVE:
+            case StoragePermissionDialog.SAVE_ARCHIVE:
                 dialogBuilder.setTitle(R.string.save_archive);
                 break;
 
-            case IMAGE:
+            case StoragePermissionDialog.SAVE_IMAGE:
                 dialogBuilder.setTitle(R.string.save_image);
                 break;
         }
@@ -177,7 +173,7 @@ public class SaveWebpageDialog extends DialogFragment {
         // Create an alert dialog from the builder.
         AlertDialog alertDialog = dialogBuilder.create();
 
-        // Remove the incorrect lint warning below that `getWindow()` might be null.
+        // Remove the incorrect lint warning below that the window might be null.
         assert alertDialog.getWindow() != null;
 
         // Disable screenshots if not allowed.
@@ -199,11 +195,11 @@ public class SaveWebpageDialog extends DialogFragment {
 
         // Set the default file name according to the type.
         switch (saveType) {
-            case ARCHIVE:
+            case StoragePermissionDialog.SAVE_ARCHIVE:
                 defaultFileName = getString(R.string.webpage_mht);
                 break;
 
-            case IMAGE:
+            case StoragePermissionDialog.SAVE_IMAGE:
                 defaultFileName = getString(R.string.webpage_png);
                 break;
         }
@@ -223,6 +219,9 @@ public class SaveWebpageDialog extends DialogFragment {
         // Display the default file path.
         fileNameEditText.setText(defaultFilePath);
 
+        // Move the cursor to the end of the default file path.
+        fileNameEditText.setSelection(defaultFilePath.length());
+
         // Update the status of the save button when the file name changes.
         fileNameEditText.addTextChangedListener(new TextWatcher() {
             @Override
@@ -237,7 +236,7 @@ public class SaveWebpageDialog extends DialogFragment {
 
             @Override
             public void afterTextChanged(Editable s) {
-                // // Enable the save button if a file name exists.
+                // Enable the save button if a file name exists.
                 saveButton.setEnabled(!fileNameEditText.getText().toString().isEmpty());
             }
         });
@@ -252,11 +251,11 @@ public class SaveWebpageDialog extends DialogFragment {
 
             // Set the initial file name according to the type.
             switch (saveType) {
-                case ARCHIVE:
+                case StoragePermissionDialog.SAVE_ARCHIVE:
                     browseIntent.putExtra(Intent.EXTRA_TITLE, getString(R.string.webpage_mht));
                     break;
 
-                case IMAGE:
+                case StoragePermissionDialog.OPEN:
                     browseIntent.putExtra(Intent.EXTRA_TITLE, getString(R.string.webpage_png));
                     break;
             }
@@ -274,7 +273,7 @@ public class SaveWebpageDialog extends DialogFragment {
         });
 
         // Hide the storage permission text view on API < 23 as permissions on older devices are automatically granted.
-        if (Build.VERSION.SDK_INT < 23) {
+        if (Build.VERSION.SDK_INT < 23 || (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED)) {
             storagePermissionTextView.setVisibility(View.GONE);
         }
 
index 1ee03b8f9db5abd4f64b4673abde160385434e6d..010dbcf690ae4490ace0c66da8fc9412c33c72b3 100644 (file)
@@ -34,12 +34,17 @@ import androidx.fragment.app.DialogFragment;
 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;
+
     // The listener is used in `onAttach()` and `onCreateDialog()`.
     private StoragePermissionDialogListener storagePermissionDialogListener;
 
     // The public interface is used to send information back to the parent activity.
     public interface StoragePermissionDialogListener {
-        void onCloseStoragePermissionDialog(int saveType);
+        void onCloseStoragePermissionDialog(int requestType);
     }
 
     @Override
@@ -51,12 +56,12 @@ public class StoragePermissionDialog extends DialogFragment {
         storagePermissionDialogListener = (StoragePermissionDialogListener) context;
     }
 
-    public static StoragePermissionDialog displayDialog(int saveType) {
+    public static StoragePermissionDialog displayDialog(int requestType) {
         // Create an arguments bundle.
         Bundle argumentsBundle = new Bundle();
 
         // Store the save type in the bundle.
-        argumentsBundle.putInt("save_type", saveType);
+        argumentsBundle.putInt("request_type", requestType);
 
         // Create a new instance of the storage permission dialog.
         StoragePermissionDialog storagePermissionDialog = new StoragePermissionDialog();
@@ -78,7 +83,7 @@ public class StoragePermissionDialog extends DialogFragment {
         assert arguments != null;
 
         // Get the save type.
-        int saveType = arguments.getInt("save_type");
+        int requestType = arguments.getInt("request_type");
 
         // Get a handle for the shared preferences.
         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext());
@@ -108,7 +113,7 @@ public class StoragePermissionDialog extends DialogFragment {
         // Set an listener on the OK button.
         dialogBuilder.setNegativeButton(R.string.ok, (DialogInterface dialog, int which) -> {
             // Inform the parent activity that the dialog was closed.
-            storagePermissionDialogListener.onCloseStoragePermissionDialog(saveType);
+            storagePermissionDialogListener.onCloseStoragePermissionDialog(requestType);
         });
 
         // Create an alert dialog from the builder.
diff --git a/app/src/main/res/layout/open_dialog.xml b/app/src/main/res/layout/open_dialog.xml
new file mode 100644 (file)
index 0000000..dd3059e
--- /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 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>
+
+        <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 973055b41cc77b7b90c02d2dbee6ff6f39b57184..dd3059e3ecff57345159033889e62c021f2c5c62 100644 (file)
         android:layout_marginStart="10dp"
         android:layout_marginEnd="10dp" >
 
-        <!-- Align the EditText and the select file button horizontally. -->
+        <!-- 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 `TextInputLayout` makes the `android:hint` float above the `EditText`. -->
+            <!-- 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. -->
+                <!-- `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"
@@ -62,8 +62,8 @@
 
         <TextView
             android:id="@+id/storage_permission_textview"
-            android:layout_width="wrap_content"
             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"
index 5dd6ab152f6b6161010c36b646eda2f51893c780..3093e1ec5b25a77c47cd84a7b225edb85959b2ee 100644 (file)
             android:title="@string/history"
             android:icon="@drawable/history"
             android:orderInCategory="50" />
+
+        <item
+            android:id="@+id/open"
+            android:title="@string/open"
+            android:icon="@drawable/proxy_enabled_light"
+            android:orderInCategory="60" />
     </group>
 
     <!-- If a group has an id, a line is drawn above it in the navigation view. -->
             android:id="@+id/requests"
             android:title="@string/requests"
             android:icon="@drawable/block_ads_enabled_light"
-            android:orderInCategory="60" />
+            android:orderInCategory="70" />
 
         <item
             android:id="@+id/downloads"
             android:title="@string/downloads"
             android:icon="@drawable/downloads_light"
-            android:orderInCategory="70" />
+            android:orderInCategory="80" />
     </group>
 
     <!-- If a group has an id, a line is drawn above it in the navigation view. -->
             android:id="@+id/domains"
             android:title="@string/domains"
             android:icon="@drawable/domains"
-            android:orderInCategory="80" />
+            android:orderInCategory="90" />
 
         <item
             android:id="@+id/settings"
             android:title="@string/settings"
             android:icon="@drawable/settings"
-            android:orderInCategory="90" />
+            android:orderInCategory="100" />
 
         <item
             android:id="@+id/import_export"
             android:title="@string/import_export"
             android:icon="@drawable/import_export_light"
-            android:orderInCategory="100" />
+            android:orderInCategory="110" />
 
         <item
             android:id="@+id/logcat"
             android:title="@string/logcat"
             android:icon="@drawable/bug"
-            android:orderInCategory="110" />
+            android:orderInCategory="120" />
     </group>
 
     <!-- If a group has an id, a line is drawn above it in the navigation view. -->
             android:id="@+id/guide"
             android:title="@string/guide"
             android:icon="@drawable/guide"
-            android:orderInCategory="120" />
+            android:orderInCategory="130" />
 
         <item
             android:id="@+id/about"
             android:title="@string/about"
             android:icon="@drawable/about_light"
-            android:orderInCategory="130" />
+            android:orderInCategory="140" />
     </group>
 </menu>
\ No newline at end of file
index c486baf3ffe2bf77c04eb5e0b93b92a1353029dc..0a196f230b9422e27d667ee36e0f5e8cec6f16c9 100644 (file)
     <string name="forward">Forward</string>
     <string name="history">History</string>
         <string name="clear_history">Clear History</string>
+    <string name="open">Open</string>
     <string name="downloads">Downloads</string>
     <string name="settings">Settings</string>
     <string name="import_export">Import/Export</string>