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