From: Soren Stoutner Date: Sat, 10 Dec 2022 17:59:36 +0000 (-0700) Subject: Replace all instances of `startActivityForResult()` with `registerForActivityResult... X-Git-Tag: v3.13~11 X-Git-Url: https://gitweb.stoutner.com/?a=commitdiff_plain;h=153a29723774971e62fa0102db87393f1afef193;p=PrivacyBrowserAndroid.git Replace all instances of `startActivityForResult()` with `registerForActivityResult()`. https://redmine.stoutner.com/issues/718 --- diff --git a/app/src/main/assets/es/guide_interface.html b/app/src/main/assets/es/guide_interface.html index 0cd63d48..6d018783 100644 --- a/app/src/main/assets/es/guide_interface.html +++ b/app/src/main/assets/es/guide_interface.html @@ -37,7 +37,8 @@ A veces puede ser difícil conseguir que el cajón de marcadores llegue a su punto máximo de forma fiable en dispositivos con bordes curvos, por lo que también es posible abrir el cajón de marcadores desde el menú de opciones.

-

Tapping on a bookmark opens it in the current tab. Long-pressing on a bookmark opens it in a new tab and long-pressing on a folder opens all the bookmarks it directly contains in new tabs. +

Al pulsar sobre un marcador, lo abre en la pestaña actual. + Una pulsación larga sobre un marcador lo abre en una nueva pestaña y una pulsación larga sobre una carpeta abre todos los marcadores que contiene directamente en nuevas pestañas. Al tocar el botón de acción superior flotante en el cajón de marcadores se abre la actividad de marcadores, donde se pueden editar y reorganizar los marcadores.

diff --git a/app/src/main/assets/fr/guide_interface.html b/app/src/main/assets/fr/guide_interface.html index 76625e1c..493b0ac1 100644 --- a/app/src/main/assets/fr/guide_interface.html +++ b/app/src/main/assets/fr/guide_interface.html @@ -38,7 +38,8 @@ Il est parfois difficile de faire apparaître le gestionnaire de favoris de manière fiable sur les appareils aux bords incurvés, c'est pourquoi il est également possible d'ouvrir le gestionnaire de favoris à partir du menu des options.

-

Tapping on a bookmark opens it in the current tab. Long-pressing on a bookmark opens it in a new tab and long-pressing on a folder opens all the bookmarks it directly contains in new tabs. +

Un appui sur un favori l'ouvre dans l'onglet actuel. + Un appui long sur un signet l'ouvre dans un nouvel onglet et un appui long sur un dossier ouvre tous les signets qu'il contient directement dans de nouveaux onglets. Si vous appuyez sur le bouton d'action flottant supérieur dans le gestionnaire de favoris, vous ouvrez un volet dans lequel vous pouvez modifier et réorganiser les favoris.

diff --git a/app/src/main/assets/ru/guide_interface.html b/app/src/main/assets/ru/guide_interface.html index eaaf3e8e..e3d585a3 100644 --- a/app/src/main/assets/ru/guide_interface.html +++ b/app/src/main/assets/ru/guide_interface.html @@ -34,7 +34,8 @@ панель можно открыть длительным нажатием на край экрана до ее появления, а затем смахнув. На устройствах с изогнутыми краями иногда бывает непросто добиться появления панели закладок, поэтому можно открыть ее из меню параметров.

-

Tapping on a bookmark opens it in the current tab. Long-pressing on a bookmark opens it in a new tab and long-pressing on a folder opens all the bookmarks it directly contains in new tabs. +

При нажатии на закладку она откроется на текущей вкладке. + При длительном нажатии на закладку она откроется на новой вкладке, а при длительном нажатии на папку все закладки, которые она содержит, откроются в новых вкладках. При нажатии верхней кнопки действия в панели закладок открывается страница действий с закладками, где их можно редактировать и упорядочивать.

diff --git a/app/src/main/java/com/stoutner/privacybrowser/activities/ImportExportActivity.java b/app/src/main/java/com/stoutner/privacybrowser/activities/ImportExportActivity.java deleted file mode 100644 index 5ff99d6f..00000000 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/ImportExportActivity.java +++ /dev/null @@ -1,964 +0,0 @@ -/* - * Copyright 2018-2022 Soren Stoutner . - * - * This file is part of 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 . - */ - -package com.stoutner.privacybrowser.activities; - -import android.app.Activity; -import android.content.Intent; -import android.content.SharedPreferences; -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.annotation.NonNull; -import androidx.appcompat.app.ActionBar; -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.BuildConfig; -import com.stoutner.privacybrowser.R; -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; -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; - -public class ImportExportActivity extends AppCompatActivity { - // Define the encryption constants. - private final int NO_ENCRYPTION = 0; - private final int PASSWORD_ENCRYPTION = 1; - private final int OPENPGP_ENCRYPTION = 2; - - // Define the activity result constants. - private final int BROWSE_RESULT_CODE = 0; - 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 ENCRYPTION_PASSWORD_TEXTINPUTLAYOUT_VISIBILITY = "encryption_password_textinputlayout_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 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. - Spinner encryptionSpinner; - TextInputLayout encryptionPasswordTextInputLayout; - EditText encryptionPasswordEditText; - TextView openKeychainRequiredTextView; - CardView fileLocationCardView; - RadioButton importRadioButton; - LinearLayout fileNameLinearLayout; - EditText fileNameEditText; - TextView openKeychainImportInstructionsTextView; - Button importExportButton; - - // Define the class variables. - private File fileProviderDirectory; - private boolean openKeychainInstalled; - private File temporaryPgpEncryptedImportFile; - private File temporaryPreEncryptedExportFile; - - @Override - public void onCreate(Bundle savedInstanceState) { - // Get a handle for the shared preferences. - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - - // Get the preferences. - boolean allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false); - boolean bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false); - - // Disable screenshots if not allowed. - if (!allowScreenshots) { - getWindow().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. - Toolbar toolbar = findViewById(R.id.import_export_toolbar); - - // Set the support action bar. - setSupportActionBar(toolbar); - - // Get a handle for the action bar. - ActionBar actionBar = getSupportActionBar(); - - // Remove the incorrect lint warning that the action bar might be null. - assert actionBar != null; - - // Display the home arrow on the support action bar. - actionBar.setDisplayHomeAsUpEnabled(true); - - // Find out if OpenKeychain is installed. - try { - openKeychainInstalled = !getPackageManager().getPackageInfo("org.sufficientlysecure.keychain", 0).versionName.isEmpty(); - } catch (PackageManager.NameNotFoundException exception) { - openKeychainInstalled = false; - } - - // Get handles for the views that need to be modified. - 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); - RadioButton exportRadioButton = findViewById(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. - ArrayAdapter 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.setAdapter(encryptionArrayAdapter); - - // Update the UI when the spinner changes. - encryptionSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - switch (position) { - case NO_ENCRYPTION: - // Hide the unneeded layout items. - encryptionPasswordTextInputLayout.setVisibility(View.GONE); - openKeychainRequiredTextView.setVisibility(View.GONE); - openKeychainImportInstructionsTextView.setVisibility(View.GONE); - - // Show the file location card. - fileLocationCardView.setVisibility(View.VISIBLE); - - // Show the file name linear layout if either import or export is checked. - if (importRadioButton.isChecked() || exportRadioButton.isChecked()) { - fileNameLinearLayout.setVisibility(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); - } - - // Enable the import/export button if the file name is populated. - importExportButton.setEnabled(!fileNameEditText.getText().toString().isEmpty()); - break; - - case PASSWORD_ENCRYPTION: - // Hide the OpenPGP layout items. - openKeychainRequiredTextView.setVisibility(View.GONE); - openKeychainImportInstructionsTextView.setVisibility(View.GONE); - - // Show the password encryption layout items. - encryptionPasswordTextInputLayout.setVisibility(View.VISIBLE); - - // Show the file location card. - fileLocationCardView.setVisibility(View.VISIBLE); - - // Show the file name linear layout if either import or export is checked. - if (importRadioButton.isChecked() || exportRadioButton.isChecked()) { - fileNameLinearLayout.setVisibility(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); - } - - // 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. - encryptionPasswordTextInputLayout.setVisibility(View.GONE); - - // Updated items based on the installation status of OpenKeychain. - if (openKeychainInstalled) { // OpenKeychain is installed. - // Show the file location card. - fileLocationCardView.setVisibility(View.VISIBLE); - - if (importRadioButton.isChecked()) { - // Show the file name linear layout and the OpenKeychain import instructions. - fileNameLinearLayout.setVisibility(View.VISIBLE); - openKeychainImportInstructionsTextView.setVisibility(View.VISIBLE); - - // 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. - openKeychainRequiredTextView.setVisibility(View.VISIBLE); - - // Hide the file location card. - fileLocationCardView.setVisibility(View.GONE); - } - break; - } - } - - @Override - public void onNothingSelected(AdapterView parent) { - - } - }); - - // Update the status of the import/export button when the password changes. - encryptionPasswordEditText.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - // Do nothing. - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - // Do nothing. - } - - @Override - public void afterTextChanged(Editable s) { - // Enable the import/export button if both the file string and the password are populated. - importExportButton.setEnabled(!fileNameEditText.getText().toString().isEmpty() && !encryptionPasswordEditText.getText().toString().isEmpty()); - } - }); - - // Update the UI when the file name EditText changes. - fileNameEditText.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - // Do nothing. - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - // Do nothing. - } - - @Override - public void afterTextChanged(Editable s) { - // Adjust the UI according to the encryption spinner position. - 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()); - } - } - }); - - // Check to see if the activity has been restarted. - if (savedInstanceState == null) { // The app has not been restarted. - // Initially hide the unneeded views. - encryptionPasswordTextInputLayout.setVisibility(View.GONE); - openKeychainRequiredTextView.setVisibility(View.GONE); - fileNameLinearLayout.setVisibility(View.GONE); - openKeychainImportInstructionsTextView.setVisibility(View.GONE); - importExportButton.setVisibility(View.GONE); - } else { // The app has been restarted. - // Restore the visibility of the views. - encryptionPasswordTextInputLayout.setVisibility(savedInstanceState.getInt(ENCRYPTION_PASSWORD_TEXTINPUTLAYOUT_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)); - openKeychainImportInstructionsTextView.setVisibility(savedInstanceState.getInt(OPEN_KEYCHAIN_IMPORT_INSTRUCTIONS_TEXTVIEW_VISIBILITY)); - importExportButton.setVisibility(savedInstanceState.getInt(IMPORT_EXPORT_BUTTON_VISIBILITY)); - - // Restore the text. - fileNameEditText.post(() -> fileNameEditText.setText(savedInstanceState.getString(FILE_NAME_TEXT))); - importExportButton.setText(savedInstanceState.getString(IMPORT_EXPORT_BUTTON_TEXT)); - } - } - - @Override - public void onSaveInstanceState (@NonNull Bundle savedInstanceState) { - // Run the default commands. - super.onSaveInstanceState(savedInstanceState); - - // Save the visibility of the views. - savedInstanceState.putInt(ENCRYPTION_PASSWORD_TEXTINPUTLAYOUT_VISIBILITY, encryptionPasswordTextInputLayout.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(OPEN_KEYCHAIN_IMPORT_INSTRUCTIONS_TEXTVIEW_VISIBILITY, openKeychainImportInstructionsTextView.getVisibility()); - savedInstanceState.putInt(IMPORT_EXPORT_BUTTON_VISIBILITY, importExportButton.getVisibility()); - - // Save the text. - savedInstanceState.putString(FILE_NAME_TEXT, fileNameEditText.getText().toString()); - savedInstanceState.putString(IMPORT_EXPORT_BUTTON_TEXT, importExportButton.getText().toString()); - } - - public void onClickRadioButton(View view) { - // 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 import or export was selected. - 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); - - // Set the text on the import/export button to be `Import`. - importExportButton.setText(R.string.import_button); - } - - // 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); - } - } 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); - - // 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); - - // 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() && !encryptionPasswordEditText.getText().toString().isEmpty()); - } - } - } - } - - public void browse(View view) { - // Check to see if import or export is selected. - if (importRadioButton.isChecked()) { // Import is selected. - // Create the file picker intent. - Intent importBrowseIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT); - - // Set the intent MIME type to include all files so that everything is visible. - importBrowseIntent.setType("*/*"); - - // Request a file that can be opened. - importBrowseIntent.addCategory(Intent.CATEGORY_OPENABLE); - - // Launch the file picker. - startActivityForResult(importBrowseIntent, BROWSE_RESULT_CODE); - } else { // Export is selected - // Create the file picker intent. - Intent exportBrowseIntent = new Intent(Intent.ACTION_CREATE_DOCUMENT); - - // Set the intent MIME type to include all files so that everything is visible. - exportBrowseIntent.setType("*/*"); - - // Set the initial export file name according to the encryption type. - if (encryptionSpinner.getSelectedItemPosition() == NO_ENCRYPTION) { // No encryption is selected. - exportBrowseIntent.putExtra(Intent.EXTRA_TITLE, getString(R.string.settings) + " " + BuildConfig.VERSION_NAME + ".pbs"); - } else { // Password encryption is selected. - exportBrowseIntent.putExtra(Intent.EXTRA_TITLE, getString(R.string.settings) + " " + BuildConfig.VERSION_NAME + ".pbs.aes"); - } - - // Request a file that can be opened. - exportBrowseIntent.addCategory(Intent.CATEGORY_OPENABLE); - - // Launch the file picker. - startActivityForResult(exportBrowseIntent, BROWSE_RESULT_CODE); - } - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent returnedIntent) { - // Run the default commands. - super.onActivityResult(requestCode, resultCode, returnedIntent); - - switch (requestCode) { - case (BROWSE_RESULT_CODE): - // Only do something if the user didn't press back from the file picker. - if (resultCode == Activity.RESULT_OK) { - // Get the file path URI from the intent. - Uri fileNameUri = returnedIntent.getData(); - - // Get the file name string from the URI. - String fileNameString = fileNameUri.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()); - } - break; - - case OPENPGP_IMPORT_RESULT_CODE: - // Delete the temporary PGP encrypted import file. - if (temporaryPgpEncryptedImportFile.exists()) { - //noinspection ResultOfMethodCallIgnored - temporaryPgpEncryptedImportFile.delete(); - } - - // Delete the file provider directory if it exists. - if (fileProviderDirectory.exists()) { - //noinspection ResultOfMethodCallIgnored - fileProviderDirectory.delete(); - } - - break; - - case OPENPGP_EXPORT_RESULT_CODE: - // Delete the temporary pre-encrypted export file if it exists. - if (temporaryPreEncryptedExportFile.exists()) { - //noinspection ResultOfMethodCallIgnored - temporaryPreEncryptedExportFile.delete(); - } - - // Delete the file provider directory if it exists. - if (fileProviderDirectory.exists()) { - //noinspection ResultOfMethodCallIgnored - fileProviderDirectory.delete(); - } - - break; - } - } - - public void importExport(View view) { - // Instantiate the import export database helper. - ImportExportDatabaseHelper importExportDatabaseHelper = new ImportExportDatabaseHelper(); - - // Check to see if import or export is selected. - if (importRadioButton.isChecked()) { // Import is selected. - // Initialize the import status string - String importStatus = ""; - - // Get the file name string. - String fileNameString = fileNameEditText.getText().toString(); - - // Import according to the encryption type. - switch (encryptionSpinner.getSelectedItemPosition()) { - case NO_ENCRYPTION: - try { - // Get an input stream for the file name. - // A file may be opened directly once the minimum API >= 29. - 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(); - } - - // Restart Privacy Browser if successful. - if (importStatus.equals(ImportExportDatabaseHelper.IMPORT_SUCCESSFUL)) { - restartPrivacyBrowser(); - } - break; - - case PASSWORD_ENCRYPTION: - try { - // Get the encryption password. - String encryptionPasswordString = encryptionPasswordEditText.getText().toString(); - - // Get an input stream for the file name. - InputStream inputStream = getContentResolver().openInputStream(Uri.parse(fileNameString)); - - // Get the salt from the beginning of the import file. - byte[] saltByteArray = new byte[32]; - //noinspection ResultOfMethodCallIgnored - inputStream.read(saltByteArray); - - // Get the initialization vector from the import file. - byte[] initializationVector = new byte[12]; - //noinspection ResultOfMethodCallIgnored - inputStream.read(initializationVector); - - // Convert the encryption password to a byte array. - byte[] encryptionPasswordByteArray = encryptionPasswordString.getBytes(StandardCharsets.UTF_8); - - // 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 SHA-512 message digest. - MessageDigest messageDigest = MessageDigest.getInstance("SHA-512"); - - // Hash the salted encryption password. Otherwise, any characters after the 32nd character in the password are ignored. - byte[] hashedEncryptionPasswordWithSaltByteArray = messageDigest.digest(encryptionPasswordWithSaltByteArray); - - // Truncate the encryption password byte array to 256 bits (32 bytes). - byte[] truncatedHashedEncryptionPasswordWithSaltByteArray = Arrays.copyOf(hashedEncryptionPasswordWithSaltByteArray, 32); - - // Create an AES secret key from the encryption password byte array. - SecretKeySpec secretKey = new 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. - Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); - - // Set the GCM tag length to be 128 bits (the maximum) and apply the initialization vector. - GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, initializationVector); - - // Initialize the cipher. - cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec); - - // Create a cipher input stream. - CipherInputStream cipherInputStream = new 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. - int numberOfBytesRead; - byte[] decryptedBytes = new byte[16]; - - - // Create a private temporary unencrypted import file. - File temporaryUnencryptedImportFile = File.createTempFile("temporary_unencrypted_import_file", null, getApplicationContext().getCacheDir()); - - // Create an temporary unencrypted import file output stream. - FileOutputStream temporaryUnencryptedImportFileOutputStream = new FileOutputStream(temporaryUnencryptedImportFile); - - - // 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); - } - - - // 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. - //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); - - // Create a temporary unencrypted import file input stream. - FileInputStream temporaryUnencryptedImportFileInputStream = new 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. - //noinspection ResultOfMethodCallIgnored - temporaryUnencryptedImportFile.delete(); - - // Restart Privacy Browser if successful. - if (importStatus.equals(ImportExportDatabaseHelper.IMPORT_SUCCESSFUL)) { - restartPrivacyBrowser(); - } - } catch (Exception exception) { - // Update the import status. - importStatus = exception.toString(); - } - break; - - case OPENPGP_ENCRYPTION: - try { - // Get a handle for the file provider directory. - fileProviderDirectory = new File(getApplicationContext().getCacheDir() + "/" + getString(R.string.file_provider_directory)); - - // Create the file provider directory. Any errors will be handled by the catch statement below. - //noinspection ResultOfMethodCallIgnored - 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. - FileOutputStream temporaryPgpEncryptedImportFileOutputStream = new FileOutputStream(temporaryPgpEncryptedImportFile); - - // Get an input stream for the file name. - InputStream inputStream = getContentResolver().openInputStream(Uri.parse(fileNameString)); - - // Create a transfer byte array. - byte[] transferByteArray = new byte[1024]; - - // Create an integer to track the number of bytes read. - int bytesRead; - - // Copy the input stream to the temporary PGP encrypted import file. - while ((bytesRead = inputStream.read(transferByteArray)) > 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 an decryption intent for OpenKeychain. - Intent openKeychainDecryptIntent = new Intent("org.sufficientlysecure.keychain.action.DECRYPT_DATA"); - - // 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; - } - - // 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. - // A file may be opened directly once the minimum API >= 29. - 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; - - case PASSWORD_ENCRYPTION: - try { - // Create a temporary unencrypted export file. - File temporaryUnencryptedExportFile = File.createTempFile("temporary_unencrypted_export_file", null, getApplicationContext().getCacheDir()); - - // Create a temporary unencrypted export output stream. - FileOutputStream temporaryUnencryptedExportOutputStream = new FileOutputStream(temporaryUnencryptedExportFile); - - // Populate the temporary unencrypted export. - String passwordEncryptionExportStatus = importExportDatabaseHelper.exportUnencrypted(temporaryUnencryptedExportOutputStream, this); - - // Close the temporary unencrypted export output stream. - temporaryUnencryptedExportOutputStream.close(); - - // Create an unencrypted export file input stream. - FileInputStream unencryptedExportFileInputStream = new FileInputStream(temporaryUnencryptedExportFile); - - // Get the encryption password. - String encryptionPasswordString = encryptionPasswordEditText.getText().toString(); - - // Initialize a secure random number generator. - SecureRandom secureRandom = new SecureRandom(); - - // Get a 256 bit (32 byte) random salt. - byte[] saltByteArray = new byte[32]; - secureRandom.nextBytes(saltByteArray); - - // Convert the encryption password to a byte array. - byte[] encryptionPasswordByteArray = encryptionPasswordString.getBytes(StandardCharsets.UTF_8); - - // 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 SHA-512 message digest. - MessageDigest messageDigest = MessageDigest.getInstance("SHA-512"); - - // Hash the salted encryption password. Otherwise, any characters after the 32nd character in the password are ignored. - byte[] hashedEncryptionPasswordWithSaltByteArray = messageDigest.digest(encryptionPasswordWithSaltByteArray); - - // Truncate the encryption password byte array to 256 bits (32 bytes). - byte[] truncatedHashedEncryptionPasswordWithSaltByteArray = Arrays.copyOf(hashedEncryptionPasswordWithSaltByteArray, 32); - - // Create an AES secret key from the encryption password byte array. - SecretKeySpec secretKey = new SecretKeySpec(truncatedHashedEncryptionPasswordWithSaltByteArray, "AES"); - - // 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"); - - // Set the GCM tag length to be 128 bits (the maximum) and apply the initialization vector. - GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, initializationVector); - - // Initialize the cipher. - cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmParameterSpec); - - // Get the file name string. - String passwordEncryptionFileNameString = fileNameEditText.getText().toString(); - - // Get the export file output stream. - OutputStream exportFileOutputStream = getContentResolver().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. - 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; - - case OPENPGP_ENCRYPTION: - try { - // Get a handle for the file provider directory. - fileProviderDirectory = new File(getApplicationContext().getCacheDir() + "/" + getString(R.string.file_provider_directory)); - - // Create the file provider directory. Any errors will be handled by the catch statement below. - //noinspection ResultOfMethodCallIgnored - fileProviderDirectory.mkdir(); - - // Set the temporary pre-encrypted export file. - temporaryPreEncryptedExportFile = new File(fileProviderDirectory + "/" + getString(R.string.settings) + " " + BuildConfig.VERSION_NAME + ".pbs"); - - // Delete the temporary pre-encrypted export file if it already exists. - if (temporaryPreEncryptedExportFile.exists()) { - //noinspection ResultOfMethodCallIgnored - temporaryPreEncryptedExportFile.delete(); - } - - // 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 an encryption intent for OpenKeychain. - Intent openKeychainEncryptIntent = new Intent("org.sufficientlysecure.keychain.action.ENCRYPT_DATA"); - - // Include the temporary unencrypted export file URI. - openKeychainEncryptIntent.setData(FileProvider.getUriForFile(this, getString(R.string.file_provider), temporaryPreEncryptedExportFile)); - - // Allow OpenKeychain to read the file URI. - openKeychainEncryptIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - - // Send the intent to the OpenKeychain package. - openKeychainEncryptIntent.setPackage("org.sufficientlysecure.keychain"); - - // 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); - } -} 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 index 00000000..2e55dd52 --- /dev/null +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/ImportExportActivity.kt @@ -0,0 +1,900 @@ +/* + * Copyright 2018-2022 Soren Stoutner . + * + * This file is part of 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 . + */ + +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(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(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. + 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. + 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 diff --git a/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java b/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java index 2f41688a..461c7405 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java @@ -99,6 +99,7 @@ import android.widget.RelativeLayout; import android.widget.TextView; import androidx.activity.OnBackPressedCallback; +import androidx.activity.result.ActivityResult; import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; @@ -209,10 +210,6 @@ 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 = 12; - // Define the start activity for result request codes. The public static entry is accessed from `OpenDialog()`. - private final int BROWSE_FILE_UPLOAD_REQUEST_CODE = 0; - public final static int BROWSE_OPEN_REQUEST_CODE = 1; - // Define the saved instance state constants. private final String BOOKMARKS_DRAWER_PINNED = "bookmarks_drawer_pinned"; private final String PROXY_MODE = "proxy_mode"; @@ -473,6 +470,16 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook } }); + // Define the save webpage image activity result launcher. It must be defined before `onCreate()` is run or the app will crash. + private final ActivityResultLauncher browseFileUploadActivityResultLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), + new ActivityResultCallback() { + @Override + public void onActivityResult(ActivityResult activityResult) { + // Pass the file to the WebView. + fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(activityResult.getResultCode(), activityResult.getData())); + } + }); + // Remove the warning about needing to override `performClick()` when using an `OnTouchListener` with WebView. @SuppressLint("ClickableViewAccessibility") @Override @@ -2718,53 +2725,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook bookmarksListView.setSelection(0); } - // Process the results of a file browse. - @Override - public void onActivityResult(int requestCode, int resultCode, Intent returnedIntent) { - // Run the default commands. - super.onActivityResult(requestCode, resultCode, returnedIntent); - - // Run the commands that correlate to the specified request code. - switch (requestCode) { - case BROWSE_FILE_UPLOAD_REQUEST_CODE: - // Pass the file to the WebView. - fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, returnedIntent)); - break; - - case BROWSE_OPEN_REQUEST_CODE: - // Don't do anything if the user pressed back from the file picker. - if (resultCode == Activity.RESULT_OK) { - // Get a handle for the open dialog fragment. - DialogFragment openDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.open)); - - // Only update the file name if the dialog still exists. - if (openDialogFragment != null) { - // Get a handle for the open dialog. - Dialog openDialog = openDialogFragment.getDialog(); - - // Remove the incorrect lint warning below that the dialog might be null. - assert openDialog != null; - - // Get a handle for the file name edit text. - EditText fileNameEditText = openDialog.findViewById(R.id.file_name_edittext); - - // Get the file name URI from the intent. - Uri fileNameUri = returnedIntent.getData(); - - // Get the file name string from the URI. - String fileNameString = fileNameUri.toString(); - - // Set the file name text. - fileNameEditText.setText(fileNameString); - - // Move the cursor to the end of the file name edit text. - fileNameEditText.setSelection(fileNameString.length()); - } - } - break; - } - } - private void loadUrlFromTextBox() { // Get the text from urlTextBox and convert it to a string. trim() removes white spaces from the beginning and end of the string. String unformattedUrlString = urlEditText.getText().toString().trim(); @@ -5396,8 +5356,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Check to see if the file chooser intent resolves to an installed package. if (fileChooserIntent.resolveActivity(packageManager) != null) { // The file chooser intent is fine. - // Start the file chooser intent. - startActivityForResult(fileChooserIntent, BROWSE_FILE_UPLOAD_REQUEST_CODE); + // Launch the file chooser intent. + browseFileUploadActivityResultLauncher.launch(fileChooserIntent); } else { // The file chooser intent will cause a crash. // Create a generic intent to open a chooser. Intent genericFileChooserIntent = new Intent(Intent.ACTION_GET_CONTENT); @@ -5408,8 +5368,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Set the file type to everything. genericFileChooserIntent.setType("*/*"); - // Start the generic file chooser intent. - startActivityForResult(genericFileChooserIntent, BROWSE_FILE_UPLOAD_REQUEST_CODE); + // Launch the generic file chooser intent. + browseFileUploadActivityResultLauncher.launch(genericFileChooserIntent); } return true; } diff --git a/app/src/main/java/com/stoutner/privacybrowser/dialogs/OpenDialog.kt b/app/src/main/java/com/stoutner/privacybrowser/dialogs/OpenDialog.kt index 6e1db124..ba422143 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/dialogs/OpenDialog.kt +++ b/app/src/main/java/com/stoutner/privacybrowser/dialogs/OpenDialog.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2019-2022 Soren Stoutner . + * Copyright 2019-2022 Soren Stoutner . * * This file is part of Privacy Browser Android . * @@ -22,7 +22,7 @@ package com.stoutner.privacybrowser.dialogs import android.app.Dialog import android.content.Context import android.content.DialogInterface -import android.content.Intent +import android.net.Uri import android.os.Bundle import android.text.Editable import android.text.TextWatcher @@ -33,12 +33,12 @@ import android.widget.CheckBox import android.widget.EditText import android.widget.TextView +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment import androidx.preference.PreferenceManager import com.stoutner.privacybrowser.R -import com.stoutner.privacybrowser.activities.MainWebViewActivity // Define the class constants. private const val MHT_EXPLANATION_VISIBILITY = "mht_explanation_visibility" @@ -48,6 +48,7 @@ class OpenDialog : DialogFragment() { private lateinit var openListener: OpenListener // Declare the class views. + private lateinit var fileNameEditText: EditText private lateinit var mhtExplanationTextView: TextView // The public interface is used to send information back to the parent activity. @@ -63,6 +64,21 @@ class OpenDialog : DialogFragment() { openListener = context as OpenListener } + // Define the browse activity result launcher. + private val browseActivityResultLauncher = 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 text. + fileNameEditText.setText(fileNameString) + + // Move the cursor to the end of the file name edit text. + fileNameEditText.setSelection(fileNameString.length) + } + } + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { // Use an alert dialog builder to create the alert dialog. val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog) @@ -103,7 +119,7 @@ class OpenDialog : DialogFragment() { alertDialog.show() // Get handles for the layout items. - val fileNameEditText = alertDialog.findViewById(R.id.file_name_edittext)!! + fileNameEditText = alertDialog.findViewById(R.id.file_name_edittext)!! val browseButton = alertDialog.findViewById