Redesign file access to work with the scoped storage. https://redmine.stoutner.com...
authorSoren Stoutner <soren@stoutner.com>
Thu, 25 Mar 2021 21:12:32 +0000 (14:12 -0700)
committerSoren Stoutner <soren@stoutner.com>
Thu, 25 Mar 2021 21:12:32 +0000 (14:12 -0700)
45 files changed:
app/build.gradle
app/src/main/AndroidManifest.xml
app/src/main/java/com/stoutner/privacybrowser/activities/AboutActivity.java
app/src/main/java/com/stoutner/privacybrowser/activities/ImportExportActivity.java
app/src/main/java/com/stoutner/privacybrowser/activities/LogcatActivity.java
app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java
app/src/main/java/com/stoutner/privacybrowser/asynctasks/GetHostIpAddresses.java
app/src/main/java/com/stoutner/privacybrowser/asynctasks/GetLogcat.java
app/src/main/java/com/stoutner/privacybrowser/asynctasks/GetUrlSize.java
app/src/main/java/com/stoutner/privacybrowser/asynctasks/PopulateBlocklists.java
app/src/main/java/com/stoutner/privacybrowser/asynctasks/PrepareSaveDialog.java
app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveAboutVersionImage.java
app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveUrl.java
app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveWebpageImage.java
app/src/main/java/com/stoutner/privacybrowser/dialogs/OpenDialog.kt
app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveDialog.kt
app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveWebpageDialog.java
app/src/main/java/com/stoutner/privacybrowser/dialogs/StoragePermissionDialog.java [deleted file]
app/src/main/java/com/stoutner/privacybrowser/fragments/SettingsFragment.java
app/src/main/java/com/stoutner/privacybrowser/helpers/CheckPinnedMismatchHelper.java
app/src/main/java/com/stoutner/privacybrowser/helpers/DownloadLocationHelper.java [deleted file]
app/src/main/java/com/stoutner/privacybrowser/helpers/FileNameHelper.java [deleted file]
app/src/main/java/com/stoutner/privacybrowser/helpers/ImportExportDatabaseHelper.java
app/src/main/res/drawable/downloads_ghosted_day.xml [deleted file]
app/src/main/res/drawable/downloads_ghosted_night.xml [deleted file]
app/src/main/res/drawable/import_export.xml [new file with mode: 0644]
app/src/main/res/drawable/import_export_day.xml [deleted file]
app/src/main/res/drawable/import_export_night.xml [deleted file]
app/src/main/res/layout/import_export_coordinatorlayout.xml
app/src/main/res/layout/open_dialog.xml
app/src/main/res/layout/save_dialog.xml
app/src/main/res/layout/save_url_dialog.xml [deleted file]
app/src/main/res/layout/save_webpage_dialog.xml [new file with mode: 0644]
app/src/main/res/menu/webview_navigation_menu.xml
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-pt-rBR/strings.xml
app/src/main/res/values-ru/strings.xml
app/src/main/res/values-tr/strings.xml
app/src/main/res/values/strings.xml
app/src/main/res/xml/preferences.xml
build.gradle

index 4223e3a8f3477e6e48762c7f66f89da41bbca86b..a40ad3d6c8711072b081137acf8c11956a4b1db7 100644 (file)
@@ -21,12 +21,11 @@ apply plugin: 'com.android.application'
 apply plugin: 'kotlin-android'
 
 android {
-    compileSdkVersion 29
-    buildToolsVersion '29.0.2'
+    compileSdkVersion 30
 
     defaultConfig {
         minSdkVersion 19
-        targetSdkVersion 29
+        targetSdkVersion 30
         versionCode 53
         versionName "3.6.1"
 
@@ -95,5 +94,5 @@ dependencies {
     implementation 'com.google.android.material:material:1.3.0'
 
     // Only compile AdMob ads for the free flavor.
-    freeImplementation 'com.google.android.gms:play-services-ads:19.7.0'
+    freeImplementation 'com.google.android.gms:play-services-ads:19.8.0'
 }
\ No newline at end of file
index 9feabcd7b05f30ac859255b4c684d4869adf2080..df1a84802c32eeeac0ae2df55401b1fa51504d78 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-  Copyright © 2015-2020 Soren Stoutner <soren@stoutner.com>.
+  Copyright © 2015-2021 Soren Stoutner <soren@stoutner.com>.
 
   This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
 
     <!-- Required to create home screen shortcuts. -->
     <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
 
-    <!-- Required to import settings from external storage. -->
-    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
-
-    <!-- Required to export settings and save files to public storage. -->
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-
 
     <!-- Support Chromebooks that don't have a touch screen. -->
     <uses-feature android:name="android.hardware.touchscreen" android:required="false" />
 
 
-    <!-- For API >= 23, app data is automatically backed up to Google cloud servers unless `android:allowBackup="false"` and `android:fullBackupContent="false"` is set.
-        `android:requestLegacyExternalStorage="true"` makes Android 10 storage permissions work like previous versions of Android.  It is a temporary workaround.  <https://redmine.stoutner.com/issues/546> -->
+    <!-- For API >= 23, app data is automatically backed up to Google cloud servers unless `android:allowBackup="false"` and `android:fullBackupContent="false"` is set. -->
     <application
         android:label="@string/privacy_browser"
         android:icon="@mipmap/privacy_browser"
@@ -52,7 +45,6 @@
         android:fullBackupContent="false"
         android:supportsRtl="true"
         android:networkSecurityConfig="@xml/network_security_config"
-        android:requestLegacyExternalStorage="true"
         tools:ignore="UnusedAttribute" >
 
         <!-- If `android:name="android.webkit.WebView.MetricsOptOut"` is not `true` then `WebViews` will upload metrics to Google.  <https://developer.android.com/reference/android/webkit/WebView.html> -->
                 <data android:mimeType="text/plain" />
             </intent-filter>
 
-            <!-- Process intents for MHT archives. -->
-            <intent-filter>
-                <action android:name="android.intent.action.VIEW" />
-
-                <category android:name="android.intent.category.BROWSABLE" />
-                <category android:name="android.intent.category.DEFAULT" />
-
-                <data android:scheme="file" />
-                <data android:scheme="content" />
-
-                <data android:host="*" />
-
-                <!-- In the path pattern syntax, `.*` is a wildcard.  Hence, this matches any file path that ends in `.mht`.  <https://developer.android.com/guide/topics/manifest/data-element#path>  -->
-                <data android:pathPattern=".*.mht" />
-                <data android:mimeType="*/*" />
-            </intent-filter>
-
             <!-- Process web search intents. -->
             <intent-filter>
                 <action android:name="android.intent.action.WEB_SEARCH" />
index 7d0228fb5a76ded69af525043f8f4d33796da62a..31e9c152f5d7e71338a5812d6fe13720d909cead 100644 (file)
 
 package com.stoutner.privacybrowser.activities;
 
-import android.Manifest;
 import android.app.Activity;
 import android.app.Dialog;
-import android.content.ContentResolver;
 import android.content.Intent;
 import android.content.SharedPreferences;
-import android.content.pm.PackageManager;
-import android.media.MediaScannerConnection;
 import android.net.Uri;
-import android.os.Build;
 import android.os.Bundle;
 import android.preference.PreferenceManager;
-import android.view.View;
 import android.view.WindowManager;
 import android.widget.EditText;
 import android.widget.LinearLayout;
-import android.widget.TextView;
 
-import androidx.annotation.NonNull;
 import androidx.appcompat.app.ActionBar;
 import androidx.appcompat.app.AppCompatActivity;
 import androidx.appcompat.widget.Toolbar;
-import androidx.core.app.ActivityCompat;
-import androidx.core.content.ContextCompat;
-import androidx.core.content.FileProvider;
 import androidx.fragment.app.DialogFragment;
 import androidx.viewpager.widget.ViewPager;
 
@@ -54,28 +43,21 @@ import com.stoutner.privacybrowser.adapters.AboutPagerAdapter;
 import com.stoutner.privacybrowser.R;
 import com.stoutner.privacybrowser.asynctasks.SaveAboutVersionImage;
 import com.stoutner.privacybrowser.dialogs.SaveDialog;
-import com.stoutner.privacybrowser.dialogs.StoragePermissionDialog;
 import com.stoutner.privacybrowser.fragments.AboutVersionFragment;
-import com.stoutner.privacybrowser.helpers.FileNameHelper;
 
 import java.io.BufferedReader;
 import java.io.BufferedWriter;
 import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.FileOutputStream;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.io.OutputStream;
 import java.io.OutputStreamWriter;
 import java.nio.charset.StandardCharsets;
 
-public class AboutActivity extends AppCompatActivity implements SaveDialog.SaveListener, StoragePermissionDialog.StoragePermissionDialogListener {
+public class AboutActivity extends AppCompatActivity implements SaveDialog.SaveListener {
     // Declare the class variables.
-    private String filePathString;
     private AboutPagerAdapter aboutPagerAdapter;
 
-    // Declare the class views.
-    private LinearLayout aboutVersionLinearLayout;
-
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         // Get a handle for the shared preferences.
@@ -137,138 +119,11 @@ public class AboutActivity extends AppCompatActivity implements SaveDialog.SaveL
         aboutTabLayout.setupWithViewPager(aboutViewPager);
     }
 
-    @Override
-    public void onSave(int saveType, DialogFragment dialogFragment) {
-        // Get a handle for the dialog.
-        Dialog dialog = dialogFragment.getDialog();
-
-        // Remove the 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.
-        filePathString = fileNameEditText.getText().toString();
-
-        // Get a handle for the about version linear layout.
-        aboutVersionLinearLayout = findViewById(R.id.about_version_linearlayout);
-
-        // 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 file according to the type.
-            switch (saveType) {
-                case SaveDialog.SAVE_ABOUT_VERSION_TEXT:
-                    // Save the about version text.
-                    saveAsText(filePathString);
-                    break;
-
-                case SaveDialog.SAVE_ABOUT_VERSION_IMAGE:
-                    // Save the about version image.
-                    new SaveAboutVersionImage(this, this, filePathString, aboutVersionLinearLayout).execute();
-                    break;
-            }
-
-            // Reset the file path string.
-            filePathString = "";
-        } 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 (filePathString.startsWith(externalPrivateDirectory)) {  // The file path is in the external private directory.
-                // Save the webpage according to the type.
-                switch (saveType) {
-                    case SaveDialog.SAVE_ABOUT_VERSION_TEXT:
-                        // Save the about version text.
-                        saveAsText(filePathString);
-                        break;
-
-                    case SaveDialog.SAVE_ABOUT_VERSION_IMAGE:
-                        // Save the about version image.
-                        new SaveAboutVersionImage(this, this, filePathString, aboutVersionLinearLayout).execute();
-                        break;
-                }
-
-                // Reset the file path string.
-                filePathString = "";
-            } 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.
-                    // Declare a storage permission dialog fragment.
-                    DialogFragment storagePermissionDialogFragment;
-
-                    // Instantiate the storage permission alert dialog according to the type.
-                    if (saveType == SaveDialog.SAVE_ABOUT_VERSION_TEXT) {
-                        storagePermissionDialogFragment = StoragePermissionDialog.displayDialog(StoragePermissionDialog.SAVE_TEXT);
-                    } else {
-                        storagePermissionDialogFragment = StoragePermissionDialog.displayDialog(StoragePermissionDialog.SAVE_IMAGE);
-                    }
-
-                    // Show the storage permission alert dialog.  The permission will be requested when the dialog is closed.
-                    storagePermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.storage_permission));
-                } else {  // Show the permission request directly.
-                    switch (saveType) {
-                        case SaveDialog.SAVE_ABOUT_VERSION_TEXT:
-                            // Request the write external storage permission.  The text will be saved when it finishes.
-                            ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, StoragePermissionDialog.SAVE_TEXT);
-                            break;
-
-                        case SaveDialog.SAVE_ABOUT_VERSION_IMAGE:
-                            // Request the write external storage permission.  The image will be saved when it finishes.
-                            ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, StoragePermissionDialog.SAVE_IMAGE);
-                            break;
-                    }
-
-                }
-            }
-        }
-    }
-
-    @Override
-    public void onCloseStoragePermissionDialog(int requestType) {
-        // Request the write external storage permission according to the request type.  About version will be saved when it finishes.
-        ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestType);
-    }
-
-    @Override
-    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
-        //Only process the results if they exist (this method is triggered when a dialog is presented the first time for an app, but no grant results are included).
-        if (grantResults.length > 0) {
-            // Check to see if the storage permission was granted.  If the dialog was canceled the grant results will be empty.
-            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {  // The storage permission was granted.
-                switch (requestCode) {
-                    case StoragePermissionDialog.SAVE_TEXT:
-                        // Save the about version text.
-                        saveAsText(filePathString);
-                        break;
-
-                    case StoragePermissionDialog.SAVE_IMAGE:
-                        // Save the about version image.
-                        new SaveAboutVersionImage(this, this, filePathString, aboutVersionLinearLayout).execute();
-                        break;
-                }
-            } else{  // the storage permission was not granted.
-                // Display an error snackbar.
-                Snackbar.make(aboutVersionLinearLayout, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show();
-            }
-
-            // Reset the file path string.
-            filePathString = "";
-        }
-    }
-
     // The activity result is called after browsing for a file in the save alert dialog.
     @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);
 
         // Only do something if the user didn't press back from the file picker.
         if (resultCode == Activity.RESULT_OK) {
@@ -285,116 +140,94 @@ public class AboutActivity extends AppCompatActivity implements SaveDialog.SaveL
 
                 // Get a handle for the dialog view.
                 EditText fileNameEditText = saveDialog.findViewById(R.id.file_name_edittext);
-                TextView fileExistsWarningTextView = saveDialog.findViewById(R.id.file_exists_warning_textview);
 
                 // Get the file name URI from the intent.
-                Uri fileNameUri = data.getData();
+                Uri fileNameUri = returnedIntent.getData();
 
-                // Process the file name URI if it is not null.
-                if (fileNameUri != null) {
-                    // Instantiate a file name helper.
-                    FileNameHelper fileNameHelper = new FileNameHelper();
+                // Get the file name string from the URI.
+                String fileNameString = fileNameUri.toString();
 
-                    // Convert the file name URI to a file name path.
-                    String fileNamePath = fileNameHelper.convertUriToFileNamePath(fileNameUri);
+                // Set the file name text.
+                fileNameEditText.setText(fileNameString);
 
-                    // Set the file name path as the text of the file nam edit text.
-                    fileNameEditText.setText(fileNamePath);
-
-                    // Move the cursor to the end of the file name edit text.
-                    fileNameEditText.setSelection(fileNamePath.length());
-
-                    // Hid ethe file exists warning.
-                    fileExistsWarningTextView.setVisibility(View.GONE);
-                }
+                // Move the cursor to the end of the file name edit text.
+                fileNameEditText.setSelection(fileNameString.length());
             }
         }
     }
 
-    private void saveAsText(String fileNameString) {
-        try {
-            // Get a handle for the about about version fragment.
-            AboutVersionFragment aboutVersionFragment = (AboutVersionFragment) aboutPagerAdapter.getTabFragment(0);
-
-            // Get the about version text.
-            String aboutVersionString = aboutVersionFragment.getAboutVersionString();
-
-            // Create an input stream with the contents of about version.
-            InputStream aboutVersionInputStream = new ByteArrayInputStream(aboutVersionString.getBytes(StandardCharsets.UTF_8));
-
-            // Create an about version buffered reader.
-            BufferedReader aboutVersionBufferedReader = new BufferedReader(new InputStreamReader(aboutVersionInputStream));
-
-            // Create a file from the file name string.
-            File saveFile = new File(fileNameString);
+    @Override
+    public void onSave(int saveType, DialogFragment dialogFragment) {
+        // Get a handle for the dialog.
+        Dialog dialog = dialogFragment.getDialog();
 
-            // Delete the file if it already exists.
-            if (saveFile.exists()) {
-                //noinspection ResultOfMethodCallIgnored
-                saveFile.delete();
-            }
+        // Remove the lint warning below that the dialog might be null.
+        assert dialog != null;
 
-            // Create a file buffered writer.
-            BufferedWriter fileBufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(saveFile)));
+        // Get a handle for the file name edit text.
+        EditText fileNameEditText = dialog.findViewById(R.id.file_name_edittext);
 
-            // Create a transfer string.
-            String transferString;
+        // Get the file name string.
+        String fileNameString = fileNameEditText.getText().toString();
 
-            // Use the transfer string to copy the about version text from the buffered reader to the buffered writer.
-            while ((transferString = aboutVersionBufferedReader.readLine()) != null) {
-                // Append the line to the buffered writer.
-                fileBufferedWriter.append(transferString);
+        // Get a handle for the about version linear layout.
+        LinearLayout aboutVersionLinearLayout = findViewById(R.id.about_version_linearlayout);
 
-                // Append a line break.
-                fileBufferedWriter.append("\n");
-            }
+        // Save the file according to the type.
+        switch (saveType) {
+            case SaveDialog.SAVE_ABOUT_VERSION_TEXT:
+                try {
+                    // Get a handle for the about version fragment.
+                    AboutVersionFragment aboutVersionFragment = (AboutVersionFragment) aboutPagerAdapter.getTabFragment(0);
 
-            // Close the buffered reader and writer.
-            aboutVersionBufferedReader.close();
-            fileBufferedWriter.close();
+                    // Get the about version text.
+                    String aboutVersionString = aboutVersionFragment.getAboutVersionString();
 
-            // Add the file to the list of recent files.  This doesn't currently work, but maybe it will someday.
-            MediaScannerConnection.scanFile(this, new String[] {fileNameString}, new String[] {"text/plain"}, null);
+                    // Create an input stream with the contents of about version.
+                    InputStream aboutVersionInputStream = new ByteArrayInputStream(aboutVersionString.getBytes(StandardCharsets.UTF_8));
 
-            // Create an about version saved snackbar.
-            Snackbar aboutVersionSavedSnackbar = Snackbar.make(aboutVersionLinearLayout, getString(R.string.file_saved) + "  " + fileNameString, Snackbar.LENGTH_SHORT);
+                    // Create an about version buffered reader.
+                    BufferedReader aboutVersionBufferedReader = new BufferedReader(new InputStreamReader(aboutVersionInputStream));
 
-            // Add an open option to the snackbar.
-            aboutVersionSavedSnackbar.setAction(R.string.open, (View view) -> {
-                // Get a file for the file name string.
-                File file = new File(fileNameString);
+                    // Open an output stream.
+                    OutputStream outputStream = getContentResolver().openOutputStream(Uri.parse(fileNameString));
 
-                // Declare a file URI variable.
-                Uri fileUri;
+                    // Create a file buffered writer.
+                    BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
 
-                // Get the URI for the file according to the Android version.
-                if (Build.VERSION.SDK_INT >= 24) {  // Use a file provider.
-                    fileUri = FileProvider.getUriForFile(this, getString(R.string.file_provider), file);
-                } else {  // Get the raw file path URI.
-                    fileUri = Uri.fromFile(file);
-                }
+                    // Create a transfer string.
+                    String transferString;
 
-                // Get a handle for the content resolver.
-                ContentResolver contentResolver = getContentResolver();
+                    // Use the transfer string to copy the about version text from the buffered reader to the buffered writer.
+                    while ((transferString = aboutVersionBufferedReader.readLine()) != null) {
+                        // Append the line to the buffered writer.
+                        bufferedWriter.append(transferString);
 
-                // Create an open intent with `ACTION_VIEW`.
-                Intent openIntent = new Intent(Intent.ACTION_VIEW);
+                        // Append a line break.
+                        bufferedWriter.append("\n");
+                    }
 
-                // Set the URI and the MIME type.
-                openIntent.setDataAndType(fileUri, contentResolver.getType(fileUri));
+                    // Flush the buffered writer.
+                    bufferedWriter.flush();
 
-                // Allow the app to read the file URI.
-                openIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+                    // Close the inputs and outputs.
+                    aboutVersionBufferedReader.close();
+                    aboutVersionInputStream.close();
+                    bufferedWriter.close();
+                    outputStream.close();
 
-                // Show the chooser.
-                startActivity(Intent.createChooser(openIntent, getString(R.string.open)));
-            });
+                    // Display a snackbar with the saved about version information.
+                    Snackbar.make(aboutVersionLinearLayout, getString(R.string.file_saved) + "  " + fileNameString, Snackbar.LENGTH_SHORT).show();
+                } catch (Exception exception) {
+                    // Display a snackbar with the error message.
+                    Snackbar.make(aboutVersionLinearLayout, getString(R.string.error_saving_file) + "  " + exception.toString(), Snackbar.LENGTH_INDEFINITE).show();
+                }
+                break;
 
-            // Show the about version saved snackbar.
-            aboutVersionSavedSnackbar.show();
-        } catch (Exception exception) {
-            // Display a snackbar with the error message.
-            Snackbar.make(aboutVersionLinearLayout, getString(R.string.error_saving_file) + "  " + exception.toString(), Snackbar.LENGTH_INDEFINITE).show();
+            case SaveDialog.SAVE_ABOUT_VERSION_IMAGE:
+                // Save the about version image.
+                new SaveAboutVersionImage(this, fileNameString, aboutVersionLinearLayout).execute();
+                break;
         }
     }
 }
\ No newline at end of file
index d740552a74cb463f3d1f451f32177657e7497775..de4ec4779fb45350bbb5a971260a376fa07acdb8 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2018-2020 Soren Stoutner <soren@stoutner.com>.
+ * Copyright © 2018-2021 Soren Stoutner <soren@stoutner.com>.
  *
  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
  *
 
 package com.stoutner.privacybrowser.activities;
 
-import android.Manifest;
 import android.app.Activity;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
-import android.media.MediaScannerConnection;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
-import android.os.Environment;
 import android.os.Handler;
 import android.preference.PreferenceManager;
-import android.provider.DocumentsContract;
 import android.text.Editable;
 import android.text.TextWatcher;
 import android.view.View;
@@ -50,24 +46,21 @@ import androidx.appcompat.app.ActionBar;
 import androidx.appcompat.app.AppCompatActivity;
 import androidx.appcompat.widget.Toolbar;
 import androidx.cardview.widget.CardView;
-import androidx.core.app.ActivityCompat;
-import androidx.core.content.ContextCompat;
 import androidx.core.content.FileProvider;
-import androidx.fragment.app.DialogFragment;
+import androidx.multidex.BuildConfig;
 
 import com.google.android.material.snackbar.Snackbar;
 import com.google.android.material.textfield.TextInputLayout;
 
-import com.stoutner.privacybrowser.BuildConfig;
 import com.stoutner.privacybrowser.R;
-import com.stoutner.privacybrowser.dialogs.StoragePermissionDialog;
-import com.stoutner.privacybrowser.helpers.DownloadLocationHelper;
-import com.stoutner.privacybrowser.helpers.FileNameHelper;
 import com.stoutner.privacybrowser.helpers.ImportExportDatabaseHelper;
 
 import java.io.File;
 import java.io.FileInputStream;
+import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
 import java.security.MessageDigest;
 import java.security.SecureRandom;
@@ -79,7 +72,7 @@ import javax.crypto.CipherOutputStream;
 import javax.crypto.spec.GCMParameterSpec;
 import javax.crypto.spec.SecretKeySpec;
 
-public class ImportExportActivity extends AppCompatActivity implements StoragePermissionDialog.StoragePermissionDialogListener {
+public class ImportExportActivity extends AppCompatActivity {
     // Define the encryption constants.
     private final int NO_ENCRYPTION = 0;
     private final int PASSWORD_ENCRYPTION = 1;
@@ -87,35 +80,37 @@ public class ImportExportActivity extends AppCompatActivity implements StoragePe
 
     // Define the activity result constants.
     private final int BROWSE_RESULT_CODE = 0;
-    private final int OPENPGP_EXPORT_RESULT_CODE = 1;
+    private final int OPENPGP_IMPORT_RESULT_CODE = 1;
+    private final int OPENPGP_EXPORT_RESULT_CODE = 2;
 
     // Define the saved instance state constants.
-    private final String PASSWORD_ENCRYPTED_TEXTINPUTLAYOUT_VISIBILITY = "password_encrypted_textinputlayout_visibility";
+    private final String ENCRYPTION_PASSWORD_TEXTINPUTLAYOUT_VISIBILITY = "encryption_password_textinputlayout_visibility";
     private final String KITKAT_PASSWORD_ENCRYPTED_TEXTVIEW_VISIBILITY = "kitkat_password_encrypted_textview_visibility";
     private final String OPEN_KEYCHAIN_REQUIRED_TEXTVIEW_VISIBILITY = "open_keychain_required_textview_visibility";
     private final String FILE_LOCATION_CARD_VIEW = "file_location_card_view";
     private final String FILE_NAME_LINEARLAYOUT_VISIBILITY = "file_name_linearlayout_visibility";
-    private final String FILE_DOES_NOT_EXIST_TEXTVIEW_VISIBILITY = "file_does_not_exist_textview_visibility";
-    private final String FILE_EXISTS_WARNING_TEXTVIEW_VISIBILITY = "file_exists_warning_textview_visibility";
     private final String OPEN_KEYCHAIN_IMPORT_INSTRUCTIONS_TEXTVIEW_VISIBILITY = "open_keychain_import_instructions_textview_visibility";
     private final String IMPORT_EXPORT_BUTTON_VISIBILITY = "import_export_button_visibility";
     private final String FILE_NAME_TEXT = "file_name_text";
     private final String IMPORT_EXPORT_BUTTON_TEXT = "import_export_button_text";
 
     // Define the class views.
-    TextInputLayout passwordEncryptionTextInputLayout;
+    Spinner encryptionSpinner;
+    TextInputLayout encryptionPasswordTextInputLayout;
+    EditText encryptionPasswordEditText;
     TextView kitKatPasswordEncryptionTextView;
     TextView openKeychainRequiredTextView;
     CardView fileLocationCardView;
+    RadioButton importRadioButton;
     LinearLayout fileNameLinearLayout;
     EditText fileNameEditText;
-    TextView fileDoesNotExistTextView;
-    TextView fileExistsWarningTextView;
     TextView openKeychainImportInstructionsTextView;
     Button importExportButton;
 
     // Define the class variables.
     private boolean openKeychainInstalled;
+    private File temporaryPgpEncryptedImportFile;
+    private File temporaryPreEncryptedExportFile;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
@@ -163,21 +158,18 @@ public class ImportExportActivity extends AppCompatActivity implements StoragePe
         }
 
         // Get handles for the views that need to be modified.
-        Spinner encryptionSpinner = findViewById(R.id.encryption_spinner);
-        passwordEncryptionTextInputLayout = findViewById(R.id.password_encryption_textinputlayout);
-        EditText encryptionPasswordEditText = findViewById(R.id.password_encryption_edittext);
+        encryptionSpinner = findViewById(R.id.encryption_spinner);
+        encryptionPasswordTextInputLayout = findViewById(R.id.encryption_password_textinputlayout);
+        encryptionPasswordEditText = findViewById(R.id.encryption_password_edittext);
         kitKatPasswordEncryptionTextView = findViewById(R.id.kitkat_password_encryption_textview);
         openKeychainRequiredTextView = findViewById(R.id.openkeychain_required_textview);
         fileLocationCardView = findViewById(R.id.file_location_cardview);
-        RadioButton importRadioButton = findViewById(R.id.import_radiobutton);
+        importRadioButton = findViewById(R.id.import_radiobutton);
         RadioButton exportRadioButton = findViewById(R.id.export_radiobutton);
         fileNameLinearLayout = findViewById(R.id.file_name_linearlayout);
         fileNameEditText = findViewById(R.id.file_name_edittext);
-        fileDoesNotExistTextView = findViewById(R.id.file_does_not_exist_textview);
-        fileExistsWarningTextView = findViewById(R.id.file_exists_warning_textview);
         openKeychainImportInstructionsTextView = findViewById(R.id.openkeychain_import_instructions_textview);
         importExportButton = findViewById(R.id.import_export_button);
-        TextView storagePermissionTextView = findViewById(R.id.import_export_storage_permission_textview);
 
         // Create an array adapter for the spinner.
         ArrayAdapter<CharSequence> encryptionArrayAdapter = ArrayAdapter.createFromResource(this, R.array.encryption_type, R.layout.spinner_item);
@@ -188,24 +180,6 @@ public class ImportExportActivity extends AppCompatActivity implements StoragePe
         // Set the array adapter for the spinner.
         encryptionSpinner.setAdapter(encryptionArrayAdapter);
 
-        // Instantiate the download location helper.
-        DownloadLocationHelper downloadLocationHelper = new DownloadLocationHelper();
-
-        // Get the default file path.
-        String defaultFilePath = downloadLocationHelper.getDownloadLocation(this) + "/" + getString(R.string.settings) + " " + BuildConfig.VERSION_NAME + ".pbs";
-
-        // Set the other default file paths.
-        String defaultPasswordEncryptionFilePath = defaultFilePath + ".aes";
-        String defaultPgpFilePath = defaultFilePath + ".pgp";
-
-        // Set the default file path.
-        fileNameEditText.setText(defaultFilePath);
-
-        // Hide the storage permission text view if the permission has already been granted.
-        if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
-            storagePermissionTextView.setVisibility(View.GONE);
-        }
-
         // Update the UI when the spinner changes.
         encryptionSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
             @Override
@@ -213,7 +187,7 @@ public class ImportExportActivity extends AppCompatActivity implements StoragePe
                 switch (position) {
                     case NO_ENCRYPTION:
                         // Hide the unneeded layout items.
-                        passwordEncryptionTextInputLayout.setVisibility(View.GONE);
+                        encryptionPasswordTextInputLayout.setVisibility(View.GONE);
                         kitKatPasswordEncryptionTextView.setVisibility(View.GONE);
                         openKeychainRequiredTextView.setVisibility(View.GONE);
                         openKeychainImportInstructionsTextView.setVisibility(View.GONE);
@@ -231,8 +205,8 @@ public class ImportExportActivity extends AppCompatActivity implements StoragePe
                             importExportButton.setText(R.string.import_button);
                         }
 
-                        // Reset the default file path.
-                        fileNameEditText.setText(defaultFilePath);
+                        // Enable the import/export button if the file name is populated.
+                        importExportButton.setEnabled(!fileNameEditText.getText().toString().isEmpty());
                         break;
 
                     case PASSWORD_ENCRYPTION:
@@ -249,7 +223,7 @@ public class ImportExportActivity extends AppCompatActivity implements StoragePe
                             openKeychainImportInstructionsTextView.setVisibility(View.GONE);
 
                             // Show the password encryption layout items.
-                            passwordEncryptionTextInputLayout.setVisibility(View.VISIBLE);
+                            encryptionPasswordTextInputLayout.setVisibility(View.VISIBLE);
 
                             // Show the file location card.
                             fileLocationCardView.setVisibility(View.VISIBLE);
@@ -264,21 +238,18 @@ public class ImportExportActivity extends AppCompatActivity implements StoragePe
                                 importExportButton.setText(R.string.import_button);
                             }
 
-                            // Update the default file path.
-                            fileNameEditText.setText(defaultPasswordEncryptionFilePath);
+                            // Enable the import/button if both the password and the file name are populated.
+                            importExportButton.setEnabled(!fileNameEditText.getText().toString().isEmpty() && !encryptionPasswordEditText.getText().toString().isEmpty());
                         }
                         break;
 
                     case OPENPGP_ENCRYPTION:
                         // Hide the password encryption layout items.
-                        passwordEncryptionTextInputLayout.setVisibility(View.GONE);
+                        encryptionPasswordTextInputLayout.setVisibility(View.GONE);
                         kitKatPasswordEncryptionTextView.setVisibility(View.GONE);
 
                         // Updated items based on the installation status of OpenKeychain.
                         if (openKeychainInstalled) {  // OpenKeychain is installed.
-                            // Update the default file path.
-                            fileNameEditText.setText(defaultPgpFilePath);
-
                             // Show the file location card.
                             fileLocationCardView.setVisibility(View.VISIBLE);
 
@@ -289,10 +260,16 @@ public class ImportExportActivity extends AppCompatActivity implements StoragePe
 
                                 // Set the text of the import button to be `Decrypt`.
                                 importExportButton.setText(R.string.decrypt);
+
+                                // Enable the import button if the file name is populated.
+                                importExportButton.setEnabled(!fileNameEditText.getText().toString().isEmpty());
                             } else if (exportRadioButton.isChecked()) {
                                 // Hide the file name linear layout and the OpenKeychain import instructions.
                                 fileNameLinearLayout.setVisibility(View.GONE);
                                 openKeychainImportInstructionsTextView.setVisibility(View.GONE);
+
+                                // Enable the export button.
+                                importExportButton.setEnabled(true);
                             }
                         } else {  // OpenKeychain is not installed.
                             // Show the OpenPGP required layout item.
@@ -325,20 +302,8 @@ public class ImportExportActivity extends AppCompatActivity implements StoragePe
 
             @Override
             public void afterTextChanged(Editable s) {
-                // Get the current file name.
-                String fileNameString = fileNameEditText.getText().toString();
-
-                // Convert the file name string to a file.
-                File file = new File(fileNameString);
-
-                // Update the import/export button.
-                if (importRadioButton.isChecked()) {  // The import radio button is checked.
-                    // Enable the import button if the file and the password exists.
-                    importExportButton.setEnabled(file.exists() && !encryptionPasswordEditText.getText().toString().isEmpty());
-                } else if (exportRadioButton.isChecked()) {  // The export radio button is checked.
-                    // Enable the export button if the file string and the password exists.
-                    importExportButton.setEnabled(!fileNameString.isEmpty() && !encryptionPasswordEditText.getText().toString().isEmpty());
-                }
+                // Enable the import/export button if both the file string and the password are populated.
+                importExportButton.setEnabled(!fileNameEditText.getText().toString().isEmpty() && !encryptionPasswordEditText.getText().toString().isEmpty());
             }
         });
 
@@ -356,123 +321,13 @@ public class ImportExportActivity extends AppCompatActivity implements StoragePe
 
             @Override
             public void afterTextChanged(Editable s) {
-                // Get the current file name.
-                String fileNameString = fileNameEditText.getText().toString();
-
-                // Convert the file name string to a file.
-                File file = new File(fileNameString);
-
                 // Adjust the UI according to the encryption spinner position.
-                switch (encryptionSpinner.getSelectedItemPosition()) {
-                    case NO_ENCRYPTION:
-                        // Determine if import or export is checked.
-                        if (exportRadioButton.isChecked()) {  // The export radio button is checked.
-                            // Hide the file does not exist text view.
-                            fileDoesNotExistTextView.setVisibility(View.GONE);
-
-                            // Display a warning if the file already exists.
-                            if (file.exists()) {
-                                fileExistsWarningTextView.setVisibility(View.VISIBLE);
-                            } else {
-                                fileExistsWarningTextView.setVisibility(View.GONE);
-                            }
-
-                            // Enable the export button if the file name is populated.
-                            importExportButton.setEnabled(!fileNameString.isEmpty());
-                        } else if (importRadioButton.isChecked()) {  // The import radio button is checked.
-                            // Hide the file exists warning text view.
-                            fileExistsWarningTextView.setVisibility(View.GONE);
-
-                            // Check if the file exists.
-                            if (file.exists()) {  // The file exists.
-                                // Hide the notification that the file does not exist.
-                                fileDoesNotExistTextView.setVisibility(View.GONE);
-
-                                // Enable the import button.
-                                importExportButton.setEnabled(true);
-                            } else {  // The file does not exist.
-                                // Show a notification that the file does not exist.
-                                fileDoesNotExistTextView.setVisibility(View.VISIBLE);
-
-                                // Disable the import button.
-                                importExportButton.setEnabled(false);
-                            }
-                        } else {  // Neither radio button is checked.
-                            // Hide the file notification text views.
-                            fileExistsWarningTextView.setVisibility(View.GONE);
-                            fileDoesNotExistTextView.setVisibility(View.GONE);
-                        }
-                        break;
-
-                    case PASSWORD_ENCRYPTION:
-                        // Determine if import or export is checked.
-                        if (exportRadioButton.isChecked()) {  // The export radio button is checked.
-                            // Hide the notification that the file does not exist.
-                            fileDoesNotExistTextView.setVisibility(View.GONE);
-
-                            // Display a warning if the file already exists.
-                            if (file.exists()) {
-                                fileExistsWarningTextView.setVisibility(View.VISIBLE);
-                            } else {
-                                fileExistsWarningTextView.setVisibility(View.GONE);
-                            }
-
-                            // Enable the export button if the file name and the password are populated.
-                            importExportButton.setEnabled(!fileNameString.isEmpty() && !encryptionPasswordEditText.getText().toString().isEmpty());
-                        } else if (importRadioButton.isChecked()) {  // The import radio button is checked.
-                            // Hide the file exists warning text view.
-                            fileExistsWarningTextView.setVisibility(View.GONE);
-
-                            // Check if the file exists.
-                            if (file.exists()) {  // The file exists.
-                                // Hide the notification that the file does not exist.
-                                fileDoesNotExistTextView.setVisibility(View.GONE);
-
-                                // Enable the import button if the password is populated.
-                                importExportButton.setEnabled(!encryptionPasswordEditText.getText().toString().isEmpty());
-                            } else {  // The file does not exist.
-                                // Show a notification that the file does not exist.
-                                fileDoesNotExistTextView.setVisibility(View.VISIBLE);
-
-                                // Disable the import button.
-                                importExportButton.setEnabled(false);
-                            }
-                        } else {  // Neither radio button is checked.
-                            // Hide the file notification text views.
-                            fileExistsWarningTextView.setVisibility(View.GONE);
-                            fileDoesNotExistTextView.setVisibility(View.GONE);
-                        }
-                        break;
-
-                    case OPENPGP_ENCRYPTION:
-                        // Hide the file exists warning text view.
-                        fileExistsWarningTextView.setVisibility(View.GONE);
-
-                        if (importRadioButton.isChecked()) {  // The import radio button is checked.
-                            if (file.exists()) {  // The file exists.
-                                // Hide the notification that the file does not exist.
-                                fileDoesNotExistTextView.setVisibility(View.GONE);
-
-                                // Enable the import button if OpenKeychain is installed.
-                                importExportButton.setEnabled(openKeychainInstalled);
-                            } else {  // The file does not exist.
-                                // Show the notification that the file does not exist.
-                                fileDoesNotExistTextView.setVisibility(View.VISIBLE);
-
-                                // Disable the import button.
-                                importExportButton.setEnabled(false);
-                            }
-                        } else if (exportRadioButton.isChecked()){  // The export radio button is checked.
-                            // Hide the notification that the file does not exist.
-                            fileDoesNotExistTextView.setVisibility(View.GONE);
-
-                            // Enable the export button.
-                            importExportButton.setEnabled(true);
-                        } else {  // Neither radio button is checked.
-                            // Hide the notification that the file does not exist.
-                            fileDoesNotExistTextView.setVisibility(View.GONE);
-                        }
-                        break;
+                if (encryptionSpinner.getSelectedItemPosition() == PASSWORD_ENCRYPTION) {
+                    // Enable the import/export button if both the file name and the password are populated.
+                    importExportButton.setEnabled(!fileNameEditText.getText().toString().isEmpty() && !encryptionPasswordEditText.getText().toString().isEmpty());
+                } else {
+                    // Enable the export button if the file name is populated.
+                    importExportButton.setEnabled(!fileNameEditText.getText().toString().isEmpty());
                 }
             }
         });
@@ -480,23 +335,19 @@ public class ImportExportActivity extends AppCompatActivity implements StoragePe
         // Check to see if the activity has been restarted.
         if (savedInstanceState == null) {  // The app has not been restarted.
             // Initially hide the unneeded views.
-            passwordEncryptionTextInputLayout.setVisibility(View.GONE);
+            encryptionPasswordTextInputLayout.setVisibility(View.GONE);
             kitKatPasswordEncryptionTextView.setVisibility(View.GONE);
             openKeychainRequiredTextView.setVisibility(View.GONE);
             fileNameLinearLayout.setVisibility(View.GONE);
-            fileDoesNotExistTextView.setVisibility(View.GONE);
-            fileExistsWarningTextView.setVisibility(View.GONE);
             openKeychainImportInstructionsTextView.setVisibility(View.GONE);
             importExportButton.setVisibility(View.GONE);
         } else {  // The app has been restarted.
             // Restore the visibility of the views.
-            passwordEncryptionTextInputLayout.setVisibility(savedInstanceState.getInt(PASSWORD_ENCRYPTED_TEXTINPUTLAYOUT_VISIBILITY));
+            encryptionPasswordTextInputLayout.setVisibility(savedInstanceState.getInt(ENCRYPTION_PASSWORD_TEXTINPUTLAYOUT_VISIBILITY));
             kitKatPasswordEncryptionTextView.setVisibility(savedInstanceState.getInt(KITKAT_PASSWORD_ENCRYPTED_TEXTVIEW_VISIBILITY));
             openKeychainRequiredTextView.setVisibility(savedInstanceState.getInt(OPEN_KEYCHAIN_REQUIRED_TEXTVIEW_VISIBILITY));
             fileLocationCardView.setVisibility(savedInstanceState.getInt(FILE_LOCATION_CARD_VIEW));
             fileNameLinearLayout.setVisibility(savedInstanceState.getInt(FILE_NAME_LINEARLAYOUT_VISIBILITY));
-            fileDoesNotExistTextView.setVisibility(savedInstanceState.getInt(FILE_DOES_NOT_EXIST_TEXTVIEW_VISIBILITY));
-            fileExistsWarningTextView.setVisibility(savedInstanceState.getInt(FILE_EXISTS_WARNING_TEXTVIEW_VISIBILITY));
             openKeychainImportInstructionsTextView.setVisibility(savedInstanceState.getInt(OPEN_KEYCHAIN_IMPORT_INSTRUCTIONS_TEXTVIEW_VISIBILITY));
             importExportButton.setVisibility(savedInstanceState.getInt(IMPORT_EXPORT_BUTTON_VISIBILITY));
 
@@ -512,13 +363,11 @@ public class ImportExportActivity extends AppCompatActivity implements StoragePe
         super.onSaveInstanceState(savedInstanceState);
 
         // Save the visibility of the views.
-        savedInstanceState.putInt(PASSWORD_ENCRYPTED_TEXTINPUTLAYOUT_VISIBILITY, passwordEncryptionTextInputLayout.getVisibility());
+        savedInstanceState.putInt(ENCRYPTION_PASSWORD_TEXTINPUTLAYOUT_VISIBILITY, encryptionPasswordTextInputLayout.getVisibility());
         savedInstanceState.putInt(KITKAT_PASSWORD_ENCRYPTED_TEXTVIEW_VISIBILITY, kitKatPasswordEncryptionTextView.getVisibility());
         savedInstanceState.putInt(OPEN_KEYCHAIN_REQUIRED_TEXTVIEW_VISIBILITY, openKeychainRequiredTextView.getVisibility());
         savedInstanceState.putInt(FILE_LOCATION_CARD_VIEW, fileLocationCardView.getVisibility());
         savedInstanceState.putInt(FILE_NAME_LINEARLAYOUT_VISIBILITY, fileNameLinearLayout.getVisibility());
-        savedInstanceState.putInt(FILE_DOES_NOT_EXIST_TEXTVIEW_VISIBILITY, fileDoesNotExistTextView.getVisibility());
-        savedInstanceState.putInt(FILE_EXISTS_WARNING_TEXTVIEW_VISIBILITY, fileExistsWarningTextView.getVisibility());
         savedInstanceState.putInt(OPEN_KEYCHAIN_IMPORT_INSTRUCTIONS_TEXTVIEW_VISIBILITY, openKeychainImportInstructionsTextView.getVisibility());
         savedInstanceState.putInt(IMPORT_EXPORT_BUTTON_VISIBILITY, importExportButton.getVisibility());
 
@@ -528,16 +377,6 @@ public class ImportExportActivity extends AppCompatActivity implements StoragePe
     }
 
     public void onClickRadioButton(View view) {
-        // Get handles for the views.
-        Spinner encryptionSpinner = findViewById(R.id.encryption_spinner);
-        LinearLayout fileNameLinearLayout = findViewById(R.id.file_name_linearlayout);
-        EditText passwordEncryptionEditText = findViewById(R.id.password_encryption_edittext);
-        EditText fileNameEditText = findViewById(R.id.file_name_edittext);
-        TextView fileDoesNotExistTextView = findViewById(R.id.file_does_not_exist_textview);
-        TextView fileExistsWarningTextView = findViewById(R.id.file_exists_warning_textview);
-        TextView openKeychainImportInstructionTextView = findViewById(R.id.openkeychain_import_instructions_textview);
-        Button importExportButton = findViewById(R.id.import_export_button);
-
         // Get the current file name.
         String fileNameString = fileNameEditText.getText().toString();
 
@@ -545,103 +384,74 @@ public class ImportExportActivity extends AppCompatActivity implements StoragePe
         File file = new File(fileNameString);
 
         // Check to see if import or export was selected.
-        switch (view.getId()) {
-            case R.id.import_radiobutton:
-                // Check to see if OpenPGP encryption is selected.
-                if (encryptionSpinner.getSelectedItemPosition() == OPENPGP_ENCRYPTION) {  // OpenPGP encryption selected.
-                    // Show the OpenKeychain import instructions.
-                    openKeychainImportInstructionTextView.setVisibility(View.VISIBLE);
-
-                    // Set the text on the import/export button to be `Decrypt`.
-                    importExportButton.setText(R.string.decrypt);
-                } else {  // OpenPGP encryption not selected.
-                    // Hide the OpenKeychain import instructions.
-                    openKeychainImportInstructionTextView.setVisibility(View.GONE);
-
-                    // Set the text on the import/export button to be `Import`.
-                    importExportButton.setText(R.string.import_button);
-                }
-
-                // Hide the file exists warning text view.
-                fileExistsWarningTextView.setVisibility(View.GONE);
+        if (view.getId() == R.id.import_radiobutton) {  // The import radio button is selected.
+            // Check to see if OpenPGP encryption is selected.
+            if (encryptionSpinner.getSelectedItemPosition() == OPENPGP_ENCRYPTION) {  // OpenPGP encryption selected.
+                // Show the OpenKeychain import instructions.
+                openKeychainImportInstructionsTextView.setVisibility(View.VISIBLE);
+
+                // Set the text on the import/export button to be `Decrypt`.
+                importExportButton.setText(R.string.decrypt);
+            } else {  // OpenPGP encryption not selected.
+                // Hide the OpenKeychain import instructions.
+                openKeychainImportInstructionsTextView.setVisibility(View.GONE);
 
-                // Display the file name views.
-                fileNameLinearLayout.setVisibility(View.VISIBLE);
-                importExportButton.setVisibility(View.VISIBLE);
-
-                // Check to see if the file exists.
-                if (file.exists()) {  // The file exists.
-                    // Hide the notification that the file does not exist.
-                    fileDoesNotExistTextView.setVisibility(View.GONE);
-
-                    // Check to see if password encryption is selected.
-                    if (encryptionSpinner.getSelectedItemPosition() == PASSWORD_ENCRYPTION) {  // Password encryption is selected.
-                        // Enable the import button if the encryption password is populated.
-                        importExportButton.setEnabled(!passwordEncryptionEditText.getText().toString().isEmpty());
-                    } else {  // Password encryption is not selected.
-                        // Enable the import/decrypt button.
-                        importExportButton.setEnabled(true);
-                    }
-                } else {  // The file does not exist.
-                    // Show the notification that the file does not exist.
-                    fileDoesNotExistTextView.setVisibility(View.VISIBLE);
+                // Set the text on the import/export button to be `Import`.
+                importExportButton.setText(R.string.import_button);
+            }
 
-                    // Disable the import/decrypt button.
-                    importExportButton.setEnabled(false);
+            // Display the file name views.
+            fileNameLinearLayout.setVisibility(View.VISIBLE);
+            importExportButton.setVisibility(View.VISIBLE);
+
+            // Check to see if the file exists.
+            if (file.exists()) {  // The file exists.
+                // Check to see if password encryption is selected.
+                if (encryptionSpinner.getSelectedItemPosition() == PASSWORD_ENCRYPTION) {  // Password encryption is selected.
+                    // Enable the import button if the encryption password is populated.
+                    importExportButton.setEnabled(!encryptionPasswordEditText.getText().toString().isEmpty());
+                } else {  // Password encryption is not selected.
+                    // Enable the import/decrypt button.
+                    importExportButton.setEnabled(true);
                 }
-                break;
-
-            case R.id.export_radiobutton:
-                // Hide the OpenKeychain import instructions.
-                openKeychainImportInstructionTextView.setVisibility(View.GONE);
+            } else {  // The file does not exist.
+                // Disable the import/decrypt button.
+                importExportButton.setEnabled(false);
+            }
+        } else {  // The export radio button is selected.
+            // Hide the OpenKeychain import instructions.
+            openKeychainImportInstructionsTextView.setVisibility(View.GONE);
 
-                // Set the text on the import/export button to be `Export`.
-                importExportButton.setText(R.string.export);
+            // Set the text on the import/export button to be `Export`.
+            importExportButton.setText(R.string.export);
 
-                // Show the import/export button.
-                importExportButton.setVisibility(View.VISIBLE);
+            // Show the import/export button.
+            importExportButton.setVisibility(View.VISIBLE);
 
-                // Check to see if OpenPGP encryption is selected.
-                if (encryptionSpinner.getSelectedItemPosition() == OPENPGP_ENCRYPTION) {  // OpenPGP encryption is selected.
-                    // Hide the file name views.
-                    fileNameLinearLayout.setVisibility(View.GONE);
-                    fileDoesNotExistTextView.setVisibility(View.GONE);
-                    fileExistsWarningTextView.setVisibility(View.GONE);
+            // Check to see if OpenPGP encryption is selected.
+            if (encryptionSpinner.getSelectedItemPosition() == OPENPGP_ENCRYPTION) {  // OpenPGP encryption is selected.
+                // Hide the file name views.
+                fileNameLinearLayout.setVisibility(View.GONE);
 
-                    // Enable the export button.
-                    importExportButton.setEnabled(true);
-                } else {  // OpenPGP encryption is not selected.
-                    // Show the file name view.
-                    fileNameLinearLayout.setVisibility(View.VISIBLE);
-
-                    // Hide the notification that the file name does not exist.
-                    fileDoesNotExistTextView.setVisibility(View.GONE);
-
-                    // Display a warning if the file already exists.
-                    if (file.exists()) {
-                        fileExistsWarningTextView.setVisibility(View.VISIBLE);
-                    } else {
-                        fileExistsWarningTextView.setVisibility(View.GONE);
-                    }
+                // Enable the export button.
+                importExportButton.setEnabled(true);
+            } else {  // OpenPGP encryption is not selected.
+                // Show the file name view.
+                fileNameLinearLayout.setVisibility(View.VISIBLE);
 
-                    // Check the encryption type.
-                    if (encryptionSpinner.getSelectedItemPosition() == NO_ENCRYPTION) {  // No encryption is selected.
-                        // Enable the export button if the file name is populated.
-                        importExportButton.setEnabled(!fileNameString.isEmpty());
-                    } else {  // Password encryption is selected.
-                        // Enable the export button if the file name and the password are populated.
-                        importExportButton.setEnabled(!fileNameString.isEmpty() && !passwordEncryptionEditText.getText().toString().isEmpty());
-                    }
+                // Check the encryption type.
+                if (encryptionSpinner.getSelectedItemPosition() == NO_ENCRYPTION) {  // No encryption is selected.
+                    // Enable the export button if the file name is populated.
+                    importExportButton.setEnabled(!fileNameString.isEmpty());
+                } else {  // Password encryption is selected.
+                    // Enable the export button if the file name and the password are populated.
+                    importExportButton.setEnabled(!fileNameString.isEmpty() && !encryptionPasswordEditText.getText().toString().isEmpty());
                 }
-                break;
+            }
         }
     }
 
     public void browse(View view) {
-        // Get a handle for the views.
-        Spinner encryptionSpinner = findViewById(R.id.encryption_spinner);
-        RadioButton importRadioButton = findViewById(R.id.import_radiobutton);
-
         // Check to see if import or export is selected.
         if (importRadioButton.isChecked()) {  // Import is selected.
             // Create the file picker intent.
@@ -650,11 +460,6 @@ public class ImportExportActivity extends AppCompatActivity implements StoragePe
             // Set the intent MIME type to include all files so that everything is visible.
             importBrowseIntent.setType("*/*");
 
-            // Set the initial directory if the minimum API >= 26.
-            if (Build.VERSION.SDK_INT >= 26) {
-                importBrowseIntent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Environment.getExternalStorageDirectory());
-            }
-
             // Request a file that can be opened.
             importBrowseIntent.addCategory(Intent.CATEGORY_OPENABLE);
 
@@ -674,11 +479,6 @@ public class ImportExportActivity extends AppCompatActivity implements StoragePe
                 exportBrowseIntent.putExtra(Intent.EXTRA_TITLE, getString(R.string.settings) + " " + BuildConfig.VERSION_NAME + ".pbs.aes");
             }
 
-            // Set the initial directory if the minimum API >= 26.
-            if (Build.VERSION.SDK_INT >= 26) {
-                exportBrowseIntent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Environment.getExternalStorageDirectory());
-            }
-
             // Request a file that can be opened.
             exportBrowseIntent.addCategory(Intent.CATEGORY_OPENABLE);
 
@@ -687,485 +487,465 @@ public class ImportExportActivity extends AppCompatActivity implements StoragePe
         }
     }
 
-    public void importExport(View view) {
-        // Get a handle for the views.
-        Spinner encryptionSpinner = findViewById(R.id.encryption_spinner);
-        RadioButton importRadioButton = findViewById(R.id.import_radiobutton);
-        RadioButton exportRadioButton = findViewById(R.id.export_radiobutton);
-
-        // Check to see if the storage permission is needed.
-        if ((encryptionSpinner.getSelectedItemPosition() == OPENPGP_ENCRYPTION) && exportRadioButton.isChecked()) {  // Permission not needed to export via OpenKeychain.
-            // Export the settings.
-            exportSettings();
-        } else if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {  // The storage permission has been granted.
-            // Check to see if import or export is selected.
-            if (importRadioButton.isChecked()) {  // Import is selected.
-                // Import the settings.
-                importSettings();
-            } else {  // Export is selected.
-                // Export the settings.
-                exportSettings();
-            }
-        } else {  // The storage permission has not been granted.
-            // Get a handle for the file name EditText.
-            EditText fileNameEditText = findViewById(R.id.file_name_edittext);
-
-            // Get the file name string.
-            String fileNameString = fileNameEditText.getText().toString();
-
-            // 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 (fileNameString.startsWith(externalPrivateDirectory)) {  // The file path is in the external private directory.
-                // Check to see if import or export is selected.
-                if (importRadioButton.isChecked()) {  // Import is selected.
-                    // Import the settings.
-                    importSettings();
-                } else {  // Export is selected.
-                    // Export the settings.
-                    exportSettings();
-                }
-            } 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(0);
-
-                    // Show the storage permission alert dialog.  The permission will be requested when the dialog is closed.
-                    storagePermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.storage_permission));
-                } else {  // Show the permission request directly.
-                    // Request the storage permission.  The export will be run when it finishes.
-                    ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
-                }
-            }
-        }
-    }
-
-    @Override
-    public void onCloseStoragePermissionDialog(int type) {
-        // Request the write external storage permission.  The import/export will be run when it finishes.
-        ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
-    }
-
     @Override
-    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
-        // Get a handle for the import radiobutton.
-        RadioButton importRadioButton = findViewById(R.id.import_radiobutton);
-
-        // 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.
-            // Run the import or export methods according to which radio button is selected.
-            if (importRadioButton.isChecked()) {  // Import is selected.
-                // Import the settings.
-                importSettings();
-            } else {  // Export is selected.
-                // Export the settings.
-                exportSettings();
-            }
-        } else {  // The storage permission was not granted.
-            // Display an error snackbar.
-            Snackbar.make(importRadioButton, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show();
-        }
-    }
-
-    @Override
-    public void onActivityResult(int requestCode, int resultCode, Intent intent) {
+    public void onActivityResult(int requestCode, int resultCode, Intent returnedIntent) {
         // Run the default commands.
-        super.onActivityResult(requestCode, resultCode, intent);
+        super.onActivityResult(requestCode, resultCode, returnedIntent);
 
         switch (requestCode) {
             case (BROWSE_RESULT_CODE):
-                // Don't do anything if the user pressed back from the file picker.
+                // Only do something if the user didn't press back from the file picker.
                 if (resultCode == Activity.RESULT_OK) {
-                    // Get a handle for the views.
-                    EditText fileNameEditText = findViewById(R.id.file_name_edittext);
-                    TextView fileExistsWarningTextView = findViewById(R.id.file_exists_warning_textview);
-
-                    // Instantiate the file name helper.
-                    FileNameHelper fileNameHelper = new FileNameHelper();
-
                     // Get the file path URI from the intent.
-                    Uri filePathUri = intent.getData();
+                    Uri fileNameUri = returnedIntent.getData();
 
-                    // Use the file path from the intent if it exists.
-                    if (filePathUri != null) {
-                        // Convert the file name URI to a file name path.
-                        String fileNamePath = fileNameHelper.convertUriToFileNamePath(filePathUri);
+                    // Get the file name string from the URI.
+                    String fileNameString = fileNameUri.toString();
 
-                        // Set the file name path as the text of the file name edit text.
-                        fileNameEditText.setText(fileNamePath);
+                    // Set the file name name text.
+                    fileNameEditText.setText(fileNameString);
 
-                        // Hide the file exists warning text view, because the file picker will have just created a file if export was selected.
-                        fileExistsWarningTextView.setVisibility(View.GONE);
-                    }
+                    // Move the cursor to the end of the file name edit text.
+                    fileNameEditText.setSelection(fileNameString.length());
                 }
                 break;
 
-            case OPENPGP_EXPORT_RESULT_CODE:
-                // Get the temporary unencrypted export file.
-                File temporaryUnencryptedExportFile = new File(getApplicationContext().getCacheDir() + "/" + getString(R.string.settings) + " " + BuildConfig.VERSION_NAME + ".pbs");
+            case OPENPGP_IMPORT_RESULT_CODE:
+                // Delete the temporary PGP encrypted import file.
+                if (temporaryPgpEncryptedImportFile.exists()) {
+                    //noinspection ResultOfMethodCallIgnored
+                    temporaryPgpEncryptedImportFile.delete();
+                }
+                break;
 
-                // Delete the temporary unencrypted export file if it exists.
-                if (temporaryUnencryptedExportFile.exists()) {
+            case OPENPGP_EXPORT_RESULT_CODE:
+                // Delete the temporary pre-encrypted export file if it exists.
+                if (temporaryPreEncryptedExportFile.exists()) {
                     //noinspection ResultOfMethodCallIgnored
-                    temporaryUnencryptedExportFile.delete();
+                    temporaryPreEncryptedExportFile.delete();
                 }
                 break;
         }
     }
 
-    private void exportSettings() {
-        // Get a handle for the views.
-        Spinner encryptionSpinner = findViewById(R.id.encryption_spinner);
-        EditText fileNameEditText = findViewById(R.id.file_name_edittext);
-
+    public void importExport(View view) {
         // Instantiate the import export database helper.
         ImportExportDatabaseHelper importExportDatabaseHelper = new ImportExportDatabaseHelper();
 
-        // Get the export file string.
-        String exportFileString = fileNameEditText.getText().toString();
+        // Check to see if import or export is selected.
+        if (importRadioButton.isChecked()) {  // Import is selected.
+            // Initialize the import status string
+            String importStatus = "";
 
-        // Get the export and temporary unencrypted export files.
-        File exportFile = new File(exportFileString);
-        File temporaryUnencryptedExportFile = new File(getApplicationContext().getCacheDir() + "/" + getString(R.string.settings) + " " + BuildConfig.VERSION_NAME + ".pbs");
+            // Get the file name string.
+            String fileNameString = fileNameEditText.getText().toString();
 
-        // Create an export status string.
-        String exportStatus;
+            // Import according to the encryption type.
+            switch (encryptionSpinner.getSelectedItemPosition()) {
+                case NO_ENCRYPTION:
+                    try {
+                        // Get an input stream for the file name.
+                        InputStream inputStream = getContentResolver().openInputStream(Uri.parse(fileNameString));
+
+                        // Import the unencrypted file.
+                        importStatus = importExportDatabaseHelper.importUnencrypted(inputStream, this);
+                    } catch (FileNotFoundException exception) {
+                        // Update the import status.
+                        importStatus = exception.toString();
+                    }
 
-        // Export according to the encryption type.
-        switch (encryptionSpinner.getSelectedItemPosition()) {
-            case NO_ENCRYPTION:
-                // Export the unencrypted file.
-                exportStatus = importExportDatabaseHelper.exportUnencrypted(exportFile, this);
+                    // Restart Privacy Browser if successful.
+                    if (importStatus.equals(ImportExportDatabaseHelper.IMPORT_SUCCESSFUL)) {
+                        restartPrivacyBrowser();
+                    }
+                    break;
 
-                // Show a disposition snackbar.
-                if (exportStatus.equals(ImportExportDatabaseHelper.EXPORT_SUCCESSFUL)) {
-                    Snackbar.make(fileNameEditText, getString(R.string.export_successful), Snackbar.LENGTH_SHORT).show();
-                } else {
-                    Snackbar.make(fileNameEditText, getString(R.string.export_failed) + "  " + exportStatus, Snackbar.LENGTH_INDEFINITE).show();
-                }
-                break;
+                case PASSWORD_ENCRYPTION:
+                    try {
+                        // Get the encryption password.
+                        String encryptionPasswordString = encryptionPasswordEditText.getText().toString();
 
-            case PASSWORD_ENCRYPTION:
-                // Create an unencrypted export in a private directory.
-                exportStatus = importExportDatabaseHelper.exportUnencrypted(temporaryUnencryptedExportFile, this);
+                        // Get an input stream for the file name.
+                        InputStream inputStream = getContentResolver().openInputStream(Uri.parse(fileNameString));
 
-                try {
-                    // Create an unencrypted export file input stream.
-                    FileInputStream unencryptedExportFileInputStream = new FileInputStream(temporaryUnencryptedExportFile);
+                        // Get the salt from the beginning of the import file.
+                        byte[] saltByteArray = new byte[32];
+                        //noinspection ResultOfMethodCallIgnored
+                        inputStream.read(saltByteArray);
 
-                    // Delete the encrypted export file if it exists.
-                    if (exportFile.exists()) {
+                        // Get the initialization vector from the import file.
+                        byte[] initializationVector = new byte[12];
                         //noinspection ResultOfMethodCallIgnored
-                        exportFile.delete();
-                    }
+                        inputStream.read(initializationVector);
 
-                    // Create an encrypted export file output stream.
-                    FileOutputStream encryptedExportFileOutputStream = new FileOutputStream(exportFile);
+                        // Convert the encryption password to a byte array.
+                        byte[] encryptionPasswordByteArray = encryptionPasswordString.getBytes(StandardCharsets.UTF_8);
 
-                    // Get a handle for the encryption password EditText.
-                    EditText encryptionPasswordEditText = findViewById(R.id.password_encryption_edittext);
+                        // Append the salt to the encryption password byte array.  This protects against rainbow table attacks.
+                        byte[] encryptionPasswordWithSaltByteArray = new byte[encryptionPasswordByteArray.length + saltByteArray.length];
+                        System.arraycopy(encryptionPasswordByteArray, 0, encryptionPasswordWithSaltByteArray, 0, encryptionPasswordByteArray.length);
+                        System.arraycopy(saltByteArray, 0, encryptionPasswordWithSaltByteArray, encryptionPasswordByteArray.length, saltByteArray.length);
 
-                    // Get the encryption password.
-                    String encryptionPasswordString = encryptionPasswordEditText.getText().toString();
+                        // Get a SHA-512 message digest.
+                        MessageDigest messageDigest = MessageDigest.getInstance("SHA-512");
 
-                    // Initialize a secure random number generator.
-                    SecureRandom secureRandom = new SecureRandom();
+                        // Hash the salted encryption password.  Otherwise, any characters after the 32nd character in the password are ignored.
+                        byte[] hashedEncryptionPasswordWithSaltByteArray = messageDigest.digest(encryptionPasswordWithSaltByteArray);
 
-                    // Get a 256 bit (32 byte) random salt.
-                    byte[] saltByteArray = new byte[32];
-                    secureRandom.nextBytes(saltByteArray);
+                        // Truncate the encryption password byte array to 256 bits (32 bytes).
+                        byte[] truncatedHashedEncryptionPasswordWithSaltByteArray = Arrays.copyOf(hashedEncryptionPasswordWithSaltByteArray, 32);
 
-                    // Convert the encryption password to a byte array.
-                    byte[] encryptionPasswordByteArray = encryptionPasswordString.getBytes(StandardCharsets.UTF_8);
+                        // Create an AES secret key from the encryption password byte array.
+                        SecretKeySpec secretKey = new SecretKeySpec(truncatedHashedEncryptionPasswordWithSaltByteArray, "AES");
 
-                    // Append the salt to the encryption password byte array.  This protects against rainbow table attacks.
-                    byte[] encryptionPasswordWithSaltByteArray = new byte[encryptionPasswordByteArray.length + saltByteArray.length];
-                    System.arraycopy(encryptionPasswordByteArray, 0, encryptionPasswordWithSaltByteArray, 0, encryptionPasswordByteArray.length);
-                    System.arraycopy(saltByteArray, 0, encryptionPasswordWithSaltByteArray, encryptionPasswordByteArray.length, saltByteArray.length);
+                        // Get a Advanced Encryption Standard, Galois/Counter Mode, No Padding cipher instance. Galois/Counter mode protects against modification of the ciphertext.  It doesn't use padding.
+                        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
 
-                    // Get a SHA-512 message digest.
-                    MessageDigest messageDigest = MessageDigest.getInstance("SHA-512");
+                        // Set the GCM tag length to be 128 bits (the maximum) and apply the initialization vector.
+                        GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, initializationVector);
 
-                    // Hash the salted encryption password.  Otherwise, any characters after the 32nd character in the password are ignored.
-                    byte[] hashedEncryptionPasswordWithSaltByteArray = messageDigest.digest(encryptionPasswordWithSaltByteArray);
+                        // Initialize the cipher.
+                        cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec);
 
-                    // Truncate the encryption password byte array to 256 bits (32 bytes).
-                    byte[] truncatedHashedEncryptionPasswordWithSaltByteArray = Arrays.copyOf(hashedEncryptionPasswordWithSaltByteArray, 32);
+                        // Create a cipher input stream.
+                        CipherInputStream cipherInputStream = new CipherInputStream(inputStream, cipher);
 
-                    // Create an AES secret key from the encryption password byte array.
-                    SecretKeySpec secretKey = new SecretKeySpec(truncatedHashedEncryptionPasswordWithSaltByteArray, "AES");
+                        // Initialize variables to store data as it is moved from the cipher input stream to the unencrypted import file output stream.  Move 128 bits (16 bytes) at a time.
+                        int numberOfBytesRead;
+                        byte[] decryptedBytes = new byte[16];
 
-                    // Generate a random 12 byte initialization vector.  According to NIST, a 12 byte initialization vector is more secure than a 16 byte one.
-                    byte[] initializationVector = new byte[12];
-                    secureRandom.nextBytes(initializationVector);
 
-                    // Get a Advanced Encryption Standard, Galois/Counter Mode, No Padding cipher instance. Galois/Counter mode protects against modification of the ciphertext.  It doesn't use padding.
-                    Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
+                        // Create a private temporary unencrypted import file.
+                        File temporaryUnencryptedImportFile = File.createTempFile("temporary_unencrypted_import_file", null, getApplicationContext().getCacheDir());
 
-                    // Set the GCM tag length to be 128 bits (the maximum) and apply the initialization vector.
-                    GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, initializationVector);
+                        // Create an temporary unencrypted import file output stream.
+                        FileOutputStream temporaryUnencryptedImportFileOutputStream = new FileOutputStream(temporaryUnencryptedImportFile);
 
-                    // Initialize the cipher.
-                    cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmParameterSpec);
 
-                    // Add the salt and the initialization vector to the export file.
-                    encryptedExportFileOutputStream.write(saltByteArray);
-                    encryptedExportFileOutputStream.write(initializationVector);
+                        // Read up to 128 bits (16 bytes) of data from the cipher input stream.  `-1` will be returned when the end fo the file is reached.
+                        while ((numberOfBytesRead = cipherInputStream.read(decryptedBytes)) != -1) {
+                            // Write the data to the temporary unencrypted import file output stream.
+                            temporaryUnencryptedImportFileOutputStream.write(decryptedBytes, 0, numberOfBytesRead);
+                        }
 
-                    // Create a cipher output stream.
-                    CipherOutputStream cipherOutputStream = new CipherOutputStream(encryptedExportFileOutputStream, cipher);
 
-                    // Initialize variables to store data as it is moved from the unencrypted export file input stream to the cipher output stream.  Move 128 bits (16 bytes) at a time.
-                    int numberOfBytesRead;
-                    byte[] encryptedBytes = new byte[16];
+                        // Flush the temporary unencrypted import file output stream.
+                        temporaryUnencryptedImportFileOutputStream.flush();
 
-                    // Read up to 128 bits (16 bytes) of data from the unencrypted export file stream.  `-1` will be returned when the end of the file is reached.
-                    while ((numberOfBytesRead = unencryptedExportFileInputStream.read(encryptedBytes)) != -1) {
-                        // Write the data to the cipher output stream.
-                        cipherOutputStream.write(encryptedBytes, 0, numberOfBytesRead);
-                    }
+                        // Close the streams.
+                        temporaryUnencryptedImportFileOutputStream.close();
+                        cipherInputStream.close();
+                        inputStream.close();
 
-                    // Close the streams.
-                    cipherOutputStream.flush();
-                    cipherOutputStream.close();
-                    encryptedExportFileOutputStream.close();
-                    unencryptedExportFileInputStream.close();
-
-                    // Wipe the encryption data from memory.
-                    //noinspection UnusedAssignment
-                    encryptionPasswordString = "";
-                    Arrays.fill(saltByteArray, (byte) 0);
-                    Arrays.fill(encryptionPasswordByteArray, (byte) 0);
-                    Arrays.fill(encryptionPasswordWithSaltByteArray, (byte) 0);
-                    Arrays.fill(hashedEncryptionPasswordWithSaltByteArray, (byte) 0);
-                    Arrays.fill(truncatedHashedEncryptionPasswordWithSaltByteArray, (byte) 0);
-                    Arrays.fill(initializationVector, (byte) 0);
-                    Arrays.fill(encryptedBytes, (byte) 0);
-
-                    // Delete the temporary unencrypted export file.
-                    //noinspection ResultOfMethodCallIgnored
-                    temporaryUnencryptedExportFile.delete();
-                } catch (Exception exception) {
-                    exportStatus = exception.toString();
-                }
+                        // Wipe the encryption data from memory.
+                        //noinspection UnusedAssignment
+                        encryptionPasswordString = "";
+                        Arrays.fill(saltByteArray, (byte) 0);
+                        Arrays.fill(initializationVector, (byte) 0);
+                        Arrays.fill(encryptionPasswordByteArray, (byte) 0);
+                        Arrays.fill(encryptionPasswordWithSaltByteArray, (byte) 0);
+                        Arrays.fill(hashedEncryptionPasswordWithSaltByteArray, (byte) 0);
+                        Arrays.fill(truncatedHashedEncryptionPasswordWithSaltByteArray, (byte) 0);
+                        Arrays.fill(decryptedBytes, (byte) 0);
 
-                // Show a disposition snackbar.
-                if (exportStatus.equals(ImportExportDatabaseHelper.EXPORT_SUCCESSFUL)) {
-                    Snackbar.make(fileNameEditText, getString(R.string.export_successful), Snackbar.LENGTH_SHORT).show();
-                } else {
-                    Snackbar.make(fileNameEditText, getString(R.string.export_failed) + "  " + exportStatus, Snackbar.LENGTH_INDEFINITE).show();
-                }
-                break;
+                        // Create a temporary unencrypted import file input stream.
+                        FileInputStream temporaryUnencryptedImportFileInputStream = new FileInputStream(temporaryUnencryptedImportFile);
 
-            case OPENPGP_ENCRYPTION:
-                // Create an unencrypted export in the private location.
-                importExportDatabaseHelper.exportUnencrypted(temporaryUnencryptedExportFile, this);
+                        // Import the temporary unencrypted import file.
+                        importStatus = importExportDatabaseHelper.importUnencrypted(temporaryUnencryptedImportFileInputStream, this);
 
-                // Create an encryption intent for OpenKeychain.
-                Intent openKeychainEncryptIntent = new Intent("org.sufficientlysecure.keychain.action.ENCRYPT_DATA");
+                        // Close the temporary unencrypted import file input stream.
+                        temporaryUnencryptedImportFileInputStream.close();
 
-                // Include the temporary unencrypted export file URI.
-                openKeychainEncryptIntent.setData(FileProvider.getUriForFile(this, getString(R.string.file_provider), temporaryUnencryptedExportFile));
+                        // Delete the temporary unencrypted import file.
+                        //noinspection ResultOfMethodCallIgnored
+                        temporaryUnencryptedImportFile.delete();
 
-                // Allow OpenKeychain to read the file URI.
-                openKeychainEncryptIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+                        // Restart Privacy Browser if successful.
+                        if (importStatus.equals(ImportExportDatabaseHelper.IMPORT_SUCCESSFUL)) {
+                            restartPrivacyBrowser();
+                        }
+                    } catch (Exception exception) {
+                        // Update the import status.
+                        importStatus = exception.toString();
+                    }
+                    break;
 
-                // Send the intent to the OpenKeychain package.
-                openKeychainEncryptIntent.setPackage("org.sufficientlysecure.keychain");
+                case OPENPGP_ENCRYPTION:
+                    try {
+                        // Set the temporary PGP encrypted import file.
+                        temporaryPgpEncryptedImportFile = File.createTempFile("temporary_pgp_encrypted_import_file", null, getApplicationContext().getCacheDir());
 
-                // Make it so.
-                startActivityForResult(openKeychainEncryptIntent, OPENPGP_EXPORT_RESULT_CODE);
-                break;
-        }
+                        // Create a temporary PGP encrypted import file output stream.
+                        FileOutputStream temporaryPgpEncryptedImportFileOutputStream = new FileOutputStream(temporaryPgpEncryptedImportFile);
 
-        // Add the file to the list of recent files.  This doesn't currently work, but maybe it will someday.
-        MediaScannerConnection.scanFile(this, new String[] {exportFileString}, new String[] {"application/x-sqlite3"}, null);
-    }
+                        // Get an input stream for the file name.
+                        InputStream inputStream = getContentResolver().openInputStream(Uri.parse(fileNameString));
 
-    private void importSettings() {
-        // Get a handle for the views.
-        Spinner encryptionSpinner = findViewById(R.id.encryption_spinner);
-        EditText fileNameEditText = findViewById(R.id.file_name_edittext);
+                        // Create a transfer byte array.
+                        byte[] transferByteArray = new byte[1024];
 
-        // Instantiate the import export database helper.
-        ImportExportDatabaseHelper importExportDatabaseHelper = new ImportExportDatabaseHelper();
+                        // Create an integer to track the number of bytes read.
+                        int bytesRead;
 
-        // Get the import file.
-        File importFile = new File(fileNameEditText.getText().toString());
+                        // Copy the input stream to the temporary PGP encrypted import file.
+                        while ((bytesRead = inputStream.read(transferByteArray)) > 0) {
+                            temporaryPgpEncryptedImportFileOutputStream.write(transferByteArray, 0, bytesRead);
+                        }
 
-        // Initialize the import status string
-        String importStatus = "";
+                        // Flush the temporary PGP encrypted import file output stream.
+                        temporaryPgpEncryptedImportFileOutputStream.flush();
 
-        // Import according to the encryption type.
-        switch (encryptionSpinner.getSelectedItemPosition()) {
-            case NO_ENCRYPTION:
-                // Import the unencrypted file.
-                importStatus = importExportDatabaseHelper.importUnencrypted(importFile, this);
-                break;
+                        // Close the streams.
+                        inputStream.close();
+                        temporaryPgpEncryptedImportFileOutputStream.flush();
 
-            case PASSWORD_ENCRYPTION:
-                // Use a private temporary import location.
-                File temporaryUnencryptedImportFile = new File(getApplicationContext().getCacheDir() + "/" + getString(R.string.settings) + " " + BuildConfig.VERSION_NAME + ".pbs");
 
-                try {
-                    // Create an encrypted import file input stream.
-                    FileInputStream encryptedImportFileInputStream = new FileInputStream(importFile);
+                        // Create an decryption intent for OpenKeychain.
+                        Intent openKeychainDecryptIntent = new Intent("org.sufficientlysecure.keychain.action.DECRYPT_DATA");
 
-                    // Delete the temporary import file if it exists.
-                    if (temporaryUnencryptedImportFile.exists()) {
-                        //noinspection ResultOfMethodCallIgnored
-                        temporaryUnencryptedImportFile.delete();
+                        // Include the URI to be decrypted.
+                        openKeychainDecryptIntent.setData(FileProvider.getUriForFile(this, getString(R.string.file_provider), temporaryPgpEncryptedImportFile));
+
+                        // Allow OpenKeychain to read the file URI.
+                        openKeychainDecryptIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+
+                        // Send the intent to the OpenKeychain package.
+                        openKeychainDecryptIntent.setPackage("org.sufficientlysecure.keychain");
+
+                        // Make it so.
+                        startActivityForResult(openKeychainDecryptIntent, OPENPGP_IMPORT_RESULT_CODE);
+
+                        // Update the import status.
+                        importStatus = ImportExportDatabaseHelper.IMPORT_SUCCESSFUL;
+                    } catch (Exception exception) {
+                        // Update the import status.
+                        importStatus = exception.toString();
                     }
+                    break;
+            }
 
-                    // Create an unencrypted import file output stream.
-                    FileOutputStream unencryptedImportFileOutputStream = new FileOutputStream(temporaryUnencryptedImportFile);
+            // Respond to the import status.
+            if (!importStatus.equals(ImportExportDatabaseHelper.IMPORT_SUCCESSFUL)) {
+                // Display a snack bar with the import error.
+                Snackbar.make(fileNameEditText, getString(R.string.import_failed) + "  " + importStatus, Snackbar.LENGTH_INDEFINITE).show();
+            }
+        } else {  // Export is selected.
+            // Export according to the encryption type.
+            switch (encryptionSpinner.getSelectedItemPosition()) {
+                case NO_ENCRYPTION:
+                    // Get the file name string.
+                    String noEncryptionFileNameString = fileNameEditText.getText().toString();
+
+                    try {
+                        // Get the export file output stream.
+                        OutputStream exportFileOutputStream = getContentResolver().openOutputStream(Uri.parse(noEncryptionFileNameString));
+
+                        // Export the unencrypted file.
+                        String noEncryptionExportStatus = importExportDatabaseHelper.exportUnencrypted(exportFileOutputStream, this);
+
+                        // Display an export disposition snackbar.
+                        if (noEncryptionExportStatus.equals(ImportExportDatabaseHelper.EXPORT_SUCCESSFUL)) {
+                            Snackbar.make(fileNameEditText, getString(R.string.export_successful), Snackbar.LENGTH_SHORT).show();
+                        } else {
+                            Snackbar.make(fileNameEditText, getString(R.string.export_failed) + "  " + noEncryptionExportStatus, Snackbar.LENGTH_INDEFINITE).show();
+                        }
+                    } catch (FileNotFoundException fileNotFoundException) {
+                        // Display a snackbar with the exception.
+                        Snackbar.make(fileNameEditText, getString(R.string.export_failed) + "  " + fileNotFoundException, Snackbar.LENGTH_INDEFINITE).show();
+                    }
+                    break;
 
-                    // Get a handle for the encryption password EditText.
-                    EditText encryptionPasswordEditText = findViewById(R.id.password_encryption_edittext);
+                case PASSWORD_ENCRYPTION:
+                    try {
+                        // Create a temporary unencrypted export file.
+                        File temporaryUnencryptedExportFile = File.createTempFile("temporary_unencrypted_export_file", null, getApplicationContext().getCacheDir());
 
-                    // Get the encryption password.
-                    String encryptionPasswordString = encryptionPasswordEditText.getText().toString();
+                        // Create a temporary unencrypted export output stream.
+                        FileOutputStream temporaryUnencryptedExportOutputStream = new FileOutputStream(temporaryUnencryptedExportFile);
 
-                    // Get the salt from the beginning of the import file.
-                    byte[] saltByteArray = new byte[32];
-                    //noinspection ResultOfMethodCallIgnored
-                    encryptedImportFileInputStream.read(saltByteArray);
+                        // Populate the temporary unencrypted export.
+                        String passwordEncryptionExportStatus = importExportDatabaseHelper.exportUnencrypted(temporaryUnencryptedExportOutputStream, this);
 
-                    // Get the initialization vector from the import file.
-                    byte[] initializationVector = new byte[12];
-                    //noinspection ResultOfMethodCallIgnored
-                    encryptedImportFileInputStream.read(initializationVector);
+                        // Close the temporary unencrypted export output stream.
+                        temporaryUnencryptedExportOutputStream.close();
 
-                    // Convert the encryption password to a byte array.
-                    byte[] encryptionPasswordByteArray = encryptionPasswordString.getBytes(StandardCharsets.UTF_8);
+                        // Create an unencrypted export file input stream.
+                        FileInputStream unencryptedExportFileInputStream = new FileInputStream(temporaryUnencryptedExportFile);
 
-                    // Append the salt to the encryption password byte array.  This protects against rainbow table attacks.
-                    byte[] encryptionPasswordWithSaltByteArray = new byte[encryptionPasswordByteArray.length + saltByteArray.length];
-                    System.arraycopy(encryptionPasswordByteArray, 0, encryptionPasswordWithSaltByteArray, 0, encryptionPasswordByteArray.length);
-                    System.arraycopy(saltByteArray, 0, encryptionPasswordWithSaltByteArray, encryptionPasswordByteArray.length, saltByteArray.length);
+                        // Get the encryption password.
+                        String encryptionPasswordString = encryptionPasswordEditText.getText().toString();
 
-                    // Get a SHA-512 message digest.
-                    MessageDigest messageDigest = MessageDigest.getInstance("SHA-512");
+                        // Initialize a secure random number generator.
+                        SecureRandom secureRandom = new SecureRandom();
 
-                    // Hash the salted encryption password.  Otherwise, any characters after the 32nd character in the password are ignored.
-                    byte[] hashedEncryptionPasswordWithSaltByteArray = messageDigest.digest(encryptionPasswordWithSaltByteArray);
+                        // Get a 256 bit (32 byte) random salt.
+                        byte[] saltByteArray = new byte[32];
+                        secureRandom.nextBytes(saltByteArray);
 
-                    // Truncate the encryption password byte array to 256 bits (32 bytes).
-                    byte[] truncatedHashedEncryptionPasswordWithSaltByteArray = Arrays.copyOf(hashedEncryptionPasswordWithSaltByteArray, 32);
+                        // Convert the encryption password to a byte array.
+                        byte[] encryptionPasswordByteArray = encryptionPasswordString.getBytes(StandardCharsets.UTF_8);
 
-                    // Create an AES secret key from the encryption password byte array.
-                    SecretKeySpec secretKey = new SecretKeySpec(truncatedHashedEncryptionPasswordWithSaltByteArray, "AES");
+                        // Append the salt to the encryption password byte array.  This protects against rainbow table attacks.
+                        byte[] encryptionPasswordWithSaltByteArray = new byte[encryptionPasswordByteArray.length + saltByteArray.length];
+                        System.arraycopy(encryptionPasswordByteArray, 0, encryptionPasswordWithSaltByteArray, 0, encryptionPasswordByteArray.length);
+                        System.arraycopy(saltByteArray, 0, encryptionPasswordWithSaltByteArray, encryptionPasswordByteArray.length, saltByteArray.length);
 
-                    // Get a Advanced Encryption Standard, Galois/Counter Mode, No Padding cipher instance. Galois/Counter mode protects against modification of the ciphertext.  It doesn't use padding.
-                    Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
+                        // Get a SHA-512 message digest.
+                        MessageDigest messageDigest = MessageDigest.getInstance("SHA-512");
 
-                    // Set the GCM tag length to be 128 bits (the maximum) and apply the initialization vector.
-                    GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, initializationVector);
+                        // Hash the salted encryption password.  Otherwise, any characters after the 32nd character in the password are ignored.
+                        byte[] hashedEncryptionPasswordWithSaltByteArray = messageDigest.digest(encryptionPasswordWithSaltByteArray);
 
-                    // Initialize the cipher.
-                    cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec);
+                        // Truncate the encryption password byte array to 256 bits (32 bytes).
+                        byte[] truncatedHashedEncryptionPasswordWithSaltByteArray = Arrays.copyOf(hashedEncryptionPasswordWithSaltByteArray, 32);
 
-                    // Create a cipher input stream.
-                    CipherInputStream cipherInputStream = new CipherInputStream(encryptedImportFileInputStream, cipher);
+                        // Create an AES secret key from the encryption password byte array.
+                        SecretKeySpec secretKey = new SecretKeySpec(truncatedHashedEncryptionPasswordWithSaltByteArray, "AES");
 
-                    // Initialize variables to store data as it is moved from the cipher input stream to the unencrypted import file output stream.  Move 128 bits (16 bytes) at a time.
-                    int numberOfBytesRead;
-                    byte[] decryptedBytes = new byte[16];
+                        // Generate a random 12 byte initialization vector.  According to NIST, a 12 byte initialization vector is more secure than a 16 byte one.
+                        byte[] initializationVector = new byte[12];
+                        secureRandom.nextBytes(initializationVector);
 
-                    // Read up to 128 bits (16 bytes) of data from the cipher input stream.  `-1` will be returned when the end fo the file is reached.
-                    while ((numberOfBytesRead = cipherInputStream.read(decryptedBytes)) != -1) {
-                        // Write the data to the unencrypted import file output stream.
-                        unencryptedImportFileOutputStream.write(decryptedBytes, 0, numberOfBytesRead);
-                    }
+                        // Get a Advanced Encryption Standard, Galois/Counter Mode, No Padding cipher instance. Galois/Counter mode protects against modification of the ciphertext.  It doesn't use padding.
+                        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
 
-                    // Close the streams.
-                    unencryptedImportFileOutputStream.flush();
-                    unencryptedImportFileOutputStream.close();
-                    cipherInputStream.close();
-                    encryptedImportFileInputStream.close();
-
-                    // Wipe the encryption data from memory.
-                    //noinspection UnusedAssignment
-                    encryptionPasswordString = "";
-                    Arrays.fill(saltByteArray, (byte) 0);
-                    Arrays.fill(initializationVector, (byte) 0);
-                    Arrays.fill(encryptionPasswordByteArray, (byte) 0);
-                    Arrays.fill(encryptionPasswordWithSaltByteArray, (byte) 0);
-                    Arrays.fill(hashedEncryptionPasswordWithSaltByteArray, (byte) 0);
-                    Arrays.fill(truncatedHashedEncryptionPasswordWithSaltByteArray, (byte) 0);
-                    Arrays.fill(decryptedBytes, (byte) 0);
-
-                    // Import the unencrypted database from the private location.
-                    importStatus = importExportDatabaseHelper.importUnencrypted(temporaryUnencryptedImportFile, this);
-
-                    // Delete the temporary unencrypted import file.
-                    //noinspection ResultOfMethodCallIgnored
-                    temporaryUnencryptedImportFile.delete();
-                } catch (Exception exception) {
-                    importStatus = exception.toString();
-                }
-                break;
+                        // Set the GCM tag length to be 128 bits (the maximum) and apply the initialization vector.
+                        GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, initializationVector);
 
-            case OPENPGP_ENCRYPTION:
-                try {
-                    // Create an decryption intent for OpenKeychain.
-                    Intent openKeychainDecryptIntent = new Intent("org.sufficientlysecure.keychain.action.DECRYPT_DATA");
+                        // Initialize the cipher.
+                        cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmParameterSpec);
 
-                    // Include the URI to be decrypted.
-                    openKeychainDecryptIntent.setData(FileProvider.getUriForFile(this, getString(R.string.file_provider), importFile));
+                        // Get the file name string.
+                        String passwordEncryptionFileNameString = fileNameEditText.getText().toString();
 
-                    // Allow OpenKeychain to read the file URI.
-                    openKeychainDecryptIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+                        // Get the export file output stream.
+                        OutputStream exportFileOutputStream = getContentResolver().openOutputStream(Uri.parse(passwordEncryptionFileNameString));
 
-                    // Send the intent to the OpenKeychain package.
-                    openKeychainDecryptIntent.setPackage("org.sufficientlysecure.keychain");
+                        // Add the salt and the initialization vector to the export file output stream.
+                        exportFileOutputStream.write(saltByteArray);
+                        exportFileOutputStream.write(initializationVector);
 
-                    // Make it so.
-                    startActivity(openKeychainDecryptIntent);
-                } catch (IllegalArgumentException exception) {  // The file import location is not valid.
-                    // Display a snack bar with the import error.
-                    Snackbar.make(fileNameEditText, getString(R.string.import_failed) + "  " + exception.toString(), Snackbar.LENGTH_INDEFINITE).show();
-                }
-                break;
-        }
+                        // Create a cipher output stream.
+                        CipherOutputStream cipherOutputStream = new CipherOutputStream(exportFileOutputStream, cipher);
+
+                        // Initialize variables to store data as it is moved from the unencrypted export file input stream to the cipher output stream.  Move 128 bits (16 bytes) at a time.
+                        int numberOfBytesRead;
+                        byte[] encryptedBytes = new byte[16];
+
+                        // Read up to 128 bits (16 bytes) of data from the unencrypted export file stream.  `-1` will be returned when the end of the file is reached.
+                        while ((numberOfBytesRead = unencryptedExportFileInputStream.read(encryptedBytes)) != -1) {
+                            // Write the data to the cipher output stream.
+                            cipherOutputStream.write(encryptedBytes, 0, numberOfBytesRead);
+                        }
+
+                        // Close the streams.
+                        cipherOutputStream.flush();
+                        cipherOutputStream.close();
+                        exportFileOutputStream.close();
+                        unencryptedExportFileInputStream.close();
+
+                        // Wipe the encryption data from memory.
+                        //noinspection UnusedAssignment
+                        encryptionPasswordString = "";
+                        Arrays.fill(saltByteArray, (byte) 0);
+                        Arrays.fill(encryptionPasswordByteArray, (byte) 0);
+                        Arrays.fill(encryptionPasswordWithSaltByteArray, (byte) 0);
+                        Arrays.fill(hashedEncryptionPasswordWithSaltByteArray, (byte) 0);
+                        Arrays.fill(truncatedHashedEncryptionPasswordWithSaltByteArray, (byte) 0);
+                        Arrays.fill(initializationVector, (byte) 0);
+                        Arrays.fill(encryptedBytes, (byte) 0);
+
+                        // Delete the temporary unencrypted export file.
+                        //noinspection ResultOfMethodCallIgnored
+                        temporaryUnencryptedExportFile.delete();
+
+                        // Display an export disposition snackbar.
+                        if (passwordEncryptionExportStatus.equals(ImportExportDatabaseHelper.EXPORT_SUCCESSFUL)) {
+                            Snackbar.make(fileNameEditText, getString(R.string.export_successful), Snackbar.LENGTH_SHORT).show();
+                        } else {
+                            Snackbar.make(fileNameEditText, getString(R.string.export_failed) + "  " + passwordEncryptionExportStatus, Snackbar.LENGTH_INDEFINITE).show();
+                        }
+                    } catch (Exception exception) {
+                        // Display a snackbar with the exception.
+                        Snackbar.make(fileNameEditText, getString(R.string.export_failed) + "  " + exception, Snackbar.LENGTH_INDEFINITE).show();
+                    }
+                    break;
 
-        // Respond to the import disposition.
-        if (importStatus.equals(ImportExportDatabaseHelper.IMPORT_SUCCESSFUL)) {  // The import was successful.
-            // Create an intent to restart Privacy Browser.
-            Intent restartIntent = getParentActivityIntent();
+                case OPENPGP_ENCRYPTION:
+                    try {
+                        // Set the temporary pre-encrypted export file.
+                        temporaryPreEncryptedExportFile = new File(getApplicationContext().getCacheDir() + "/" + getString(R.string.settings) + " " + BuildConfig.VERSION_NAME + ".pbs");
 
-            // Assert that the intent is not null to remove the lint error below.
-            assert restartIntent != null;
+                        // Delete the temporary pre-encrypted export file if it already exists.
+                        if (temporaryPreEncryptedExportFile.exists()) {
+                            //noinspection ResultOfMethodCallIgnored
+                            temporaryPreEncryptedExportFile.delete();
+                        }
 
-            // `Intent.FLAG_ACTIVITY_CLEAR_TASK` removes all activities from the stack.  It requires `Intent.FLAG_ACTIVITY_NEW_TASK`.
-            restartIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+                        // Create a temporary pre-encrypted export output stream.
+                        FileOutputStream temporaryPreEncryptedExportOutputStream = new FileOutputStream(temporaryPreEncryptedExportFile);
+
+                        // Populate the temporary pre-encrypted export file.
+                        String openpgpEncryptionExportStatus = importExportDatabaseHelper.exportUnencrypted(temporaryPreEncryptedExportOutputStream, this);
+
+                        // Flush the temporary pre-encryption export output stream.
+                        temporaryPreEncryptedExportOutputStream.flush();
+
+                        // Close the temporary pre-encryption export output stream.
+                        temporaryPreEncryptedExportOutputStream.close();
+
+                        // Display an export error snackbar if the temporary pre-encrypted export failed.
+                        if (!openpgpEncryptionExportStatus.equals(ImportExportDatabaseHelper.EXPORT_SUCCESSFUL)) {
+                            Snackbar.make(fileNameEditText, getString(R.string.export_failed) + "  " + openpgpEncryptionExportStatus, Snackbar.LENGTH_INDEFINITE).show();
+                        }
 
-            // Create a restart handler.
-            Handler restartHandler = new Handler();
+                        // Create an encryption intent for OpenKeychain.
+                        Intent openKeychainEncryptIntent = new Intent("org.sufficientlysecure.keychain.action.ENCRYPT_DATA");
 
-            // Create a restart runnable.
-            Runnable restartRunnable =  () -> {
-                // Restart Privacy Browser.
-                startActivity(restartIntent);
+                        // Include the temporary unencrypted export file URI.
+                        openKeychainEncryptIntent.setData(FileProvider.getUriForFile(this, getString(R.string.file_provider), temporaryPreEncryptedExportFile));
 
-                // Kill this instance of Privacy Browser.  Otherwise, the app exhibits sporadic behavior after the restart.
-                System.exit(0);
-            };
+                        // Allow OpenKeychain to read the file URI.
+                        openKeychainEncryptIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
 
-            // Restart Privacy Browser after 150 milliseconds to allow enough time for the preferences to be saved.
-            restartHandler.postDelayed(restartRunnable, 150);
+                        // Send the intent to the OpenKeychain package.
+                        openKeychainEncryptIntent.setPackage("org.sufficientlysecure.keychain");
 
-        } else if (!(encryptionSpinner.getSelectedItemPosition() == OPENPGP_ENCRYPTION)){  // The import was not successful.
-            // Display a snack bar with the import error.
-            Snackbar.make(fileNameEditText, getString(R.string.import_failed) + "  " + importStatus, Snackbar.LENGTH_INDEFINITE).show();
+                        // Make it so.
+                        startActivityForResult(openKeychainEncryptIntent, OPENPGP_EXPORT_RESULT_CODE);
+                    } catch (Exception exception) {
+                        // Display a snackbar with the exception.
+                        Snackbar.make(fileNameEditText, getString(R.string.export_failed) + "  " + exception, Snackbar.LENGTH_INDEFINITE).show();
+                    }
+                    break;
+            }
         }
     }
+
+    private void restartPrivacyBrowser() {
+        // Create an intent to restart Privacy Browser.
+        Intent restartIntent = getParentActivityIntent();
+
+        // Assert that the intent is not null to remove the lint error below.
+        assert restartIntent != null;
+
+        // `Intent.FLAG_ACTIVITY_CLEAR_TASK` removes all activities from the stack.  It requires `Intent.FLAG_ACTIVITY_NEW_TASK`.
+        restartIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+
+        // Create a restart handler.
+        Handler restartHandler = new Handler();
+
+        // Create a restart runnable.
+        Runnable restartRunnable =  () -> {
+            // Restart Privacy Browser.
+            startActivity(restartIntent);
+
+            // Kill this instance of Privacy Browser.  Otherwise, the app exhibits sporadic behavior after the restart.
+            System.exit(0);
+        };
+
+        // Restart Privacy Browser after 150 milliseconds to allow enough time for the preferences to be saved.
+        restartHandler.postDelayed(restartRunnable, 150);
+    }
 }
\ No newline at end of file
index 3200c2be0b6fbfae60966ad6edcf12ef87570e1a..8e54bb43a119d6e16c5d9e425d1d0e2854650f6b 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2019-2020 Soren Stoutner <soren@stoutner.com>.
+ * Copyright © 2019-2021 Soren Stoutner <soren@stoutner.com>.
  *
  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
  *
 
 package com.stoutner.privacybrowser.activities;
 
-import android.Manifest;
 import android.app.Activity;
 import android.app.Dialog;
 import android.content.ClipData;
 import android.content.ClipboardManager;
-import android.content.ContentResolver;
 import android.content.Intent;
 import android.content.SharedPreferences;
-import android.content.pm.PackageManager;
 import android.content.res.Configuration;
-import android.media.MediaScannerConnection;
 import android.net.Uri;
-import android.os.Build;
 import android.os.Bundle;
 import android.preference.PreferenceManager;
 import android.util.TypedValue;
 import android.view.Menu;
 import android.view.MenuItem;
-import android.view.View;
 import android.view.WindowManager;
 import android.widget.EditText;
 import android.widget.ScrollView;
@@ -47,9 +41,6 @@ import androidx.annotation.NonNull;
 import androidx.appcompat.app.ActionBar;
 import androidx.appcompat.app.AppCompatActivity;
 import androidx.appcompat.widget.Toolbar;
-import androidx.core.app.ActivityCompat;
-import androidx.core.content.ContextCompat;
-import androidx.core.content.FileProvider;
 import androidx.fragment.app.DialogFragment;
 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
 
@@ -57,28 +48,22 @@ import com.google.android.material.snackbar.Snackbar;
 
 import com.stoutner.privacybrowser.R;
 import com.stoutner.privacybrowser.asynctasks.GetLogcat;
-import com.stoutner.privacybrowser.dialogs.StoragePermissionDialog;
 import com.stoutner.privacybrowser.dialogs.SaveDialog;
-import com.stoutner.privacybrowser.helpers.FileNameHelper;
 
 import java.io.BufferedReader;
 import java.io.BufferedWriter;
 import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.io.OutputStream;
 import java.io.OutputStreamWriter;
 import java.nio.charset.StandardCharsets;
 
-public class LogcatActivity extends AppCompatActivity implements SaveDialog.SaveListener, StoragePermissionDialog.StoragePermissionDialogListener {
+public class LogcatActivity extends AppCompatActivity implements SaveDialog.SaveListener {
     // Declare the class constants.
     private final String SCROLLVIEW_POSITION = "scrollview_position";
 
-    // Declare the class variables.
-    private String filePathString;
-
     // Define the class views.
     private TextView logcatTextView;
 
@@ -104,8 +89,11 @@ public class LogcatActivity extends AppCompatActivity implements SaveDialog.Save
         // Set the content view.
         setContentView(R.layout.logcat_coordinatorlayout);
 
-        // Set the toolbar as the action bar.
+        // Get handles for the views.
         Toolbar toolbar = findViewById(R.id.logcat_toolbar);
+        SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.logcat_swiperefreshlayout);
+
+        // Set the toolbar as the action bar.
         setSupportActionBar(toolbar);
 
         // Get a handle for the action bar.
@@ -121,7 +109,6 @@ public class LogcatActivity extends AppCompatActivity implements SaveDialog.Save
         logcatTextView = findViewById(R.id.logcat_textview);
 
         // Implement swipe to refresh.
-        SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.logcat_swiperefreshlayout);
         swipeRefreshLayout.setOnRefreshListener(() -> {
             // Get the current logcat.
             new GetLogcat(this, 0).execute();
@@ -241,78 +228,11 @@ public class LogcatActivity extends AppCompatActivity implements SaveDialog.Save
         savedInstanceState.putInt(SCROLLVIEW_POSITION, scrollViewYPositionInt);
     }
 
-    @Override
-    public void onSave(int saveType, DialogFragment dialogFragment) {
-        // Get a handle for the dialog.
-        Dialog dialog = dialogFragment.getDialog();
-
-        // Remove the 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.
-        filePathString = 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 logcat.
-            saveLogcat(filePathString);
-        } 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 (filePathString.startsWith(externalPrivateDirectory)) {  // The file path is in the external private directory.
-                // Save the logcat.
-                saveLogcat(filePathString);
-            } 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.  The type is specified as `0` because it currently isn't used for this activity.
-                    DialogFragment storagePermissionDialogFragment = StoragePermissionDialog.displayDialog(0);
-
-                    // Show the storage permission alert dialog.  The permission will be requested when the dialog is closed.
-                    storagePermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.storage_permission));
-                } else {  // Show the permission request directly.
-                    // Request the write external storage permission.  The logcat will be saved when it finishes.
-                    ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
-
-                }
-            }
-        }
-    }
-
-    @Override
-    public void onCloseStoragePermissionDialog(int requestType) {
-        // Request the write external storage permission.  The logcat will be saved when it finishes.
-        ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
-    }
-
-    @Override
-    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
-        // Check to see if the storage permission was granted.  If the dialog was canceled the grant result will be empty.
-        if ((grantResults.length > 0) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) {  // The storage permission was granted.
-            // Save the logcat.
-            saveLogcat(filePathString);
-        } else {  // The storage permission was not granted.
-            // Display an error snackbar.
-            Snackbar.make(logcatTextView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show();
-        }
-    }
-
     // The activity result is called after browsing for a file in the save alert dialog.
     @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);
 
         // Only do something if the user didn't press back from the file picker.
         if (resultCode == Activity.RESULT_OK) {
@@ -327,35 +247,38 @@ public class LogcatActivity extends AppCompatActivity implements SaveDialog.Save
                 // Remove the lint warning below that the save dialog might be null.
                 assert saveDialog != null;
 
-                // Get a handle for the dialog views.
+                // Get a handle for the file name edit text.
                 EditText fileNameEditText = saveDialog.findViewById(R.id.file_name_edittext);
-                TextView fileExistsWarningTextView = saveDialog.findViewById(R.id.file_exists_warning_textview);
 
                 // Get the file name URI from the intent.
-                Uri fileNameUri = data.getData();
+                Uri fileNameUri = returnedIntent.getData();
 
-                // Process the file name URI if it is not null.
-                if (fileNameUri != null) {
-                    // Instantiate a file name helper.
-                    FileNameHelper fileNameHelper = new FileNameHelper();
+                // Get the file name string from the URI.
+                String fileNameString = fileNameUri.toString();
 
-                    // Convert the file name URI to a file name path.
-                    String fileNamePath = fileNameHelper.convertUriToFileNamePath(fileNameUri);
+                // Set the file name text.
+                fileNameEditText.setText(fileNameString);
 
-                    // 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());
-
-                    // Hide the file exists warning.
-                    fileExistsWarningTextView.setVisibility(View.GONE);
-                }
+                // Move the cursor to the end of the file name edit text.
+                fileNameEditText.setSelection(fileNameString.length());
             }
         }
     }
 
-    private void saveLogcat(String fileNameString) {
+    @Override
+    public void onSave(int saveType, DialogFragment dialogFragment) {
+        // Get a handle for the dialog.
+        Dialog dialog = dialogFragment.getDialog();
+
+        // Remove the 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.
+        String fileNameString = fileNameEditText.getText().toString();
+
         try {
             // Get the logcat as a string.
             String logcatString = logcatTextView.getText().toString();
@@ -366,17 +289,11 @@ public class LogcatActivity extends AppCompatActivity implements SaveDialog.Save
             // Create a logcat buffered reader.
             BufferedReader logcatBufferedReader = new BufferedReader(new InputStreamReader(logcatInputStream));
 
-            // Create a file from the file name string.
-            File saveFile = new File(fileNameString);
-
-            // Delete the file if it already exists.
-            if (saveFile.exists()) {
-                //noinspection ResultOfMethodCallIgnored
-                saveFile.delete();
-            }
+            // Open an output stream.
+            OutputStream outputStream = getContentResolver().openOutputStream(Uri.parse(fileNameString));
 
             // Create a file buffered writer.
-            BufferedWriter fileBufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(saveFile)));
+            BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
 
             // Create a transfer string.
             String transferString;
@@ -384,55 +301,23 @@ public class LogcatActivity extends AppCompatActivity implements SaveDialog.Save
             // Use the transfer string to copy the logcat from the buffered reader to the buffered writer.
             while ((transferString = logcatBufferedReader.readLine()) != null) {
                 // Append the line to the buffered writer.
-                fileBufferedWriter.append(transferString);
+                bufferedWriter.append(transferString);
 
                 // Append a line break.
-                fileBufferedWriter.append("\n");
+                bufferedWriter.append("\n");
             }
 
-            // Close the buffered reader and writer.
-            logcatBufferedReader.close();
-            fileBufferedWriter.close();
-
-            // Add the file to the list of recent files.  This doesn't currently work, but maybe it will someday.
-            MediaScannerConnection.scanFile(this, new String[] {fileNameString}, new String[] {"text/plain"}, null);
-
-            // Create a logcat saved snackbar.
-            Snackbar logcatSavedSnackbar = Snackbar.make(logcatTextView, getString(R.string.file_saved) + "  " + fileNameString, Snackbar.LENGTH_SHORT);
-
-            // Add an open action to the snackbar.
-            logcatSavedSnackbar.setAction(R.string.open, (View view) -> {
-                // Get a file for the file name string.
-                File file = new File(fileNameString);
+            // Flush the buffered writer.
+            bufferedWriter.flush();
 
-                // Declare a file URI variable.
-                Uri fileUri;
-
-                // Get the URI for the file according to the Android version.
-                if (Build.VERSION.SDK_INT >= 24) {  // Use a file provider.
-                    fileUri = FileProvider.getUriForFile(this, getString(R.string.file_provider), file);
-                } else {  // Get the raw file path URI.
-                    fileUri = Uri.fromFile(file);
-                }
-
-                // Get a handle for the content resolver.
-                ContentResolver contentResolver = getContentResolver();
-
-                // Create an open intent with `ACTION_VIEW`.
-                Intent openIntent = new Intent(Intent.ACTION_VIEW);
-
-                // Set the URI and the MIME type.
-                openIntent.setDataAndType(fileUri, contentResolver.getType(fileUri));
-
-                // Allow the app to read the file URI.
-                openIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
-
-                // Show the chooser.
-                startActivity(Intent.createChooser(openIntent, getString(R.string.open)));
-            });
+            // Close the inputs and outputs.
+            logcatBufferedReader.close();
+            logcatInputStream.close();
+            bufferedWriter.close();
+            outputStream.close();
 
-            // Show the logcat saved snackbar.
-            logcatSavedSnackbar.show();
+            // Display a snackbar with the saved logcat information.
+            Snackbar.make(logcatTextView, getString(R.string.file_saved) + "  " + fileNameString, Snackbar.LENGTH_SHORT).show();
         } catch (Exception exception) {
             // Display a snackbar with the error message.
             Snackbar.make(logcatTextView, getString(R.string.error_saving_file) + "  " + exception.toString(), Snackbar.LENGTH_INDEFINITE).show();
index bef36d6a3de96444a723cf23bb4503c04be2c226..88d418677a12c3e2b4299a0433837451a5fe8e0b 100644 (file)
@@ -21,7 +21,6 @@
 
 package com.stoutner.privacybrowser.activities;
 
-import android.Manifest;
 import android.annotation.SuppressLint;
 import android.app.Activity;
 import android.app.Dialog;
@@ -31,7 +30,6 @@ import android.content.ActivityNotFoundException;
 import android.content.BroadcastReceiver;
 import android.content.ClipData;
 import android.content.ClipboardManager;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
@@ -102,9 +100,6 @@ import androidx.appcompat.app.AppCompatActivity;
 import androidx.appcompat.app.AppCompatDelegate;
 import androidx.appcompat.widget.Toolbar;
 import androidx.coordinatorlayout.widget.CoordinatorLayout;
-import androidx.core.app.ActivityCompat;
-import androidx.core.content.ContextCompat;
-import androidx.core.content.FileProvider;
 import androidx.core.content.res.ResourcesCompat;
 import androidx.core.view.GravityCompat;
 import androidx.drawerlayout.widget.DrawerLayout;
@@ -140,7 +135,6 @@ import com.stoutner.privacybrowser.dialogs.ProxyNotInstalledDialog;
 import com.stoutner.privacybrowser.dialogs.PinnedMismatchDialog;
 import com.stoutner.privacybrowser.dialogs.SaveWebpageDialog;
 import com.stoutner.privacybrowser.dialogs.SslCertificateErrorDialog;
-import com.stoutner.privacybrowser.dialogs.StoragePermissionDialog;
 import com.stoutner.privacybrowser.dialogs.UrlHistoryDialog;
 import com.stoutner.privacybrowser.dialogs.ViewSslCertificateDialog;
 import com.stoutner.privacybrowser.dialogs.WaitingForProxyDialog;
@@ -149,7 +143,6 @@ import com.stoutner.privacybrowser.helpers.AdHelper;
 import com.stoutner.privacybrowser.helpers.BlocklistHelper;
 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper;
 import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper;
-import com.stoutner.privacybrowser.helpers.FileNameHelper;
 import com.stoutner.privacybrowser.helpers.ProxyHelper;
 import com.stoutner.privacybrowser.views.NestedScrollWebView;
 
@@ -176,8 +169,8 @@ import java.util.concurrent.Executors;
 
 public class MainWebViewActivity extends AppCompatActivity implements CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener,
         EditBookmarkFolderDialog.EditBookmarkFolderListener, FontSizeDialog.UpdateFontSizeListener, NavigationView.OnNavigationItemSelectedListener, OpenDialog.OpenListener,
-        PinnedMismatchDialog.PinnedMismatchListener, PopulateBlocklists.PopulateBlocklistsListener, SaveWebpageDialog.SaveWebpageListener, StoragePermissionDialog.StoragePermissionDialogListener,
-        UrlHistoryDialog.NavigateHistoryListener, WebViewTabFragment.NewTabListener {
+        PinnedMismatchDialog.PinnedMismatchListener, PopulateBlocklists.PopulateBlocklistsListener, SaveWebpageDialog.SaveWebpageListener, UrlHistoryDialog.NavigateHistoryListener,
+        WebViewTabFragment.NewTabListener {
 
     // The executor service handles background tasks.  It is accessed from `ViewSourceActivity`.
     public static ExecutorService executorService = Executors.newFixedThreadPool(4);
@@ -203,10 +196,10 @@ 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.  The public static entries are accessed from `OpenDialog()` and `SaveWebpageDialog()`.
-    public final static int BROWSE_OPEN_REQUEST_CODE = 0;
-    public final static int BROWSE_SAVE_WEBPAGE_REQUEST_CODE = 1;
-    private final int BROWSE_FILE_UPLOAD_REQUEST_CODE = 2;
+    // Define the start activity for result request codes.  The public static entries are accessed from `OpenDialog()` and `SaveWebpageDialog()`.
+    private final int BROWSE_FILE_UPLOAD_REQUEST_CODE = 0;
+    public final static int BROWSE_OPEN_REQUEST_CODE = 1;
+    public final static int BROWSE_SAVE_WEBPAGE_REQUEST_CODE = 2;
 
     // The proxy mode is public static so it can be accessed from `ProxyHelper()`.
     // It is also used in `onRestart()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `applyAppSettings()`, and `applyProxy()`.
@@ -315,11 +308,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     private boolean sanitizeFacebookClickIds;
     private boolean sanitizeTwitterAmpRedirects;
 
-    // The file path strings are used in `onSaveWebpage()` and `onRequestPermissionResult()`
-    private String openFilePath;
-    private String saveWebpageUrl;
-    private String saveWebpageFilePath;
-
     // Declare the class views.
     private FrameLayout rootFrameLayout;
     private DrawerLayout drawerLayout;
@@ -1762,24 +1750,14 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             return true;
         } else if (menuItemId == R.id.save_url) {  // Save URL.
             // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
-            new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
+            new PrepareSaveDialog(this, this, getSupportFragmentManager(), SaveWebpageDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
                     currentWebView.getAcceptFirstPartyCookies()).execute(currentWebView.getCurrentUrl());
 
-            // Consume the event.
-            return true;
-        } else if (menuItemId == R.id.save_archive) {  // Save archive.
-            // Instantiate the save dialog.
-            DialogFragment saveArchiveFragment = SaveWebpageDialog.saveWebpage(StoragePermissionDialog.SAVE_ARCHIVE, null, null, getString(R.string.webpage_mht), null,
-                    false);
-
-            // Show the save dialog.  It must be named `save_dialog` so that the file picker can update the file name.
-            saveArchiveFragment.show(getSupportFragmentManager(), getString(R.string.save_dialog));
-
             // Consume the event.
             return true;
         } else if (menuItemId == R.id.save_image) {  // Save image.
             // Instantiate the save dialog.
-            DialogFragment saveImageFragment = SaveWebpageDialog.saveWebpage(StoragePermissionDialog.SAVE_IMAGE, null, null, getString(R.string.webpage_png), null,
+            DialogFragment saveImageFragment = SaveWebpageDialog.saveWebpage(SaveWebpageDialog.SAVE_IMAGE, null, null, getString(R.string.webpage_png), null,
                     false);
 
             // Show the save dialog.  It must be named `save_dialog` so that the file picker can update the file name.
@@ -2240,7 +2218,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Add a Save URL entry.
                 menu.add(R.string.save_url).setOnMenuItemClickListener((MenuItem item) -> {
                     // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
-                    new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
+                    new PrepareSaveDialog(this, this, getSupportFragmentManager(), SaveWebpageDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
                             currentWebView.getAcceptFirstPartyCookies()).execute(linkUrl);
 
                     // Consume the event.
@@ -2307,7 +2285,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Add a Save Image entry.
                 menu.add(R.string.save_image).setOnMenuItemClickListener((MenuItem item) -> {
                    // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
-                    new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
+                    new PrepareSaveDialog(this, this, getSupportFragmentManager(), SaveWebpageDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
                             currentWebView.getAcceptFirstPartyCookies()).execute(imageUrl);
 
                     // Consume the event.
@@ -2407,7 +2385,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Add a Save Image entry.
                 menu.add(R.string.save_image).setOnMenuItemClickListener((MenuItem item) -> {
                     // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
-                    new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
+                    new PrepareSaveDialog(this, this, getSupportFragmentManager(), SaveWebpageDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
                             currentWebView.getAcceptFirstPartyCookies()).execute(imageUrl);
 
                     // Consume the event.
@@ -2429,7 +2407,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Add a Save URL entry.
                 menu.add(R.string.save_url).setOnMenuItemClickListener((MenuItem item) -> {
                     // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
-                    new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
+                    new PrepareSaveDialog(this, this, getSupportFragmentManager(), SaveWebpageDialog.SAVE_URL, currentWebView.getSettings().getUserAgentString(),
                             currentWebView.getAcceptFirstPartyCookies()).execute(linkUrl);
 
                     // Consume the event.
@@ -2799,76 +2777,66 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 }
                 break;
 
-            case BROWSE_SAVE_WEBPAGE_REQUEST_CODE:
+            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 save dialog fragment.
-                    DialogFragment saveWebpageDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.save_dialog));
+                    // 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 (saveWebpageDialogFragment != null) {
-                        // Get a handle for the save webpage dialog.
-                        Dialog saveWebpageDialog = saveWebpageDialogFragment.getDialog();
+                    if (openDialogFragment != null) {
+                        // Get a handle for the open dialog.
+                        Dialog openDialog = openDialogFragment.getDialog();
 
                         // Remove the incorrect lint warning below that the dialog might be null.
-                        assert saveWebpageDialog != null;
+                        assert openDialog != null;
 
                         // Get a handle for the file name edit text.
-                        EditText fileNameEditText = saveWebpageDialog.findViewById(R.id.file_name_edittext);
-                        TextView fileExistsWarningTextView = saveWebpageDialog.findViewById(R.id.file_exists_warning_textview);
-
-                        // Instantiate the file name helper.
-                        FileNameHelper fileNameHelper = new FileNameHelper();
+                        EditText fileNameEditText = openDialog.findViewById(R.id.file_name_edittext);
 
-                        // 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());
+                        // Get the file name URI from the intent.
+                        Uri fileNameUri = returnedIntent.getData();
 
-                            // Set the file name path as the text of the file name edit text.
-                            fileNameEditText.setText(fileNamePath);
+                        // Get the file name string from the URI.
+                        String fileNameString = fileNameUri.toString();
 
-                            // Move the cursor to the end of the file name edit text.
-                            fileNameEditText.setSelection(fileNamePath.length());
+                        // Set the file name text.
+                        fileNameEditText.setText(fileNameString);
 
-                            // Hide the file exists warning.
-                            fileExistsWarningTextView.setVisibility(View.GONE);
-                        }
+                        // Move the cursor to the end of the file name edit text.
+                        fileNameEditText.setSelection(fileNameString.length());
                     }
                 }
                 break;
 
-            case BROWSE_OPEN_REQUEST_CODE:
+            case BROWSE_SAVE_WEBPAGE_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));
+                    // Get a handle for the save dialog fragment.
+                    DialogFragment saveWebpageDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.save_dialog));
 
                     // Only update the file name if the dialog still exists.
-                    if (openDialogFragment != null) {
-                        // Get a handle for the open dialog.
-                        Dialog openDialog = openDialogFragment.getDialog();
+                    if (saveWebpageDialogFragment != null) {
+                        // Get a handle for the save webpage dialog.
+                        Dialog saveWebpageDialog = saveWebpageDialogFragment.getDialog();
 
                         // Remove the incorrect lint warning below that the dialog might be null.
-                        assert openDialog != null;
+                        assert saveWebpageDialog != null;
 
                         // Get a handle for the file name edit text.
-                        EditText fileNameEditText = openDialog.findViewById(R.id.file_name_edittext);
+                        EditText fileNameEditText = saveWebpageDialog.findViewById(R.id.file_name_edittext);
 
-                        // Instantiate the file name helper.
-                        FileNameHelper fileNameHelper = new FileNameHelper();
+                        // Get the file name URI from the intent.
+                        Uri fileNameUri = returnedIntent.getData();
 
-                        // 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());
+                        // Get the file name string from the URI.
+                        String fileNameString = fileNameUri.toString();
 
-                            // Set the file name path as the text of the file name edit text.
-                            fileNameEditText.setText(fileNamePath);
+                        // Set the file name text.
+                        fileNameEditText.setText(fileNameString);
 
-                            // Move the cursor to the end of the file name edit text.
-                            fileNameEditText.setSelection(fileNamePath.length());
-                        }
+                        // Move the cursor to the end of the file name edit text.
+                        fileNameEditText.setSelection(fileNameString.length());
                     }
                 }
                 break;
@@ -3030,43 +2998,13 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         EditText fileNameEditText = dialog.findViewById(R.id.file_name_edittext);
 
         // Get the file path string.
-        openFilePath = fileNameEditText.getText().toString();
+        String openFilePath = fileNameEditText.getText().toString();
 
         // Apply the domain settings.  This resets the favorite icon and removes any domain settings.
-        applyDomainSettings(currentWebView, "file://" + openFilePath, true, false, false);
-
-        // 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}, StoragePermissionDialog.OPEN);
-                }
-            }
-        }
+        applyDomainSettings(currentWebView, openFilePath, true, false, false);
+
+        // Open the file.
+        currentWebView.loadUrl(openFilePath);
     }
 
     @Override
@@ -3081,6 +3019,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         EditText dialogUrlEditText = dialog.findViewById(R.id.url_edittext);
         EditText fileNameEditText = dialog.findViewById(R.id.file_name_edittext);
 
+        // Define the save webpage URL.
+        String saveWebpageUrl;
+
         // Store the URL.
         if ((originalUrlString != null) && originalUrlString.startsWith("data:")) {
             // Save the original URL.
@@ -3091,141 +3032,19 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         }
 
         // Get the file path from the edit text.
-        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_URL:
-                    // Save the URL.
-                    new SaveUrl(this, this, saveWebpageFilePath, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptFirstPartyCookies()).execute(saveWebpageUrl);
-                    break;
-
-                case StoragePermissionDialog.SAVE_ARCHIVE:
-                    // Save the webpage archive.
-                    saveWebpageArchive(saveWebpageFilePath);
-                    break;
-
-                case StoragePermissionDialog.SAVE_IMAGE:
-                    // Save the webpage image.
-                    new SaveWebpageImage(this, this, saveWebpageFilePath, currentWebView).execute();
-                    break;
-            }
-
-            // Reset the strings.
-            saveWebpageUrl = "";
-            saveWebpageFilePath = "";
-        } 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 (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_URL:
-                        // Save the URL.
-                        new SaveUrl(this, this, saveWebpageFilePath, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptFirstPartyCookies()).execute(saveWebpageUrl);
-                        break;
-
-                    case StoragePermissionDialog.SAVE_ARCHIVE:
-                        // Save the webpage archive.
-                        saveWebpageArchive(saveWebpageFilePath);
-                        break;
-
-                    case StoragePermissionDialog.SAVE_IMAGE:
-                        // Save the webpage image.
-                        new SaveWebpageImage(this, this, saveWebpageFilePath, currentWebView).execute();
-                        break;
-                }
+        String saveWebpageFilePath = fileNameEditText.getText().toString();
 
-                // Reset the strings.
-                saveWebpageUrl = "";
-                saveWebpageFilePath = "";
-            } 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(saveType);
-
-                    // Show the storage permission alert dialog.  The permission will be requested when the dialog is closed.
-                    storagePermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.storage_permission));
-                } else {  // Show the permission request directly.
-                    // Request the write external storage permission according to the save type.  The URL will be saved when it finishes.
-                    ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, saveType);
-                }
-            }
-        }
-    }
-
-    @Override
-    public void onCloseStoragePermissionDialog(int requestType) {
-        // Request the write external storage permission according to the request type.  The file will be opened when it finishes.
-        ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestType);
-
-    }
-
-    @Override
-    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
-        //Only process the results if they exist (this method is triggered when a dialog is presented the first time for an app, but no grant results are included).
-        if (grantResults.length > 0) {
-            switch (requestCode) {
-                case StoragePermissionDialog.OPEN:
-                    // Check to see if the storage permission was granted.  If the dialog was canceled the grant results will be empty.
-                    if (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();
-                    }
-                    break;
-
-                case StoragePermissionDialog.SAVE_URL:
-                    // Check to see if the storage permission was granted.  If the dialog was canceled the grant results will be empty.
-                    if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {  // The storage permission was granted.
-                        // Save the raw URL.
-                        new SaveUrl(this, 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();
-                    }
-                    break;
-
-                case StoragePermissionDialog.SAVE_ARCHIVE:
-                    // Check to see if the storage permission was granted.  If the dialog was canceled the grant results will be empty.
-                    if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {  // The storage permission was granted.
-                        // Save the webpage archive.
-                        saveWebpageArchive(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();
-                    }
-                    break;
-
-                case StoragePermissionDialog.SAVE_IMAGE:
-                    // Check to see if the storage permission was granted.  If the dialog was canceled the grant results will be empty.
-                    if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {  // The storage permission was granted.
-                        // Save the webpage image.
-                        new SaveWebpageImage(this, this, saveWebpageFilePath, currentWebView).execute();
-                    } else {  // The storage permission was not granted.
-                        // Display an error snackbar.
-                        Snackbar.make(currentWebView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show();
-                    }
-                    break;
-            }
+        //Save the webpage according to the save type.
+        switch (saveType) {
+            case SaveWebpageDialog.SAVE_URL:
+                // Save the URL.
+                new SaveUrl(this, this, saveWebpageFilePath, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptFirstPartyCookies()).execute(saveWebpageUrl);
+                break;
 
-            // Reset the strings.
-            openFilePath = "";
-            saveWebpageUrl = "";
-            saveWebpageFilePath = "";
+            case SaveWebpageDialog.SAVE_IMAGE:
+                // Save the webpage image.
+                new SaveWebpageImage(this, saveWebpageFilePath, currentWebView).execute();
+                break;
         }
     }
 
@@ -4417,7 +4236,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     PackageManager packageManager = getPackageManager();
 
                     // Check to see if I2P is in the list.  This will throw an error and drop to the catch section if it isn't installed.
-                    packageManager.getPackageInfo("org.torproject.android", 0);
+                    packageManager.getPackageInfo("net.i2p.android.router", 0);
                 } catch (PackageManager.NameNotFoundException exception) {  // I2P is not installed.
                     // Sow the I2P not installed dialog if it is not already displayed.
                     if (getSupportFragmentManager().findFragmentByTag(getString(R.string.proxy_not_installed_dialog)) == null) {
@@ -4872,48 +4691,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         }
     }
 
-    private void saveWebpageArchive(String filePath) {
-        // Save the webpage archive.
-        currentWebView.saveWebArchive(filePath);
-
-        // Display a snackbar.
-        Snackbar saveWebpageArchiveSnackbar = Snackbar.make(currentWebView, getString(R.string.file_saved) + "  " + filePath, Snackbar.LENGTH_SHORT);
-
-        // Add an open option to the snackbar.
-        saveWebpageArchiveSnackbar.setAction(R.string.open, (View view) -> {
-            // Get a file for the file name string.
-            File file = new File(filePath);
-
-            // Declare a file URI variable.
-            Uri fileUri;
-
-            // Get the URI for the file according to the Android version.
-            if (Build.VERSION.SDK_INT >= 24) {  // Use a file provider.
-                fileUri = FileProvider.getUriForFile(this, getString(R.string.file_provider), file);
-            } else {  // Get the raw file path URI.
-                fileUri = Uri.fromFile(file);
-            }
-
-            // Get a handle for the content resolver.
-            ContentResolver contentResolver = getContentResolver();
-
-            // Create an open intent with `ACTION_VIEW`.
-            Intent openIntent = new Intent(Intent.ACTION_VIEW);
-
-            // Set the URI and the MIME type.
-            openIntent.setDataAndType(fileUri, contentResolver.getType(fileUri));
-
-            // Allow the app to read the file URI.
-            openIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
-
-            // Show the chooser.
-            startActivity(Intent.createChooser(openIntent, getString(R.string.open)));
-        });
-
-        // Show the snackbar.
-        saveWebpageArchiveSnackbar.show();
-    }
-
     private void clearAndExit() {
         // Get a handle for the shared preferences.
         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
@@ -5440,7 +5217,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             }
 
             // Instantiate the save dialog.
-            DialogFragment saveDialogFragment = SaveWebpageDialog.saveWebpage(StoragePermissionDialog.SAVE_URL, downloadUrl, formattedFileSizeString, fileNameString, userAgent,
+            DialogFragment saveDialogFragment = SaveWebpageDialog.saveWebpage(SaveWebpageDialog.SAVE_URL, downloadUrl, formattedFileSizeString, fileNameString, userAgent,
                     nestedScrollWebView.getAcceptFirstPartyCookies());
 
             // Show the save dialog.  It must be named `save_dialog` so that the file picker can update the file name.
index 811eb7d322192a445a55c1ca5d3b08f98972bd45..96a223f0e6eba42bb63b018e8d9be837105353c0 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2019 Soren Stoutner <soren@stoutner.com>.
+ * Copyright © 2019,2021 Soren Stoutner <soren@stoutner.com>.
  *
  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
  *
@@ -102,7 +102,7 @@ public class GetHostIpAddresses extends AsyncTask<String, Void, String> {
 
         // Checked for pinned mismatches if there is pinned information and it is not ignored.
         if ((nestedScrollWebView.hasPinnedSslCertificate() || nestedScrollWebView.hasPinnedIpAddresses()) && !nestedScrollWebView.ignorePinnedDomainInformation()) {
-            CheckPinnedMismatchHelper.checkPinnedMismatch(fragmentManager, nestedScrollWebView);
+            CheckPinnedMismatchHelper.checkPinnedMismatch(activity, fragmentManager, nestedScrollWebView);
         }
     }
 }
\ No newline at end of file
index 4722dbfcbb80fadcf5b19cdb0ed236da7822f4c5..8abd75acd0c30339e1cec3d3d3e00574ab8837e4 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2020 Soren Stoutner <soren@stoutner.com>.
+ * Copyright © 2020-2021 Soren Stoutner <soren@stoutner.com>.
  *
  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
  *
@@ -37,7 +37,7 @@ import java.lang.ref.WeakReference;
 public class GetLogcat extends AsyncTask<Void, Void, String> {
     // Define the class variables.
     private final WeakReference<Activity> activityWeakReference;
-    private int scrollViewYPositionInt;
+    private final int scrollViewYPositionInt;
 
     // The public constructor.
     public GetLogcat(Activity activity, int scrollViewYPositionInt) {
index 46c3a039f4ad4a578e43fb94a7509f4c819426d6..f85952e270a6dfe247dd6167f6d71c029934d54b 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2020 Soren Stoutner <soren@stoutner.com>.
+ * Copyright © 2020-2021 Soren Stoutner <soren@stoutner.com>.
  *
  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
  *
@@ -37,12 +37,12 @@ import java.text.NumberFormat;
 
 public class GetUrlSize extends AsyncTask<String, Void, String> {
     // Define weak references for the calling context and alert dialog.
-    private WeakReference<Context> contextWeakReference;
-    private WeakReference<AlertDialog> alertDialogWeakReference;
+    private final WeakReference<Context> contextWeakReference;
+    private final WeakReference<AlertDialog> alertDialogWeakReference;
 
     // Define the class variables.
-    private String userAgent;
-    private boolean cookiesEnabled;
+    private final String userAgent;
+    private final boolean cookiesEnabled;
 
     // The public constructor.
     public GetUrlSize(Context context, AlertDialog alertDialog, String userAgent, boolean cookiesEnabled) {
index 4474285f934d00d423471d5c85e86750b0b18ea1..e9e151f5df069c77273e6860c2995dcfb9cea4bb 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2019 Soren Stoutner <soren@stoutner.com>.
+ * Copyright © 2019,2021 Soren Stoutner <soren@stoutner.com>.
  *
  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
  *
@@ -44,11 +44,11 @@ public class PopulateBlocklists extends AsyncTask<Void, String, ArrayList<ArrayL
     }
 
     // Define a populate blocklists listener.
-    private PopulateBlocklistsListener populateBlocklistsListener;
+    private final PopulateBlocklistsListener populateBlocklistsListener;
 
     // Define weak references for the activity and context.
-    private WeakReference<Context> contextWeakReference;
-    private WeakReference<Activity> activityWeakReference;
+    private final WeakReference<Context> contextWeakReference;
+    private final WeakReference<Activity> activityWeakReference;
 
     // The public constructor.
     public PopulateBlocklists(Context context, Activity activity) {
index 9e8b1fd83b39d4e366afdbdc10e1582a3fe6eba6..d858f1cba98127b34aefd1cbbf4410afb64410a1 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2020 Soren Stoutner <soren@stoutner.com>.
+ * Copyright © 2020-2021 Soren Stoutner <soren@stoutner.com>.
  *
  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
  *
@@ -41,14 +41,14 @@ import java.text.NumberFormat;
 
 public class PrepareSaveDialog extends AsyncTask<String, Void, String[]> {
     // Define weak references.
-    private WeakReference<Activity> activityWeakReference;
-    private WeakReference<Context> contextWeakReference;
-    private WeakReference<FragmentManager> fragmentManagerWeakReference;
+    private final WeakReference<Activity> activityWeakReference;
+    private final WeakReference<Context> contextWeakReference;
+    private final WeakReference<FragmentManager> fragmentManagerWeakReference;
 
     // Define the class variables.
-    private int saveType;
-    private String userAgent;
-    private boolean cookiesEnabled;
+    private final int saveType;
+    private final String userAgent;
+    private final boolean cookiesEnabled;
     private String urlString;
 
     // The public constructor.
@@ -198,6 +198,17 @@ public class PrepareSaveDialog extends AsyncTask<String, Void, String[]> {
             return;
         }
 
+        // Prevent the dialog from displaying if the app window is not visible.
+        // The asynctask continues to function even when the app is paused.  Attempting to display a dialog in that state leads to a crash.
+        while (!activity.getWindow().isActive()) {
+            try {
+                // The window is not active.  Wait 1 second.
+                wait(1000);
+            } catch (InterruptedException e) {
+                // Do nothing.
+            }
+        }
+
         // Instantiate the save dialog.
         DialogFragment saveDialogFragment = SaveWebpageDialog.saveWebpage(saveType, urlString, fileStringArray[0], fileStringArray[1], userAgent, cookiesEnabled);
 
index eda5726c49c5e92c0a5b638333af042924044dda..ed762a1484c247e433734cc023108c430f8f44e9 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2020 Soren Stoutner <soren@stoutner.com>.
+ * Copyright © 2020-2021 Soren Stoutner <soren@stoutner.com>.
  *
  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
  *
 package com.stoutner.privacybrowser.asynctasks;
 
 import android.app.Activity;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.net.Uri;
 import android.os.AsyncTask;
-import android.os.Build;
-import android.view.View;
 import android.widget.LinearLayout;
 
-import androidx.core.content.FileProvider;
-
 import com.google.android.material.snackbar.Snackbar;
 
 import com.stoutner.privacybrowser.R;
 
 import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileOutputStream;
+import java.io.OutputStream;
 import java.lang.ref.WeakReference;
 
 public class SaveAboutVersionImage extends AsyncTask<Void, Void, String> {
     // Declare the weak references.
-    private WeakReference<Context> contextWeakReference;
-    private WeakReference<Activity> activityWeakReference;
-    private WeakReference<LinearLayout> aboutVersionLinearLayoutWeakReference;
+    private final WeakReference<Activity> activityWeakReference;
+    private final WeakReference<LinearLayout> aboutVersionLinearLayoutWeakReference;
 
     // Declare the class constants.
     private final String SUCCESS = "Success";
@@ -54,17 +45,16 @@ public class SaveAboutVersionImage extends AsyncTask<Void, Void, String> {
     // Declare the class variables.
     private Snackbar savingImageSnackbar;
     private Bitmap aboutVersionBitmap;
-    private String filePathString;
+    private final String fileNameString;
 
     // The public constructor.
-    public SaveAboutVersionImage(Context context, Activity activity, String filePathString, LinearLayout aboutVersionLinearLayout) {
+    public SaveAboutVersionImage(Activity activity, String fileNameString, LinearLayout aboutVersionLinearLayout) {
         // Populate the weak references.
-        contextWeakReference = new WeakReference<>(context);
         activityWeakReference = new WeakReference<>(activity);
         aboutVersionLinearLayoutWeakReference = new WeakReference<>(aboutVersionLinearLayout);
 
         // Store the class variables.
-        this.filePathString = filePathString;
+        this.fileNameString = fileNameString;
     }
 
     // `onPreExecute()` operates on the UI thread.
@@ -80,7 +70,7 @@ public class SaveAboutVersionImage extends AsyncTask<Void, Void, String> {
         }
 
         // Create a saving image snackbar.
-        savingImageSnackbar = Snackbar.make(aboutVersionLinearLayout, activity.getString(R.string.processing_image) + "  " + filePathString, Snackbar.LENGTH_INDEFINITE);
+        savingImageSnackbar = Snackbar.make(aboutVersionLinearLayout, activity.getString(R.string.processing_image) + "  " + fileNameString, Snackbar.LENGTH_INDEFINITE);
 
         // Display the saving image snackbar.
         savingImageSnackbar.show();
@@ -112,33 +102,21 @@ public class SaveAboutVersionImage extends AsyncTask<Void, Void, String> {
         // Convert the bitmap to a PNG.  `0` is for lossless compression (the only option for a PNG).  This compression takes a long time.  Once the minimum API >= 30 this could be replaced with WEBP_LOSSLESS.
         aboutVersionBitmap.compress(Bitmap.CompressFormat.PNG, 0, aboutVersionByteArrayOutputStream);
 
-        // Get a file for the image.
-        File imageFile = new File(filePathString);
-
-        // Delete the current file if it exists.
-        if (imageFile.exists()) {
-            //noinspection ResultOfMethodCallIgnored
-            imageFile.delete();
-        }
-
         // Create a file creation disposition string.
         String fileCreationDisposition = SUCCESS;
 
         try {
-            // Create an image file output stream.
-            FileOutputStream imageFileOutputStream = new FileOutputStream(imageFile);
+            // Open an output stream.
+            OutputStream outputStream = activity.getContentResolver().openOutputStream(Uri.parse(fileNameString));
 
             // Write the webpage image to the image file.
-            aboutVersionByteArrayOutputStream.writeTo(imageFileOutputStream);
+            aboutVersionByteArrayOutputStream.writeTo(outputStream);
 
-            // Create a media scanner intent, which adds items like pictures to Android's recent file list.
-            Intent mediaScannerIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
+            // Flush the output stream.
+            outputStream.flush();
 
-            // Add the URI to the media scanner intent.
-            mediaScannerIntent.setData(Uri.fromFile(imageFile));
-
-            // Make it so.
-            activity.sendBroadcast(mediaScannerIntent);
+            // Close the output stream.
+            outputStream.close();
         } catch (Exception exception) {
             // Store the error in the file creation disposition string.
             fileCreationDisposition = exception.toString();
@@ -152,7 +130,6 @@ public class SaveAboutVersionImage extends AsyncTask<Void, Void, String> {
     @Override
     protected void onPostExecute(String fileCreationDisposition) {
         // Get handles for the weak references.
-        Context context = contextWeakReference.get();
         Activity activity = activityWeakReference.get();
         LinearLayout aboutVersionLinearLayout = aboutVersionLinearLayoutWeakReference.get();
 
@@ -167,41 +144,7 @@ public class SaveAboutVersionImage extends AsyncTask<Void, Void, String> {
         // Display a file creation disposition snackbar.
         if (fileCreationDisposition.equals(SUCCESS)) {
             // Create a file saved snackbar.
-            Snackbar imageSavedSnackbar = Snackbar.make(aboutVersionLinearLayout, activity.getString(R.string.file_saved) + "  " + filePathString, Snackbar.LENGTH_SHORT);
-
-            // Add an open action.
-            imageSavedSnackbar.setAction(R.string.open, (View view) -> {
-                // Get a file for the file path string.
-                File file = new File(filePathString);
-
-                // Declare a file URI variable.
-                Uri fileUri;
-
-                // Get the URI for the file according to the Android version.
-                if (Build.VERSION.SDK_INT >= 24) {  // Use a file provider.
-                    fileUri = FileProvider.getUriForFile(context, activity.getString(R.string.file_provider), file);
-                } else {  // Get the raw file path URI.
-                    fileUri = Uri.fromFile(file);
-                }
-
-                // Get a handle for the content resolver.
-                ContentResolver contentResolver = context.getContentResolver();
-
-                // Create an open intent with `ACTION_VIEW`.
-                Intent openIntent = new Intent(Intent.ACTION_VIEW);
-
-                // Autodetect the MIME type.
-                openIntent.setDataAndType(fileUri, contentResolver.getType(fileUri));
-
-                // Allow the app to read the file URI.
-                openIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
-
-                // Show the chooser.
-                activity.startActivity(Intent.createChooser(openIntent, context.getString(R.string.open)));
-            });
-
-            // Show the image saved snackbar.
-            imageSavedSnackbar.show();
+            Snackbar.make(aboutVersionLinearLayout, activity.getString(R.string.file_saved) + "  " + fileNameString, Snackbar.LENGTH_SHORT).show();
         } else {
             Snackbar.make(aboutVersionLinearLayout, activity.getString(R.string.error_saving_file) + "  " + fileCreationDisposition, Snackbar.LENGTH_INDEFINITE).show();
         }
index 2096e13907350100b73661efbb4ac4d16efd5ccb..8956296d12bee513b6e4ef534216eaad04121694 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2020 Soren Stoutner <soren@stoutner.com>.
+ * Copyright © 2020-2021 Soren Stoutner <soren@stoutner.com>.
  *
  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
  *
 package com.stoutner.privacybrowser.asynctasks;
 
 import android.app.Activity;
-import android.content.ContentResolver;
 import android.content.Context;
-import android.content.Intent;
 import android.net.Uri;
 import android.os.AsyncTask;
-import android.os.Build;
 import android.util.Base64;
-import android.view.View;
 import android.webkit.CookieManager;
-import android.webkit.MimeTypeMap;
-
-import androidx.core.content.FileProvider;
 
 import com.google.android.material.snackbar.Snackbar;
 import com.stoutner.privacybrowser.R;
@@ -39,8 +32,6 @@ import com.stoutner.privacybrowser.helpers.ProxyHelper;
 import com.stoutner.privacybrowser.views.NoSwipeViewPager;
 
 import java.io.BufferedInputStream;
-import java.io.File;
-import java.io.FileOutputStream;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.lang.ref.WeakReference;
@@ -111,21 +102,8 @@ public class SaveUrl extends AsyncTask<String, Long, String> {
         String saveDisposition = SUCCESS;
 
         try {
-            // 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 fileOutputStream = new FileOutputStream(file);
+            // Open an output stream.
+            OutputStream outputStream = activity.getContentResolver().openOutputStream(Uri.parse(filePathString));
 
             // Save the URL.
             if (urlToSave[0].startsWith("data:")) {  // The URL contains the entire data of an image.
@@ -135,11 +113,8 @@ public class SaveUrl extends AsyncTask<String, Long, String> {
                 // Decode the Base64 string to a byte array.
                 byte[] base64DecodedDataByteArray = Base64.decode(base64DataString, Base64.DEFAULT);
 
-                // Write the Base64 byte array to the file output stream.
-                fileOutputStream.write(base64DecodedDataByteArray);
-
-                // Close the file output stream.
-                fileOutputStream.close();
+                // Write the Base64 byte array to the output stream.
+                outputStream.write(base64DecodedDataByteArray);
             } else {  // The URL points to the data location on the internet.
                 // Get the URL from the calling activity.
                 URL url = new URL(urlToSave[0]);
@@ -200,7 +175,7 @@ public class SaveUrl extends AsyncTask<String, Long, String> {
                     // 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 file output stream.
-                        fileOutputStream.write(conversionBufferByteArray, 0, bufferLength);
+                        outputStream.write(conversionBufferByteArray, 0, bufferLength);
 
                         // Update the file download progress snackbar.
                         if (fileSize == -1) {  // The file size is unknown.
@@ -222,23 +197,17 @@ public class SaveUrl extends AsyncTask<String, Long, String> {
 
                     // Close the input stream.
                     inputStream.close();
-
-                    // Close the file output stream.
-                    fileOutputStream.close();
                 } finally {
                     // Disconnect the HTTP URL connection.
                     httpUrlConnection.disconnect();
                 }
             }
 
-            // Create a media scanner intent, which adds items like pictures to Android's recent file list.
-            Intent mediaScannerIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
+            // Flush the output stream.
+            outputStream.flush();
 
-            // Add the URI to the media scanner intent.
-            mediaScannerIntent.setData(Uri.fromFile(file));
-
-            // Make it so.
-            activity.sendBroadcast(mediaScannerIntent);
+            // Close the output stream.
+            outputStream.close();
         } catch (Exception exception) {
             // Store the error in the save disposition string.
             saveDisposition = exception.toString();
@@ -279,7 +248,6 @@ public class SaveUrl extends AsyncTask<String, Long, String> {
     @Override
     protected void onPostExecute(String saveDisposition) {
         // Get handles for the context and activity.
-        Context context = contextWeakReference.get();
         Activity activity = activityWeakReference.get();
 
         // Abort if the activity is gone.
@@ -295,48 +263,8 @@ public class SaveUrl extends AsyncTask<String, Long, String> {
 
         // Display a save disposition snackbar.
         if (saveDisposition.equals(SUCCESS)) {
-            // Create a file saved snackbar.
-            Snackbar fileSavedSnackbar = Snackbar.make(noSwipeViewPager, activity.getString(R.string.file_saved) + "  " + filePathString, Snackbar.LENGTH_LONG);
-
-            // Add an open action if the file is not an APK on API >= 26 (that scenario requires the REQUEST_INSTALL_PACKAGES permission).
-            if (!(Build.VERSION.SDK_INT >= 26 && filePathString.endsWith(".apk"))) {
-                fileSavedSnackbar.setAction(R.string.open, (View view) -> {
-                    // Get a file for the file path string.
-                    File file = new File(filePathString);
-
-                    // Declare a file URI variable.
-                    Uri fileUri;
-
-                    // Get the URI for the file according to the Android version.
-                    if (Build.VERSION.SDK_INT >= 24) {  // Use a file provider.
-                        fileUri = FileProvider.getUriForFile(context, activity.getString(R.string.file_provider), file);
-                    } else {  // Get the raw file path URI.
-                        fileUri = Uri.fromFile(file);
-                    }
-
-                    // Get a handle for the content resolver.
-                    ContentResolver contentResolver = context.getContentResolver();
-
-                    // Create an open intent with `ACTION_VIEW`.
-                    Intent openIntent = new Intent(Intent.ACTION_VIEW);
-
-                    // Set the URI and the MIME type.
-                    if (filePathString.endsWith("apk") || filePathString.endsWith("APK")) {  // Force detection of APKs.
-                        openIntent.setDataAndType(fileUri, MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"));
-                    } else {  // Autodetect the MIME type.
-                        openIntent.setDataAndType(fileUri, contentResolver.getType(fileUri));
-                    }
-
-                    // Allow the app to read the file URI.
-                    openIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
-
-                    // Show the chooser.
-                    activity.startActivity(Intent.createChooser(openIntent, context.getString(R.string.open)));
-                });
-            }
-
-            // Show the file saved snackbar.
-            fileSavedSnackbar.show();
+            // Display the file saved snackbar.
+            Snackbar.make(noSwipeViewPager, activity.getString(R.string.file_saved) + "  " + filePathString, Snackbar.LENGTH_LONG).show();
         } else {
             // Display the file saving error.
             Snackbar.make(noSwipeViewPager, activity.getString(R.string.error_saving_file) + "  " + saveDisposition, Snackbar.LENGTH_INDEFINITE).show();
index 65463f35ed1b4521d67ff76d14ff13067a985a2b..27acbc5c6bcf015ebc447d6393d967c2cb7ba7e0 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2019-2020 Soren Stoutner <soren@stoutner.com>.
+ * Copyright © 2019-2021 Soren Stoutner <soren@stoutner.com>.
  *
  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
  *
 package com.stoutner.privacybrowser.asynctasks;
 
 import android.app.Activity;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.net.Uri;
 import android.os.AsyncTask;
-import android.os.Build;
-import android.view.View;
-
-import androidx.core.content.FileProvider;
 
 import com.google.android.material.snackbar.Snackbar;
 
@@ -38,15 +31,13 @@ import com.stoutner.privacybrowser.R;
 import com.stoutner.privacybrowser.views.NestedScrollWebView;
 
 import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileOutputStream;
+import java.io.OutputStream;
 import java.lang.ref.WeakReference;
 
 public class SaveWebpageImage extends AsyncTask<Void, Void, String> {
     // Declare the weak references.
-    private WeakReference<Context> contextWeakReference;
-    private WeakReference<Activity> activityWeakReference;
-    private WeakReference<NestedScrollWebView> nestedScrollWebViewWeakReference;
+    private final WeakReference<Activity> activityWeakReference;
+    private final WeakReference<NestedScrollWebView> nestedScrollWebViewWeakReference;
 
     // Declare the class constants.
     private final String SUCCESS = "Success";
@@ -54,12 +45,11 @@ public class SaveWebpageImage extends AsyncTask<Void, Void, String> {
     // Declare the class variables.
     private Snackbar savingImageSnackbar;
     private Bitmap webpageBitmap;
-    private String filePathString;
+    private final String filePathString;
 
     // The public constructor.
-    public SaveWebpageImage(Context context, Activity activity, String filePathString, NestedScrollWebView nestedScrollWebView) {
+    public SaveWebpageImage(Activity activity, String filePathString, NestedScrollWebView nestedScrollWebView) {
         // Populate the weak references.
-        contextWeakReference = new WeakReference<>(context);
         activityWeakReference = new WeakReference<>(activity);
         nestedScrollWebViewWeakReference = new WeakReference<>(nestedScrollWebView);
 
@@ -111,33 +101,15 @@ public class SaveWebpageImage extends AsyncTask<Void, Void, String> {
         // Convert the bitmap to a PNG.  `0` is for lossless compression (the only option for a PNG).  This compression takes a long time.  Once the minimum API >= 30 this could be replaced with WEBP_LOSSLESS.
         webpageBitmap.compress(Bitmap.CompressFormat.PNG, 0, webpageByteArrayOutputStream);
 
-        // Get a file for the image.
-        File imageFile = new File(filePathString);
-
-        // Delete the current file if it exists.
-        if (imageFile.exists()) {
-            //noinspection ResultOfMethodCallIgnored
-            imageFile.delete();
-        }
-
         // Create a file creation disposition string.
         String fileCreationDisposition = SUCCESS;
 
         try {
             // Create an image file output stream.
-            FileOutputStream imageFileOutputStream = new FileOutputStream(imageFile);
+            OutputStream imageFileOutputStream = activity.getContentResolver().openOutputStream(Uri.parse(filePathString));
 
             // Write the webpage image to the image file.
             webpageByteArrayOutputStream.writeTo(imageFileOutputStream);
-
-            // Create a media scanner intent, which adds items like pictures to Android's recent file list.
-            Intent mediaScannerIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
-
-            // Add the URI to the media scanner intent.
-            mediaScannerIntent.setData(Uri.fromFile(imageFile));
-
-            // Make it so.
-            activity.sendBroadcast(mediaScannerIntent);
         } catch (Exception exception) {
             // Store the error in the file creation disposition string.
             fileCreationDisposition = exception.toString();
@@ -151,7 +123,6 @@ public class SaveWebpageImage extends AsyncTask<Void, Void, String> {
     @Override
     protected void onPostExecute(String fileCreationDisposition) {
         // Get handles for the weak references.
-        Context context = contextWeakReference.get();
         Activity activity = activityWeakReference.get();
         NestedScrollWebView nestedScrollWebView = nestedScrollWebViewWeakReference.get();
 
@@ -165,42 +136,8 @@ public class SaveWebpageImage extends AsyncTask<Void, Void, String> {
 
         // Display a file creation disposition snackbar.
         if (fileCreationDisposition.equals(SUCCESS)) {
-            // Create a file saved snackbar.
-            Snackbar imageSavedSnackbar = Snackbar.make(nestedScrollWebView, activity.getString(R.string.file_saved) + "  " + filePathString, Snackbar.LENGTH_SHORT);
-
-            // Add an open action.
-            imageSavedSnackbar.setAction(R.string.open, (View view) -> {
-                // Get a file for the file path string.
-                File file = new File(filePathString);
-
-                // Declare a file URI variable.
-                Uri fileUri;
-
-                // Get the URI for the file according to the Android version.
-                if (Build.VERSION.SDK_INT >= 24) {  // Use a file provider.
-                    fileUri = FileProvider.getUriForFile(context, activity.getString(R.string.file_provider), file);
-                } else {  // Get the raw file path URI.
-                    fileUri = Uri.fromFile(file);
-                }
-
-                // Get a handle for the content resolver.
-                ContentResolver contentResolver = context.getContentResolver();
-
-                // Create an open intent with `ACTION_VIEW`.
-                Intent openIntent = new Intent(Intent.ACTION_VIEW);
-
-                // Autodetect the MIME type.
-                openIntent.setDataAndType(fileUri, contentResolver.getType(fileUri));
-
-                // Allow the app to read the file URI.
-                openIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
-
-                // Show the chooser.
-                activity.startActivity(Intent.createChooser(openIntent, context.getString(R.string.open)));
-            });
-
-            // Show the image saved snackbar.
-            imageSavedSnackbar.show();
+            // Display the file saved snackbar.
+            Snackbar.make(nestedScrollWebView, activity.getString(R.string.file_saved) + "  " + filePathString, Snackbar.LENGTH_SHORT).show();
         } else {
             // Display the file saving error.
             Snackbar.make(nestedScrollWebView, activity.getString(R.string.error_saving_file) + "  " + fileCreationDisposition, Snackbar.LENGTH_INDEFINITE).show();
index 123c6d30017641c44bdffa8e8f9fbc373c82bd06..5bdf65acba0caa4ad4205f459fd667d727a735c1 100644 (file)
 
 package com.stoutner.privacybrowser.dialogs
 
-import android.Manifest
 import android.annotation.SuppressLint
 import android.app.Dialog
 import android.content.Context
 import android.content.DialogInterface
 import android.content.Intent
-import android.content.pm.PackageManager
 import android.content.res.Configuration
 import android.os.Bundle
 import android.text.Editable
 import android.text.TextWatcher
-import android.view.View
 import android.view.WindowManager
 import android.widget.Button
 import android.widget.EditText
-import android.widget.TextView
 
 import androidx.appcompat.app.AlertDialog
-import androidx.core.content.ContextCompat
 import androidx.fragment.app.DialogFragment
 import androidx.preference.PreferenceManager
 
 import com.stoutner.privacybrowser.R
 import com.stoutner.privacybrowser.activities.MainWebViewActivity
-import com.stoutner.privacybrowser.helpers.DownloadLocationHelper
-
-import java.io.File
 
 class OpenDialog : DialogFragment() {
     // Define the open listener.
@@ -115,10 +107,11 @@ class OpenDialog : DialogFragment() {
         // Get handles for the layout items.
         val fileNameEditText = alertDialog.findViewById<EditText>(R.id.file_name_edittext)!!
         val browseButton = alertDialog.findViewById<Button>(R.id.browse_button)!!
-        val fileDoesNotExistTextView = alertDialog.findViewById<TextView>(R.id.file_does_not_exist_textview)!!
-        val storagePermissionTextView = alertDialog.findViewById<TextView>(R.id.storage_permission_textview)!!
         val openButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
 
+        // Initially disable the open button.
+        openButton.isEnabled = false
+
         // Update the status of the open button when the file name changes.
         fileNameEditText.addTextChangedListener(object : TextWatcher {
             override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
@@ -133,48 +126,19 @@ class OpenDialog : DialogFragment() {
                 // Get the current file name.
                 val fileNameString = fileNameEditText.text.toString()
 
-                // Convert the file name string to a file.
-                val file = File(fileNameString)
-
-                // Check to see if the file exists.
-                if (file.exists()) {  // The file exists.
-                    // Hide the notification that the file does not exist.
-                    fileDoesNotExistTextView.visibility = View.GONE
-
-                    // Enable the open button.
-                    openButton.isEnabled = true
-                } else {  // The file does not exist.
-                    // Show the notification that the file does not exist.
-                    fileDoesNotExistTextView.visibility = View.VISIBLE
-
-                    // Disable the open button.
-                    openButton.isEnabled = false
-                }
+                // Enable the open button if the file name is populated.
+                openButton.isEnabled = fileNameString.isNotEmpty()
             }
         })
 
-        // Instantiate the download location helper.
-        val downloadLocationHelper = DownloadLocationHelper()
-
-        // Get the default file path.
-        val defaultFilePath = downloadLocationHelper.getDownloadLocation(context) + "/"
-
-        // Display the default file path.
-        fileNameEditText.setText(defaultFilePath)
-
-        // Move the cursor to the end of the default file path.
-        fileNameEditText.setSelection(defaultFilePath.length)
-
-        // Hide the storage permission text view if the permission has already been granted.
-        if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
-            storagePermissionTextView.visibility = View.GONE
-        }
-
         // Handle clicks on the browse button.
         browseButton.setOnClickListener {
             // Create the file picker intent.
             val browseIntent = Intent(Intent.ACTION_OPEN_DOCUMENT)
 
+            // Only display files that can be opened.
+            browseIntent.addCategory(Intent.CATEGORY_OPENABLE)
+
             // Set the intent MIME type to include all files so that everything is visible.
             browseIntent.type = "*/*"
 
index d643aa7c4a23915efa9a1ae9c9c06f6c342dc32c..35eb2e0db537786fabb4416fdb2d96e9be66f0c1 100644 (file)
 
 package com.stoutner.privacybrowser.dialogs
 
-import android.Manifest
 import android.annotation.SuppressLint
-import android.os.Bundle
 import android.app.Dialog
 import android.content.Context
 import android.content.DialogInterface
 import android.content.Intent
-import android.content.pm.PackageManager
 import android.content.res.Configuration
-import android.view.View
+import android.os.Bundle
+import android.text.Editable
+import android.text.TextWatcher
 import android.view.WindowManager
 import android.widget.Button
 import android.widget.EditText
-import android.widget.TextView
-import android.text.TextWatcher
-import android.text.Editable
-
 import androidx.appcompat.app.AlertDialog
-import androidx.core.content.ContextCompat
 import androidx.fragment.app.DialogFragment
 import androidx.preference.PreferenceManager
-
 import com.stoutner.privacybrowser.R
-import com.stoutner.privacybrowser.helpers.DownloadLocationHelper
-
-import java.io.File
 
 // Declare the class constants.
 private const val SAVE_TYPE = "save_type"
@@ -176,10 +166,11 @@ class SaveDialog : DialogFragment() {
         // Get handles for the layout items.
         val fileNameEditText = alertDialog.findViewById<EditText>(R.id.file_name_edittext)!!
         val browseButton = alertDialog.findViewById<Button>(R.id.browse_button)!!
-        val fileExistsWarningTextView = alertDialog.findViewById<TextView>(R.id.file_exists_warning_textview)!!
-        val storagePermissionTextView = alertDialog.findViewById<TextView>(R.id.storage_permission_textview)!!
         val saveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
 
+        // Initially disable the save button.
+        saveButton.isEnabled = false
+
         // Update the status of the save button when the file name changes.
         fileNameEditText.addTextChangedListener(object : TextWatcher {
             override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
@@ -194,18 +185,6 @@ class SaveDialog : DialogFragment() {
                 // Get the current file name.
                 val fileNameString = fileNameEditText.text.toString()
 
-                // Convert the file name string to a file.
-                val file = File(fileNameString)
-
-                // Check to see if the file exists.
-                if (file.exists()) {
-                    // Show the file exists warning.
-                    fileExistsWarningTextView.visibility = View.VISIBLE
-                } else {
-                    // Hide the file exists warning.
-                    fileExistsWarningTextView.visibility = View.GONE
-                }
-
                 // Enable the save button if the file name is populated.
                 saveButton.isEnabled = fileNameString.isNotEmpty()
             }
@@ -218,20 +197,6 @@ class SaveDialog : DialogFragment() {
             SAVE_ABOUT_VERSION_IMAGE -> fileName = getString(R.string.privacy_browser_version_png)
         }
 
-        // Instantiate the download location helper.
-        val downloadLocationHelper = DownloadLocationHelper()
-
-        // Get the default file path.
-        val defaultFilePath = downloadLocationHelper.getDownloadLocation(context) + "/" + fileName
-
-        // Display the default file path.
-        fileNameEditText.setText(defaultFilePath)
-
-        // Hide the storage permission text view if the permission has already been granted.
-        if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
-            storagePermissionTextView.visibility = View.GONE
-        }
-
         // Handle clicks on the browse button.
         browseButton.setOnClickListener {
             // Create the file picker intent.
@@ -246,7 +211,7 @@ class SaveDialog : DialogFragment() {
             // Request a file that can be opened.
             browseIntent.addCategory(Intent.CATEGORY_OPENABLE)
 
-            // Launch the file picker.  There is only one `startActivityForResult()`, so the request code is simply set to 0, but it must be run under `activity` so the request code is correct.
+            // Launch the file picker.  There is only one `startActivityForResult()`, so the request code is simply set to 0, but it must be run under `activity` so the response is processed correctly.
             requireActivity().startActivityForResult(browseIntent, 0)
         }
 
index 57d9baa219777afa489a6991dc5df0a29bf26930..40fdbda6be5ecf81dd13777b33292bd69ebda03f 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2019-2020 Soren Stoutner <soren@stoutner.com>.
+ * Copyright © 2019-2021 Soren Stoutner <soren@stoutner.com>.
  *
  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
  *
@@ -19,7 +19,6 @@
 
 package com.stoutner.privacybrowser.dialogs;
 
-import android.Manifest;
 import android.annotation.SuppressLint;
 import android.app.Activity;
 import android.app.Dialog;
@@ -27,13 +26,9 @@ import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.SharedPreferences;
-import android.content.pm.PackageManager;
 import android.content.res.Configuration;
 import android.os.AsyncTask;
-import android.os.Build;
 import android.os.Bundle;
-import android.os.Environment;
-import android.provider.DocumentsContract;
 import android.text.Editable;
 import android.text.InputType;
 import android.text.TextWatcher;
@@ -45,7 +40,6 @@ import android.widget.TextView;
 
 import androidx.annotation.NonNull;
 import androidx.appcompat.app.AlertDialog;
-import androidx.core.content.ContextCompat;
 import androidx.fragment.app.DialogFragment;
 import androidx.preference.PreferenceManager;
 
@@ -53,11 +47,11 @@ import com.google.android.material.textfield.TextInputLayout;
 import com.stoutner.privacybrowser.R;
 import com.stoutner.privacybrowser.activities.MainWebViewActivity;
 import com.stoutner.privacybrowser.asynctasks.GetUrlSize;
-import com.stoutner.privacybrowser.helpers.DownloadLocationHelper;
-
-import java.io.File;
 
 public class SaveWebpageDialog extends DialogFragment {
+    public static final int SAVE_URL = 0;
+    public static final int SAVE_IMAGE = 1;
+
     // Define the save webpage listener.
     private SaveWebpageListener saveWebpageListener;
 
@@ -67,6 +61,7 @@ public class SaveWebpageDialog extends DialogFragment {
     }
 
     // Define the get URL size AsyncTask.  This allows previous instances of the task to be cancelled if a new one is run.
+    @SuppressWarnings("rawtypes")
     private AsyncTask getUrlSize;
 
     @Override
@@ -113,15 +108,15 @@ public class SaveWebpageDialog extends DialogFragment {
 
         // Get the arguments from the bundle.
         int saveType = arguments.getInt("save_type");
-        String urlString = arguments.getString("url_string");
+        String originalUrlString = arguments.getString("url_string");
         String fileSizeString = arguments.getString("file_size_string");
         String contentDispositionFileNameString = arguments.getString("content_disposition_file_name_string");
         String userAgentString = arguments.getString("user_agent_string");
         boolean cookiesEnabled = arguments.getBoolean("cookies_enabled");
 
-        // Get a handle for the activity and the context.
-        Activity activity = requireActivity();
+        // Get handles for the context and the activity.
         Context context = requireContext();
+        Activity activity = requireActivity();
 
         // Use an alert dialog builder to create the alert dialog.
         AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context, R.style.PrivacyBrowserAlertDialog);
@@ -129,56 +124,36 @@ public class SaveWebpageDialog extends DialogFragment {
         // Get the current theme status.
         int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
 
-        // Set the icon according to the theme.
-        if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) {  // The night theme is enabled.
-            // Set the icon according to the save type.
-            switch (saveType) {
-                case StoragePermissionDialog.SAVE_URL:
-                    dialogBuilder.setIcon(R.drawable.copy_enabled_night);
-                    break;
-
-                case StoragePermissionDialog.SAVE_ARCHIVE:
-                    dialogBuilder.setIcon(R.drawable.dom_storage_cleared_night);
-                    break;
+        // Set the title and icon according to the save type.
+        switch (saveType) {
+            case SAVE_URL:
+                // Set the title.
+                dialogBuilder.setTitle(R.string.save_url);
 
-                case StoragePermissionDialog.SAVE_IMAGE:
-                    dialogBuilder.setIcon(R.drawable.images_enabled_night);
-                    break;
-            }
-        } else {  // The day theme is enabled.
-            // Set the icon according to the save type.
-            switch (saveType) {
-                case StoragePermissionDialog.SAVE_URL:
+                // Set the icon according to the theme.
+                if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
                     dialogBuilder.setIcon(R.drawable.copy_enabled_day);
-                    break;
+                } else {
+                    dialogBuilder.setIcon(R.drawable.copy_enabled_night);
+                }
+                break;
 
-                case StoragePermissionDialog.SAVE_ARCHIVE:
-                    dialogBuilder.setIcon(R.drawable.dom_storage_cleared_day);
-                    break;
+            case SAVE_IMAGE:
+                // Set the title.
+                dialogBuilder.setTitle(R.string.save_image);
 
-                case StoragePermissionDialog.SAVE_IMAGE:
+                // Set the icon according to the theme.
+                if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
                     dialogBuilder.setIcon(R.drawable.images_enabled_day);
-                    break;
-            }
-        }
-
-        // Set the title according to the type.
-        switch (saveType) {
-            case StoragePermissionDialog.SAVE_URL:
-                dialogBuilder.setTitle(R.string.save);
-                break;
-
-            case StoragePermissionDialog.SAVE_ARCHIVE:
-                dialogBuilder.setTitle(R.string.save_archive);
-                break;
+                } else {
 
-            case StoragePermissionDialog.SAVE_IMAGE:
-                dialogBuilder.setTitle(R.string.save_image);
+                    dialogBuilder.setIcon(R.drawable.images_enabled_night);
+                }
                 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_url_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);
@@ -186,7 +161,7 @@ public class SaveWebpageDialog extends DialogFragment {
         // Set the save button listener.
         dialogBuilder.setPositiveButton(R.string.save, (DialogInterface dialog, int which) -> {
             // Return the dialog fragment to the parent activity.
-            saveWebpageListener.onSaveWebpage(saveType, urlString, this);
+            saveWebpageListener.onSaveWebpage(saveType, originalUrlString, this);
         });
 
         // Create an alert dialog from the builder.
@@ -199,7 +174,7 @@ public class SaveWebpageDialog extends DialogFragment {
         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
 
         // Get the screenshot preference.
-        boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
+        boolean allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false);
 
         // Disable screenshots if not allowed.
         if (!allowScreenshots) {
@@ -215,8 +190,6 @@ public class SaveWebpageDialog extends DialogFragment {
         EditText fileNameEditText = alertDialog.findViewById(R.id.file_name_edittext);
         Button browseButton = alertDialog.findViewById(R.id.browse_button);
         TextView fileSizeTextView = alertDialog.findViewById(R.id.file_size_textview);
-        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);
 
         // Remove the incorrect warnings that the views might be null.
@@ -225,21 +198,19 @@ public class SaveWebpageDialog extends DialogFragment {
         assert fileNameEditText != null;
         assert browseButton != null;
         assert fileSizeTextView != null;
-        assert fileExistsWarningTextView != null;
-        assert storagePermissionTextView != null;
 
         // Set the file size text view.
         fileSizeTextView.setText(fileSizeString);
 
         // Modify the layout based on the save type.
-        if (saveType == StoragePermissionDialog.SAVE_URL) {  // A URL is being saved.
+        if (saveType == SAVE_URL) {  // A URL is being saved.
             // Remove the incorrect lint error below that the URL string might be null.
-            assert urlString != null;
+            assert originalUrlString != null;
 
             // Populate the URL edit text according to the type.  This must be done before the text change listener is created below so that the file size isn't requested again.
-            if (urlString.startsWith("data:")) {  // The URL contains the entire data of an image.
+            if (originalUrlString.startsWith("data:")) {  // The URL contains the entire data of an image.
                 // Get a substring of the data URL with the first 100 characters.  Otherwise, the user interface will freeze while trying to layout the edit text.
-                String urlSubstring = urlString.substring(0, 100) + "…";
+                String urlSubstring = originalUrlString.substring(0, 100) + "…";
 
                 // Populate the URL edit text with the truncated URL.
                 urlEditText.setText(urlSubstring);
@@ -248,7 +219,7 @@ public class SaveWebpageDialog extends DialogFragment {
                 urlEditText.setInputType(InputType.TYPE_NULL);
             } else {  // The URL contains a reference to the location of the data.
                 // Populate the URL edit text with the full URL.
-                urlEditText.setText(urlString);
+                urlEditText.setText(originalUrlString);
             }
 
             // Update the file size and the status of the save button when the URL changes.
@@ -289,6 +260,9 @@ public class SaveWebpageDialog extends DialogFragment {
             fileSizeTextView.setVisibility(View.GONE);
         }
 
+        // Initially disable the save button.
+        saveButton.setEnabled(false);
+
         // Update the status of the save button when the file name changes.
         fileNameEditText.addTextChangedListener(new TextWatcher() {
             @Override
@@ -306,20 +280,8 @@ public class SaveWebpageDialog extends DialogFragment {
                 // Get the current file name.
                 String fileNameString = fileNameEditText.getText().toString();
 
-                // Convert the file name string to a file.
-                File file = new File(fileNameString);
-
-                // Check to see if the file exists.
-                if (file.exists()) {
-                    // Show the file exists warning.
-                    fileExistsWarningTextView.setVisibility(View.VISIBLE);
-                } else {
-                    // Hide the file exists warning.
-                    fileExistsWarningTextView.setVisibility(View.GONE);
-                }
-
                 // Enable the save button based on the save type.
-                if (saveType == StoragePermissionDialog.SAVE_URL) {  // A URL is being saved.
+                if (saveType == SAVE_URL) {  // A URL is being saved.
                     // Enable the save button if the file name and the URL is populated.
                     saveButton.setEnabled(!fileNameString.isEmpty() && !urlEditText.getText().toString().isEmpty());
                 } else {  // An archive or an image is being saved.
@@ -334,17 +296,12 @@ public class SaveWebpageDialog extends DialogFragment {
 
         // Set the file name according to the type.
         switch (saveType) {
-            case StoragePermissionDialog.SAVE_URL:
+            case SAVE_URL:
                 // Use the file name from the content disposition.
                 fileName = contentDispositionFileNameString;
                 break;
 
-            case StoragePermissionDialog.SAVE_ARCHIVE:
-                // Use an archive name ending in `.mht`.
-                fileName = getString(R.string.webpage_mht);
-                break;
-
-            case StoragePermissionDialog.SAVE_IMAGE:
+            case SAVE_IMAGE:
                 // Use a file name ending in `.png`.
                 fileName = getString(R.string.webpage_png);
                 break;
@@ -353,18 +310,6 @@ public class SaveWebpageDialog extends DialogFragment {
         // Save the file name as the default file name.  This must be final to be used in the lambda below.
         final String defaultFileName = fileName;
 
-        // Instantiate the download location helper.
-        DownloadLocationHelper downloadLocationHelper = new DownloadLocationHelper();
-
-        // Get the default file path.
-        String defaultFilePath = downloadLocationHelper.getDownloadLocation(context) + "/" + defaultFileName;
-
-        // Populate the file name edit text.
-        fileNameEditText.setText(defaultFilePath);
-
-        // Move the cursor to the end of the default file path.
-        fileNameEditText.setSelection(defaultFilePath.length());
-
         // Handle clicks on the browse button.
         browseButton.setOnClickListener((View view) -> {
             // Create the file picker intent.
@@ -376,11 +321,6 @@ public class SaveWebpageDialog extends DialogFragment {
             // Set the initial file name according to the type.
             browseIntent.putExtra(Intent.EXTRA_TITLE, defaultFileName);
 
-            // Set the initial directory if the minimum API >= 26.
-            if (Build.VERSION.SDK_INT >= 26) {
-                browseIntent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Environment.getExternalStorageDirectory());
-            }
-
             // Request a file that can be opened.
             browseIntent.addCategory(Intent.CATEGORY_OPENABLE);
 
@@ -388,11 +328,6 @@ public class SaveWebpageDialog extends DialogFragment {
             activity.startActivityForResult(browseIntent, MainWebViewActivity.BROWSE_SAVE_WEBPAGE_REQUEST_CODE);
         });
 
-        // Hide the storage permission text view if the permission has already been granted.
-        if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
-            storagePermissionTextView.setVisibility(View.GONE);
-        }
-
         // Return the alert dialog.
         return alertDialog;
     }
diff --git a/app/src/main/java/com/stoutner/privacybrowser/dialogs/StoragePermissionDialog.java b/app/src/main/java/com/stoutner/privacybrowser/dialogs/StoragePermissionDialog.java
deleted file mode 100644 (file)
index e1d0c40..0000000
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * Copyright © 2018-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.dialogs;
-
-import android.app.Dialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.SharedPreferences;
-import android.content.res.Configuration;
-import android.os.Bundle;
-import android.preference.PreferenceManager;
-import android.view.WindowManager;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-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_URL = 1;
-    public static final int SAVE_ARCHIVE = 2;
-    public static final int SAVE_TEXT = 3;
-    public static final int SAVE_IMAGE = 4;
-
-    // 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 requestType);
-    }
-
-    @Override
-    public void onAttach(@NonNull Context context) {
-        // Run the default commands.
-        super.onAttach(context);
-
-        // Get a handle for the listener from the launching context.
-        storagePermissionDialogListener = (StoragePermissionDialogListener) context;
-    }
-
-    public static StoragePermissionDialog displayDialog(int requestType) {
-        // Create an arguments bundle.
-        Bundle argumentsBundle = new Bundle();
-
-        // Store the save type in the bundle.
-        argumentsBundle.putInt("request_type", requestType);
-
-        // Create a new instance of the storage permission dialog.
-        StoragePermissionDialog storagePermissionDialog = new StoragePermissionDialog();
-
-        // Add the arguments bundle to the new dialog.
-        storagePermissionDialog.setArguments(argumentsBundle);
-
-        // Return the new dialog.
-        return storagePermissionDialog;
-    }
-
-    @NonNull
-    @Override
-    public Dialog onCreateDialog(Bundle savedInstanceState) {
-        // Get a handle for the arguments.
-        Bundle arguments = getArguments();
-
-        // Remove the incorrect lint warning that the arguments might be null.
-        assert arguments != null;
-
-        // Get the save type.
-        int requestType = arguments.getInt("request_type");
-
-        // Use a builder to create the alert dialog.
-        AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog);
-
-        // Get the current theme status.
-        int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
-
-        // Set the icon according to the theme.
-        if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) {
-            dialogBuilder.setIcon(R.drawable.import_export_night);
-        } else {
-            dialogBuilder.setIcon(R.drawable.import_export_day);
-        }
-
-        // Set the title.
-        dialogBuilder.setTitle(R.string.storage_permission);
-
-        // Set the text.
-        dialogBuilder.setMessage(R.string.storage_permission_message);
-
-        // 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(requestType);
-        });
-
-        // Create an alert dialog from the builder.
-        final AlertDialog alertDialog = dialogBuilder.create();
-
-        // Get a handle for the shared preferences.
-        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext());
-
-        // Get the screenshot preference.
-        boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
-
-        // Disable screenshots if not allowed.
-        if (!allowScreenshots) {
-            // Remove the warning below that `getWindow()` might be null.
-            assert alertDialog.getWindow() != null;
-
-            // Disable screenshots.
-            alertDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
-        }
-
-        // Return the alert dialog.
-        return alertDialog;
-    }
-}
\ No newline at end of file
index b7a829da4b1bf1642316bd002ae3259309c68ed7..df93fa106cbad10807bbf2c9e99d491cbfcbaa38 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2016-2020 Soren Stoutner <soren@stoutner.com>.
+ * Copyright © 2016-2021 Soren Stoutner <soren@stoutner.com>.
  *
  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
  *
@@ -29,6 +29,7 @@ import android.content.res.Resources;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.Looper;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.webkit.WebView;
@@ -41,7 +42,6 @@ import androidx.preference.PreferenceFragmentCompat;
 
 import com.stoutner.privacybrowser.R;
 import com.stoutner.privacybrowser.activities.MainWebViewActivity;
-import com.stoutner.privacybrowser.helpers.DownloadLocationHelper;
 import com.stoutner.privacybrowser.helpers.ProxyHelper;
 
 public class SettingsFragment extends PreferenceFragmentCompat {
@@ -105,8 +105,6 @@ public class SettingsFragment extends PreferenceFragmentCompat {
         Preference clearLogcatPreference = findPreference(getString(R.string.clear_logcat_key));
         Preference clearCachePreference = findPreference("clear_cache");
         Preference homepagePreference = findPreference("homepage");
-        Preference downloadLocationPreference = findPreference("download_location");
-        Preference downloadCustomLocationPreference = findPreference("download_custom_location");
         Preference fontSizePreference = findPreference("font_size");
         Preference openIntentsInNewTabPreference = findPreference("open_intents_in_new_tab");
         Preference swipeToRefreshPreference = findPreference("swipe_to_refresh");
@@ -151,8 +149,6 @@ public class SettingsFragment extends PreferenceFragmentCompat {
         assert clearLogcatPreference != null;
         assert clearCachePreference != null;
         assert homepagePreference != null;
-        assert downloadLocationPreference != null;
-        assert downloadCustomLocationPreference != null;
         assert fontSizePreference != null;
         assert openIntentsInNewTabPreference != null;
         assert swipeToRefreshPreference != null;
@@ -171,7 +167,6 @@ public class SettingsFragment extends PreferenceFragmentCompat {
         String userAgentName = savedPreferences.getString("user_agent", getString(R.string.user_agent_default_value));
         String searchString = savedPreferences.getString("search", getString(R.string.search_default_value));
         String proxyString = savedPreferences.getString("proxy", getString(R.string.proxy_default_value));
-        String downloadLocationString = savedPreferences.getString("download_location", getString(R.string.download_location_default_value));
 
         // Get booleans that are used in multiple places from the preferences.
         boolean javaScriptEnabled = savedPreferences.getBoolean("javascript", false);
@@ -307,29 +302,6 @@ public class SettingsFragment extends PreferenceFragmentCompat {
         homepagePreference.setSummary(savedPreferences.getString("homepage", getString(R.string.homepage_default_value)));
 
 
-        // Get the download location string arrays.
-        String[] downloadLocationEntriesStringArray = resources.getStringArray(R.array.download_location_entries);
-        String[] downloadLocationEntryValuesStringArray = resources.getStringArray(R.array.download_location_entry_values);
-
-        // Instantiate the download location helper.
-        DownloadLocationHelper downloadLocationHelper = new DownloadLocationHelper();
-
-        // Check to see if a download custom location is selected.
-        if (downloadLocationString.equals(downloadLocationEntryValuesStringArray[3])) {  // A download custom location is selected.
-            // Set the download location summary text to be `Custom`.
-            downloadLocationPreference.setSummary(downloadLocationEntriesStringArray[3]);
-        } else {  // A custom download location is not selected.
-            // Set the download location summary text to be the download location.
-            downloadLocationPreference.setSummary(downloadLocationHelper.getDownloadLocation(context));
-
-            // Disable the download custom location preference.
-            downloadCustomLocationPreference.setEnabled(false);
-        }
-
-        // Set the summary text for the download custom location (the default is `"`).
-        downloadCustomLocationPreference.setSummary(savedPreferences.getString("download_custom_location", getString(R.string.download_custom_location_default_value)));
-
-
         // Set the font size as the summary text for the preference.
         fontSizePreference.setSummary(savedPreferences.getString("font_size", getString(R.string.font_size_default_value)) + "%");
 
@@ -844,21 +816,6 @@ public class SettingsFragment extends PreferenceFragmentCompat {
             clearCachePreference.setIcon(R.drawable.cache_warning);
         }
 
-        // Set the download custom location icon.
-        if (downloadCustomLocationPreference.isEnabled()) {
-            if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) {
-                downloadCustomLocationPreference.setIcon(R.drawable.downloads_enabled_night);
-            } else {
-                downloadCustomLocationPreference.setIcon(R.drawable.downloads_enabled_day);
-            }
-        } else {
-            if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) {
-                downloadCustomLocationPreference.setIcon(R.drawable.downloads_ghosted_night);
-            } else {
-                downloadCustomLocationPreference.setIcon(R.drawable.downloads_ghosted_day);
-            }
-        }
-
         // Set the open intents in new tab preference icon.
         if (savedPreferences.getBoolean("open_intents_in_new_tab", true)) {
             if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) {
@@ -1225,7 +1182,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
                     allowScreenshotsRestartIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
 
                     // Create a handler to restart the activity.
-                    Handler allowScreenshotsRestartHandler = new Handler();
+                    Handler allowScreenshotsRestartHandler = new Handler(Looper.getMainLooper());
 
                     // Create a runnable to restart the activity.
                     Runnable allowScreenshotsRestartRunnable = () -> {
@@ -1761,46 +1718,6 @@ public class SettingsFragment extends PreferenceFragmentCompat {
                     homepagePreference.setSummary(sharedPreferences.getString("homepage", getString(R.string.homepage_default_value)));
                     break;
 
-                case "download_location":
-                    // Get the new download location.
-                    String newDownloadLocationString = sharedPreferences.getString("download_location", getString(R.string.download_location_default_value));
-
-                    // Check to see if a download custom location is selected.
-                    if (newDownloadLocationString.equals(downloadLocationEntryValuesStringArray[3])) {  // A download custom location is selected.
-                        // Set the download location summary text to be `Custom`.
-                        downloadLocationPreference.setSummary(downloadLocationEntriesStringArray[3]);
-
-                        // Enable the download custom location preference.
-                        downloadCustomLocationPreference.setEnabled(true);
-                    } else {  // A download custom location is not selected.
-                        // Set the download location summary text to be the download location.
-                        downloadLocationPreference.setSummary(downloadLocationHelper.getDownloadLocation(context));
-
-                        // Disable the download custom location.
-                        downloadCustomLocationPreference.setEnabled(newDownloadLocationString.equals(downloadLocationEntryValuesStringArray[3]));
-                    }
-
-                    // Update the download custom location icon.
-                    if (downloadCustomLocationPreference.isEnabled()) {
-                        if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) {
-                            downloadCustomLocationPreference.setIcon(R.drawable.downloads_enabled_night);
-                        } else {
-                            downloadCustomLocationPreference.setIcon(R.drawable.downloads_enabled_day);
-                        }
-                    } else {
-                        if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) {
-                            downloadCustomLocationPreference.setIcon(R.drawable.downloads_ghosted_night);
-                        } else {
-                            downloadCustomLocationPreference.setIcon(R.drawable.downloads_ghosted_day);
-                        }
-                    }
-                    break;
-
-                case "download_custom_location":
-                    // Set the new download custom location as the summary text for the preference.
-                    downloadCustomLocationPreference.setSummary(sharedPreferences.getString("download_custom_location", getString(R.string.download_custom_location_default_value)));
-                    break;
-
                 case "font_size":
                     // Update the font size summary text.
                     fontSizePreference.setSummary(sharedPreferences.getString("font_size", getString(R.string.font_size_default_value)) + "%");
index 25d84144b836306294ce5c8e3404d601ce4d66f9..5f1ab22f65c38a0901cc013a9dead62d34f3d13e 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2018-2019 Soren Stoutner <soren@stoutner.com>.
+ * Copyright © 2018-2019,2021 Soren Stoutner <soren@stoutner.com>.
  *
  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
  *
@@ -19,6 +19,7 @@
 
 package com.stoutner.privacybrowser.helpers;
 
+import android.app.Activity;
 import android.net.http.SslCertificate;
 
 import androidx.fragment.app.DialogFragment;
@@ -31,7 +32,7 @@ import java.util.ArrayList;
 import java.util.Date;
 
 public class CheckPinnedMismatchHelper {
-    public static void checkPinnedMismatch(FragmentManager fragmentManager, NestedScrollWebView nestedScrollWebView) {
+    public static void checkPinnedMismatch(Activity activity, FragmentManager fragmentManager, NestedScrollWebView nestedScrollWebView) {
         // Initialize the current SSL certificate variables.
         String currentWebsiteIssuedToCName = "";
         String currentWebsiteIssuedToOName = "";
@@ -120,6 +121,17 @@ public class CheckPinnedMismatchHelper {
                 !currentWebsiteIssuedByUName.equals(pinnedSslIssuedByUName) || !currentWebsiteSslStartDateString.equals(pinnedSslStartDateString) ||
                 !currentWebsiteSslEndDateString.equals(pinnedSslEndDateString)))) {
 
+            // Prevent the dialog from displaying if the app window is not visible.
+            // The check pinned mismatch helper continues to function even when the app is paused.  Attempting to display a dialog in that state leads to a crash.
+            while (!activity.getWindow().isActive()) {
+                try {
+                    // The window is not active.  Wait 1 second.
+                    activity.wait(1000);
+                } catch (InterruptedException e) {
+                    // Do nothing.
+                }
+            }
+
             // Get a handle for the pinned mismatch alert dialog.
             DialogFragment pinnedMismatchDialogFragment = PinnedMismatchDialog.displayDialog(nestedScrollWebView.getWebViewFragmentId());
 
diff --git a/app/src/main/java/com/stoutner/privacybrowser/helpers/DownloadLocationHelper.java b/app/src/main/java/com/stoutner/privacybrowser/helpers/DownloadLocationHelper.java
deleted file mode 100644 (file)
index f23aaea..0000000
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * 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.helpers;
-
-import android.Manifest;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.content.pm.PackageManager;
-import android.os.Environment;
-
-import androidx.core.content.ContextCompat;
-import androidx.preference.PreferenceManager;
-
-import com.stoutner.privacybrowser.R;
-
-import java.io.File;
-
-public class DownloadLocationHelper {
-    public String getDownloadLocation(Context context) {
-        // Get the download location entry values string array.
-        String[] downloadLocationEntryValuesStringArray = context.getResources().getStringArray(R.array.download_location_entry_values);
-
-        // Get the two standard download directories.
-        File publicDownloadDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
-        File publicAppFilesDirectory = context.getExternalFilesDir(null);
-
-        // Remove the incorrect lint warning below that the public app files directory might be null.
-        assert publicAppFilesDirectory != null;
-
-        // Convert the download directories to strings.
-        String publicDownloadDirectoryString = publicDownloadDirectory.toString();
-        String publicAppFilesDirectoryString = publicAppFilesDirectory.toString();
-
-        // Get a handle for the shared preferences.
-        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
-
-        // Get the download location strings from the preferences.
-        String downloadLocationString = sharedPreferences.getString("download_location", context.getString(R.string.download_location_default_value));
-        String downloadCustomLocationString = sharedPreferences.getString("download_custom_location", context.getString(R.string.download_custom_location_default_value));
-
-        // Define a string for the default file path.
-        String defaultFilePath;
-
-        // Set the default file path according to the download location.
-        if (downloadLocationString.equals(downloadLocationEntryValuesStringArray[0])) {  // the download location is set to auto.
-            // Set the download location summary text according to the storage permission status.
-            if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {  // The storage permission has been granted.
-                // Use the public download directory.
-                defaultFilePath = publicDownloadDirectoryString;
-            } else {  // The storage permission has not been granted.
-                // Use the public app files directory.
-                defaultFilePath = publicAppFilesDirectoryString;
-            }
-        } else if (downloadLocationString.equals(downloadLocationEntryValuesStringArray[1])) {  // The download location is set to the app directory.
-            // Use the public app files directory.
-            defaultFilePath = publicAppFilesDirectoryString;
-        } else if (downloadLocationString.equals(downloadLocationEntryValuesStringArray[2])) {  // The download location is set to the public directory.
-            // Use the public download directory.
-            defaultFilePath = publicDownloadDirectoryString;
-        } else {  // The download location is set to custom.
-            // Use the download custom location.
-            defaultFilePath = downloadCustomLocationString;
-        }
-
-        // Return the default file path.
-        return defaultFilePath;
-    }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/stoutner/privacybrowser/helpers/FileNameHelper.java b/app/src/main/java/com/stoutner/privacybrowser/helpers/FileNameHelper.java
deleted file mode 100644 (file)
index 980a838..0000000
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * 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/>.
- */
-
-package com.stoutner.privacybrowser.helpers;
-
-import android.net.Uri;
-import android.os.Environment;
-
-public class FileNameHelper {
-    public String convertUriToFileNamePath(Uri Uri) {
-        // Initialize a file name path string.
-        String fileNamePath = "";
-
-        // Convert the URI to a raw file name path.
-        String rawFileNamePath = Uri.getPath();
-
-        // Only process the raw file name path if it is not null.
-        if (rawFileNamePath != null) {
-            // Check to see if the file name Path includes a valid storage location.
-            if (rawFileNamePath.contains(":")) {  // The path contains a `:`.
-                // Split the path into the initial content uri and the final path information.
-                String fileNameContentPath = rawFileNamePath.substring(0, rawFileNamePath.indexOf(":"));
-                String fileNameFinalPath = rawFileNamePath.substring(rawFileNamePath.indexOf(":") + 1);
-
-                // Check to see if the current file name final patch is a complete, valid path.
-                if (fileNameFinalPath.startsWith("/storage/emulated/")) {  // The existing file name final path is a complete, valid path.
-                    // Use the provided file name path as is.
-                    fileNamePath = fileNameFinalPath;
-                } else { // The existing file name final path is not a complete, valid path.
-                    // Construct the file name path.
-                    switch (fileNameContentPath) {
-                        // The documents home has a special content path.
-                        case "/document/home":
-                            fileNamePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) + "/" + fileNameFinalPath;
-                            break;
-
-                        // Everything else for the primary user should be in `/document/primary`.
-                        case "/document/primary":
-                            fileNamePath = Environment.getExternalStorageDirectory() + "/" + fileNameFinalPath;
-                            break;
-
-                        // Just in case, catch everything else and place it in the external storage directory.
-                        default:
-                            fileNamePath = Environment.getExternalStorageDirectory() + "/" + fileNameFinalPath;
-                            break;
-                    }
-                }
-            } else {  // The path does not contain a `:`.
-                // Use the raw file name path.
-                fileNamePath = rawFileNamePath;
-            }
-        }
-
-        // Return the file name path string.
-        return fileNamePath;
-    }
-}
\ No newline at end of file
index c5bc00aceff6327fe14664c1557a7cf27b2ea0bc..f3a4a8db969315af11156c8edff844ff79ea0759 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2018-2020 Soren Stoutner <soren@stoutner.com>.
+ * Copyright © 2018-2021 Soren Stoutner <soren@stoutner.com>.
  *
  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
  *
@@ -41,7 +41,7 @@ public class ImportExportDatabaseHelper {
     public static final String IMPORT_SUCCESSFUL = "Import Successful";
 
     // Declare the class constants.
-    private static final int SCHEMA_VERSION = 12;
+    private static final int SCHEMA_VERSION = 13;
     private static final String PREFERENCES_TABLE = "preferences";
 
     // Declare the preferences constants.
@@ -79,8 +79,6 @@ public class ImportExportDatabaseHelper {
     private static final String CLEAR_LOGCAT = "clear_logcat";
     private static final String CLEAR_CACHE = "clear_cache";
     private static final String HOMEPAGE = "homepage";
-    private static final String DOWNLOAD_LOCATION = "download_location";
-    private static final String DOWNLOAD_CUSTOM_LOCATION = "download_custom_location";
     private static final String FONT_SIZE = "font_size";
     private static final String OPEN_INTENTS_IN_NEW_TAB = "open_intents_in_new_tab";
     private static final String SWIPE_TO_REFRESH = "swipe_to_refresh";
@@ -91,274 +89,28 @@ public class ImportExportDatabaseHelper {
     private static final String WIDE_VIEWPORT = "wide_viewport";
     private static final String DISPLAY_WEBPAGE_IMAGES = "display_webpage_images";
 
-    public String exportUnencrypted(File exportFile, Context context) {
+    public String importUnencrypted(InputStream importFileInputStream, Context context){
         try {
-            // Delete the current file if it exists.
-            if (exportFile.exists()) {
-                //noinspection ResultOfMethodCallIgnored
-                exportFile.delete();
-            }
-
-            // Create the export database.
-            SQLiteDatabase exportDatabase = SQLiteDatabase.openOrCreateDatabase(exportFile, null);
-
-            // Set the export database version number.
-            exportDatabase.setVersion(SCHEMA_VERSION);
-
-            // Create the export database domains table.
-            exportDatabase.execSQL(DomainsDatabaseHelper.CREATE_DOMAINS_TABLE);
-
-
-            // Create the export database bookmarks table.
-            exportDatabase.execSQL(BookmarksDatabaseHelper.CREATE_BOOKMARKS_TABLE);
-
-            // Open the bookmarks database.  The `0` specifies the database version, but that is ignored and set instead using a constant in `BookmarksDatabaseHelper`.
-            BookmarksDatabaseHelper bookmarksDatabaseHelper = new BookmarksDatabaseHelper(context, null, null, 0);
-
-            // Get a full bookmarks cursor.
-            Cursor bookmarksCursor = bookmarksDatabaseHelper.getAllBookmarks();
-
-            // Move to the first bookmark.
-            bookmarksCursor.moveToFirst();
-
-            // Copy the data from the bookmarks cursor into the export database.
-            for (int i = 0; i < bookmarksCursor.getCount(); i++) {
-                // Extract the record from the cursor and store the data in a ContentValues.
-                ContentValues bookmarksContentValues = new ContentValues();
-                bookmarksContentValues.put(BookmarksDatabaseHelper.BOOKMARK_NAME, bookmarksCursor.getString(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME)));
-                bookmarksContentValues.put(BookmarksDatabaseHelper.BOOKMARK_URL, bookmarksCursor.getString(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_URL)));
-                bookmarksContentValues.put(BookmarksDatabaseHelper.PARENT_FOLDER, bookmarksCursor.getString(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.PARENT_FOLDER)));
-                bookmarksContentValues.put(BookmarksDatabaseHelper.DISPLAY_ORDER, bookmarksCursor.getInt(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.DISPLAY_ORDER)));
-                bookmarksContentValues.put(BookmarksDatabaseHelper.IS_FOLDER, bookmarksCursor.getInt(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)));
-                bookmarksContentValues.put(BookmarksDatabaseHelper.FAVORITE_ICON, bookmarksCursor.getBlob(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON)));
-
-                // Insert the record into the export database.
-                exportDatabase.insert(BookmarksDatabaseHelper.BOOKMARKS_TABLE, null, bookmarksContentValues);
-
-                // Advance to the next record.
-                bookmarksCursor.moveToNext();
-            }
-
-            // Close the bookmarks database.
-            bookmarksCursor.close();
-            bookmarksDatabaseHelper.close();
-
-
-            // Open the domains database.  The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
-            DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(context, null, null, 0);
-
-            // Get a full domains database cursor.
-            Cursor domainsCursor = domainsDatabaseHelper.getCompleteCursorOrderedByDomain();
-
-            // Move to the first domain.
-            domainsCursor.moveToFirst();
-
-            // Copy the data from the domains cursor into the export database.
-            for (int i = 0; i < domainsCursor.getCount(); i++) {
-                // Extract the record from the cursor and store the data in a ContentValues.
-                ContentValues domainsContentValues = new ContentValues();
-                domainsContentValues.put(DomainsDatabaseHelper.DOMAIN_NAME, domainsCursor.getString(domainsCursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME)));
-                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_JAVASCRIPT, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)));
-                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_FIRST_PARTY_COOKIES, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FIRST_PARTY_COOKIES)));
-                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_THIRD_PARTY_COOKIES, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_THIRD_PARTY_COOKIES)));
-                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_DOM_STORAGE, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_DOM_STORAGE)));
-                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_FORM_DATA, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FORM_DATA)));
-                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_EASYLIST, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYLIST)));
-                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_EASYPRIVACY, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYPRIVACY)));
-                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST)));
-                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)));
-                domainsContentValues.put(DomainsDatabaseHelper.ULTRALIST, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.ULTRALIST)));
-                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY)));
-                domainsContentValues.put(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS)));
-                domainsContentValues.put(DomainsDatabaseHelper.USER_AGENT, domainsCursor.getString(domainsCursor.getColumnIndex(DomainsDatabaseHelper.USER_AGENT)));
-                domainsContentValues.put(DomainsDatabaseHelper.FONT_SIZE, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.FONT_SIZE)));
-                domainsContentValues.put(DomainsDatabaseHelper.SWIPE_TO_REFRESH, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.SWIPE_TO_REFRESH)));
-                domainsContentValues.put(DomainsDatabaseHelper.WEBVIEW_THEME, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.WEBVIEW_THEME)));
-                domainsContentValues.put(DomainsDatabaseHelper.WIDE_VIEWPORT, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.WIDE_VIEWPORT)));
-                domainsContentValues.put(DomainsDatabaseHelper.DISPLAY_IMAGES, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.DISPLAY_IMAGES)));
-                domainsContentValues.put(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)));
-                domainsContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME, domainsCursor.getString(domainsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME)));
-                domainsContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION, domainsCursor.getString(domainsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION)));
-                domainsContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT, domainsCursor.getString(domainsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT)));
-                domainsContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME, domainsCursor.getString(domainsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME)));
-                domainsContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION, domainsCursor.getString(domainsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION)));
-                domainsContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT, domainsCursor.getString(domainsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT)));
-                domainsContentValues.put(DomainsDatabaseHelper.SSL_START_DATE, domainsCursor.getLong(domainsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)));
-                domainsContentValues.put(DomainsDatabaseHelper.SSL_END_DATE, domainsCursor.getLong(domainsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)));
-                domainsContentValues.put(DomainsDatabaseHelper.PINNED_IP_ADDRESSES, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_IP_ADDRESSES)));
-                domainsContentValues.put(DomainsDatabaseHelper.IP_ADDRESSES, domainsCursor.getString(domainsCursor.getColumnIndex(DomainsDatabaseHelper.IP_ADDRESSES)));
+            // Create a temporary import file.
+            File temporaryImportFile = File.createTempFile("temporary_import_file", null, context.getCacheDir());
 
-                // Insert the record into the export database.
-                exportDatabase.insert(DomainsDatabaseHelper.DOMAINS_TABLE, null, domainsContentValues);
+            // Create a temporary file output stream.
+            FileOutputStream temporaryImportFileOutputStream = new FileOutputStream(temporaryImportFile);
 
-                // Advance to the next record.
-                domainsCursor.moveToNext();
-            }
-
-            // Close the domains database.
-            domainsCursor.close();
-            domainsDatabaseHelper.close();
-
-
-            // Prepare the preferences table SQL creation string.
-            String CREATE_PREFERENCES_TABLE = "CREATE TABLE " + PREFERENCES_TABLE + " (" +
-                    _ID + " INTEGER PRIMARY KEY, " +
-                    JAVASCRIPT + " BOOLEAN, " +
-                    FIRST_PARTY_COOKIES + " BOOLEAN, " +
-                    THIRD_PARTY_COOKIES + " BOOLEAN, " +
-                    DOM_STORAGE + " BOOLEAN, " +
-                    SAVE_FORM_DATA + " BOOLEAN, " +
-                    USER_AGENT + " TEXT, " +
-                    CUSTOM_USER_AGENT + " TEXT, " +
-                    INCOGNITO_MODE + " BOOLEAN, " +
-                    DO_NOT_TRACK + " BOOLEAN, " +
-                    ALLOW_SCREENSHOTS + " BOOLEAN, " +
-                    EASYLIST + " BOOLEAN, " +
-                    EASYPRIVACY + " BOOLEAN, " +
-                    FANBOYS_ANNOYANCE_LIST + " BOOLEAN, " +
-                    FANBOYS_SOCIAL_BLOCKING_LIST + " BOOLEAN, " +
-                    ULTRALIST + " BOOLEAN, " +
-                    ULTRAPRIVACY + " BOOLEAN, " +
-                    BLOCK_ALL_THIRD_PARTY_REQUESTS + " BOOLEAN, " +
-                    GOOGLE_ANALYTICS + " BOOLEAN, " +
-                    FACEBOOK_CLICK_IDS + " BOOLEAN, " +
-                    TWITTER_AMP_REDIRECTS + " BOOLEAN, " +
-                    SEARCH + " TEXT, " +
-                    SEARCH_CUSTOM_URL + " TEXT, " +
-                    PROXY + " TEXT, " +
-                    PROXY_CUSTOM_URL + " TEXT, " +
-                    FULL_SCREEN_BROWSING_MODE + " BOOLEAN, " +
-                    HIDE_APP_BAR + " BOOLEAN, " +
-                    CLEAR_EVERYTHING + " BOOLEAN, " +
-                    CLEAR_COOKIES + " BOOLEAN, " +
-                    CLEAR_DOM_STORAGE + " BOOLEAN, " +
-                    CLEAR_FORM_DATA + " BOOLEAN, " +
-                    CLEAR_LOGCAT + " BOOLEAN, " +
-                    CLEAR_CACHE + " BOOLEAN, " +
-                    HOMEPAGE + " TEXT, " +
-                    DOWNLOAD_LOCATION + " TEXT, " +
-                    DOWNLOAD_CUSTOM_LOCATION + " TEXT, " +
-                    FONT_SIZE + " TEXT, " +
-                    OPEN_INTENTS_IN_NEW_TAB + " BOOLEAN, " +
-                    SWIPE_TO_REFRESH + " BOOLEAN, " +
-                    SCROLL_APP_BAR + " BOOLEAN, " +
-                    DISPLAY_ADDITIONAL_APP_BAR_ICONS + " BOOLEAN, " +
-                    APP_THEME + " TEXT, " +
-                    WEBVIEW_THEME + " TEXT, " +
-                    WIDE_VIEWPORT + " BOOLEAN, " +
-                    DISPLAY_WEBPAGE_IMAGES + " BOOLEAN)";
-
-            // Create the export database preferences table.
-            exportDatabase.execSQL(CREATE_PREFERENCES_TABLE);
-
-            // Get a handle for the shared preference.
-            SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
-
-            // Create a ContentValues with the preferences information.
-            ContentValues preferencesContentValues = new ContentValues();
-            preferencesContentValues.put(JAVASCRIPT, sharedPreferences.getBoolean(JAVASCRIPT, false));
-            preferencesContentValues.put(FIRST_PARTY_COOKIES, sharedPreferences.getBoolean(FIRST_PARTY_COOKIES, false));
-            preferencesContentValues.put(THIRD_PARTY_COOKIES, sharedPreferences.getBoolean(THIRD_PARTY_COOKIES, false));
-            preferencesContentValues.put(DOM_STORAGE, sharedPreferences.getBoolean(DOM_STORAGE, false));
-            preferencesContentValues.put(SAVE_FORM_DATA, sharedPreferences.getBoolean(SAVE_FORM_DATA, false));  // Save form data can be removed once the minimum API >= 26.
-            preferencesContentValues.put(USER_AGENT, sharedPreferences.getString(USER_AGENT, context.getString(R.string.user_agent_default_value)));
-            preferencesContentValues.put(CUSTOM_USER_AGENT, sharedPreferences.getString(CUSTOM_USER_AGENT, context.getString(R.string.custom_user_agent_default_value)));
-            preferencesContentValues.put(INCOGNITO_MODE, sharedPreferences.getBoolean(INCOGNITO_MODE, false));
-            preferencesContentValues.put(DO_NOT_TRACK, sharedPreferences.getBoolean(DO_NOT_TRACK, false));
-            preferencesContentValues.put(ALLOW_SCREENSHOTS, sharedPreferences.getBoolean(ALLOW_SCREENSHOTS, false));
-            preferencesContentValues.put(EASYLIST, sharedPreferences.getBoolean(EASYLIST, true));
-            preferencesContentValues.put(EASYPRIVACY, sharedPreferences.getBoolean(EASYPRIVACY, true));
-            preferencesContentValues.put(FANBOYS_ANNOYANCE_LIST, sharedPreferences.getBoolean(FANBOYS_ANNOYANCE_LIST, true));
-            preferencesContentValues.put(FANBOYS_SOCIAL_BLOCKING_LIST, sharedPreferences.getBoolean(FANBOYS_SOCIAL_BLOCKING_LIST, true));
-            preferencesContentValues.put(ULTRALIST, sharedPreferences.getBoolean(ULTRALIST, true));
-            preferencesContentValues.put(ULTRAPRIVACY, sharedPreferences.getBoolean(ULTRAPRIVACY, true));
-            preferencesContentValues.put(BLOCK_ALL_THIRD_PARTY_REQUESTS, sharedPreferences.getBoolean(BLOCK_ALL_THIRD_PARTY_REQUESTS, false));
-            preferencesContentValues.put(GOOGLE_ANALYTICS, sharedPreferences.getBoolean(GOOGLE_ANALYTICS, true));
-            preferencesContentValues.put(FACEBOOK_CLICK_IDS, sharedPreferences.getBoolean(FACEBOOK_CLICK_IDS, true));
-            preferencesContentValues.put(TWITTER_AMP_REDIRECTS, sharedPreferences.getBoolean(TWITTER_AMP_REDIRECTS, true));
-            preferencesContentValues.put(SEARCH, sharedPreferences.getString(SEARCH, context.getString(R.string.search_default_value)));
-            preferencesContentValues.put(SEARCH_CUSTOM_URL, sharedPreferences.getString(SEARCH_CUSTOM_URL, context.getString(R.string.search_custom_url_default_value)));
-            preferencesContentValues.put(PROXY, sharedPreferences.getString(PROXY, context.getString(R.string.proxy_default_value)));
-            preferencesContentValues.put(PROXY_CUSTOM_URL, sharedPreferences.getString(PROXY_CUSTOM_URL, context.getString(R.string.proxy_custom_url_default_value)));
-            preferencesContentValues.put(FULL_SCREEN_BROWSING_MODE, sharedPreferences.getBoolean(FULL_SCREEN_BROWSING_MODE, false));
-            preferencesContentValues.put(HIDE_APP_BAR, sharedPreferences.getBoolean(HIDE_APP_BAR, true));
-            preferencesContentValues.put(CLEAR_EVERYTHING, sharedPreferences.getBoolean(CLEAR_EVERYTHING, true));
-            preferencesContentValues.put(CLEAR_COOKIES, sharedPreferences.getBoolean(CLEAR_COOKIES, true));
-            preferencesContentValues.put(CLEAR_DOM_STORAGE, sharedPreferences.getBoolean(CLEAR_DOM_STORAGE, true));
-            preferencesContentValues.put(CLEAR_FORM_DATA, sharedPreferences.getBoolean(CLEAR_FORM_DATA, true));  // Clear form data can be removed once the minimum API >= 26.
-            preferencesContentValues.put(CLEAR_LOGCAT, sharedPreferences.getBoolean(CLEAR_LOGCAT, true));
-            preferencesContentValues.put(CLEAR_CACHE, sharedPreferences.getBoolean(CLEAR_CACHE, true));
-            preferencesContentValues.put(HOMEPAGE, sharedPreferences.getString(HOMEPAGE, context.getString(R.string.homepage_default_value)));
-            preferencesContentValues.put(DOWNLOAD_LOCATION, sharedPreferences.getString(DOWNLOAD_LOCATION, context.getString(R.string.download_location_default_value)));
-            preferencesContentValues.put(DOWNLOAD_CUSTOM_LOCATION, sharedPreferences.getString(DOWNLOAD_CUSTOM_LOCATION, context.getString(R.string.download_custom_location_default_value)));
-            preferencesContentValues.put(FONT_SIZE, sharedPreferences.getString(FONT_SIZE, context.getString(R.string.font_size_default_value)));
-            preferencesContentValues.put(OPEN_INTENTS_IN_NEW_TAB, sharedPreferences.getBoolean(OPEN_INTENTS_IN_NEW_TAB, true));
-            preferencesContentValues.put(SWIPE_TO_REFRESH, sharedPreferences.getBoolean(SWIPE_TO_REFRESH, true));
-            preferencesContentValues.put(SCROLL_APP_BAR, sharedPreferences.getBoolean(SCROLL_APP_BAR, true));
-            preferencesContentValues.put(DISPLAY_ADDITIONAL_APP_BAR_ICONS, sharedPreferences.getBoolean(DISPLAY_ADDITIONAL_APP_BAR_ICONS, false));
-            preferencesContentValues.put(APP_THEME, sharedPreferences.getString(APP_THEME, context.getString(R.string.app_theme_default_value)));
-            preferencesContentValues.put(WEBVIEW_THEME, sharedPreferences.getString(WEBVIEW_THEME, context.getString(R.string.webview_theme_default_value)));
-            preferencesContentValues.put(WIDE_VIEWPORT, sharedPreferences.getBoolean(WIDE_VIEWPORT, true));
-            preferencesContentValues.put(DISPLAY_WEBPAGE_IMAGES, sharedPreferences.getBoolean(DISPLAY_WEBPAGE_IMAGES, true));
-
-            // Insert the preferences into the export database.
-            exportDatabase.insert(PREFERENCES_TABLE, null, preferencesContentValues);
-
-            // Close the export database.
-            exportDatabase.close();
-
-            // Convert the database file to a string.
-            String exportFileString = exportFile.toString();
-
-            // Create strings for the temporary database files.
-            String journalFileString = exportFileString + "-journal";
-
-            // Get `Files` for the temporary database files.
-            File journalFile = new File(journalFileString);
-
-            // Delete the Journal file if it exists.
-            if (journalFile.exists()) {
-                //noinspection ResultOfMethodCallIgnored
-                journalFile.delete();
-            }
-
-            // Export successful.
-            return EXPORT_SUCCESSFUL;
-        } catch (Exception exception) {
-            // Return the export error.
-            return exception.toString();
-        }
-    }
-
-    public String importUnencrypted(File importFile, Context context){
-        try {
-            // Create a temporary import file string.
-            String temporaryImportFileString = context.getCacheDir() + "/" + "temporary_import_file";
-
-            // Get a handle for a temporary import file.
-            File temporaryImportFile = new File(temporaryImportFileString);
-
-            // Delete the temporary import file if it already exists.
-            if (temporaryImportFile.exists()) {
-                //noinspection ResultOfMethodCallIgnored
-                temporaryImportFile.delete();
-            }
-
-            // Create input and output streams.
-            InputStream importFileInputStream = new FileInputStream(importFile);
-            OutputStream temporaryImportFileOutputStream = new FileOutputStream(temporaryImportFile);
-
-            // Create a byte array.
+            // Create a transfer byte array.
             byte[] transferByteArray = new byte[1024];
 
             // Create an integer to track the number of bytes read.
             int bytesRead;
 
-            // Copy the import file to the temporary import file.  Once the minimum API >= 26 `Files.copy` can be used instead.
+            // Copy the import file to the temporary import file.
             while ((bytesRead = importFileInputStream.read(transferByteArray)) > 0) {
                 temporaryImportFileOutputStream.write(transferByteArray, 0, bytesRead);
             }
 
+            // Flush the temporary import file output stream.
+            temporaryImportFileOutputStream.flush();
+
             // Close the file streams.
             importFileInputStream.close();
             temporaryImportFileOutputStream.close();
@@ -368,14 +120,14 @@ public class ImportExportDatabaseHelper {
             SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
 
             // Open the import database.  Once the minimum API >= 27 the file can be opened directly without using the string.
-            SQLiteDatabase importDatabase = SQLiteDatabase.openDatabase(temporaryImportFileString, null, SQLiteDatabase.OPEN_READWRITE);
+            SQLiteDatabase importDatabase = SQLiteDatabase.openDatabase(temporaryImportFile.toString(), null, SQLiteDatabase.OPEN_READWRITE);
 
             // Get the database version.
             int importDatabaseVersion = importDatabase.getVersion();
 
             // Upgrade the database if needed.
             if (importDatabaseVersion < SCHEMA_VERSION) {
-                switch (importDatabaseVersion){
+                switch (importDatabaseVersion) {
                     // Upgrade from schema version 1, Privacy Browser 2.13.
                     case 1:
                         // Previously this upgrade added `download_with_external_app` to the Preferences table.  But that is now removed in schema version 10.
@@ -535,17 +287,7 @@ public class ImportExportDatabaseHelper {
 
                     // Upgrade from schema version 9, Privacy Browser 3.3.
                     case 9:
-                        // Add the download location columns to the preferences table.
-                        importDatabase.execSQL("ALTER TABLE " + PREFERENCES_TABLE + " ADD COLUMN " + DOWNLOAD_LOCATION + " TEXT");
-                        importDatabase.execSQL("ALTER TABLE " + PREFERENCES_TABLE + " ADD COLUMN " + DOWNLOAD_CUSTOM_LOCATION + " TEXT");
-
-                        // Get the current download location values.
-                        String downloadLocation = sharedPreferences.getString(DOWNLOAD_LOCATION, context.getString(R.string.download_location_default_value));
-                        String downloadCustomLocation = sharedPreferences.getString(DOWNLOAD_CUSTOM_LOCATION, context.getString(R.string.download_custom_location_default_value));
-
-                        // Populate the preferences table with the current download location values.
-                        importDatabase.execSQL("UPDATE " + PREFERENCES_TABLE + " SET " + DOWNLOAD_LOCATION + " = '" + downloadLocation + "'");
-                        importDatabase.execSQL("UPDATE " + PREFERENCES_TABLE + " SET " + DOWNLOAD_CUSTOM_LOCATION + " = '" + downloadCustomLocation + "'");
+                        // Previously this upgrade added `download_location` and `download_custom_location` to the Preferences table.  But they are now removed in schema version 13.
 
                     // Upgrade from schema version 10, Privacy Browser 3.4.
                     case 10:
@@ -599,6 +341,10 @@ public class ImportExportDatabaseHelper {
                         } else {
                             importDatabase.execSQL("UPDATE " + PREFERENCES_TABLE + " SET " + CLEAR_LOGCAT + " = " + 0);
                         }
+
+                    // Upgrade from schema version 12, Privacy Browser 3.6.
+                    case 12:
+                        // Do nothing.  `download_location` and `download_custom_location` were removed from the preferences table.
                 }
             }
 
@@ -749,8 +495,6 @@ public class ImportExportDatabaseHelper {
                     .putBoolean(CLEAR_LOGCAT, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(CLEAR_LOGCAT)) == 1)
                     .putBoolean(CLEAR_CACHE, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(CLEAR_CACHE)) == 1)
                     .putString(HOMEPAGE, importPreferencesCursor.getString(importPreferencesCursor.getColumnIndex(HOMEPAGE)))
-                    .putString(DOWNLOAD_LOCATION, importPreferencesCursor.getString(importPreferencesCursor.getColumnIndex(DOWNLOAD_LOCATION)))
-                    .putString(DOWNLOAD_CUSTOM_LOCATION, importPreferencesCursor.getString(importPreferencesCursor.getColumnIndex(DOWNLOAD_CUSTOM_LOCATION)))
                     .putString(FONT_SIZE, importPreferencesCursor.getString(importPreferencesCursor.getColumnIndex(FONT_SIZE)))
                     .putBoolean(OPEN_INTENTS_IN_NEW_TAB, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(OPEN_INTENTS_IN_NEW_TAB)) == 1)
                     .putBoolean(SWIPE_TO_REFRESH, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(SWIPE_TO_REFRESH)) == 1)
@@ -765,46 +509,258 @@ public class ImportExportDatabaseHelper {
             // Close the preferences cursor.
             importPreferencesCursor.close();
 
-
             // Close the import database.
             importDatabase.close();
 
-            // Create strings for the temporary database files.
-            String shmFileString = temporaryImportFileString + "-shm";
-            String walFileString = temporaryImportFileString + "-wal";
-            String journalFileString = temporaryImportFileString + "-journal";
+            // Delete the temporary import file database, journal, and other related auxiliary files.
+            SQLiteDatabase.deleteDatabase(temporaryImportFile);
+
+            // Import successful.
+            return IMPORT_SUCCESSFUL;
+        } catch (Exception exception) {
+            // Return the import error.
+            return exception.toString();
+        }
+    }
+
+    public String exportUnencrypted(OutputStream exportFileOutputStream, Context context) {
+        try {
+            // Create a temporary export file.
+            File temporaryExportFile = File.createTempFile("temporary_export_file", null, context.getCacheDir());
+
+            // Create the temporary export database.
+            SQLiteDatabase temporaryExportDatabase = SQLiteDatabase.openOrCreateDatabase(temporaryExportFile, null);
+
+            // Set the temporary export database version number.
+            temporaryExportDatabase.setVersion(SCHEMA_VERSION);
 
-            // Get `Files` for the temporary database files.
-            File shmFile = new File(shmFileString);
-            File walFile = new File(walFileString);
-            File journalFile = new File(journalFileString);
 
-            // Delete the Shared Memory file if it exists.
-            if (shmFile.exists()) {
-                //noinspection ResultOfMethodCallIgnored
-                shmFile.delete();
+            // Create the temporary export database bookmarks table.
+            temporaryExportDatabase.execSQL(BookmarksDatabaseHelper.CREATE_BOOKMARKS_TABLE);
+
+            // Open the bookmarks database.  The `0` specifies the database version, but that is ignored and set instead using a constant in `BookmarksDatabaseHelper`.
+            BookmarksDatabaseHelper bookmarksDatabaseHelper = new BookmarksDatabaseHelper(context, null, null, 0);
+
+            // Get a full bookmarks cursor.
+            Cursor bookmarksCursor = bookmarksDatabaseHelper.getAllBookmarks();
+
+            // Move to the first bookmark.
+            bookmarksCursor.moveToFirst();
+
+            // Copy the data from the bookmarks cursor into the export database.
+            for (int i = 0; i < bookmarksCursor.getCount(); i++) {
+                // Extract the record from the cursor and store the data in a ContentValues.
+                ContentValues bookmarksContentValues = new ContentValues();
+                bookmarksContentValues.put(BookmarksDatabaseHelper.BOOKMARK_NAME, bookmarksCursor.getString(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME)));
+                bookmarksContentValues.put(BookmarksDatabaseHelper.BOOKMARK_URL, bookmarksCursor.getString(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_URL)));
+                bookmarksContentValues.put(BookmarksDatabaseHelper.PARENT_FOLDER, bookmarksCursor.getString(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.PARENT_FOLDER)));
+                bookmarksContentValues.put(BookmarksDatabaseHelper.DISPLAY_ORDER, bookmarksCursor.getInt(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.DISPLAY_ORDER)));
+                bookmarksContentValues.put(BookmarksDatabaseHelper.IS_FOLDER, bookmarksCursor.getInt(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)));
+                bookmarksContentValues.put(BookmarksDatabaseHelper.FAVORITE_ICON, bookmarksCursor.getBlob(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON)));
+
+                // Insert the record into the temporary export database.
+                temporaryExportDatabase.insert(BookmarksDatabaseHelper.BOOKMARKS_TABLE, null, bookmarksContentValues);
+
+                // Advance to the next record.
+                bookmarksCursor.moveToNext();
             }
 
-            // Delete the Write Ahead Log file if it exists.
-            if (walFile.exists()) {
-                //noinspection ResultOfMethodCallIgnored
-                walFile.delete();
+            // Close the bookmarks database.
+            bookmarksCursor.close();
+            bookmarksDatabaseHelper.close();
+
+
+            // Create the temporary export database domains table.
+            temporaryExportDatabase.execSQL(DomainsDatabaseHelper.CREATE_DOMAINS_TABLE);
+
+            // Open the domains database.  The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
+            DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(context, null, null, 0);
+
+            // Get a full domains database cursor.
+            Cursor domainsCursor = domainsDatabaseHelper.getCompleteCursorOrderedByDomain();
+
+            // Move to the first domain.
+            domainsCursor.moveToFirst();
+
+            // Copy the data from the domains cursor into the export database.
+            for (int i = 0; i < domainsCursor.getCount(); i++) {
+                // Extract the record from the cursor and store the data in a ContentValues.
+                ContentValues domainsContentValues = new ContentValues();
+                domainsContentValues.put(DomainsDatabaseHelper.DOMAIN_NAME, domainsCursor.getString(domainsCursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME)));
+                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_JAVASCRIPT, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)));
+                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_FIRST_PARTY_COOKIES, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FIRST_PARTY_COOKIES)));
+                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_THIRD_PARTY_COOKIES, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_THIRD_PARTY_COOKIES)));
+                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_DOM_STORAGE, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_DOM_STORAGE)));
+                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_FORM_DATA, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FORM_DATA)));
+                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_EASYLIST, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYLIST)));
+                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_EASYPRIVACY, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYPRIVACY)));
+                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST)));
+                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)));
+                domainsContentValues.put(DomainsDatabaseHelper.ULTRALIST, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.ULTRALIST)));
+                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY)));
+                domainsContentValues.put(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS)));
+                domainsContentValues.put(DomainsDatabaseHelper.USER_AGENT, domainsCursor.getString(domainsCursor.getColumnIndex(DomainsDatabaseHelper.USER_AGENT)));
+                domainsContentValues.put(DomainsDatabaseHelper.FONT_SIZE, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.FONT_SIZE)));
+                domainsContentValues.put(DomainsDatabaseHelper.SWIPE_TO_REFRESH, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.SWIPE_TO_REFRESH)));
+                domainsContentValues.put(DomainsDatabaseHelper.WEBVIEW_THEME, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.WEBVIEW_THEME)));
+                domainsContentValues.put(DomainsDatabaseHelper.WIDE_VIEWPORT, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.WIDE_VIEWPORT)));
+                domainsContentValues.put(DomainsDatabaseHelper.DISPLAY_IMAGES, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.DISPLAY_IMAGES)));
+                domainsContentValues.put(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)));
+                domainsContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME, domainsCursor.getString(domainsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME)));
+                domainsContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION, domainsCursor.getString(domainsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION)));
+                domainsContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT, domainsCursor.getString(domainsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT)));
+                domainsContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME, domainsCursor.getString(domainsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME)));
+                domainsContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION, domainsCursor.getString(domainsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION)));
+                domainsContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT, domainsCursor.getString(domainsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT)));
+                domainsContentValues.put(DomainsDatabaseHelper.SSL_START_DATE, domainsCursor.getLong(domainsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)));
+                domainsContentValues.put(DomainsDatabaseHelper.SSL_END_DATE, domainsCursor.getLong(domainsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)));
+                domainsContentValues.put(DomainsDatabaseHelper.PINNED_IP_ADDRESSES, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_IP_ADDRESSES)));
+                domainsContentValues.put(DomainsDatabaseHelper.IP_ADDRESSES, domainsCursor.getString(domainsCursor.getColumnIndex(DomainsDatabaseHelper.IP_ADDRESSES)));
+
+                // Insert the record into the temporary export database.
+                temporaryExportDatabase.insert(DomainsDatabaseHelper.DOMAINS_TABLE, null, domainsContentValues);
+
+                // Advance to the next record.
+                domainsCursor.moveToNext();
             }
 
-            // Delete the Journal file if it exists.
-            if (journalFile.exists()) {
-                //noinspection ResultOfMethodCallIgnored
-                journalFile.delete();
+            // Close the domains database.
+            domainsCursor.close();
+            domainsDatabaseHelper.close();
+
+
+            // Prepare the preferences table SQL creation string.
+            String CREATE_PREFERENCES_TABLE = "CREATE TABLE " + PREFERENCES_TABLE + " (" +
+                    _ID + " INTEGER PRIMARY KEY, " +
+                    JAVASCRIPT + " BOOLEAN, " +
+                    FIRST_PARTY_COOKIES + " BOOLEAN, " +
+                    THIRD_PARTY_COOKIES + " BOOLEAN, " +
+                    DOM_STORAGE + " BOOLEAN, " +
+                    SAVE_FORM_DATA + " BOOLEAN, " +
+                    USER_AGENT + " TEXT, " +
+                    CUSTOM_USER_AGENT + " TEXT, " +
+                    INCOGNITO_MODE + " BOOLEAN, " +
+                    DO_NOT_TRACK + " BOOLEAN, " +
+                    ALLOW_SCREENSHOTS + " BOOLEAN, " +
+                    EASYLIST + " BOOLEAN, " +
+                    EASYPRIVACY + " BOOLEAN, " +
+                    FANBOYS_ANNOYANCE_LIST + " BOOLEAN, " +
+                    FANBOYS_SOCIAL_BLOCKING_LIST + " BOOLEAN, " +
+                    ULTRALIST + " BOOLEAN, " +
+                    ULTRAPRIVACY + " BOOLEAN, " +
+                    BLOCK_ALL_THIRD_PARTY_REQUESTS + " BOOLEAN, " +
+                    GOOGLE_ANALYTICS + " BOOLEAN, " +
+                    FACEBOOK_CLICK_IDS + " BOOLEAN, " +
+                    TWITTER_AMP_REDIRECTS + " BOOLEAN, " +
+                    SEARCH + " TEXT, " +
+                    SEARCH_CUSTOM_URL + " TEXT, " +
+                    PROXY + " TEXT, " +
+                    PROXY_CUSTOM_URL + " TEXT, " +
+                    FULL_SCREEN_BROWSING_MODE + " BOOLEAN, " +
+                    HIDE_APP_BAR + " BOOLEAN, " +
+                    CLEAR_EVERYTHING + " BOOLEAN, " +
+                    CLEAR_COOKIES + " BOOLEAN, " +
+                    CLEAR_DOM_STORAGE + " BOOLEAN, " +
+                    CLEAR_FORM_DATA + " BOOLEAN, " +
+                    CLEAR_LOGCAT + " BOOLEAN, " +
+                    CLEAR_CACHE + " BOOLEAN, " +
+                    HOMEPAGE + " TEXT, " +
+                    FONT_SIZE + " TEXT, " +
+                    OPEN_INTENTS_IN_NEW_TAB + " BOOLEAN, " +
+                    SWIPE_TO_REFRESH + " BOOLEAN, " +
+                    SCROLL_APP_BAR + " BOOLEAN, " +
+                    DISPLAY_ADDITIONAL_APP_BAR_ICONS + " BOOLEAN, " +
+                    APP_THEME + " TEXT, " +
+                    WEBVIEW_THEME + " TEXT, " +
+                    WIDE_VIEWPORT + " BOOLEAN, " +
+                    DISPLAY_WEBPAGE_IMAGES + " BOOLEAN)";
+
+            // Create the temporary export database preferences table.
+            temporaryExportDatabase.execSQL(CREATE_PREFERENCES_TABLE);
+
+            // Get a handle for the shared preference.
+            SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+
+            // Create a ContentValues with the preferences information.
+            ContentValues preferencesContentValues = new ContentValues();
+            preferencesContentValues.put(JAVASCRIPT, sharedPreferences.getBoolean(JAVASCRIPT, false));
+            preferencesContentValues.put(FIRST_PARTY_COOKIES, sharedPreferences.getBoolean(FIRST_PARTY_COOKIES, false));
+            preferencesContentValues.put(THIRD_PARTY_COOKIES, sharedPreferences.getBoolean(THIRD_PARTY_COOKIES, false));
+            preferencesContentValues.put(DOM_STORAGE, sharedPreferences.getBoolean(DOM_STORAGE, false));
+            preferencesContentValues.put(SAVE_FORM_DATA, sharedPreferences.getBoolean(SAVE_FORM_DATA, false));  // Save form data can be removed once the minimum API >= 26.
+            preferencesContentValues.put(USER_AGENT, sharedPreferences.getString(USER_AGENT, context.getString(R.string.user_agent_default_value)));
+            preferencesContentValues.put(CUSTOM_USER_AGENT, sharedPreferences.getString(CUSTOM_USER_AGENT, context.getString(R.string.custom_user_agent_default_value)));
+            preferencesContentValues.put(INCOGNITO_MODE, sharedPreferences.getBoolean(INCOGNITO_MODE, false));
+            preferencesContentValues.put(DO_NOT_TRACK, sharedPreferences.getBoolean(DO_NOT_TRACK, false));
+            preferencesContentValues.put(ALLOW_SCREENSHOTS, sharedPreferences.getBoolean(ALLOW_SCREENSHOTS, false));
+            preferencesContentValues.put(EASYLIST, sharedPreferences.getBoolean(EASYLIST, true));
+            preferencesContentValues.put(EASYPRIVACY, sharedPreferences.getBoolean(EASYPRIVACY, true));
+            preferencesContentValues.put(FANBOYS_ANNOYANCE_LIST, sharedPreferences.getBoolean(FANBOYS_ANNOYANCE_LIST, true));
+            preferencesContentValues.put(FANBOYS_SOCIAL_BLOCKING_LIST, sharedPreferences.getBoolean(FANBOYS_SOCIAL_BLOCKING_LIST, true));
+            preferencesContentValues.put(ULTRALIST, sharedPreferences.getBoolean(ULTRALIST, true));
+            preferencesContentValues.put(ULTRAPRIVACY, sharedPreferences.getBoolean(ULTRAPRIVACY, true));
+            preferencesContentValues.put(BLOCK_ALL_THIRD_PARTY_REQUESTS, sharedPreferences.getBoolean(BLOCK_ALL_THIRD_PARTY_REQUESTS, false));
+            preferencesContentValues.put(GOOGLE_ANALYTICS, sharedPreferences.getBoolean(GOOGLE_ANALYTICS, true));
+            preferencesContentValues.put(FACEBOOK_CLICK_IDS, sharedPreferences.getBoolean(FACEBOOK_CLICK_IDS, true));
+            preferencesContentValues.put(TWITTER_AMP_REDIRECTS, sharedPreferences.getBoolean(TWITTER_AMP_REDIRECTS, true));
+            preferencesContentValues.put(SEARCH, sharedPreferences.getString(SEARCH, context.getString(R.string.search_default_value)));
+            preferencesContentValues.put(SEARCH_CUSTOM_URL, sharedPreferences.getString(SEARCH_CUSTOM_URL, context.getString(R.string.search_custom_url_default_value)));
+            preferencesContentValues.put(PROXY, sharedPreferences.getString(PROXY, context.getString(R.string.proxy_default_value)));
+            preferencesContentValues.put(PROXY_CUSTOM_URL, sharedPreferences.getString(PROXY_CUSTOM_URL, context.getString(R.string.proxy_custom_url_default_value)));
+            preferencesContentValues.put(FULL_SCREEN_BROWSING_MODE, sharedPreferences.getBoolean(FULL_SCREEN_BROWSING_MODE, false));
+            preferencesContentValues.put(HIDE_APP_BAR, sharedPreferences.getBoolean(HIDE_APP_BAR, true));
+            preferencesContentValues.put(CLEAR_EVERYTHING, sharedPreferences.getBoolean(CLEAR_EVERYTHING, true));
+            preferencesContentValues.put(CLEAR_COOKIES, sharedPreferences.getBoolean(CLEAR_COOKIES, true));
+            preferencesContentValues.put(CLEAR_DOM_STORAGE, sharedPreferences.getBoolean(CLEAR_DOM_STORAGE, true));
+            preferencesContentValues.put(CLEAR_FORM_DATA, sharedPreferences.getBoolean(CLEAR_FORM_DATA, true));  // Clear form data can be removed once the minimum API >= 26.
+            preferencesContentValues.put(CLEAR_LOGCAT, sharedPreferences.getBoolean(CLEAR_LOGCAT, true));
+            preferencesContentValues.put(CLEAR_CACHE, sharedPreferences.getBoolean(CLEAR_CACHE, true));
+            preferencesContentValues.put(HOMEPAGE, sharedPreferences.getString(HOMEPAGE, context.getString(R.string.homepage_default_value)));
+            preferencesContentValues.put(FONT_SIZE, sharedPreferences.getString(FONT_SIZE, context.getString(R.string.font_size_default_value)));
+            preferencesContentValues.put(OPEN_INTENTS_IN_NEW_TAB, sharedPreferences.getBoolean(OPEN_INTENTS_IN_NEW_TAB, true));
+            preferencesContentValues.put(SWIPE_TO_REFRESH, sharedPreferences.getBoolean(SWIPE_TO_REFRESH, true));
+            preferencesContentValues.put(SCROLL_APP_BAR, sharedPreferences.getBoolean(SCROLL_APP_BAR, true));
+            preferencesContentValues.put(DISPLAY_ADDITIONAL_APP_BAR_ICONS, sharedPreferences.getBoolean(DISPLAY_ADDITIONAL_APP_BAR_ICONS, false));
+            preferencesContentValues.put(APP_THEME, sharedPreferences.getString(APP_THEME, context.getString(R.string.app_theme_default_value)));
+            preferencesContentValues.put(WEBVIEW_THEME, sharedPreferences.getString(WEBVIEW_THEME, context.getString(R.string.webview_theme_default_value)));
+            preferencesContentValues.put(WIDE_VIEWPORT, sharedPreferences.getBoolean(WIDE_VIEWPORT, true));
+            preferencesContentValues.put(DISPLAY_WEBPAGE_IMAGES, sharedPreferences.getBoolean(DISPLAY_WEBPAGE_IMAGES, true));
+
+            // Insert the preferences into the temporary export database.
+            temporaryExportDatabase.insert(PREFERENCES_TABLE, null, preferencesContentValues);
+
+            // Close the temporary export database.
+            temporaryExportDatabase.close();
+
+
+            // Create the temporary export file input stream.
+            FileInputStream temporaryExportFileInputStream = new FileInputStream(temporaryExportFile);
+
+            // Create a byte array.
+            byte[] transferByteArray = new byte[1024];
+
+            // Create an integer to track the number of bytes read.
+            int bytesRead;
+
+            // Copy the temporary export file to the export file output stream.
+            while ((bytesRead = temporaryExportFileInputStream.read(transferByteArray)) > 0) {
+                exportFileOutputStream.write(transferByteArray, 0, bytesRead);
             }
 
-            // Delete the temporary import file.
-            //noinspection ResultOfMethodCallIgnored
-            temporaryImportFile.delete();
+            // Flush the export file output stream.
+            exportFileOutputStream.flush();
 
-            // Import successful.
-            return IMPORT_SUCCESSFUL;
+            // Close the file streams.
+            temporaryExportFileInputStream.close();
+            exportFileOutputStream.close();
+
+            // Delete the temporary export file database, journal, and other related auxiliary files.
+            SQLiteDatabase.deleteDatabase(temporaryExportFile);
+
+            // Export successful.
+            return EXPORT_SUCCESSFUL;
         } catch (Exception exception) {
-            // Return the import error.
+            // Return the export error.
             return exception.toString();
         }
     }
diff --git a/app/src/main/res/drawable/downloads_ghosted_day.xml b/app/src/main/res/drawable/downloads_ghosted_day.xml
deleted file mode 100644 (file)
index eea35fe..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-<!-- This file comes from the Android Material icon set, where it is called `file_download`.  It is released under the Apache License 2.0. -->
-<vector
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:height="24dp"
-    android:width="24dp"
-    android:viewportHeight="24"
-    android:viewportWidth="24" >
-
-    <!-- A hard coded color must be used until API >= 21.  Then `@color` or `?attr/colorControlNormal` may be used. -->
-    <path
-        android:fillColor="#FFB7B7B7"
-        android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z" />
-</vector>
\ No newline at end of file
diff --git a/app/src/main/res/drawable/downloads_ghosted_night.xml b/app/src/main/res/drawable/downloads_ghosted_night.xml
deleted file mode 100644 (file)
index 1927527..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-<!-- This file comes from the Android Material icon set, where it is called `file_download`.  It is released under the Apache License 2.0. -->
-<vector
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:height="24dp"
-    android:width="24dp"
-    android:viewportHeight="24"
-    android:viewportWidth="24" >
-
-    <!-- A hard coded color must be used until API >= 21.  Then `@color` or `?attr/colorControlNormal` may be used. -->
-    <path
-        android:fillColor="#FF616161"
-        android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z" />
-</vector>
\ No newline at end of file
diff --git a/app/src/main/res/drawable/import_export.xml b/app/src/main/res/drawable/import_export.xml
new file mode 100644 (file)
index 0000000..577fde7
--- /dev/null
@@ -0,0 +1,18 @@
+<!-- This file comes from the Android Material icon set, where it is called `import_export`.  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 API >= 21.  Then `@color` or `?attr/colorControlNormal` may be used instead. -->
+    <path
+        android:fillColor="#FF5785C5"
+        android:pathData="M9,3L5,6.99h3L8,14h2L10,6.99h3L9,3zM16,17.01L16,10h-2v7.01h-3L15,21l4,-3.99h-3z" />
+</vector>
\ No newline at end of file
diff --git a/app/src/main/res/drawable/import_export_day.xml b/app/src/main/res/drawable/import_export_day.xml
deleted file mode 100644 (file)
index 577fde7..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-<!-- This file comes from the Android Material icon set, where it is called `import_export`.  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 API >= 21.  Then `@color` or `?attr/colorControlNormal` may be used instead. -->
-    <path
-        android:fillColor="#FF5785C5"
-        android:pathData="M9,3L5,6.99h3L8,14h2L10,6.99h3L9,3zM16,17.01L16,10h-2v7.01h-3L15,21l4,-3.99h-3z" />
-</vector>
\ No newline at end of file
diff --git a/app/src/main/res/drawable/import_export_night.xml b/app/src/main/res/drawable/import_export_night.xml
deleted file mode 100644 (file)
index 59fe9d0..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-<!-- This file comes from the Android Material icon set, where it is called `import_export`.  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 API >= 21.  Then `@color` or `?attr/colorControlNormal` may be used instead. -->
-    <path
-        android:fillColor="#FF8AB4F8"
-        android:pathData="M9,3L5,6.99h3L8,14h2L10,6.99h3L9,3zM16,17.01L16,10h-2v7.01h-3L15,21l4,-3.99h-3z" />
-</vector>
\ No newline at end of file
index f5bcfe5818e39447b54b375aee6cecc6635793de..14ed65558c65a0906fa2826caa5a808a2c2f9e34 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-  Copyright © 2018-2020 Soren Stoutner <soren@stoutner.com>.
+  Copyright © 2018-2021 Soren Stoutner <soren@stoutner.com>.
 
   This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
 
 
                         <!-- The encryption password. -->
                         <com.google.android.material.textfield.TextInputLayout
-                            android:id="@+id/password_encryption_textinputlayout"
+                            android:id="@+id/encryption_password_textinputlayout"
                             android:layout_height="wrap_content"
                             android:layout_width="match_parent"
                             app:passwordToggleEnabled="true" >
 
                             <com.google.android.material.textfield.TextInputEditText
-                                android:id="@+id/password_encryption_edittext"
+                                android:id="@+id/encryption_password_edittext"
                                 android:layout_height="wrap_content"
                                 android:layout_width="match_parent"
                                 android:hint="@string/password"
                                 android:onClick="browse" />
                         </LinearLayout>
 
-                        <!-- File notices. -->
-                        <TextView
-                            android:id="@+id/file_does_not_exist_textview"
-                            android:layout_height="wrap_content"
-                            android:layout_width="wrap_content"
-                            android:layout_gravity="center_horizontal"
-                            android:layout_margin="5dp"
-                            android:text="@string/file_does_not_exist"
-                            android:textColor="?attr/redTextColor"
-                            android:textAlignment="center" />
-
-                        <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/redTextColor"
-                            android:textAlignment="center" />
-
                         <!-- OpenKeychain import instructions -->
                         <TextView
                             android:id="@+id/openkeychain_import_instructions_textview"
                             android:textColor="?attr/buttonTextColorSelector" />
                     </LinearLayout>
                 </androidx.cardview.widget.CardView>
-
-                <TextView
-                    android:id="@+id/import_export_storage_permission_textview"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_gravity="center_horizontal"
-                    android:layout_marginBottom="10dp"
-                    android:layout_marginStart="15dp"
-                    android:layout_marginEnd="15dp"
-                    android:text="@string/storage_permission_explanation"
-                    android:textAlignment="center" />
             </LinearLayout>
         </ScrollView>
     </LinearLayout>
index aa2b55072afb6b4437c5f4f61062e9601d89ce97..a7c9d7d6c31d2635708a29772ba04fe4dd726c0d 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-  Copyright © 2019-2020 Soren Stoutner <soren@stoutner.com>.
+  Copyright © 2019-2021 Soren Stoutner <soren@stoutner.com>.
 
   This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
 
     android:layout_height="wrap_content"
     android:layout_width="match_parent" >
 
+    <!-- Align the edit text and the select file button horizontally. -->
     <LinearLayout
         android:layout_height="wrap_content"
         android:layout_width="match_parent"
-        android:orientation="vertical"
+        android:orientation="horizontal"
         android:layout_marginTop="10dp"
         android:layout_marginStart="10dp"
         android:layout_marginEnd="10dp" >
 
-        <!-- Align the edit text and the select file button horizontally. -->
-        <LinearLayout
+        <!-- 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:orientation="horizontal" >
+            android:layout_width="0dp"
+            android:layout_weight="1" >
 
-            <!-- The text input layout makes the `android:hint` float above the edit text. -->
-            <com.google.android.material.textfield.TextInputLayout
+            <!-- `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="0dp"
-                android:layout_weight="1" >
+                android:layout_width="match_parent"
+                android:hint="@string/file_name"
+                android:inputType="textMultiLine|textUri" />
+        </com.google.android.material.textfield.TextInputLayout>
 
-                <!-- `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 does not exist warning. -->
-        <TextView
-            android:id="@+id/file_does_not_exist_textview"
-            android:layout_height="wrap_content"
-            android:layout_width="wrap_content"
-            android:layout_gravity="center_horizontal"
-            android:layout_margin="5dp"
-            android:text="@string/file_does_not_exist"
-            android:textColor="?attr/redTextColor"
-            android:textAlignment="center" />
-
-        <!-- Storage permission explanation. -->
-        <TextView
-            android:id="@+id/storage_permission_textview"
+        <Button
+            android:id="@+id/browse_button"
             android:layout_height="wrap_content"
             android:layout_width="wrap_content"
-            android:layout_gravity="center_horizontal"
-            android:text="@string/storage_permission_explanation"
-            android:textAlignment="center" />
+            android:layout_gravity="center_vertical"
+            android:text="@string/browse" />
     </LinearLayout>
 </ScrollView>
\ No newline at end of file
index 57e6d293e1b33966d19771ac817952d7a35ceb03..a7c9d7d6c31d2635708a29772ba04fe4dd726c0d 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-  Copyright © 2019-2020 Soren Stoutner <soren@stoutner.com>.
+  Copyright © 2019-2021 Soren Stoutner <soren@stoutner.com>.
 
   This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
 
     android:layout_height="wrap_content"
     android:layout_width="match_parent" >
 
+    <!-- Align the edit text and the select file button horizontally. -->
     <LinearLayout
         android:layout_height="wrap_content"
         android:layout_width="match_parent"
-        android:orientation="vertical"
+        android:orientation="horizontal"
         android:layout_marginTop="10dp"
         android:layout_marginStart="10dp"
         android:layout_marginEnd="10dp" >
 
-        <!-- Align the edit text and the select file button horizontally. -->
-        <LinearLayout
+        <!-- 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:orientation="horizontal" >
+            android:layout_width="0dp"
+            android:layout_weight="1" >
 
-            <!-- The text input layout makes the `android:hint` float above the edit text. -->
-            <com.google.android.material.textfield.TextInputLayout
+            <!-- `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="0dp"
-                android:layout_weight="1" >
+                android:layout_width="match_parent"
+                android:hint="@string/file_name"
+                android:inputType="textMultiLine|textUri" />
+        </com.google.android.material.textfield.TextInputLayout>
 
-                <!-- `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/redTextColor"
-            android:textAlignment="center" />
-
-        <!-- Storage permission explanation. -->
-        <TextView
-            android:id="@+id/storage_permission_textview"
+        <Button
+            android:id="@+id/browse_button"
             android:layout_height="wrap_content"
             android:layout_width="wrap_content"
-            android:layout_gravity="center_horizontal"
-            android:text="@string/storage_permission_explanation"
-            android:textAlignment="center" />
+            android:layout_gravity="center_vertical"
+            android:text="@string/browse" />
     </LinearLayout>
 </ScrollView>
\ No newline at end of file
diff --git a/app/src/main/res/layout/save_url_dialog.xml b/app/src/main/res/layout/save_url_dialog.xml
deleted file mode 100644 (file)
index 4cad8ce..0000000
+++ /dev/null
@@ -1,107 +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" >
-
-        <!-- The text input layout makes the `android:hint` float above the edit text. -->
-        <com.google.android.material.textfield.TextInputLayout
-            android:id="@+id/url_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>
-
-        <!-- File size. -->
-        <TextView
-            android:id="@+id/file_size_textview"
-            android:layout_height="wrap_content"
-            android:layout_width="wrap_content"
-            android:layout_marginEnd="3dp"
-            android:layout_marginBottom="5dp"
-            android:layout_gravity="end" />
-
-        <!-- 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/redTextColor"
-            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: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..3ff59a5
--- /dev/null
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+  Copyright © 2019-2021 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:id="@+id/url_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>
+
+        <!-- File size. -->
+        <TextView
+            android:id="@+id/file_size_textview"
+            android:layout_height="wrap_content"
+            android:layout_width="wrap_content"
+            android:layout_marginEnd="3dp"
+            android:layout_marginBottom="5dp"
+            android:layout_gravity="end" />
+
+        <!-- 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>
+    </LinearLayout>
+</ScrollView>
\ No newline at end of file
index 3854811f03f03d46e08e6bdc5298bbc317904531..fac6f0b489614c97cf56db076c163c5b4cb858b6 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-  Copyright © 2016-2020 Soren Stoutner <soren@stoutner.com>.
+  Copyright © 2016-2021 Soren Stoutner <soren@stoutner.com>.
 
   This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
 
@@ -98,7 +98,7 @@
         <item
             android:id="@+id/import_export"
             android:title="@string/import_export"
-            android:icon="@drawable/import_export_day"
+            android:icon="@drawable/import_export"
             android:orderInCategory="110" />
 
         <item
index 4b6b610cd545d536654d25192cbdd11183150a79..08946609bda616a88367b7ae246500226d9f8296 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-  Cop