]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/activities/ImportExportActivity.java
ca4703af5243f936774f8ffd68cf2ec7e57a7ab1
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / activities / ImportExportActivity.java
1 /*
2  * Copyright © 2018-2019 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
5  *
6  * Privacy Browser is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * Privacy Browser is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with Privacy Browser.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 package com.stoutner.privacybrowser.activities;
21
22 import android.Manifest;
23 import android.app.Activity;
24 import android.content.Intent;
25 import android.content.SharedPreferences;
26 import android.content.pm.PackageManager;
27 import android.media.MediaScannerConnection;
28 import android.net.Uri;
29 import android.os.Build;
30 import android.os.Bundle;
31 import android.os.Environment;
32 import android.os.Handler;
33 import android.preference.PreferenceManager;
34 import android.provider.DocumentsContract;
35 import android.text.Editable;
36 import android.text.TextWatcher;
37 import android.view.View;
38 import android.view.WindowManager;
39 import android.widget.AdapterView;
40 import android.widget.ArrayAdapter;
41 import android.widget.Button;
42 import android.widget.EditText;
43 import android.widget.LinearLayout;
44 import android.widget.RadioButton;
45 import android.widget.Spinner;
46 import android.widget.TextView;
47
48 import androidx.annotation.NonNull;
49 import androidx.appcompat.app.ActionBar;
50 import androidx.appcompat.app.AppCompatActivity;
51 import androidx.appcompat.widget.Toolbar;
52 import androidx.cardview.widget.CardView;
53 import androidx.core.app.ActivityCompat;
54 import androidx.core.content.ContextCompat;
55 import androidx.core.content.FileProvider;
56 import androidx.fragment.app.DialogFragment;
57
58 import com.google.android.material.snackbar.Snackbar;
59 import com.google.android.material.textfield.TextInputLayout;
60
61 import com.stoutner.privacybrowser.R;
62 import com.stoutner.privacybrowser.dialogs.StoragePermissionDialog;
63 import com.stoutner.privacybrowser.helpers.FileNameHelper;
64 import com.stoutner.privacybrowser.helpers.ImportExportDatabaseHelper;
65
66 import java.io.File;
67 import java.io.FileInputStream;
68 import java.io.FileOutputStream;
69 import java.nio.charset.StandardCharsets;
70 import java.security.MessageDigest;
71 import java.security.SecureRandom;
72 import java.util.Arrays;
73
74 import javax.crypto.Cipher;
75 import javax.crypto.CipherInputStream;
76 import javax.crypto.CipherOutputStream;
77 import javax.crypto.spec.GCMParameterSpec;
78 import javax.crypto.spec.SecretKeySpec;
79
80 public class ImportExportActivity extends AppCompatActivity implements StoragePermissionDialog.StoragePermissionDialogListener {
81     // Create the encryption constants.
82     private final int NO_ENCRYPTION = 0;
83     private final int PASSWORD_ENCRYPTION = 1;
84     private final int OPENPGP_ENCRYPTION = 2;
85
86     // Create the activity result constants.
87     private final int BROWSE_RESULT_CODE = 0;
88     private final int OPENPGP_EXPORT_RESULT_CODE = 1;
89
90     // `openKeychainInstalled` is accessed from an inner class.
91     private boolean openKeychainInstalled;
92
93     @Override
94     public void onCreate(Bundle savedInstanceState) {
95         // Get a handle for the shared preferences.
96         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
97
98         // Get the theme and screenshot preferences.
99         boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
100         boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
101
102         // Disable screenshots if not allowed.
103         if (!allowScreenshots) {
104             getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
105         }
106
107         // Set the activity theme.
108         if (darkTheme) {
109             setTheme(R.style.PrivacyBrowserDark_SecondaryActivity);
110         } else {
111             setTheme(R.style.PrivacyBrowserLight_SecondaryActivity);
112         }
113
114         // Run the default commands.
115         super.onCreate(savedInstanceState);
116
117         // Set the content view.
118         setContentView(R.layout.import_export_coordinatorlayout);
119
120         // Set the support action bar.
121         Toolbar toolbar = findViewById(R.id.import_export_toolbar);
122         setSupportActionBar(toolbar);
123
124         // Get a handle for the action bar.
125         ActionBar actionBar = getSupportActionBar();
126
127         // Remove the incorrect lint warning that the action bar might be null.
128         assert actionBar != null;
129
130         // Display the home arrow on the support action bar.
131         actionBar.setDisplayHomeAsUpEnabled(true);
132
133         // Find out if the system is running KitKat
134         boolean runningKitKat = (Build.VERSION.SDK_INT == 19);
135
136         // Find out if OpenKeychain is installed.
137         try {
138             openKeychainInstalled = !getPackageManager().getPackageInfo("org.sufficientlysecure.keychain", 0).versionName.isEmpty();
139         } catch (PackageManager.NameNotFoundException exception) {
140             openKeychainInstalled = false;
141         }
142
143         // Get handles for the views that need to be modified.
144         Spinner encryptionSpinner = findViewById(R.id.encryption_spinner);
145         TextInputLayout passwordEncryptionTextInputLayout = findViewById(R.id.password_encryption_textinputlayout);
146         EditText encryptionPasswordEditText = findViewById(R.id.password_encryption_edittext);
147         TextView kitKatPasswordEncryptionTextView = findViewById(R.id.kitkat_password_encryption_textview);
148         TextView openKeychainRequiredTextView = findViewById(R.id.openkeychain_required_textview);
149         CardView fileLocationCardView = findViewById(R.id.file_location_cardview);
150         RadioButton importRadioButton = findViewById(R.id.import_radiobutton);
151         RadioButton exportRadioButton = findViewById(R.id.export_radiobutton);
152         LinearLayout fileNameLinearLayout = findViewById(R.id.file_name_linearlayout);
153         EditText fileNameEditText = findViewById(R.id.file_name_edittext);
154         TextView openKeychainImportInstructionsTextView = findViewById(R.id.openkeychain_import_instructions_textview);
155         Button importExportButton = findViewById(R.id.import_export_button);
156         TextView storagePermissionTextView = findViewById(R.id.import_export_storage_permission_textview);
157
158         // Create an array adapter for the spinner.
159         ArrayAdapter<CharSequence> encryptionArrayAdapter = ArrayAdapter.createFromResource(this, R.array.encryption_type, R.layout.spinner_item);
160
161         // Set the drop down view resource on the spinner.
162         encryptionArrayAdapter.setDropDownViewResource(R.layout.spinner_dropdown_items);
163
164         // Set the array adapter for the spinner.
165         encryptionSpinner.setAdapter(encryptionArrayAdapter);
166
167         // Initially hide the unneeded views.
168         passwordEncryptionTextInputLayout.setVisibility(View.GONE);
169         kitKatPasswordEncryptionTextView.setVisibility(View.GONE);
170         openKeychainRequiredTextView.setVisibility(View.GONE);
171         fileNameLinearLayout.setVisibility(View.GONE);
172         openKeychainImportInstructionsTextView.setVisibility(View.GONE);
173         importExportButton.setVisibility(View.GONE);
174
175         // Create strings for the default file paths.
176         String defaultFilePath;
177         String defaultPasswordEncryptionFilePath;
178
179         // Set the default file paths according to the storage permission status.
180         if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {  // The storage permission has been granted.
181             // Set the default file paths to use the external public directory.
182             defaultFilePath = Environment.getExternalStorageDirectory() + "/" + getString(R.string.settings_pbs);
183             defaultPasswordEncryptionFilePath = defaultFilePath + ".aes";
184         } else {  // The storage permission has not been granted.
185             // Set the default file paths to use the external private directory.
186             defaultFilePath = getApplicationContext().getExternalFilesDir(null) + "/" + getString(R.string.settings_pbs);
187             defaultPasswordEncryptionFilePath = defaultFilePath + ".aes";
188         }
189
190         // Set the default file path.
191         fileNameEditText.setText(defaultFilePath);
192
193         // Display the encryption information when the spinner changes.
194         encryptionSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
195             @Override
196             public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
197                 switch (position) {
198                     case NO_ENCRYPTION:
199                         // Hide the unneeded layout items.
200                         passwordEncryptionTextInputLayout.setVisibility(View.GONE);
201                         kitKatPasswordEncryptionTextView.setVisibility(View.GONE);
202                         openKeychainRequiredTextView.setVisibility(View.GONE);
203                         openKeychainImportInstructionsTextView.setVisibility(View.GONE);
204
205                         // Show the file location card.
206                         fileLocationCardView.setVisibility(View.VISIBLE);
207
208                         // Show the file name linear layout if either import or export is checked.
209                         if (importRadioButton.isChecked() || exportRadioButton.isChecked()) {
210                             fileNameLinearLayout.setVisibility(View.VISIBLE);
211                         }
212
213                         // Reset the text of the import button, which may have been changed to `Decrypt`.
214                         if (importRadioButton.isChecked()) {
215                             importExportButton.setText(R.string.import_button);
216                         }
217
218                         // Reset the default file path.
219                         fileNameEditText.setText(defaultFilePath);
220
221                         // Enable the import/export button if a file name exists.
222                         importExportButton.setEnabled(!fileNameEditText.getText().toString().isEmpty());
223                         break;
224
225                     case PASSWORD_ENCRYPTION:
226                         if (runningKitKat) {
227                             // Show the KitKat password encryption message.
228                             kitKatPasswordEncryptionTextView.setVisibility(View.VISIBLE);
229
230                             // Hide the OpenPGP required text view and the file location card.
231                             openKeychainRequiredTextView.setVisibility(View.GONE);
232                             fileLocationCardView.setVisibility(View.GONE);
233                         } else {
234                             // Hide the OpenPGP layout items.
235                             openKeychainRequiredTextView.setVisibility(View.GONE);
236                             openKeychainImportInstructionsTextView.setVisibility(View.GONE);
237
238                             // Show the password encryption layout items.
239                             passwordEncryptionTextInputLayout.setVisibility(View.VISIBLE);
240
241                             // Show the file location card.
242                             fileLocationCardView.setVisibility(View.VISIBLE);
243
244                             // Show the file name linear layout if either import or export is checked.
245                             if (importRadioButton.isChecked() || exportRadioButton.isChecked()) {
246                                 fileNameLinearLayout.setVisibility(View.VISIBLE);
247                             }
248
249                             // Reset the text of the import button, which may have been changed to `Decrypt`.
250                             if (importRadioButton.isChecked()) {
251                                 importExportButton.setText(R.string.import_button);
252                             }
253
254                             // Update the default file path.
255                             fileNameEditText.setText(defaultPasswordEncryptionFilePath);
256
257                             // Enable the import/export button if a password exists.
258                             importExportButton.setEnabled(!encryptionPasswordEditText.getText().toString().isEmpty());
259                         }
260                         break;
261
262                     case OPENPGP_ENCRYPTION:
263                         // Hide the password encryption layout items.
264                         passwordEncryptionTextInputLayout.setVisibility(View.GONE);
265                         kitKatPasswordEncryptionTextView.setVisibility(View.GONE);
266
267                         // Updated items based on the installation status of OpenKeychain.
268                         if (openKeychainInstalled) {  // OpenKeychain is installed.
269                             // Remove the default file path.
270                             fileNameEditText.setText("");
271
272                             // Show the file location card.
273                             fileLocationCardView.setVisibility(View.VISIBLE);
274
275                             if (importRadioButton.isChecked()) {
276                                 // Show the file name linear layout and the OpenKeychain import instructions.
277                                 fileNameLinearLayout.setVisibility(View.VISIBLE);
278                                 openKeychainImportInstructionsTextView.setVisibility(View.VISIBLE);
279
280                                 // Set the text of the import button to be `Decrypt`.
281                                 importExportButton.setText(R.string.decrypt);
282
283                                 // Disable the import/export button.  The user needs to select a file to import first.
284                                 importExportButton.setEnabled(false);
285                             } else if (exportRadioButton.isChecked()) {
286                                 // Hide the file name linear layout and the OpenKeychain import instructions.
287                                 fileNameLinearLayout.setVisibility(View.GONE);
288                                 openKeychainImportInstructionsTextView.setVisibility(View.GONE);
289
290                                 // Enable the import/export button.
291                                 importExportButton.setEnabled(true);
292                             }
293                         } else {  // OpenKeychain is not installed.
294                             // Show the OpenPGP required layout item.
295                             openKeychainRequiredTextView.setVisibility(View.VISIBLE);
296
297                             // Hide the file location card.
298                             fileLocationCardView.setVisibility(View.GONE);
299                         }
300                         break;
301                 }
302             }
303
304             @Override
305             public void onNothingSelected(AdapterView<?> parent) {
306
307             }
308         });
309
310         // Update the status of the import/export button when the password changes.
311         encryptionPasswordEditText.addTextChangedListener(new TextWatcher() {
312             @Override
313             public void beforeTextChanged(CharSequence s, int start, int count, int after) {
314                 // Do nothing.
315             }
316
317             @Override
318             public void onTextChanged(CharSequence s, int start, int before, int count) {
319                 // Do nothing.
320             }
321
322             @Override
323             public void afterTextChanged(Editable s) {
324                 // Enable the import/export button if a file name and password exists.
325                 importExportButton.setEnabled(!fileNameEditText.getText().toString().isEmpty() && !encryptionPasswordEditText.getText().toString().isEmpty());
326             }
327         });
328
329         // Update the status of the import/export button when the file name EditText changes.
330         fileNameEditText.addTextChangedListener(new TextWatcher() {
331             @Override
332             public void beforeTextChanged(CharSequence s, int start, int count, int after) {
333                 // Do nothing.
334             }
335
336             @Override
337             public void onTextChanged(CharSequence s, int start, int before, int count) {
338                 // Do nothing.
339             }
340
341             @Override
342             public void afterTextChanged(Editable s) {
343                 // Adjust the export button according to the encryption spinner position.
344                 switch (encryptionSpinner.getSelectedItemPosition()) {
345                     case NO_ENCRYPTION:
346                         // Enable the import/export button if a file name exists.
347                         importExportButton.setEnabled(!fileNameEditText.getText().toString().isEmpty());
348                         break;
349
350                     case PASSWORD_ENCRYPTION:
351                         // Enable the import/export button if a file name and password exists.
352                         importExportButton.setEnabled(!fileNameEditText.getText().toString().isEmpty() && !encryptionPasswordEditText.getText().toString().isEmpty());
353                         break;
354
355                     case OPENPGP_ENCRYPTION:
356                         // Enable the import/export button if OpenKeychain is installed and a file name exists.
357                         importExportButton.setEnabled(openKeychainInstalled && !fileNameEditText.getText().toString().isEmpty());
358                         break;
359                 }
360             }
361         });
362
363         // Hide the storage permissions text view on API < 23 as permissions on older devices are automatically granted.
364         if (Build.VERSION.SDK_INT < 23) {
365             storagePermissionTextView.setVisibility(View.GONE);
366         }
367     }
368
369     public void onClickRadioButton(View view) {
370         // Get handles for the views.
371         Spinner encryptionSpinner = findViewById(R.id.encryption_spinner);
372         LinearLayout fileNameLinearLayout = findViewById(R.id.file_name_linearlayout);
373         EditText fileNameEditText = findViewById(R.id.file_name_edittext);
374         TextView openKeychainImportInstructionTextView = findViewById(R.id.openkeychain_import_instructions_textview);
375         Button importExportButton = findViewById(R.id.import_export_button);
376
377         // Check to see if import or export was selected.
378         switch (view.getId()) {
379             case R.id.import_radiobutton:
380                 // Check to see if OpenPGP encryption is selected.
381                 if (encryptionSpinner.getSelectedItemPosition() == OPENPGP_ENCRYPTION) {  // OpenPGP encryption selected.
382                     // Show the OpenKeychain import instructions.
383                     openKeychainImportInstructionTextView.setVisibility(View.VISIBLE);
384
385                     // Set the text on the import/export button to be `Decrypt`.
386                     importExportButton.setText(R.string.decrypt);
387
388                     // Enable the decrypt button if there is a file name.
389                     importExportButton.setEnabled(!fileNameEditText.getText().toString().isEmpty());
390                 } else {  // OpenPGP encryption not selected.
391                     // Hide the OpenKeychain import instructions.
392                     openKeychainImportInstructionTextView.setVisibility(View.GONE);
393
394                     // Set the text on the import/export button to be `Import`.
395                     importExportButton.setText(R.string.import_button);
396                 }
397
398                 // Display the file name views.
399                 fileNameLinearLayout.setVisibility(View.VISIBLE);
400                 importExportButton.setVisibility(View.VISIBLE);
401                 break;
402
403             case R.id.export_radiobutton:
404                 // Hide the OpenKeychain import instructions.
405                 openKeychainImportInstructionTextView.setVisibility(View.GONE);
406
407                 // Set the text on the import/export button to be `Export`.
408                 importExportButton.setText(R.string.export);
409
410                 // Show the import/export button.
411                 importExportButton.setVisibility(View.VISIBLE);
412
413                 // Check to see if OpenPGP encryption is selected.
414                 if (encryptionSpinner.getSelectedItemPosition() == OPENPGP_ENCRYPTION) {  // OpenPGP encryption is selected.
415                     // Hide the file name views.
416                     fileNameLinearLayout.setVisibility(View.GONE);
417
418                     // Enable the export button.
419                     importExportButton.setEnabled(true);
420                 } else {  // OpenPGP encryption is not selected.
421                     // Show the file name views.
422                     fileNameLinearLayout.setVisibility(View.VISIBLE);
423                 }
424                 break;
425         }
426     }
427
428     public void browse(View view) {
429         // Get a handle for the import radiobutton.
430         RadioButton importRadioButton = findViewById(R.id.import_radiobutton);
431
432         // Check to see if import or export is selected.
433         if (importRadioButton.isChecked()) {  // Import is selected.
434             // Create the file picker intent.
435             Intent importBrowseIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
436
437             // Set the intent MIME type to include all files so that everything is visible.
438             importBrowseIntent.setType("*/*");
439
440             // Set the initial directory if the minimum API >= 26.
441             if (Build.VERSION.SDK_INT >= 26) {
442                 importBrowseIntent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Environment.getExternalStorageDirectory());
443             }
444
445             // Request a file that can be opened.
446             importBrowseIntent.addCategory(Intent.CATEGORY_OPENABLE);
447
448             // Launch the file picker.
449             startActivityForResult(importBrowseIntent, BROWSE_RESULT_CODE);
450         } else {  // Export is selected
451             // Create the file picker intent.
452             Intent exportBrowseIntent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
453
454             // Set the intent MIME type to include all files so that everything is visible.
455             exportBrowseIntent.setType("*/*");
456
457             // Set the initial export file name.
458             exportBrowseIntent.putExtra(Intent.EXTRA_TITLE, getString(R.string.settings_pbs));
459
460             // Set the initial directory if the minimum API >= 26.
461             if (Build.VERSION.SDK_INT >= 26) {
462                 exportBrowseIntent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Environment.getExternalStorageDirectory());
463             }
464
465             // Request a file that can be opened.
466             exportBrowseIntent.addCategory(Intent.CATEGORY_OPENABLE);
467
468             // Launch the file picker.
469             startActivityForResult(exportBrowseIntent, BROWSE_RESULT_CODE);
470         }
471     }
472
473     public void importExport(View view) {
474         // Get a handle for the views.
475         Spinner encryptionSpinner = findViewById(R.id.encryption_spinner);
476         RadioButton importRadioButton = findViewById(R.id.import_radiobutton);
477         RadioButton exportRadioButton = findViewById(R.id.export_radiobutton);
478
479         // Check to see if the storage permission is needed.
480         if ((encryptionSpinner.getSelectedItemPosition() == OPENPGP_ENCRYPTION) && exportRadioButton.isChecked()) {  // Permission not needed to export via OpenKeychain.
481             // Export the settings.
482             exportSettings();
483         } else if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {  // The storage permission has been granted.
484             // Check to see if import or export is selected.
485             if (importRadioButton.isChecked()) {  // Import is selected.
486                 // Import the settings.
487                 importSettings();
488             } else {  // Export is selected.
489                 // Export the settings.
490                 exportSettings();
491             }
492         } else {  // The storage permission has not been granted.
493             // Get a handle for the file name EditText.
494             EditText fileNameEditText = findViewById(R.id.file_name_edittext);
495
496             // Get the file name string.
497             String fileNameString = fileNameEditText.getText().toString();
498
499             // Get the external private directory `File`.
500             File externalPrivateDirectoryFile = getExternalFilesDir(null);
501
502             // Remove the incorrect lint error below that the file might be null.
503             assert externalPrivateDirectoryFile != null;
504
505             // Get the external private directory string.
506             String externalPrivateDirectory = externalPrivateDirectoryFile.toString();
507
508             // Check to see if the file path is in the external private directory.
509             if (fileNameString.startsWith(externalPrivateDirectory)) {  // The file path is in the external private directory.
510                 // Check to see if import or export is selected.
511                 if (importRadioButton.isChecked()) {  // Import is selected.
512                     // Import the settings.
513                     importSettings();
514                 } else {  // Export is selected.
515                     // Export the settings.
516                     exportSettings();
517                 }
518             } else {  // The file path is in a public directory.
519                 // Check if the user has previously denied the storage permission.
520                 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
521                     // Instantiate the storage permission alert dialog.
522                     DialogFragment storagePermissionDialogFragment = StoragePermissionDialog.displayDialog(0);
523
524                     // Show the storage permission alert dialog.  The permission will be requested when the dialog is closed.
525                     storagePermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.storage_permission));
526                 } else {  // Show the permission request directly.
527                     // Request the storage permission.  The export will be run when it finishes.
528                     ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
529                 }
530             }
531         }
532     }
533
534     @Override
535     public void onCloseStoragePermissionDialog(int type) {
536         // Request the write external storage permission.  The import/export will be run when it finishes.
537         ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
538     }
539
540     @Override
541     public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
542         // Get a handle for the import radiobutton.
543         RadioButton importRadioButton = findViewById(R.id.import_radiobutton);
544
545         // Check to see if the storage permission was granted.  If the dialog was canceled the grant results will be empty.
546         if ((grantResults.length > 0) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) {  // The storage permission was granted.
547             // Run the import or export methods according to which radio button is selected.
548             if (importRadioButton.isChecked()) {  // Import is selected.
549                 // Import the settings.
550                 importSettings();
551             } else {  // Export is selected.
552                 // Export the settings.
553                 exportSettings();
554             }
555         } else {  // The storage permission was not granted.
556             // Display an error snackbar.
557             Snackbar.make(importRadioButton, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show();
558         }
559     }
560
561     @Override
562     public void onActivityResult(int requestCode, int resultCode, Intent intent) {
563         // Run the default commands.
564         super.onActivityResult(requestCode, resultCode, intent);
565
566         switch (requestCode) {
567             case (BROWSE_RESULT_CODE):
568                 // Don't do anything if the user pressed back from the file picker.
569                 if (resultCode == Activity.RESULT_OK) {
570                     // Get a handle for the file name edit text.
571                     EditText fileNameEditText = findViewById(R.id.file_name_edittext);
572
573                     // Instantiate the file name helper.
574                     FileNameHelper fileNameHelper = new FileNameHelper();
575
576                     // Get the file path URI from the intent.
577                     Uri filePathUri = intent.getData();
578
579                     // Use the file path from the intent if it exists.
580                     if (filePathUri != null) {
581                         // Convert the file name URI to a file name path.
582                         String fileNamePath = fileNameHelper.convertUriToFileNamePath(filePathUri);
583
584                         // Set the file name path as the text of the file name edit text.
585                         fileNameEditText.setText(fileNamePath);
586                     }
587                 }
588                 break;
589
590             case OPENPGP_EXPORT_RESULT_CODE:
591                 // Get the temporary unencrypted export file.
592                 File temporaryUnencryptedExportFile = new File(getApplicationContext().getCacheDir() + "/" + getString(R.string.settings_pbs));
593
594                 // Delete the temporary unencrypted export file if it exists.
595                 if (temporaryUnencryptedExportFile.exists()) {
596                     //noinspection ResultOfMethodCallIgnored
597                     temporaryUnencryptedExportFile.delete();
598                 }
599                 break;
600         }
601     }
602
603     private void exportSettings() {
604         // Get a handle for the views.
605         Spinner encryptionSpinner = findViewById(R.id.encryption_spinner);
606         EditText fileNameEditText = findViewById(R.id.file_name_edittext);
607
608         // Instantiate the import export database helper.
609         ImportExportDatabaseHelper importExportDatabaseHelper = new ImportExportDatabaseHelper();
610
611         // Get the export file string.
612         String exportFileString = fileNameEditText.getText().toString();
613
614         // Get the export and temporary unencrypted export files.
615         File exportFile = new File(exportFileString);
616         File temporaryUnencryptedExportFile = new File(getApplicationContext().getCacheDir() + "/" + getString(R.string.settings_pbs));
617
618         // Create an export status string.
619         String exportStatus;
620
621         // Export according to the encryption type.
622         switch (encryptionSpinner.getSelectedItemPosition()) {
623             case NO_ENCRYPTION:
624                 // Export the unencrypted file.
625                 exportStatus = importExportDatabaseHelper.exportUnencrypted(exportFile, this);
626
627                 // Show a disposition snackbar.
628                 if (exportStatus.equals(ImportExportDatabaseHelper.EXPORT_SUCCESSFUL)) {
629                     Snackbar.make(fileNameEditText, getString(R.string.export_successful), Snackbar.LENGTH_SHORT).show();
630                 } else {
631                     Snackbar.make(fileNameEditText, getString(R.string.export_failed) + "  " + exportStatus, Snackbar.LENGTH_INDEFINITE).show();
632                 }
633                 break;
634
635             case PASSWORD_ENCRYPTION:
636                 // Create an unencrypted export in a private directory.
637                 exportStatus = importExportDatabaseHelper.exportUnencrypted(temporaryUnencryptedExportFile, this);
638
639                 try {
640                     // Create an unencrypted export file input stream.
641                     FileInputStream unencryptedExportFileInputStream = new FileInputStream(temporaryUnencryptedExportFile);
642
643                     // Delete the encrypted export file if it exists.
644                     if (exportFile.exists()) {
645                         //noinspection ResultOfMethodCallIgnored
646                         exportFile.delete();
647                     }
648
649                     // Create an encrypted export file output stream.
650                     FileOutputStream encryptedExportFileOutputStream = new FileOutputStream(exportFile);
651
652                     // Get a handle for the encryption password EditText.
653                     EditText encryptionPasswordEditText = findViewById(R.id.password_encryption_edittext);
654
655                     // Get the encryption password.
656                     String encryptionPasswordString = encryptionPasswordEditText.getText().toString();
657
658                     // Initialize a secure random number generator.
659                     SecureRandom secureRandom = new SecureRandom();
660
661                     // Get a 256 bit (32 byte) random salt.
662                     byte[] saltByteArray = new byte[32];
663                     secureRandom.nextBytes(saltByteArray);
664
665                     // Convert the encryption password to a byte array.
666                     byte[] encryptionPasswordByteArray = encryptionPasswordString.getBytes(StandardCharsets.UTF_8);
667
668                     // Append the salt to the encryption password byte array.  This protects against rainbow table attacks.
669                     byte[] encryptionPasswordWithSaltByteArray = new byte[encryptionPasswordByteArray.length + saltByteArray.length];
670                     System.arraycopy(encryptionPasswordByteArray, 0, encryptionPasswordWithSaltByteArray, 0, encryptionPasswordByteArray.length);
671                     System.arraycopy(saltByteArray, 0, encryptionPasswordWithSaltByteArray, encryptionPasswordByteArray.length, saltByteArray.length);
672
673                     // Get a SHA-512 message digest.
674                     MessageDigest messageDigest = MessageDigest.getInstance("SHA-512");
675
676                     // Hash the salted encryption password.  Otherwise, any characters after the 32nd character in the password are ignored.
677                     byte[] hashedEncryptionPasswordWithSaltByteArray = messageDigest.digest(encryptionPasswordWithSaltByteArray);
678
679                     // Truncate the encryption password byte array to 256 bits (32 bytes).
680                     byte[] truncatedHashedEncryptionPasswordWithSaltByteArray = Arrays.copyOf(hashedEncryptionPasswordWithSaltByteArray, 32);
681
682                     // Create an AES secret key from the encryption password byte array.
683                     SecretKeySpec secretKey = new SecretKeySpec(truncatedHashedEncryptionPasswordWithSaltByteArray, "AES");
684
685                     // Generate a random 12 byte initialization vector.  According to NIST, a 12 byte initialization vector is more secure than a 16 byte one.
686                     byte[] initializationVector = new byte[12];
687                     secureRandom.nextBytes(initializationVector);
688
689                     // 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.
690                     Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
691
692                     // Set the GCM tag length to be 128 bits (the maximum) and apply the initialization vector.
693                     GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, initializationVector);
694
695                     // Initialize the cipher.
696                     cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmParameterSpec);
697
698                     // Add the salt and the initialization vector to the export file.
699                     encryptedExportFileOutputStream.write(saltByteArray);
700                     encryptedExportFileOutputStream.write(initializationVector);
701
702                     // Create a cipher output stream.
703                     CipherOutputStream cipherOutputStream = new CipherOutputStream(encryptedExportFileOutputStream, cipher);
704
705                     // 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.
706                     int numberOfBytesRead;
707                     byte[] encryptedBytes = new byte[16];
708
709                     // 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.
710                     while ((numberOfBytesRead = unencryptedExportFileInputStream.read(encryptedBytes)) != -1) {
711                         // Write the data to the cipher output stream.
712                         cipherOutputStream.write(encryptedBytes, 0, numberOfBytesRead);
713                     }
714
715                     // Close the streams.
716                     cipherOutputStream.flush();
717                     cipherOutputStream.close();
718                     encryptedExportFileOutputStream.close();
719                     unencryptedExportFileInputStream.close();
720
721                     // Wipe the encryption data from memory.
722                     //noinspection UnusedAssignment
723                     encryptionPasswordString = "";
724                     Arrays.fill(saltByteArray, (byte) 0);
725                     Arrays.fill(encryptionPasswordByteArray, (byte) 0);
726                     Arrays.fill(encryptionPasswordWithSaltByteArray, (byte) 0);
727                     Arrays.fill(hashedEncryptionPasswordWithSaltByteArray, (byte) 0);
728                     Arrays.fill(truncatedHashedEncryptionPasswordWithSaltByteArray, (byte) 0);
729                     Arrays.fill(initializationVector, (byte) 0);
730                     Arrays.fill(encryptedBytes, (byte) 0);
731
732                     // Delete the temporary unencrypted export file.
733                     //noinspection ResultOfMethodCallIgnored
734                     temporaryUnencryptedExportFile.delete();
735                 } catch (Exception exception) {
736                     exportStatus = exception.toString();
737                 }
738
739                 // Show a disposition snackbar.
740                 if (exportStatus.equals(ImportExportDatabaseHelper.EXPORT_SUCCESSFUL)) {
741                     Snackbar.make(fileNameEditText, getString(R.string.export_successful), Snackbar.LENGTH_SHORT).show();
742                 } else {
743                     Snackbar.make(fileNameEditText, getString(R.string.export_failed) + "  " + exportStatus, Snackbar.LENGTH_INDEFINITE).show();
744                 }
745                 break;
746
747             case OPENPGP_ENCRYPTION:
748                 // Create an unencrypted export in the private location.
749                 importExportDatabaseHelper.exportUnencrypted(temporaryUnencryptedExportFile, this);
750
751                 // Create an encryption intent for OpenKeychain.
752                 Intent openKeychainEncryptIntent = new Intent("org.sufficientlysecure.keychain.action.ENCRYPT_DATA");
753
754                 // Include the temporary unencrypted export file URI.
755                 openKeychainEncryptIntent.setData(FileProvider.getUriForFile(this, getString(R.string.file_provider), temporaryUnencryptedExportFile));
756
757                 // Allow OpenKeychain to read the file URI.
758                 openKeychainEncryptIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
759
760                 // Send the intent to the OpenKeychain package.
761                 openKeychainEncryptIntent.setPackage("org.sufficientlysecure.keychain");
762
763                 // Make it so.
764                 startActivityForResult(openKeychainEncryptIntent, OPENPGP_EXPORT_RESULT_CODE);
765                 break;
766         }
767
768         // Add the file to the list of recent files.  This doesn't currently work, but maybe it will someday.
769         MediaScannerConnection.scanFile(this, new String[] {exportFileString}, new String[] {"application/x-sqlite3"}, null);
770     }
771
772     private void importSettings() {
773         // Get a handle for the views.
774         Spinner encryptionSpinner = findViewById(R.id.encryption_spinner);
775         EditText fileNameEditText = findViewById(R.id.file_name_edittext);
776
777         // Instantiate the import export database helper.
778         ImportExportDatabaseHelper importExportDatabaseHelper = new ImportExportDatabaseHelper();
779
780         // Get the import file.
781         File importFile = new File(fileNameEditText.getText().toString());
782
783         // Initialize the import status string
784         String importStatus = "";
785
786         // Import according to the encryption type.
787         switch (encryptionSpinner.getSelectedItemPosition()) {
788             case NO_ENCRYPTION:
789                 // Import the unencrypted file.
790                 importStatus = importExportDatabaseHelper.importUnencrypted(importFile, this);
791                 break;
792
793             case PASSWORD_ENCRYPTION:
794                 // Use a private temporary import location.
795                 File temporaryUnencryptedImportFile = new File(getApplicationContext().getCacheDir() + "/" + getString(R.string.settings_pbs));
796
797                 try {
798                     // Create an encrypted import file input stream.
799                     FileInputStream encryptedImportFileInputStream = new FileInputStream(importFile);
800
801                     // Delete the temporary import file if it exists.
802                     if (temporaryUnencryptedImportFile.exists()) {
803                         //noinspection ResultOfMethodCallIgnored
804                         temporaryUnencryptedImportFile.delete();
805                     }
806
807                     // Create an unencrypted import file output stream.
808                     FileOutputStream unencryptedImportFileOutputStream = new FileOutputStream(temporaryUnencryptedImportFile);
809
810                     // Get a handle for the encryption password EditText.
811                     EditText encryptionPasswordEditText = findViewById(R.id.password_encryption_edittext);
812
813                     // Get the encryption password.
814                     String encryptionPasswordString = encryptionPasswordEditText.getText().toString();
815
816                     // Get the salt from the beginning of the import file.
817                     byte[] saltByteArray = new byte[32];
818                     //noinspection ResultOfMethodCallIgnored
819                     encryptedImportFileInputStream.read(saltByteArray);
820
821                     // Get the initialization vector from the import file.
822                     byte[] initializationVector = new byte[12];
823                     //noinspection ResultOfMethodCallIgnored
824                     encryptedImportFileInputStream.read(initializationVector);
825
826                     // Convert the encryption password to a byte array.
827                     byte[] encryptionPasswordByteArray = encryptionPasswordString.getBytes(StandardCharsets.UTF_8);
828
829                     // Append the salt to the encryption password byte array.  This protects against rainbow table attacks.
830                     byte[] encryptionPasswordWithSaltByteArray = new byte[encryptionPasswordByteArray.length + saltByteArray.length];
831                     System.arraycopy(encryptionPasswordByteArray, 0, encryptionPasswordWithSaltByteArray, 0, encryptionPasswordByteArray.length);
832                     System.arraycopy(saltByteArray, 0, encryptionPasswordWithSaltByteArray, encryptionPasswordByteArray.length, saltByteArray.length);
833
834                     // Get a SHA-512 message digest.
835                     MessageDigest messageDigest = MessageDigest.getInstance("SHA-512");
836
837                     // Hash the salted encryption password.  Otherwise, any characters after the 32nd character in the password are ignored.
838                     byte[] hashedEncryptionPasswordWithSaltByteArray = messageDigest.digest(encryptionPasswordWithSaltByteArray);
839
840                     // Truncate the encryption password byte array to 256 bits (32 bytes).
841                     byte[] truncatedHashedEncryptionPasswordWithSaltByteArray = Arrays.copyOf(hashedEncryptionPasswordWithSaltByteArray, 32);
842
843                     // Create an AES secret key from the encryption password byte array.
844                     SecretKeySpec secretKey = new SecretKeySpec(truncatedHashedEncryptionPasswordWithSaltByteArray, "AES");
845
846                     // 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.
847                     Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
848
849                     // Set the GCM tag length to be 128 bits (the maximum) and apply the initialization vector.
850                     GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, initializationVector);
851
852                     // Initialize the cipher.
853                     cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec);
854
855                     // Create a cipher input stream.
856                     CipherInputStream cipherInputStream = new CipherInputStream(encryptedImportFileInputStream, cipher);
857
858                     // 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.
859                     int numberOfBytesRead;
860                     byte[] decryptedBytes = new byte[16];
861
862                     // 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.
863                     while ((numberOfBytesRead = cipherInputStream.read(decryptedBytes)) != -1) {
864                         // Write the data to the unencrypted import file output stream.
865                         unencryptedImportFileOutputStream.write(decryptedBytes, 0, numberOfBytesRead);
866                     }
867
868                     // Close the streams.
869                     unencryptedImportFileOutputStream.flush();
870                     unencryptedImportFileOutputStream.close();
871                     cipherInputStream.close();
872                     encryptedImportFileInputStream.close();
873
874                     // Wipe the encryption data from memory.
875                     //noinspection UnusedAssignment
876                     encryptionPasswordString = "";
877                     Arrays.fill(saltByteArray, (byte) 0);
878                     Arrays.fill(initializationVector, (byte) 0);
879                     Arrays.fill(encryptionPasswordByteArray, (byte) 0);
880                     Arrays.fill(encryptionPasswordWithSaltByteArray, (byte) 0);
881                     Arrays.fill(hashedEncryptionPasswordWithSaltByteArray, (byte) 0);
882                     Arrays.fill(truncatedHashedEncryptionPasswordWithSaltByteArray, (byte) 0);
883                     Arrays.fill(decryptedBytes, (byte) 0);
884
885                     // Import the unencrypted database from the private location.
886                     importStatus = importExportDatabaseHelper.importUnencrypted(temporaryUnencryptedImportFile, this);
887
888                     // Delete the temporary unencrypted import file.
889                     //noinspection ResultOfMethodCallIgnored
890                     temporaryUnencryptedImportFile.delete();
891                 } catch (Exception exception) {
892                     importStatus = exception.toString();
893                 }
894                 break;
895
896             case OPENPGP_ENCRYPTION:
897                 try {
898                     // Create an decryption intent for OpenKeychain.
899                     Intent openKeychainDecryptIntent = new Intent("org.sufficientlysecure.keychain.action.DECRYPT_DATA");
900
901                     // Include the URI to be decrypted.
902                     openKeychainDecryptIntent.setData(FileProvider.getUriForFile(this, getString(R.string.file_provider), importFile));
903
904                     // Allow OpenKeychain to read the file URI.
905                     openKeychainDecryptIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
906
907                     // Send the intent to the OpenKeychain package.
908                     openKeychainDecryptIntent.setPackage("org.sufficientlysecure.keychain");
909
910                     // Make it so.
911                     startActivity(openKeychainDecryptIntent);
912                 } catch (IllegalArgumentException exception) {  // The file import location is not valid.
913                     // Display a snack bar with the import error.
914                     Snackbar.make(fileNameEditText, getString(R.string.import_failed) + "  " + exception.toString(), Snackbar.LENGTH_INDEFINITE).show();
915                 }
916                 break;
917         }
918
919         // Respond to the import disposition.
920         if (importStatus.equals(ImportExportDatabaseHelper.IMPORT_SUCCESSFUL)) {  // The import was successful.
921             // Create an intent to restart Privacy Browser.
922             Intent restartIntent = getParentActivityIntent();
923
924             // Assert that the intent is not null to remove the lint error below.
925             assert restartIntent != null;
926
927             // `Intent.FLAG_ACTIVITY_CLEAR_TASK` removes all activities from the stack.  It requires `Intent.FLAG_ACTIVITY_NEW_TASK`.
928             restartIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
929
930             // Create a restart handler.
931             Handler restartHandler = new Handler();
932
933             // Create a restart runnable.
934             Runnable restartRunnable =  () -> {
935                 // Restart Privacy Browser.
936                 startActivity(restartIntent);
937
938                 // Kill this instance of Privacy Browser.  Otherwise, the app exhibits sporadic behavior after the restart.
939                 System.exit(0);
940             };
941
942             // Restart Privacy Browser after 150 milliseconds to allow enough time for the preferences to be saved.
943             restartHandler.postDelayed(restartRunnable, 150);
944
945         } else if (!(encryptionSpinner.getSelectedItemPosition() == OPENPGP_ENCRYPTION)){  // The import was not successful.
946             // Display a snack bar with the import error.
947             Snackbar.make(fileNameEditText, getString(R.string.import_failed) + "  " + importStatus, Snackbar.LENGTH_INDEFINITE).show();
948         }
949     }
950 }