]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/activities/ImportExportActivity.java
df41cf31f6502f5aa86f506e3be11ab3dbac9285
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / activities / ImportExportActivity.java
1 /*
2  * Copyright © 2018-2020 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 fileDoesNotExistTextView = findViewById(R.id.file_does_not_exist_textview);
155         TextView fileExistsWarningTextView = findViewById(R.id.file_exists_warning_textview);
156         TextView openKeychainImportInstructionsTextView = findViewById(R.id.openkeychain_import_instructions_textview);
157         Button importExportButton = findViewById(R.id.import_export_button);
158         TextView storagePermissionTextView = findViewById(R.id.import_export_storage_permission_textview);
159
160         // Create an array adapter for the spinner.
161         ArrayAdapter<CharSequence> encryptionArrayAdapter = ArrayAdapter.createFromResource(this, R.array.encryption_type, R.layout.spinner_item);
162
163         // Set the drop down view resource on the spinner.
164         encryptionArrayAdapter.setDropDownViewResource(R.layout.spinner_dropdown_items);
165
166         // Set the array adapter for the spinner.
167         encryptionSpinner.setAdapter(encryptionArrayAdapter);
168
169         // Initially hide the unneeded views.
170         passwordEncryptionTextInputLayout.setVisibility(View.GONE);
171         kitKatPasswordEncryptionTextView.setVisibility(View.GONE);
172         openKeychainRequiredTextView.setVisibility(View.GONE);
173         fileNameLinearLayout.setVisibility(View.GONE);
174         fileDoesNotExistTextView.setVisibility(View.GONE);
175         fileExistsWarningTextView.setVisibility(View.GONE);
176         openKeychainImportInstructionsTextView.setVisibility(View.GONE);
177         importExportButton.setVisibility(View.GONE);
178
179         // Create strings for the default file paths.
180         String defaultFilePath;
181         String defaultPasswordEncryptionFilePath;
182         String defaultPgpFilePath;
183
184         // Set the default file paths according to the storage permission status.
185         if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {  // The storage permission has been granted.
186             // Set the default file paths to use the external public directory.
187             defaultFilePath = Environment.getExternalStorageDirectory() + "/" + getString(R.string.settings_pbs);
188             defaultPasswordEncryptionFilePath = defaultFilePath + ".aes";
189             defaultPgpFilePath = defaultFilePath + ".pgp";
190
191             // Hide the storage permission text view.
192             storagePermissionTextView.setVisibility(View.GONE);
193         } else {  // The storage permission has not been granted.
194             // Set the default file paths to use the external private directory.
195             defaultFilePath = getApplicationContext().getExternalFilesDir(null) + "/" + getString(R.string.settings_pbs);
196             defaultPasswordEncryptionFilePath = defaultFilePath + ".aes";
197             defaultPgpFilePath = defaultFilePath + ".pgp";
198         }
199
200         // Set the default file path.
201         fileNameEditText.setText(defaultFilePath);
202
203         // Update the UI when the spinner changes.
204         encryptionSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
205             @Override
206             public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
207                 switch (position) {
208                     case NO_ENCRYPTION:
209                         // Hide the unneeded layout items.
210                         passwordEncryptionTextInputLayout.setVisibility(View.GONE);
211                         kitKatPasswordEncryptionTextView.setVisibility(View.GONE);
212                         openKeychainRequiredTextView.setVisibility(View.GONE);
213                         openKeychainImportInstructionsTextView.setVisibility(View.GONE);
214
215                         // Show the file location card.
216                         fileLocationCardView.setVisibility(View.VISIBLE);
217
218                         // Show the file name linear layout if either import or export is checked.
219                         if (importRadioButton.isChecked() || exportRadioButton.isChecked()) {
220                             fileNameLinearLayout.setVisibility(View.VISIBLE);
221                         }
222
223                         // Reset the text of the import button, which may have been changed to `Decrypt`.
224                         if (importRadioButton.isChecked()) {
225                             importExportButton.setText(R.string.import_button);
226                         }
227
228                         // Reset the default file path.
229                         fileNameEditText.setText(defaultFilePath);
230                         break;
231
232                     case PASSWORD_ENCRYPTION:
233                         if (runningKitKat) {
234                             // Show the KitKat password encryption message.
235                             kitKatPasswordEncryptionTextView.setVisibility(View.VISIBLE);
236
237                             // Hide the OpenPGP required text view and the file location card.
238                             openKeychainRequiredTextView.setVisibility(View.GONE);
239                             fileLocationCardView.setVisibility(View.GONE);
240                         } else {
241                             // Hide the OpenPGP layout items.
242                             openKeychainRequiredTextView.setVisibility(View.GONE);
243                             openKeychainImportInstructionsTextView.setVisibility(View.GONE);
244
245                             // Show the password encryption layout items.
246                             passwordEncryptionTextInputLayout.setVisibility(View.VISIBLE);
247
248                             // Show the file location card.
249                             fileLocationCardView.setVisibility(View.VISIBLE);
250
251                             // Show the file name linear layout if either import or export is checked.
252                             if (importRadioButton.isChecked() || exportRadioButton.isChecked()) {
253                                 fileNameLinearLayout.setVisibility(View.VISIBLE);
254                             }
255
256                             // Reset the text of the import button, which may have been changed to `Decrypt`.
257                             if (importRadioButton.isChecked()) {
258                                 importExportButton.setText(R.string.import_button);
259                             }
260
261                             // Update the default file path.
262                             fileNameEditText.setText(defaultPasswordEncryptionFilePath);
263                         }
264                         break;
265
266                     case OPENPGP_ENCRYPTION:
267                         // Hide the password encryption layout items.
268                         passwordEncryptionTextInputLayout.setVisibility(View.GONE);
269                         kitKatPasswordEncryptionTextView.setVisibility(View.GONE);
270
271                         // Updated items based on the installation status of OpenKeychain.
272                         if (openKeychainInstalled) {  // OpenKeychain is installed.
273                             // Update the default file path.
274                             fileNameEditText.setText(defaultPgpFilePath);
275
276                             // Show the file location card.
277                             fileLocationCardView.setVisibility(View.VISIBLE);
278
279                             if (importRadioButton.isChecked()) {
280                                 // Show the file name linear layout and the OpenKeychain import instructions.
281                                 fileNameLinearLayout.setVisibility(View.VISIBLE);
282                                 openKeychainImportInstructionsTextView.setVisibility(View.VISIBLE);
283
284                                 // Set the text of the import button to be `Decrypt`.
285                                 importExportButton.setText(R.string.decrypt);
286                             } else if (exportRadioButton.isChecked()) {
287                                 // Hide the file name linear layout and the OpenKeychain import instructions.
288                                 fileNameLinearLayout.setVisibility(View.GONE);
289                                 openKeychainImportInstructionsTextView.setVisibility(View.GONE);
290                             }
291                         } else {  // OpenKeychain is not installed.
292                             // Show the OpenPGP required layout item.
293                             openKeychainRequiredTextView.setVisibility(View.VISIBLE);
294
295                             // Hide the file location card.
296                             fileLocationCardView.setVisibility(View.GONE);
297                         }
298                         break;
299                 }
300             }
301
302             @Override
303             public void onNothingSelected(AdapterView<?> parent) {
304
305             }
306         });
307
308         // Update the status of the import/export button when the password changes.
309         encryptionPasswordEditText.addTextChangedListener(new TextWatcher() {
310             @Override
311             public void beforeTextChanged(CharSequence s, int start, int count, int after) {
312                 // Do nothing.
313             }
314
315             @Override
316             public void onTextChanged(CharSequence s, int start, int before, int count) {
317                 // Do nothing.
318             }
319
320             @Override
321             public void afterTextChanged(Editable s) {
322                 // Get the current file name.
323                 String fileNameString = fileNameEditText.getText().toString();
324
325                 // Convert the file name string to a file.
326                 File file = new File(fileNameString);
327
328                 // Update the import/export button.
329                 if (importRadioButton.isChecked()) {  // The import radio button is checked.
330                     // Enable the import button if the file and the password exists.
331                     importExportButton.setEnabled(file.exists() && !encryptionPasswordEditText.getText().toString().isEmpty());
332                 } else if (exportRadioButton.isChecked()) {  // The export radio button is checked.
333                     // Enable the export button if the file string and the password exists.
334                     importExportButton.setEnabled(!fileNameString.isEmpty() && !encryptionPasswordEditText.getText().toString().isEmpty());
335                 }
336             }
337         });
338
339         // Update the UI when the file name EditText changes.
340         fileNameEditText.addTextChangedListener(new TextWatcher() {
341             @Override
342             public void beforeTextChanged(CharSequence s, int start, int count, int after) {
343                 // Do nothing.
344             }
345
346             @Override
347             public void onTextChanged(CharSequence s, int start, int before, int count) {
348                 // Do nothing.
349             }
350
351             @Override
352             public void afterTextChanged(Editable s) {
353                 // Get the current file name.
354                 String fileNameString = fileNameEditText.getText().toString();
355
356                 // Convert the file name string to a file.
357                 File file = new File(fileNameString);
358
359                 // Adjust the UI according to the encryption spinner position.
360                 switch (encryptionSpinner.getSelectedItemPosition()) {
361                     case NO_ENCRYPTION:
362                         // Determine if import or export is checked.
363                         if (exportRadioButton.isChecked()) {  // The export radio button is checked.
364                             // Hide the file does not exist text view.
365                             fileDoesNotExistTextView.setVisibility(View.GONE);
366
367                             // Display a warning if the file already exists.
368                             if (file.exists()) {
369                                 fileExistsWarningTextView.setVisibility(View.VISIBLE);
370                             } else {
371                                 fileExistsWarningTextView.setVisibility(View.GONE);
372                             }
373
374                             // Enable the export button if the file name is populated.
375                             importExportButton.setEnabled(!fileNameString.isEmpty());
376                         } else if (importRadioButton.isChecked()) {  // The import radio button is checked.
377                             // Hide the file exists warning text view.
378                             fileExistsWarningTextView.setVisibility(View.GONE);
379
380                             // Check if the file exists.
381                             if (file.exists()) {  // The file exists.
382                                 // Hide the notification that the file does not exist.
383                                 fileDoesNotExistTextView.setVisibility(View.GONE);
384
385                                 // Enable the import button.
386                                 importExportButton.setEnabled(true);
387                             } else {  // The file does not exist.
388                                 // Show a notification that the file does not exist.
389                                 fileDoesNotExistTextView.setVisibility(View.VISIBLE);
390
391                                 // Disable the import button.
392                                 importExportButton.setEnabled(false);
393                             }
394                         } else {  // Neither radio button is checked.
395                             // Hide the file notification text views.
396                             fileExistsWarningTextView.setVisibility(View.GONE);
397                             fileDoesNotExistTextView.setVisibility(View.GONE);
398                         }
399                         break;
400
401                     case PASSWORD_ENCRYPTION:
402                         // Determine if import or export is checked.
403                         if (exportRadioButton.isChecked()) {  // The export radio button is checked.
404                             // Hide the notification that the file does not exist.
405                             fileDoesNotExistTextView.setVisibility(View.GONE);
406
407                             // Display a warning if the file already exists.
408                             if (file.exists()) {
409                                 fileExistsWarningTextView.setVisibility(View.VISIBLE);
410                             } else {
411                                 fileExistsWarningTextView.setVisibility(View.GONE);
412                             }
413
414                             // Enable the export button if the file name and the password are populated.
415                             importExportButton.setEnabled(!fileNameString.isEmpty() && !encryptionPasswordEditText.getText().toString().isEmpty());
416                         } else if (importRadioButton.isChecked()) {  // The import radio button is checked.
417                             // Hide the file exists warning text view.
418                             fileExistsWarningTextView.setVisibility(View.GONE);
419
420                             // Check if the file exists.
421                             if (file.exists()) {  // The file exists.
422                                 // Hide the notification that the file does not exist.
423                                 fileDoesNotExistTextView.setVisibility(View.GONE);
424
425                                 // Enable the import button if the password is populated.
426                                 importExportButton.setEnabled(!encryptionPasswordEditText.getText().toString().isEmpty());
427                             } else {  // The file does not exist.
428                                 // Show a notification that the file does not exist.
429                                 fileDoesNotExistTextView.setVisibility(View.VISIBLE);
430
431                                 // Disable the import button.
432                                 importExportButton.setEnabled(false);
433                             }
434                         } else {  // Neither radio button is checked.
435                             // Hide the file notification text views.
436                             fileExistsWarningTextView.setVisibility(View.GONE);
437                             fileDoesNotExistTextView.setVisibility(View.GONE);
438                         }
439                         break;
440
441                     case OPENPGP_ENCRYPTION:
442                         // Hide the file exists warning text view.
443                         fileExistsWarningTextView.setVisibility(View.GONE);
444
445                         if (importRadioButton.isChecked()) {  // The import radio button is checked.
446                             if (file.exists()) {  // The file exists.
447                                 // Hide the notification that the file does not exist.
448                                 fileDoesNotExistTextView.setVisibility(View.GONE);
449
450                                 // Enable the import button if OpenKeychain is installed.
451                                 importExportButton.setEnabled(openKeychainInstalled);
452                             } else {  // The file does not exist.
453                                 // Show the notification that the file does not exist.
454                                 fileDoesNotExistTextView.setVisibility(View.VISIBLE);
455
456                                 // Disable the import button.
457                                 importExportButton.setEnabled(false);
458                             }
459                         } else if (exportRadioButton.isChecked()){  // The export radio button is checked.
460                             // Hide the notification that the file does not exist.
461                             fileDoesNotExistTextView.setVisibility(View.GONE);
462
463                             // Enable the export button.
464                             importExportButton.setEnabled(true);
465                         } else {  // Neither radio button is checked.
466                             // Hide the notification that the file does not exist.
467                             fileDoesNotExistTextView.setVisibility(View.GONE);
468                         }
469                         break;
470                 }
471             }
472         });
473     }
474
475     public void onClickRadioButton(View view) {
476         // Get handles for the views.
477         Spinner encryptionSpinner = findViewById(R.id.encryption_spinner);
478         LinearLayout fileNameLinearLayout = findViewById(R.id.file_name_linearlayout);
479         EditText passwordEncryptionEditText = findViewById(R.id.password_encryption_edittext);
480         EditText fileNameEditText = findViewById(R.id.file_name_edittext);
481         TextView fileDoesNotExistTextView = findViewById(R.id.file_does_not_exist_textview);
482         TextView fileExistsWarningTextView = findViewById(R.id.file_exists_warning_textview);
483         TextView openKeychainImportInstructionTextView = findViewById(R.id.openkeychain_import_instructions_textview);
484         Button importExportButton = findViewById(R.id.import_export_button);
485
486         // Get the current file name.
487         String fileNameString = fileNameEditText.getText().toString();
488
489         // Convert the file name string to a file.
490         File file = new File(fileNameString);
491
492         // Check to see if import or export was selected.
493         switch (view.getId()) {
494             case R.id.import_radiobutton:
495                 // Check to see if OpenPGP encryption is selected.
496                 if (encryptionSpinner.getSelectedItemPosition() == OPENPGP_ENCRYPTION) {  // OpenPGP encryption selected.
497                     // Show the OpenKeychain import instructions.
498                     openKeychainImportInstructionTextView.setVisibility(View.VISIBLE);
499
500                     // Set the text on the import/export button to be `Decrypt`.
501                     importExportButton.setText(R.string.decrypt);
502                 } else {  // OpenPGP encryption not selected.
503                     // Hide the OpenKeychain import instructions.
504                     openKeychainImportInstructionTextView.setVisibility(View.GONE);
505
506                     // Set the text on the import/export button to be `Import`.
507                     importExportButton.setText(R.string.import_button);
508                 }
509
510                 // Hide the file exists warning text view.
511                 fileExistsWarningTextView.setVisibility(View.GONE);
512
513                 // Display the file name views.
514                 fileNameLinearLayout.setVisibility(View.VISIBLE);
515                 importExportButton.setVisibility(View.VISIBLE);
516
517                 // Check to see if the file exists.
518                 if (file.exists()) {  // The file exists.
519                     // Hide the notification that the file does not exist.
520                     fileDoesNotExistTextView.setVisibility(View.GONE);
521
522                     // Check to see if password encryption is selected.
523                     if (encryptionSpinner.getSelectedItemPosition() == PASSWORD_ENCRYPTION) {  // Password encryption is selected.
524                         // Enable the import button if the encryption password is populated.
525                         importExportButton.setEnabled(!passwordEncryptionEditText.getText().toString().isEmpty());
526                     } else {  // Password encryption is not selected.
527                         // Enable the import/decrypt button.
528                         importExportButton.setEnabled(true);
529                     }
530                 } else {  // The file does not exist.
531                     // Show the notification that the file does not exist.
532                     fileDoesNotExistTextView.setVisibility(View.VISIBLE);
533
534                     // Disable the import/decrypt button.
535                     importExportButton.setEnabled(false);
536                 }
537                 break;
538
539             case R.id.export_radiobutton:
540                 // Hide the OpenKeychain import instructions.
541                 openKeychainImportInstructionTextView.setVisibility(View.GONE);
542
543                 // Set the text on the import/export button to be `Export`.
544                 importExportButton.setText(R.string.export);
545
546                 // Show the import/export button.
547                 importExportButton.setVisibility(View.VISIBLE);
548
549                 // Check to see if OpenPGP encryption is selected.
550                 if (encryptionSpinner.getSelectedItemPosition() == OPENPGP_ENCRYPTION) {  // OpenPGP encryption is selected.
551                     // Hide the file name views.
552                     fileNameLinearLayout.setVisibility(View.GONE);
553                     fileDoesNotExistTextView.setVisibility(View.GONE);
554                     fileExistsWarningTextView.setVisibility(View.GONE);
555
556                     // Enable the export button.
557                     importExportButton.setEnabled(true);
558                 } else {  // OpenPGP encryption is not selected.
559                     // Show the file name view.
560                     fileNameLinearLayout.setVisibility(View.VISIBLE);
561
562                     // Hide the notification that the file name does not exist.
563                     fileDoesNotExistTextView.setVisibility(View.GONE);
564
565                     // Display a warning if the file already exists.
566                     if (file.exists()) {
567                         fileExistsWarningTextView.setVisibility(View.VISIBLE);
568                     } else {
569                         fileExistsWarningTextView.setVisibility(View.GONE);
570                     }
571
572                     // Check the encryption type.
573                     if (encryptionSpinner.getSelectedItemPosition() == NO_ENCRYPTION) {  // No encryption is selected.
574                         // Enable the export button if the file name is populated.
575                         importExportButton.setEnabled(!fileNameString.isEmpty());
576                     } else {  // Password encryption is selected.
577                         // Enable the export button if the file name and the password are populated.
578                         importExportButton.setEnabled(!fileNameString.isEmpty() && !passwordEncryptionEditText.getText().toString().isEmpty());
579                     }
580                 }
581                 break;
582         }
583     }
584
585     public void browse(View view) {
586         // Get a handle for the views.
587         Spinner encryptionSpinner = findViewById(R.id.encryption_spinner);
588         RadioButton importRadioButton = findViewById(R.id.import_radiobutton);
589
590         // Check to see if import or export is selected.
591         if (importRadioButton.isChecked()) {  // Import is selected.
592             // Create the file picker intent.
593             Intent importBrowseIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
594
595             // Set the intent MIME type to include all files so that everything is visible.
596             importBrowseIntent.setType("*/*");
597
598             // Set the initial directory if the minimum API >= 26.
599             if (Build.VERSION.SDK_INT >= 26) {
600                 importBrowseIntent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Environment.getExternalStorageDirectory());
601             }
602
603             // Request a file that can be opened.
604             importBrowseIntent.addCategory(Intent.CATEGORY_OPENABLE);
605
606             // Launch the file picker.
607             startActivityForResult(importBrowseIntent, BROWSE_RESULT_CODE);
608         } else {  // Export is selected
609             // Create the file picker intent.
610             Intent exportBrowseIntent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
611
612             // Set the intent MIME type to include all files so that everything is visible.
613             exportBrowseIntent.setType("*/*");
614
615             // Set the initial export file name according to the encryption type.
616             if (encryptionSpinner.getSelectedItemPosition() == NO_ENCRYPTION) {  // No encryption is selected.
617                 exportBrowseIntent.putExtra(Intent.EXTRA_TITLE, getString(R.string.settings_pbs));
618             } else {  // Password encryption is selected.
619                 exportBrowseIntent.putExtra(Intent.EXTRA_TITLE, getString(R.string.settings_pbs) + ".aes");
620             }
621
622             // Set the initial directory if the minimum API >= 26.
623             if (Build.VERSION.SDK_INT >= 26) {
624                 exportBrowseIntent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Environment.getExternalStorageDirectory());
625             }
626
627             // Request a file that can be opened.
628             exportBrowseIntent.addCategory(Intent.CATEGORY_OPENABLE);
629
630             // Launch the file picker.
631             startActivityForResult(exportBrowseIntent, BROWSE_RESULT_CODE);
632         }
633     }
634
635     public void importExport(View view) {
636         // Get a handle for the views.
637         Spinner encryptionSpinner = findViewById(R.id.encryption_spinner);
638         RadioButton importRadioButton = findViewById(R.id.import_radiobutton);
639         RadioButton exportRadioButton = findViewById(R.id.export_radiobutton);
640
641         // Check to see if the storage permission is needed.
642         if ((encryptionSpinner.getSelectedItemPosition() == OPENPGP_ENCRYPTION) && exportRadioButton.isChecked()) {  // Permission not needed to export via OpenKeychain.
643             // Export the settings.
644             exportSettings();
645         } else if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {  // The storage permission has been granted.
646             // Check to see if import or export is selected.
647             if (importRadioButton.isChecked()) {  // Import is selected.
648                 // Import the settings.
649                 importSettings();
650             } else {  // Export is selected.
651                 // Export the settings.
652                 exportSettings();
653             }
654         } else {  // The storage permission has not been granted.
655             // Get a handle for the file name EditText.
656             EditText fileNameEditText = findViewById(R.id.file_name_edittext);
657
658             // Get the file name string.
659             String fileNameString = fileNameEditText.getText().toString();
660
661             // Get the external private directory `File`.
662             File externalPrivateDirectoryFile = getExternalFilesDir(null);
663
664             // Remove the incorrect lint error below that the file might be null.
665             assert externalPrivateDirectoryFile != null;
666
667             // Get the external private directory string.
668             String externalPrivateDirectory = externalPrivateDirectoryFile.toString();
669
670             // Check to see if the file path is in the external private directory.
671             if (fileNameString.startsWith(externalPrivateDirectory)) {  // The file path is in the external private directory.
672                 // Check to see if import or export is selected.
673                 if (importRadioButton.isChecked()) {  // Import is selected.
674                     // Import the settings.
675                     importSettings();
676                 } else {  // Export is selected.
677                     // Export the settings.
678                     exportSettings();
679                 }
680             } else {  // The file path is in a public directory.
681                 // Check if the user has previously denied the storage permission.
682                 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
683                     // Instantiate the storage permission alert dialog.
684                     DialogFragment storagePermissionDialogFragment = StoragePermissionDialog.displayDialog(0);
685
686                     // Show the storage permission alert dialog.  The permission will be requested when the dialog is closed.
687                     storagePermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.storage_permission));
688                 } else {  // Show the permission request directly.
689                     // Request the storage permission.  The export will be run when it finishes.
690                     ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
691                 }
692             }
693         }
694     }
695
696     @Override
697     public void onCloseStoragePermissionDialog(int type) {
698         // Request the write external storage permission.  The import/export will be run when it finishes.
699         ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
700     }
701
702     @Override
703     public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
704         // Get a handle for the import radiobutton.
705         RadioButton importRadioButton = findViewById(R.id.import_radiobutton);
706
707         // Check to see if the storage permission was granted.  If the dialog was canceled the grant results will be empty.
708         if ((grantResults.length > 0) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) {  // The storage permission was granted.
709             // Run the import or export methods according to which radio button is selected.
710             if (importRadioButton.isChecked()) {  // Import is selected.
711                 // Import the settings.
712                 importSettings();
713             } else {  // Export is selected.
714                 // Export the settings.
715                 exportSettings();
716             }
717         } else {  // The storage permission was not granted.
718             // Display an error snackbar.
719             Snackbar.make(importRadioButton, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show();
720         }
721     }
722
723     @Override
724     public void onActivityResult(int requestCode, int resultCode, Intent intent) {
725         // Run the default commands.
726         super.onActivityResult(requestCode, resultCode, intent);
727
728         switch (requestCode) {
729             case (BROWSE_RESULT_CODE):
730                 // Don't do anything if the user pressed back from the file picker.
731                 if (resultCode == Activity.RESULT_OK) {
732                     // Get a handle for the views.
733                     EditText fileNameEditText = findViewById(R.id.file_name_edittext);
734                     TextView fileExistsWarningTextView = findViewById(R.id.file_exists_warning_textview);
735
736                     // Instantiate the file name helper.
737                     FileNameHelper fileNameHelper = new FileNameHelper();
738
739                     // Get the file path URI from the intent.
740                     Uri filePathUri = intent.getData();
741
742                     // Use the file path from the intent if it exists.
743                     if (filePathUri != null) {
744                         // Convert the file name URI to a file name path.
745                         String fileNamePath = fileNameHelper.convertUriToFileNamePath(filePathUri);
746
747                         // Set the file name path as the text of the file name edit text.
748                         fileNameEditText.setText(fileNamePath);
749
750                         // Hide the file exists warning text view, because the file picker will have just created a file if export was selected.
751                         fileExistsWarningTextView.setVisibility(View.GONE);
752                     }
753                 }
754                 break;
755
756             case OPENPGP_EXPORT_RESULT_CODE:
757                 // Get the temporary unencrypted export file.
758                 File temporaryUnencryptedExportFile = new File(getApplicationContext().getCacheDir() + "/" + getString(R.string.settings_pbs));
759
760                 // Delete the temporary unencrypted export file if it exists.
761                 if (temporaryUnencryptedExportFile.exists()) {
762                     //noinspection ResultOfMethodCallIgnored
763                     temporaryUnencryptedExportFile.delete();
764                 }
765                 break;
766         }
767     }
768
769     private void exportSettings() {
770         // Get a handle for the views.
771         Spinner encryptionSpinner = findViewById(R.id.encryption_spinner);
772         EditText fileNameEditText = findViewById(R.id.file_name_edittext);
773
774         // Instantiate the import export database helper.
775         ImportExportDatabaseHelper importExportDatabaseHelper = new ImportExportDatabaseHelper();
776
777         // Get the export file string.
778         String exportFileString = fileNameEditText.getText().toString();
779
780         // Get the export and temporary unencrypted export files.
781         File exportFile = new File(exportFileString);
782         File temporaryUnencryptedExportFile = new File(getApplicationContext().getCacheDir() + "/" + getString(R.string.settings_pbs));
783
784         // Create an export status string.
785         String exportStatus;
786
787         // Export according to the encryption type.
788         switch (encryptionSpinner.getSelectedItemPosition()) {
789             case NO_ENCRYPTION:
790                 // Export the unencrypted file.
791                 exportStatus = importExportDatabaseHelper.exportUnencrypted(exportFile, this);
792
793                 // Show a disposition snackbar.
794                 if (exportStatus.equals(ImportExportDatabaseHelper.EXPORT_SUCCESSFUL)) {
795                     Snackbar.make(fileNameEditText, getString(R.string.export_successful), Snackbar.LENGTH_SHORT).show();
796                 } else {
797                     Snackbar.make(fileNameEditText, getString(R.string.export_failed) + "  " + exportStatus, Snackbar.LENGTH_INDEFINITE).show();
798                 }
799                 break;
800
801             case PASSWORD_ENCRYPTION:
802                 // Create an unencrypted export in a private directory.
803                 exportStatus = importExportDatabaseHelper.exportUnencrypted(temporaryUnencryptedExportFile, this);
804
805                 try {
806                     // Create an unencrypted export file input stream.
807                     FileInputStream unencryptedExportFileInputStream = new FileInputStream(temporaryUnencryptedExportFile);
808
809                     // Delete the encrypted export file if it exists.
810                     if (exportFile.exists()) {
811                         //noinspection ResultOfMethodCallIgnored
812                         exportFile.delete();
813                     }
814
815                     // Create an encrypted export file output stream.
816                     FileOutputStream encryptedExportFileOutputStream = new FileOutputStream(exportFile);
817
818                     // Get a handle for the encryption password EditText.
819                     EditText encryptionPasswordEditText = findViewById(R.id.password_encryption_edittext);
820
821                     // Get the encryption password.
822                     String encryptionPasswordString = encryptionPasswordEditText.getText().toString();
823
824                     // Initialize a secure random number generator.
825                     SecureRandom secureRandom = new SecureRandom();
826
827                     // Get a 256 bit (32 byte) random salt.
828                     byte[] saltByteArray = new byte[32];
829                     secureRandom.nextBytes(saltByteArray);
830
831                     // Convert the encryption password to a byte array.
832                     byte[] encryptionPasswordByteArray = encryptionPasswordString.getBytes(StandardCharsets.UTF_8);
833
834                     // Append the salt to the encryption password byte array.  This protects against rainbow table attacks.
835                     byte[] encryptionPasswordWithSaltByteArray = new byte[encryptionPasswordByteArray.length + saltByteArray.length];
836                     System.arraycopy(encryptionPasswordByteArray, 0, encryptionPasswordWithSaltByteArray, 0, encryptionPasswordByteArray.length);
837                     System.arraycopy(saltByteArray, 0, encryptionPasswordWithSaltByteArray, encryptionPasswordByteArray.length, saltByteArray.length);
838
839                     // Get a SHA-512 message digest.
840                     MessageDigest messageDigest = MessageDigest.getInstance("SHA-512");
841
842                     // Hash the salted encryption password.  Otherwise, any characters after the 32nd character in the password are ignored.
843                     byte[] hashedEncryptionPasswordWithSaltByteArray = messageDigest.digest(encryptionPasswordWithSaltByteArray);
844
845                     // Truncate the encryption password byte array to 256 bits (32 bytes).
846                     byte[] truncatedHashedEncryptionPasswordWithSaltByteArray = Arrays.copyOf(hashedEncryptionPasswordWithSaltByteArray, 32);
847
848                     // Create an AES secret key from the encryption password byte array.
849                     SecretKeySpec secretKey = new SecretKeySpec(truncatedHashedEncryptionPasswordWithSaltByteArray, "AES");
850
851                     // Generate a random 12 byte initialization vector.  According to NIST, a 12 byte initialization vector is more secure than a 16 byte one.
852                     byte[] initializationVector = new byte[12];
853                     secureRandom.nextBytes(initializationVector);
854
855                     // 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.
856                     Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
857
858                     // Set the GCM tag length to be 128 bits (the maximum) and apply the initialization vector.
859                     GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, initializationVector);
860
861                     // Initialize the cipher.
862                     cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmParameterSpec);
863
864                     // Add the salt and the initialization vector to the export file.
865                     encryptedExportFileOutputStream.write(saltByteArray);
866                     encryptedExportFileOutputStream.write(initializationVector);
867
868                     // Create a cipher output stream.
869                     CipherOutputStream cipherOutputStream = new CipherOutputStream(encryptedExportFileOutputStream, cipher);
870
871                     // 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.
872                     int numberOfBytesRead;
873                     byte[] encryptedBytes = new byte[16];
874
875                     // 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.
876                     while ((numberOfBytesRead = unencryptedExportFileInputStream.read(encryptedBytes)) != -1) {
877                         // Write the data to the cipher output stream.
878                         cipherOutputStream.write(encryptedBytes, 0, numberOfBytesRead);
879                     }
880
881                     // Close the streams.
882                     cipherOutputStream.flush();
883                     cipherOutputStream.close();
884                     encryptedExportFileOutputStream.close();
885                     unencryptedExportFileInputStream.close();
886
887                     // Wipe the encryption data from memory.
888                     //noinspection UnusedAssignment
889                     encryptionPasswordString = "";
890                     Arrays.fill(saltByteArray, (byte) 0);
891                     Arrays.fill(encryptionPasswordByteArray, (byte) 0);
892                     Arrays.fill(encryptionPasswordWithSaltByteArray, (byte) 0);
893                     Arrays.fill(hashedEncryptionPasswordWithSaltByteArray, (byte) 0);
894                     Arrays.fill(truncatedHashedEncryptionPasswordWithSaltByteArray, (byte) 0);
895                     Arrays.fill(initializationVector, (byte) 0);
896                     Arrays.fill(encryptedBytes, (byte) 0);
897
898                     // Delete the temporary unencrypted export file.
899                     //noinspection ResultOfMethodCallIgnored
900                     temporaryUnencryptedExportFile.delete();
901                 } catch (Exception exception) {
902                     exportStatus = exception.toString();
903                 }
904
905                 // Show a disposition snackbar.
906                 if (exportStatus.equals(ImportExportDatabaseHelper.EXPORT_SUCCESSFUL)) {
907                     Snackbar.make(fileNameEditText, getString(R.string.export_successful), Snackbar.LENGTH_SHORT).show();
908                 } else {
909                     Snackbar.make(fileNameEditText, getString(R.string.export_failed) + "  " + exportStatus, Snackbar.LENGTH_INDEFINITE).show();
910                 }
911                 break;
912
913             case OPENPGP_ENCRYPTION:
914                 // Create an unencrypted export in the private location.
915                 importExportDatabaseHelper.exportUnencrypted(temporaryUnencryptedExportFile, this);
916
917                 // Create an encryption intent for OpenKeychain.
918                 Intent openKeychainEncryptIntent = new Intent("org.sufficientlysecure.keychain.action.ENCRYPT_DATA");
919
920                 // Include the temporary unencrypted export file URI.
921                 openKeychainEncryptIntent.setData(FileProvider.getUriForFile(this, getString(R.string.file_provider), temporaryUnencryptedExportFile));
922
923                 // Allow OpenKeychain to read the file URI.
924                 openKeychainEncryptIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
925
926                 // Send the intent to the OpenKeychain package.
927                 openKeychainEncryptIntent.setPackage("org.sufficientlysecure.keychain");
928
929                 // Make it so.
930                 startActivityForResult(openKeychainEncryptIntent, OPENPGP_EXPORT_RESULT_CODE);
931                 break;
932         }
933
934         // Add the file to the list of recent files.  This doesn't currently work, but maybe it will someday.
935         MediaScannerConnection.scanFile(this, new String[] {exportFileString}, new String[] {"application/x-sqlite3"}, null);
936     }
937
938     private void importSettings() {
939         // Get a handle for the views.
940         Spinner encryptionSpinner = findViewById(R.id.encryption_spinner);
941         EditText fileNameEditText = findViewById(R.id.file_name_edittext);
942
943         // Instantiate the import export database helper.
944         ImportExportDatabaseHelper importExportDatabaseHelper = new ImportExportDatabaseHelper();
945
946         // Get the import file.
947         File importFile = new File(fileNameEditText.getText().toString());
948
949         // Initialize the import status string
950         String importStatus = "";
951
952         // Import according to the encryption type.
953         switch (encryptionSpinner.getSelectedItemPosition()) {
954             case NO_ENCRYPTION:
955                 // Import the unencrypted file.
956                 importStatus = importExportDatabaseHelper.importUnencrypted(importFile, this);
957                 break;
958
959             case PASSWORD_ENCRYPTION:
960                 // Use a private temporary import location.
961                 File temporaryUnencryptedImportFile = new File(getApplicationContext().getCacheDir() + "/" + getString(R.string.settings_pbs));
962
963                 try {
964                     // Create an encrypted import file input stream.
965                     FileInputStream encryptedImportFileInputStream = new FileInputStream(importFile);
966
967                     // Delete the temporary import file if it exists.
968                     if (temporaryUnencryptedImportFile.exists()) {
969                         //noinspection ResultOfMethodCallIgnored
970                         temporaryUnencryptedImportFile.delete();
971                     }
972
973                     // Create an unencrypted import file output stream.
974                     FileOutputStream unencryptedImportFileOutputStream = new FileOutputStream(temporaryUnencryptedImportFile);
975
976                     // Get a handle for the encryption password EditText.
977                     EditText encryptionPasswordEditText = findViewById(R.id.password_encryption_edittext);
978
979                     // Get the encryption password.
980                     String encryptionPasswordString = encryptionPasswordEditText.getText().toString();
981
982                     // Get the salt from the beginning of the import file.
983                     byte[] saltByteArray = new byte[32];
984                     //noinspection ResultOfMethodCallIgnored
985                     encryptedImportFileInputStream.read(saltByteArray);
986
987                     // Get the initialization vector from the import file.
988                     byte[] initializationVector = new byte[12];
989                     //noinspection ResultOfMethodCallIgnored
990                     encryptedImportFileInputStream.read(initializationVector);
991
992                     // Convert the encryption password to a byte array.
993                     byte[] encryptionPasswordByteArray = encryptionPasswordString.getBytes(StandardCharsets.UTF_8);
994
995                     // Append the salt to the encryption password byte array.  This protects against rainbow table attacks.
996                     byte[] encryptionPasswordWithSaltByteArray = new byte[encryptionPasswordByteArray.length + saltByteArray.length];
997                     System.arraycopy(encryptionPasswordByteArray, 0, encryptionPasswordWithSaltByteArray, 0, encryptionPasswordByteArray.length);
998                     System.arraycopy(saltByteArray, 0, encryptionPasswordWithSaltByteArray, encryptionPasswordByteArray.length, saltByteArray.length);
999
1000                     // Get a SHA-512 message digest.
1001                     MessageDigest messageDigest = MessageDigest.getInstance("SHA-512");
1002
1003                     // Hash the salted encryption password.  Otherwise, any characters after the 32nd character in the password are ignored.
1004                     byte[] hashedEncryptionPasswordWithSaltByteArray = messageDigest.digest(encryptionPasswordWithSaltByteArray);
1005
1006                     // Truncate the encryption password byte array to 256 bits (32 bytes).
1007                     byte[] truncatedHashedEncryptionPasswordWithSaltByteArray = Arrays.copyOf(hashedEncryptionPasswordWithSaltByteArray, 32);
1008
1009                     // Create an AES secret key from the encryption password byte array.
1010                     SecretKeySpec secretKey = new SecretKeySpec(truncatedHashedEncryptionPasswordWithSaltByteArray, "AES");
1011
1012                     // 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.
1013                     Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
1014
1015                     // Set the GCM tag length to be 128 bits (the maximum) and apply the initialization vector.
1016                     GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, initializationVector);
1017
1018                     // Initialize the cipher.
1019                     cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec);
1020
1021                     // Create a cipher input stream.
1022                     CipherInputStream cipherInputStream = new CipherInputStream(encryptedImportFileInputStream, cipher);
1023
1024                     // 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.
1025                     int numberOfBytesRead;
1026                     byte[] decryptedBytes = new byte[16];
1027
1028                     // 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.
1029                     while ((numberOfBytesRead = cipherInputStream.read(decryptedBytes)) != -1) {
1030                         // Write the data to the unencrypted import file output stream.
1031                         unencryptedImportFileOutputStream.write(decryptedBytes, 0, numberOfBytesRead);
1032                     }
1033
1034                     // Close the streams.
1035                     unencryptedImportFileOutputStream.flush();
1036                     unencryptedImportFileOutputStream.close();
1037                     cipherInputStream.close();
1038                     encryptedImportFileInputStream.close();
1039
1040                     // Wipe the encryption data from memory.
1041                     //noinspection UnusedAssignment
1042                     encryptionPasswordString = "";
1043                     Arrays.fill(saltByteArray, (byte) 0);
1044                     Arrays.fill(initializationVector, (byte) 0);
1045                     Arrays.fill(encryptionPasswordByteArray, (byte) 0);
1046                     Arrays.fill(encryptionPasswordWithSaltByteArray, (byte) 0);
1047                     Arrays.fill(hashedEncryptionPasswordWithSaltByteArray, (byte) 0);
1048                     Arrays.fill(truncatedHashedEncryptionPasswordWithSaltByteArray, (byte) 0);
1049                     Arrays.fill(decryptedBytes, (byte) 0);
1050
1051                     // Import the unencrypted database from the private location.
1052                     importStatus = importExportDatabaseHelper.importUnencrypted(temporaryUnencryptedImportFile, this);
1053
1054                     // Delete the temporary unencrypted import file.
1055                     //noinspection ResultOfMethodCallIgnored
1056                     temporaryUnencryptedImportFile.delete();
1057                 } catch (Exception exception) {
1058                     importStatus = exception.toString();
1059                 }
1060                 break;
1061
1062             case OPENPGP_ENCRYPTION:
1063                 try {
1064                     // Create an decryption intent for OpenKeychain.
1065                     Intent openKeychainDecryptIntent = new Intent("org.sufficientlysecure.keychain.action.DECRYPT_DATA");
1066
1067                     // Include the URI to be decrypted.
1068                     openKeychainDecryptIntent.setData(FileProvider.getUriForFile(this, getString(R.string.file_provider), importFile));
1069
1070                     // Allow OpenKeychain to read the file URI.
1071                     openKeychainDecryptIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
1072
1073                     // Send the intent to the OpenKeychain package.
1074                     openKeychainDecryptIntent.setPackage("org.sufficientlysecure.keychain");
1075
1076                     // Make it so.
1077                     startActivity(openKeychainDecryptIntent);
1078                 } catch (IllegalArgumentException exception) {  // The file import location is not valid.
1079                     // Display a snack bar with the import error.
1080                     Snackbar.make(fileNameEditText, getString(R.string.import_failed) + "  " + exception.toString(), Snackbar.LENGTH_INDEFINITE).show();
1081                 }
1082                 break;
1083         }
1084
1085         // Respond to the import disposition.
1086         if (importStatus.equals(ImportExportDatabaseHelper.IMPORT_SUCCESSFUL)) {  // The import was successful.
1087             // Create an intent to restart Privacy Browser.
1088             Intent restartIntent = getParentActivityIntent();
1089
1090             // Assert that the intent is not null to remove the lint error below.
1091             assert restartIntent != null;
1092
1093             // `Intent.FLAG_ACTIVITY_CLEAR_TASK` removes all activities from the stack.  It requires `Intent.FLAG_ACTIVITY_NEW_TASK`.
1094             restartIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
1095
1096             // Create a restart handler.
1097             Handler restartHandler = new Handler();
1098
1099             // Create a restart runnable.
1100             Runnable restartRunnable =  () -> {
1101                 // Restart Privacy Browser.
1102                 startActivity(restartIntent);
1103
1104                 // Kill this instance of Privacy Browser.  Otherwise, the app exhibits sporadic behavior after the restart.
1105                 System.exit(0);
1106             };
1107
1108             // Restart Privacy Browser after 150 milliseconds to allow enough time for the preferences to be saved.
1109             restartHandler.postDelayed(restartRunnable, 150);
1110
1111         } else if (!(encryptionSpinner.getSelectedItemPosition() == OPENPGP_ENCRYPTION)){  // The import was not successful.
1112             // Display a snack bar with the import error.
1113             Snackbar.make(fileNameEditText, getString(R.string.import_failed) + "  " + importStatus, Snackbar.LENGTH_INDEFINITE).show();
1114         }
1115     }
1116 }