]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blobdiff - app/src/main/java/com/stoutner/privacybrowser/activities/ImportExportActivity.kt
Replace all instances of `startActivityForResult()` with `registerForActivityResult...
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / activities / ImportExportActivity.kt
diff --git a/app/src/main/java/com/stoutner/privacybrowser/activities/ImportExportActivity.kt b/app/src/main/java/com/stoutner/privacybrowser/activities/ImportExportActivity.kt
new file mode 100644 (file)
index 0000000..2e55dd5
--- /dev/null
@@ -0,0 +1,900 @@
+/*
+ * Copyright 2018-2022 Soren Stoutner <soren@stoutner.com>.
+ *
+ * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
+ *
+ * Privacy Browser Android 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 Android 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 Android.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.stoutner.privacybrowser.activities
+
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Bundle
+import android.os.Handler
+import android.text.Editable
+import android.text.TextWatcher
+import android.view.View
+import android.view.WindowManager
+import android.widget.AdapterView
+import android.widget.ArrayAdapter
+import android.widget.Button
+import android.widget.EditText
+import android.widget.LinearLayout
+import android.widget.RadioButton
+import android.widget.Spinner
+import android.widget.TextView
+import androidx.activity.result.contract.ActivityResultContracts
+
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.widget.Toolbar
+import androidx.cardview.widget.CardView
+import androidx.core.content.FileProvider
+import androidx.preference.PreferenceManager
+
+import com.google.android.material.snackbar.Snackbar
+import com.google.android.material.textfield.TextInputLayout
+
+import com.stoutner.privacybrowser.R
+import com.stoutner.privacybrowser.BuildConfig
+import com.stoutner.privacybrowser.helpers.ImportExportDatabaseHelper
+
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileNotFoundException
+import java.io.FileOutputStream
+import java.lang.Exception
+import java.nio.charset.StandardCharsets
+import java.security.MessageDigest
+import java.security.SecureRandom
+import java.util.Arrays
+
+import javax.crypto.Cipher
+import javax.crypto.CipherInputStream
+import javax.crypto.CipherOutputStream
+import javax.crypto.spec.GCMParameterSpec
+import javax.crypto.spec.SecretKeySpec
+import kotlin.system.exitProcess
+
+// Define the encryption constants.
+private const val NO_ENCRYPTION = 0
+private const val PASSWORD_ENCRYPTION = 1
+private const val OPENPGP_ENCRYPTION = 2
+
+// Define the saved instance state constants.
+private const val ENCRYPTION_PASSWORD_TEXTINPUTLAYOUT_VISIBILITY = "A"
+private const val OPEN_KEYCHAIN_REQUIRED_TEXTVIEW_VISIBILITY = "B"
+private const val FILE_LOCATION_CARD_VIEW = "C"
+private const val FILE_NAME_LINEARLAYOUT_VISIBILITY = "D"
+private const val OPEN_KEYCHAIN_IMPORT_INSTRUCTIONS_TEXTVIEW_VISIBILITY = "E"
+private const val IMPORT_EXPORT_BUTTON_VISIBILITY = "F"
+private const val FILE_NAME_TEXT = "G"
+private const val IMPORT_EXPORT_BUTTON_TEXT = "H"
+
+class ImportExportActivity : AppCompatActivity() {
+    // Define the class views.
+    private lateinit var encryptionSpinner: Spinner
+    private lateinit var encryptionPasswordTextInputLayout: TextInputLayout
+    private lateinit var encryptionPasswordEditText: EditText
+    private lateinit var openKeychainRequiredTextView: TextView
+    private lateinit var fileLocationCardView: CardView
+    private lateinit var importRadioButton: RadioButton
+    private lateinit var fileNameLinearLayout: LinearLayout
+    private lateinit var fileNameEditText: EditText
+    private lateinit var openKeychainImportInstructionsTextView: TextView
+    private lateinit var importExportButton: Button
+
+    // Define the class variables.
+    private lateinit var fileProviderDirectory: File
+    private var openKeychainInstalled = false
+    private lateinit var temporaryPgpEncryptedImportFile: File
+    private lateinit var temporaryPreEncryptedExportFile: File
+
+    // Define the browse for import activity result launcher.  It must be defined before `onCreate()` is run or the app will crash.
+    private val browseForImportActivityResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { fileUri: Uri? ->
+        // Only do something if the user didn't press back from the file picker.
+        if (fileUri != null) {
+            // Get the file name string from the URI.
+            val fileNameString = fileUri.toString()
+
+            // Set the file name name text.
+            fileNameEditText.setText(fileNameString)
+
+            // Move the cursor to the end of the file name edit text.
+            fileNameEditText.setSelection(fileNameString.length)
+        }
+    }
+
+    private val browseForExportActivityResultLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { fileUri: Uri? ->
+        // Only do something if the user didn't press back from the file picker.
+        if (fileUri != null) {
+            // Get the file name string from the URI.
+            val fileNameString = fileUri.toString()
+
+            // Set the file name name text.
+            fileNameEditText.setText(fileNameString)
+
+            // Move the cursor to the end of the file name edit text.
+            fileNameEditText.setSelection(fileNameString.length)
+        }
+    }
+
+    private val openKeychainDecryptActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+        // Delete the temporary PGP encrypted import file.
+        if (temporaryPgpEncryptedImportFile.exists())
+            temporaryPgpEncryptedImportFile.delete()
+
+        // Delete the file provider directory if it exists.
+        if (fileProviderDirectory.exists())
+            fileProviderDirectory.delete()
+    }
+
+    private val openKeychainEncryptActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+        // Delete the temporary pre-encrypted export file if it exists.
+        if (temporaryPreEncryptedExportFile.exists())
+            temporaryPreEncryptedExportFile.delete()
+
+        // Delete the file provider directory if it exists.
+        if (fileProviderDirectory.exists())
+            fileProviderDirectory.delete()
+    }
+
+    public override fun onCreate(savedInstanceState: Bundle?) {
+        // Get a handle for the shared preferences.
+        val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
+
+        // Get the preferences.
+        val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
+        val bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false)
+
+        // Disable screenshots if not allowed.
+        if (!allowScreenshots)
+            window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
+
+        // Run the default commands.
+        super.onCreate(savedInstanceState)
+
+        // Set the content view.
+        if (bottomAppBar)
+            setContentView(R.layout.import_export_bottom_appbar)
+        else
+            setContentView(R.layout.import_export_top_appbar)
+
+        // Get a handle for the toolbar.
+        val toolbar = findViewById<Toolbar>(R.id.import_export_toolbar)
+
+        // Set the support action bar.
+        setSupportActionBar(toolbar)
+
+        // Get a handle for the action bar.
+        val actionBar = supportActionBar!!
+
+        // Display the home arrow on the support action bar.
+        actionBar.setDisplayHomeAsUpEnabled(true)
+
+        // Find out if OpenKeychain is installed.
+        openKeychainInstalled = try {
+            // The newer method can be used once the minimum API >= 33.
+            @Suppress("DEPRECATION")
+            packageManager.getPackageInfo("org.sufficientlysecure.keychain", 0).versionName.isNotEmpty()
+        } catch (exception: PackageManager.NameNotFoundException) {
+            false
+        }
+
+        // Get handles for the views.
+        encryptionSpinner = findViewById(R.id.encryption_spinner)
+        encryptionPasswordTextInputLayout = findViewById(R.id.encryption_password_textinputlayout)
+        encryptionPasswordEditText = findViewById(R.id.encryption_password_edittext)
+        openKeychainRequiredTextView = findViewById(R.id.openkeychain_required_textview)
+        fileLocationCardView = findViewById(R.id.file_location_cardview)
+        importRadioButton = findViewById(R.id.import_radiobutton)
+        val exportRadioButton = findViewById<RadioButton>(R.id.export_radiobutton)
+        fileNameLinearLayout = findViewById(R.id.file_name_linearlayout)
+        fileNameEditText = findViewById(R.id.file_name_edittext)
+        openKeychainImportInstructionsTextView = findViewById(R.id.openkeychain_import_instructions_textview)
+        importExportButton = findViewById(R.id.import_export_button)
+
+        // Create an array adapter for the spinner.
+        val encryptionArrayAdapter = ArrayAdapter.createFromResource(this, R.array.encryption_type, R.layout.spinner_item)
+
+        // Set the drop down view resource on the spinner.
+        encryptionArrayAdapter.setDropDownViewResource(R.layout.spinner_dropdown_items)
+
+        // Set the array adapter for the spinner.
+        encryptionSpinner.adapter = encryptionArrayAdapter
+
+        // Update the UI when the spinner changes.
+        encryptionSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
+            override fun onItemSelected(parent: AdapterView<*>?, view: View, position: Int, id: Long) {
+                when (position) {
+                    NO_ENCRYPTION -> {
+                        // Hide the unneeded layout items.
+                        encryptionPasswordTextInputLayout.visibility = View.GONE
+                        openKeychainRequiredTextView.visibility = View.GONE
+                        openKeychainImportInstructionsTextView.visibility = View.GONE
+
+                        // Show the file location card.
+                        fileLocationCardView.visibility = View.VISIBLE
+
+                        // Show the file name linear layout if either import or export is checked.
+                        if (importRadioButton.isChecked || exportRadioButton.isChecked)
+                            fileNameLinearLayout.visibility = View.VISIBLE
+
+                        // Reset the text of the import button, which may have been changed to `Decrypt`.
+                        if (importRadioButton.isChecked)
+                            importExportButton.setText(R.string.import_button)
+
+                        // Clear the file name edit text.
+                        fileNameEditText.text.clear()
+
+                        // Disable the import/export button.
+                        importExportButton.isEnabled = false
+                    }
+
+                    PASSWORD_ENCRYPTION -> {
+                        // Hide the OpenPGP layout items.
+                        openKeychainRequiredTextView.visibility = View.GONE
+                        openKeychainImportInstructionsTextView.visibility = View.GONE
+
+                        // Show the password encryption layout items.
+                        encryptionPasswordTextInputLayout.visibility = View.VISIBLE
+
+                        // Show the file location card.
+                        fileLocationCardView.visibility = View.VISIBLE
+
+                        // Show the file name linear layout if either import or export is checked.
+                        if (importRadioButton.isChecked || exportRadioButton.isChecked)
+                            fileNameLinearLayout.visibility = View.VISIBLE
+
+                        // Reset the text of the import button, which may have been changed to `Decrypt`.
+                        if (importRadioButton.isChecked)
+                            importExportButton.setText(R.string.import_button)
+
+                        // Clear the file name edit text.
+                        fileNameEditText.text.clear()
+
+                        // Disable the import/export button.
+                        importExportButton.isEnabled = false
+                    }
+
+                    OPENPGP_ENCRYPTION -> {
+                        // Hide the password encryption layout items.
+                        encryptionPasswordTextInputLayout.visibility = View.GONE
+
+                        // Updated items based on the installation status of OpenKeychain.
+                        if (openKeychainInstalled) {  // OpenKeychain is installed.
+                            // Show the file location card.
+                            fileLocationCardView.visibility = View.VISIBLE
+
+                            // Update the layout based on the checked radio button.
+                            if (importRadioButton.isChecked) {
+                                // Show the file name linear layout and the OpenKeychain import instructions.
+                                fileNameLinearLayout.visibility = View.VISIBLE
+                                openKeychainImportInstructionsTextView.visibility = View.VISIBLE
+
+                                // Set the text of the import button to be `Decrypt`.
+                                importExportButton.setText(R.string.decrypt)
+
+                                // Clear the file name edit text.
+                                fileNameEditText.text.clear()
+
+                                // Disable the import/export button.
+                                importExportButton.isEnabled = false
+                            } else if (exportRadioButton.isChecked) {
+                                // Hide the file name linear layout and the OpenKeychain import instructions.
+                                fileNameLinearLayout.visibility = View.GONE
+                                openKeychainImportInstructionsTextView.visibility = View.GONE
+
+                                // Enable the export button.
+                                importExportButton.isEnabled = true
+                            }
+                        } else {  // OpenKeychain is not installed.
+                            // Show the OpenPGP required layout item.
+                            openKeychainRequiredTextView.visibility = View.VISIBLE
+
+                            // Hide the file location card.
+                            fileLocationCardView.visibility = View.GONE
+                        }
+                    }
+                }
+            }
+
+            override fun onNothingSelected(parent: AdapterView<*>?) {}
+        }
+
+        // Update the status of the import/export button when the password changes.
+        encryptionPasswordEditText.addTextChangedListener(object : TextWatcher {
+            override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
+                // Do nothing.
+            }
+
+            override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
+                // Do nothing.
+            }
+
+            override fun afterTextChanged(s: Editable) {
+                // Enable the import/export button if both the file string and the password are populated.
+                importExportButton.isEnabled = fileNameEditText.text.toString().isNotEmpty() && encryptionPasswordEditText.text.toString().isNotEmpty()
+            }
+        })
+
+        // Update the UI when the file name edit text changes.
+        fileNameEditText.addTextChangedListener(object : TextWatcher {
+            override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
+                // Do nothing.
+            }
+
+            override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
+                // Do nothing.
+            }
+
+            override fun afterTextChanged(s: Editable) {
+                // Adjust the UI according to the encryption spinner position.
+                if (encryptionSpinner.selectedItemPosition == PASSWORD_ENCRYPTION) {
+                    // Enable the import/export button if both the file name and the password are populated.
+                    importExportButton.isEnabled = fileNameEditText.text.toString().isNotEmpty() && encryptionPasswordEditText.text.toString().isNotEmpty()
+                } else {
+                    // Enable the export button if the file name is populated.
+                    importExportButton.isEnabled = fileNameEditText.text.toString().isNotEmpty()
+                }
+            }
+        })
+
+        // Check to see if the activity has been restarted.
+        if (savedInstanceState == null) {  // The app has not been restarted.
+            // Initially hide the unneeded views.
+            encryptionPasswordTextInputLayout.visibility = View.GONE
+            openKeychainRequiredTextView.visibility = View.GONE
+            fileNameLinearLayout.visibility = View.GONE
+            openKeychainImportInstructionsTextView.visibility = View.GONE
+            importExportButton.visibility = View.GONE
+        } else {  // The app has been restarted.
+            // Restore the visibility of the views.
+            encryptionPasswordTextInputLayout.visibility = savedInstanceState.getInt(ENCRYPTION_PASSWORD_TEXTINPUTLAYOUT_VISIBILITY)
+            openKeychainRequiredTextView.visibility = savedInstanceState.getInt(OPEN_KEYCHAIN_REQUIRED_TEXTVIEW_VISIBILITY)
+            fileLocationCardView.visibility = savedInstanceState.getInt(FILE_LOCATION_CARD_VIEW)
+            fileNameLinearLayout.visibility = savedInstanceState.getInt(FILE_NAME_LINEARLAYOUT_VISIBILITY)
+            openKeychainImportInstructionsTextView.visibility = savedInstanceState.getInt(OPEN_KEYCHAIN_IMPORT_INSTRUCTIONS_TEXTVIEW_VISIBILITY)
+            importExportButton.visibility = savedInstanceState.getInt(IMPORT_EXPORT_BUTTON_VISIBILITY)
+
+            // Restore the text.
+            fileNameEditText.post { fileNameEditText.setText(savedInstanceState.getString(FILE_NAME_TEXT)) }
+            importExportButton.text = savedInstanceState.getString(IMPORT_EXPORT_BUTTON_TEXT)
+        }
+    }
+
+    public override fun onSaveInstanceState(savedInstanceState: Bundle) {
+        // Run the default commands.
+        super.onSaveInstanceState(savedInstanceState)
+
+        // Save the visibility of the views.
+        savedInstanceState.putInt(ENCRYPTION_PASSWORD_TEXTINPUTLAYOUT_VISIBILITY, encryptionPasswordTextInputLayout.visibility)
+        savedInstanceState.putInt(OPEN_KEYCHAIN_REQUIRED_TEXTVIEW_VISIBILITY, openKeychainRequiredTextView.visibility)
+        savedInstanceState.putInt(FILE_LOCATION_CARD_VIEW, fileLocationCardView.visibility)
+        savedInstanceState.putInt(FILE_NAME_LINEARLAYOUT_VISIBILITY, fileNameLinearLayout.visibility)
+        savedInstanceState.putInt(OPEN_KEYCHAIN_IMPORT_INSTRUCTIONS_TEXTVIEW_VISIBILITY, openKeychainImportInstructionsTextView.visibility)
+        savedInstanceState.putInt(IMPORT_EXPORT_BUTTON_VISIBILITY, importExportButton.visibility)
+
+        // Save the text.
+        savedInstanceState.putString(FILE_NAME_TEXT, fileNameEditText.text.toString())
+        savedInstanceState.putString(IMPORT_EXPORT_BUTTON_TEXT, importExportButton.text.toString())
+    }
+
+    fun onClickRadioButton(@Suppress("UNUSED_PARAMETER") view: View) {
+        // Check to see if import or export was selected.
+        if (view.id == R.id.import_radiobutton) {  // The import radio button is selected.
+            // Check to see if OpenPGP encryption is selected.
+            if (encryptionSpinner.selectedItemPosition == OPENPGP_ENCRYPTION) {  // OpenPGP encryption selected.
+                // Show the OpenKeychain import instructions.
+                openKeychainImportInstructionsTextView.visibility = 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.visibility = View.GONE
+
+                // Set the text on the import/export button to be `Import`.
+                importExportButton.setText(R.string.import_button)
+            }
+
+            // Display the file name views.
+            fileNameLinearLayout.visibility = View.VISIBLE
+            importExportButton.visibility = View.VISIBLE
+
+            // Clear the file name edit text.
+            fileNameEditText.text.clear()
+
+            // Disable the import/export button.
+            importExportButton.isEnabled = false
+        } else {  // The export radio button is selected.
+            // Hide the OpenKeychain import instructions.
+            openKeychainImportInstructionsTextView.visibility = View.GONE
+
+            // Set the text on the import/export button to be `Export`.
+            importExportButton.setText(R.string.export)
+
+            // Show the import/export button.
+            importExportButton.visibility = View.VISIBLE
+
+            // Check to see if OpenPGP encryption is selected.
+            if (encryptionSpinner.selectedItemPosition == OPENPGP_ENCRYPTION) {  // OpenPGP encryption is selected.
+                // Hide the file name views.
+                fileNameLinearLayout.visibility = View.GONE
+
+                // Enable the export button.
+                importExportButton.isEnabled = true
+            } else {  // OpenPGP encryption is not selected.
+                // Show the file name view.
+                fileNameLinearLayout.visibility = View.VISIBLE
+
+                // Clear the file name edit text.
+                fileNameEditText.text.clear()
+
+                // Disable the import/export button.
+                importExportButton.isEnabled = false
+            }
+        }
+    }
+
+    fun browse(@Suppress("UNUSED_PARAMETER") view: View) {
+        // Check to see if import or export is selected.
+        if (importRadioButton.isChecked) {  // Import is selected.
+            // Open the file picker.
+            browseForImportActivityResultLauncher.launch("*/*")
+        } else {  // Export is selected
+            // Open the file picker with the export name according to the encryption type.
+            if (encryptionSpinner.selectedItemPosition == NO_ENCRYPTION)  // No encryption is selected.
+                browseForExportActivityResultLauncher.launch(getString(R.string.privacy_browser_settings_pbs, BuildConfig.VERSION_NAME))
+            else  // Password encryption is selected.
+                browseForExportActivityResultLauncher.launch(getString(R.string.privacy_browser_settings_pbs_aes, BuildConfig.VERSION_NAME))
+        }
+    }
+
+    fun importExport(@Suppress("UNUSED_PARAMETER") view: View) {
+        // Instantiate the import export database helper.
+        val importExportDatabaseHelper = ImportExportDatabaseHelper()
+
+        // Check to see if import or export is selected.
+        if (importRadioButton.isChecked) {  // Import is selected.
+            // Initialize the import status string
+            var importStatus = ""
+
+            // Get the file name string.
+            val fileNameString = fileNameEditText.text.toString()
+
+            // Import according to the encryption type.
+            when (encryptionSpinner.selectedItemPosition) {
+                NO_ENCRYPTION -> {
+                    try {
+                        // Get an input stream for the file name.
+                        // A file may be opened directly once the minimum API >= 29.  <https://developer.android.com/reference/kotlin/android/content/ContentResolver#openfile>
+                        val inputStream = contentResolver.openInputStream(Uri.parse(fileNameString))!!
+
+                        // Import the unencrypted file.
+                        importStatus = importExportDatabaseHelper.importUnencrypted(inputStream, this)
+
+                        // Close the input stream.
+                        inputStream.close()
+                    } catch (exception: FileNotFoundException) {
+                        // Update the import status.
+                        importStatus = exception.toString()
+                    }
+
+                    // Restart Privacy Browser if successful.
+                    if (importStatus == ImportExportDatabaseHelper.IMPORT_SUCCESSFUL)
+                        restartPrivacyBrowser()
+                }
+
+                PASSWORD_ENCRYPTION -> {
+                    try {
+                        // Get the encryption password.
+                        val encryptionPasswordString = encryptionPasswordEditText.text.toString()
+
+                        // Get an input stream for the file name.
+                        val inputStream = contentResolver.openInputStream(Uri.parse(fileNameString))!!
+
+                        // Initialize a salt byte array.  Salt protects against rainbow table attacks.
+                        val saltByteArray = ByteArray(32)
+
+                        // Get the salt from the beginning of the import file.
+                        inputStream.read(saltByteArray)
+
+                        // Create an initialization vector.
+                        val initializationVector = ByteArray(12)
+
+                        // Get the initialization vector from the import file.
+                        inputStream.read(initializationVector)
+
+                        // Convert the encryption password to a byte array.
+                        val encryptionPasswordByteArray = encryptionPasswordString.toByteArray(StandardCharsets.UTF_8)
+
+                        // Create an encryption password with salt byte array.
+                        val encryptionPasswordWithSaltByteArray = ByteArray(encryptionPasswordByteArray.size + saltByteArray.size)
+
+                        // Populate the first part of the encryption password with salt byte array with the encryption password.
+                        System.arraycopy(encryptionPasswordByteArray, 0, encryptionPasswordWithSaltByteArray, 0, encryptionPasswordByteArray.size)
+
+                        // Populate the second part of the encryption password with salt byte array with the salt.
+                        System.arraycopy(saltByteArray, 0, encryptionPasswordWithSaltByteArray, encryptionPasswordByteArray.size, saltByteArray.size)
+
+                        // Get a SHA-512 message digest.
+                        val messageDigest = MessageDigest.getInstance("SHA-512")
+
+                        // Hash the salted encryption password.  Otherwise, any characters after the 32nd character in the password are ignored.
+                        val hashedEncryptionPasswordWithSaltByteArray = messageDigest.digest(encryptionPasswordWithSaltByteArray)
+
+                        // Truncate the encryption password byte array to 256 bits (32 bytes).
+                        val truncatedHashedEncryptionPasswordWithSaltByteArray = Arrays.copyOf(hashedEncryptionPasswordWithSaltByteArray, 32)
+
+                        // Create an AES secret key from the encryption password byte array.
+                        val secretKey = SecretKeySpec(truncatedHashedEncryptionPasswordWithSaltByteArray, "AES")
+
+                        // 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.
+                        val cipher = Cipher.getInstance("AES/GCM/NoPadding")
+
+                        // Set the GCM tag length to be 128 bits (the maximum) and apply the initialization vector.
+                        val gcmParameterSpec = GCMParameterSpec(128, initializationVector)
+
+                        // Initialize the cipher.
+                        cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec)
+
+                        // Create a cipher input stream.
+                        val cipherInputStream = CipherInputStream(inputStream, cipher)
+
+                        // 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.
+                        var numberOfBytesRead: Int
+                        val decryptedBytes = ByteArray(16)
+
+                        // Create a private temporary unencrypted import file.
+                        val temporaryUnencryptedImportFile = File.createTempFile("temporary_unencrypted_import_file", null, applicationContext.cacheDir)
+
+                        // Create an temporary unencrypted import file output stream.
+                        val temporaryUnencryptedImportFileOutputStream = FileOutputStream(temporaryUnencryptedImportFile)
+
+                        // Read up to 128 bits (16 bytes) of data from the cipher input stream.  `-1` will be returned when the end of the file is reached.
+                        while (cipherInputStream.read(decryptedBytes).also { numberOfBytesRead = it } != -1) {
+                            // Write the data to the temporary unencrypted import file output stream.
+                            temporaryUnencryptedImportFileOutputStream.write(decryptedBytes, 0, numberOfBytesRead)
+                        }
+
+                        // Flush the temporary unencrypted import file output stream.
+                        temporaryUnencryptedImportFileOutputStream.flush()
+
+                        // Close the streams.
+                        temporaryUnencryptedImportFileOutputStream.close()
+                        cipherInputStream.close()
+                        inputStream.close()
+
+                        // Wipe the encryption data from memory.
+                        Arrays.fill(saltByteArray, 0.toByte())
+                        Arrays.fill(initializationVector, 0.toByte())
+                        Arrays.fill(encryptionPasswordByteArray, 0.toByte())
+                        Arrays.fill(encryptionPasswordWithSaltByteArray, 0.toByte())
+                        Arrays.fill(hashedEncryptionPasswordWithSaltByteArray, 0.toByte())
+                        Arrays.fill(truncatedHashedEncryptionPasswordWithSaltByteArray, 0.toByte())
+                        Arrays.fill(decryptedBytes, 0.toByte())
+
+                        // Create a temporary unencrypted import file input stream.
+                        val temporaryUnencryptedImportFileInputStream = FileInputStream(temporaryUnencryptedImportFile)
+
+                        // Import the temporary unencrypted import file.
+                        importStatus = importExportDatabaseHelper.importUnencrypted(temporaryUnencryptedImportFileInputStream, this)
+
+                        // Close the temporary unencrypted import file input stream.
+                        temporaryUnencryptedImportFileInputStream.close()
+
+                        // Delete the temporary unencrypted import file.
+                        temporaryUnencryptedImportFile.delete()
+
+                        // Restart Privacy Browser if successful.
+                        if (importStatus == ImportExportDatabaseHelper.IMPORT_SUCCESSFUL)
+                            restartPrivacyBrowser()
+                    } catch (exception: Exception) {
+                        // Update the import status.
+                        importStatus = exception.toString()
+                    }
+                }
+
+                OPENPGP_ENCRYPTION -> {
+                    try {
+                        // Get a handle for the file provider directory.
+                        fileProviderDirectory = File(applicationContext.cacheDir.toString() + "/" + getString(R.string.file_provider_directory))
+
+                        // Create the file provider directory.  Any errors will be handled by the catch statement below.
+                        fileProviderDirectory.mkdir()
+
+                        // Set the temporary PGP encrypted import file.
+                        temporaryPgpEncryptedImportFile = File.createTempFile("temporary_pgp_encrypted_import_file", null, fileProviderDirectory)
+
+                        // Create a temporary PGP encrypted import file output stream.
+                        val temporaryPgpEncryptedImportFileOutputStream = FileOutputStream(temporaryPgpEncryptedImportFile)
+
+                        // Get an input stream for the file name.
+                        val inputStream = contentResolver.openInputStream(Uri.parse(fileNameString))!!
+
+                        // Create a transfer byte array.
+                        val transferByteArray = ByteArray(1024)
+
+                        // Create an integer to track the number of bytes read.
+                        var bytesRead: Int
+
+                        // Copy the input stream to the temporary PGP encrypted import file.
+                        while (inputStream.read(transferByteArray).also { bytesRead = it } > 0)
+                            temporaryPgpEncryptedImportFileOutputStream.write(transferByteArray, 0, bytesRead)
+
+                        // Flush the temporary PGP encrypted import file output stream.
+                        temporaryPgpEncryptedImportFileOutputStream.flush()
+
+                        // Close the streams.
+                        inputStream.close()
+                        temporaryPgpEncryptedImportFileOutputStream.close()
+
+                        // Create a decryption intent for OpenKeychain.
+                        val openKeychainDecryptIntent = Intent("org.sufficientlysecure.keychain.action.DECRYPT_DATA")
+
+                        // Include the URI to be decrypted.
+                        openKeychainDecryptIntent.data = FileProvider.getUriForFile(this, getString(R.string.file_provider), temporaryPgpEncryptedImportFile)
+
+                        // Allow OpenKeychain to read the file URI.
+                        openKeychainDecryptIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
+
+                        // Send the intent to the OpenKeychain package.
+                        openKeychainDecryptIntent.setPackage("org.sufficientlysecure.keychain")
+
+                        // Make it so.
+                        openKeychainDecryptActivityResultLauncher.launch(openKeychainDecryptIntent)
+
+                        // Update the import status.
+                        importStatus = ImportExportDatabaseHelper.IMPORT_SUCCESSFUL
+                    } catch (exception: Exception) {
+                        // Update the import status.
+                        importStatus = exception.toString()
+                    }
+                }
+            }
+
+            // Respond to the import status.
+            if (importStatus != 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.
+            when (encryptionSpinner.selectedItemPosition) {
+                NO_ENCRYPTION -> {
+                    // Get the file name string.
+                    val noEncryptionFileNameString = fileNameEditText.text.toString()
+
+                    try {
+                        // Get the export file output stream.
+                        // A file may be opened directly once the minimum API >= 29.  <https://developer.android.com/reference/kotlin/android/content/ContentResolver#openfile>
+                        val exportFileOutputStream = contentResolver.openOutputStream(Uri.parse(noEncryptionFileNameString))!!
+
+                        // Export the unencrypted file.
+                        val noEncryptionExportStatus = importExportDatabaseHelper.exportUnencrypted(exportFileOutputStream, this)
+
+                        // Close the output stream.
+                        exportFileOutputStream.close()
+
+                        // Display an export disposition snackbar.
+                        if (noEncryptionExportStatus == 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()
+                    }
+                }
+
+                PASSWORD_ENCRYPTION -> {
+                    try {
+                        // Create a temporary unencrypted export file.
+                        val temporaryUnencryptedExportFile = File.createTempFile("temporary_unencrypted_export_file", null, applicationContext.cacheDir)
+
+                        // Create a temporary unencrypted export output stream.
+                        val temporaryUnencryptedExportOutputStream = FileOutputStream(temporaryUnencryptedExportFile)
+
+                        // Populate the temporary unencrypted export.
+                        val passwordEncryptionExportStatus = importExportDatabaseHelper.exportUnencrypted(temporaryUnencryptedExportOutputStream, this)
+
+                        // Close the temporary unencrypted export output stream.
+                        temporaryUnencryptedExportOutputStream.close()
+
+                        // Create an unencrypted export file input stream.
+                        val unencryptedExportFileInputStream = FileInputStream(temporaryUnencryptedExportFile)
+
+                        // Get the encryption password.
+                        val encryptionPasswordString = encryptionPasswordEditText.text.toString()
+
+                        // Initialize a secure random number generator.
+                        val secureRandom = SecureRandom()
+
+                        // Initialize a salt byte array.  Salt protects against rainbow table attacks.
+                        val saltByteArray = ByteArray(32)
+
+                        // Get a 256 bit (32 byte) random salt.
+                        secureRandom.nextBytes(saltByteArray)
+
+                        // Convert the encryption password to a byte array.
+                        val encryptionPasswordByteArray = encryptionPasswordString.toByteArray(StandardCharsets.UTF_8)
+
+                        // Create an encryption password with salt byte array.
+                        val encryptionPasswordWithSaltByteArray = ByteArray(encryptionPasswordByteArray.size + saltByteArray.size)
+
+                        // Populate the first part of the encryption password with salt byte array with the encryption password.
+                        System.arraycopy(encryptionPasswordByteArray, 0, encryptionPasswordWithSaltByteArray, 0, encryptionPasswordByteArray.size)
+
+                        // Populate the second part of the encryption password with salt byte array with the salt.
+                        System.arraycopy(saltByteArray, 0, encryptionPasswordWithSaltByteArray, encryptionPasswordByteArray.size, saltByteArray.size)
+
+                        // Get a SHA-512 message digest.
+                        val messageDigest = MessageDigest.getInstance("SHA-512")
+
+                        // Hash the salted encryption password.  Otherwise, any characters after the 32nd character in the password are ignored.
+                        val hashedEncryptionPasswordWithSaltByteArray = messageDigest.digest(encryptionPasswordWithSaltByteArray)
+
+                        // Truncate the encryption password byte array to 256 bits (32 bytes).
+                        val truncatedHashedEncryptionPasswordWithSaltByteArray = Arrays.copyOf(hashedEncryptionPasswordWithSaltByteArray, 32)
+
+                        // Create an AES secret key from the encryption password byte array.
+                        val secretKey = SecretKeySpec(truncatedHashedEncryptionPasswordWithSaltByteArray, "AES")
+
+                        // Create an initialization vector.  According to NIST, a 12 byte initialization vector is more secure than a 16 byte one.
+                        val initializationVector = ByteArray(12)
+
+                        // Populate the initialization vector with random data.
+                        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.
+                        val cipher = Cipher.getInstance("AES/GCM/NoPadding")
+
+                        // Set the GCM tag length to be 128 bits (the maximum) and apply the initialization vector.
+                        val gcmParameterSpec = GCMParameterSpec(128, initializationVector)
+
+                        // Initialize the cipher.
+                        cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmParameterSpec)
+
+                        // Get the file name string.
+                        val passwordEncryptionFileNameString = fileNameEditText.text.toString()
+
+                        // Get the export file output stream.
+                        val exportFileOutputStream = contentResolver.openOutputStream(Uri.parse(passwordEncryptionFileNameString))!!
+
+                        // Add the salt and the initialization vector to the export file output stream.
+                        exportFileOutputStream.write(saltByteArray)
+                        exportFileOutputStream.write(initializationVector)
+
+                        // Create a cipher output stream.
+                        val cipherOutputStream = 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.
+                        var numberOfBytesRead: Int
+                        val encryptedBytes = ByteArray(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 (unencryptedExportFileInputStream.read(encryptedBytes).also { numberOfBytesRead = it } != -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.
+                        Arrays.fill(saltByteArray, 0.toByte())
+                        Arrays.fill(encryptionPasswordByteArray, 0.toByte())
+                        Arrays.fill(encryptionPasswordWithSaltByteArray, 0.toByte())
+                        Arrays.fill(hashedEncryptionPasswordWithSaltByteArray, 0.toByte())
+                        Arrays.fill(truncatedHashedEncryptionPasswordWithSaltByteArray, 0.toByte())
+                        Arrays.fill(initializationVector, 0.toByte())
+                        Arrays.fill(encryptedBytes, 0.toByte())
+
+                        // Delete the temporary unencrypted export file.
+                        temporaryUnencryptedExportFile.delete()
+
+                        // Display an export disposition snackbar.
+                        if (passwordEncryptionExportStatus == 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()
+                    }
+                }
+
+                OPENPGP_ENCRYPTION -> {
+                    try {
+                        // Get a handle for the file provider directory.
+                        fileProviderDirectory = File(applicationContext.cacheDir.toString() + "/" + getString(R.string.file_provider_directory))
+
+                        // Create the file provider directory.  Any errors will be handled by the catch statement below.
+                        fileProviderDirectory.mkdir()
+
+                        // Set the temporary pre-encrypted export file.
+                        temporaryPreEncryptedExportFile = File(fileProviderDirectory.toString() + "/" + getString(R.string.privacy_browser_settings_pbs, BuildConfig.VERSION_NAME))
+
+                        // Delete the temporary pre-encrypted export file if it already exists.
+                        if (temporaryPreEncryptedExportFile.exists())
+                            temporaryPreEncryptedExportFile.delete()
+
+                        // Create a temporary pre-encrypted export output stream.
+                        val temporaryPreEncryptedExportOutputStream = FileOutputStream(temporaryPreEncryptedExportFile)
+
+                        // Populate the temporary pre-encrypted export file.
+                        val 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 != ImportExportDatabaseHelper.EXPORT_SUCCESSFUL)
+                            Snackbar.make(fileNameEditText, getString(R.string.export_failed, openpgpEncryptionExportStatus), Snackbar.LENGTH_INDEFINITE).show()
+
+                        // Create an encryption intent for OpenKeychain.
+                        val openKeychainEncryptIntent = Intent("org.sufficientlysecure.keychain.action.ENCRYPT_DATA")
+
+                        // Include the temporary unencrypted export file URI.
+                        openKeychainEncryptIntent.data = FileProvider.getUriForFile(this, getString(R.string.file_provider), temporaryPreEncryptedExportFile)
+
+                        // Allow OpenKeychain to read the file URI.
+                        openKeychainEncryptIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
+
+                        // Send the intent to the OpenKeychain package.
+                        openKeychainEncryptIntent.setPackage("org.sufficientlysecure.keychain")
+
+                        // Make it so.
+                        openKeychainEncryptActivityResultLauncher.launch(openKeychainEncryptIntent)
+                    } catch (exception: Exception) {
+                        // Display a snackbar with the exception.
+                        Snackbar.make(fileNameEditText, getString(R.string.export_failed, exception), Snackbar.LENGTH_INDEFINITE).show()
+                    }
+                }
+            }
+        }
+    }
+
+    private fun restartPrivacyBrowser() {
+        // Create an intent to restart Privacy Browser.
+        val restartIntent = parentActivityIntent!!
+
+        // `Intent.FLAG_ACTIVITY_CLEAR_TASK` removes all activities from the stack.  It requires `Intent.FLAG_ACTIVITY_NEW_TASK`.
+        restartIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+
+        // Create a restart handler.
+        val restartHandler = Handler(mainLooper)
+
+        // Create a restart runnable.
+        val restartRunnable = Runnable {
+
+            // Restart Privacy Browser.
+            startActivity(restartIntent)
+
+            // Kill this instance of Privacy Browser.  Otherwise, the app exhibits sporadic behavior after the restart.
+            exitProcess(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