]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - ImportExportActivity.kt
36d1e725a3b190cecfa5c77c35d59a3bee6b79dd
[PrivacyBrowserAndroid.git] / ImportExportActivity.kt
1 /*
2  * Copyright 2018-2023 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
5  *
6  * Privacy Browser Android 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 Android 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 Android.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 package com.stoutner.privacybrowser.activities
21
22 import android.content.Intent
23 import android.content.pm.PackageManager
24 import android.net.Uri
25 import android.os.Bundle
26 import android.os.Handler
27 import android.text.Editable
28 import android.text.TextWatcher
29 import android.view.View
30 import android.view.WindowManager
31 import android.widget.AdapterView
32 import android.widget.ArrayAdapter
33 import android.widget.Button
34 import android.widget.EditText
35 import android.widget.LinearLayout
36 import android.widget.RadioButton
37 import android.widget.ScrollView
38 import android.widget.Spinner
39 import android.widget.TextView
40
41 import androidx.activity.result.contract.ActivityResultContracts
42 import androidx.appcompat.app.AppCompatActivity
43 import androidx.appcompat.widget.Toolbar
44 import androidx.cardview.widget.CardView
45 import androidx.core.content.FileProvider
46 import androidx.preference.PreferenceManager
47
48 import com.google.android.material.snackbar.Snackbar
49 import com.google.android.material.textfield.TextInputLayout
50
51 import com.stoutner.privacybrowser.BuildConfig
52 import com.stoutner.privacybrowser.R
53 import com.stoutner.privacybrowser.helpers.EXPORT_SUCCESSFUL
54 import com.stoutner.privacybrowser.helpers.IMPORT_EXPORT_SCHEMA_VERSION
55 import com.stoutner.privacybrowser.helpers.IMPORT_SUCCESSFUL
56 import com.stoutner.privacybrowser.helpers.ImportExportBookmarksHelper
57 import com.stoutner.privacybrowser.helpers.ImportExportDatabaseHelper
58
59 import java.io.File
60 import java.io.FileInputStream
61 import java.io.FileNotFoundException
62 import java.io.FileOutputStream
63 import java.lang.Exception
64 import java.nio.charset.StandardCharsets
65 import java.security.MessageDigest
66 import java.security.SecureRandom
67 import java.util.Arrays
68
69 import javax.crypto.Cipher
70 import javax.crypto.CipherInputStream
71 import javax.crypto.CipherOutputStream
72 import javax.crypto.spec.GCMParameterSpec
73 import javax.crypto.spec.SecretKeySpec
74 import kotlin.system.exitProcess
75
76 // Define the encryption constants.
77 private const val NO_ENCRYPTION = 0
78 private const val PASSWORD_ENCRYPTION = 1
79 private const val OPENPGP_ENCRYPTION = 2
80
81 // Define the saved instance state constants.
82 private const val ENCRYPTION_PASSWORD_TEXTINPUTLAYOUT_VISIBILITY = "A"
83 private const val OPEN_KEYCHAIN_REQUIRED_TEXTVIEW_VISIBILITY = "B"
84 private const val SETTINGS_FILE_LOCATION_CARDVIEW_VISIBILITY = "C"
85 private const val SETTINGS_FILE_NAME_LINEARLAYOUT_VISIBILITY = "D"
86 private const val OPEN_KEYCHAIN_IMPORT_INSTRUCTIONS_TEXTVIEW_VISIBILITY = "E"
87 private const val SETTINGS_IMPORT_EXPORT_BUTTON_VISIBILITY = "F"
88 private const val SETTINGS_FILE_NAME_TEXT = "G"
89 private const val SETTINGS_IMPORT_EXPORT_BUTTON_TEXT = "H"
90 private const val BOOKMARKS_FILE_NAME_LINEARLAYOUT_VISIBILITY = "I"
91 private const val BOOKMARKS_IMPORT_EXPORT_BUTTON_VISIBILITY = "J"
92 private const val BOOKMARKS_FILE_NAME_TEXT = "K"
93 private const val BOOKMARKS_IMPORT_EXPORT_BUTTON_TEXT = "L"
94
95 class ImportExportActivity : AppCompatActivity() {
96     // Define the class views.
97     private lateinit var scrollView: ScrollView
98     private lateinit var encryptionSpinner: Spinner
99     private lateinit var encryptionPasswordTextInputLayout: TextInputLayout
100     private lateinit var encryptionPasswordEditText: EditText
101     private lateinit var openKeychainRequiredTextView: TextView
102     private lateinit var settingsFileLocationCardView: CardView
103     private lateinit var settingsImportRadioButton: RadioButton
104     private lateinit var settingsFileNameLinearLayout: LinearLayout
105     private lateinit var settingsFileNameEditText: EditText
106     private lateinit var openKeychainImportInstructionsTextView: TextView
107     private lateinit var settingsImportExportButton: Button
108     private lateinit var bookmarksImportRadioButton: RadioButton
109     private lateinit var bookmarksFileNameLinearLayout: LinearLayout
110     private lateinit var bookmarksFileNameEditText: EditText
111     private lateinit var bookmarksImportExportButton: Button
112
113     // Define the class variables.
114     private lateinit var fileProviderDirectory: File
115     private var openKeychainInstalled = false
116     private lateinit var temporaryPgpEncryptedImportFile: File
117     private lateinit var temporaryPreEncryptedExportFile: File
118
119     // Define the result launchers.  They must be defined before `onCreate()` is run or the app will crash.
120     private val settingsBrowseForImportActivityResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { fileUri: Uri? ->
121         // Only do something if the user didn't press back from the file picker.
122         if (fileUri != null) {
123             // Get the file name string from the URI.
124             val fileNameString = fileUri.toString()
125
126             // Set the settings file name text.
127             settingsFileNameEditText.setText(fileNameString)
128
129             // Move the cursor to the end of the file name edit text.
130             settingsFileNameEditText.setSelection(fileNameString.length)
131         }
132     }
133
134     private val settingsBrowseForExportActivityResultLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("*/*")) { fileUri: Uri? ->
135         // Only do something if the user didn't press back from the file picker.
136         if (fileUri != null) {
137             // Get the file name string from the URI.
138             val fileNameString = fileUri.toString()
139
140             // Set the settings file name text.
141             settingsFileNameEditText.setText(fileNameString)
142
143             // Move the cursor to the end of the file name edit text.
144             settingsFileNameEditText.setSelection(fileNameString.length)
145         }
146     }
147
148     private val bookmarksBrowseForImportActivityResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { fileUri: Uri? ->
149         // Only do something if the user didn't press back from the file picker.
150         if (fileUri != null) {
151             // Get the file name string from the URI.
152             val fileNameString = fileUri.toString()
153
154             // Set the bookmarks file name text.
155             bookmarksFileNameEditText.setText(fileNameString)
156
157             // Move the cursor to the end of the file name edit text.
158             bookmarksFileNameEditText.setSelection(fileNameString.length)
159
160             // Scroll to the bottom.
161             scrollView.post {
162                 scrollView.scrollY = scrollView.height
163             }
164         }
165     }
166
167     private val bookmarksBrowseForExportActivityResultLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("*/*")) { fileUri: Uri? ->
168         // Only do something if the user didn't press back from the file picker.
169         if (fileUri != null) {
170             // Get the file name string from the URI.
171             val fileNameString = fileUri.toString()
172
173             // Set the bookmarks file name text.
174             bookmarksFileNameEditText.setText(fileNameString)
175
176             // Move the cursor to the end of the file name edit text.
177             bookmarksFileNameEditText.setSelection(fileNameString.length)
178
179             // Scroll to the bottom.
180             scrollView.post {
181                 scrollView.scrollY = scrollView.height
182             }
183         }
184     }
185
186     private val openKeychainDecryptActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
187         // Delete the temporary PGP encrypted import file.
188         if (temporaryPgpEncryptedImportFile.exists())
189             temporaryPgpEncryptedImportFile.delete()
190
191         // Delete the file provider directory if it exists.
192         if (fileProviderDirectory.exists())
193             fileProviderDirectory.delete()
194     }
195
196     private val openKeychainEncryptActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
197         // Delete the temporary pre-encrypted export file if it exists.
198         if (temporaryPreEncryptedExportFile.exists())
199             temporaryPreEncryptedExportFile.delete()
200
201         // Delete the file provider directory if it exists.
202         if (fileProviderDirectory.exists())
203             fileProviderDirectory.delete()
204     }
205
206     public override fun onCreate(savedInstanceState: Bundle?) {
207         // Get a handle for the shared preferences.
208         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
209
210         // Get the preferences.
211         val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
212         val bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false)
213
214         // Disable screenshots if not allowed.
215         if (!allowScreenshots)
216             window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
217
218         // Run the default commands.
219         super.onCreate(savedInstanceState)
220
221         // Set the content view.
222         if (bottomAppBar)
223             setContentView(R.layout.import_export_bottom_appbar)
224         else
225             setContentView(R.layout.import_export_top_appbar)
226
227         // Get a handle for the toolbar.
228         val toolbar = findViewById<Toolbar>(R.id.import_export_toolbar)
229
230         // Set the support action bar.
231         setSupportActionBar(toolbar)
232
233         // Get a handle for the action bar.
234         val actionBar = supportActionBar!!
235
236         // Display the home arrow on the support action bar.
237         actionBar.setDisplayHomeAsUpEnabled(true)
238
239         // Find out if OpenKeychain is installed.
240         openKeychainInstalled = try {
241             packageManager.getPackageInfo("org.sufficientlysecure.keychain", 0).versionName.isNotEmpty()
242         } catch (exception: PackageManager.NameNotFoundException) {
243             false
244         }
245
246         // Get handles for the views.
247         scrollView = findViewById(R.id.scrollview)
248         encryptionSpinner = findViewById(R.id.encryption_spinner)
249         encryptionPasswordTextInputLayout = findViewById(R.id.encryption_password_textinputlayout)
250         encryptionPasswordEditText = findViewById(R.id.encryption_password_edittext)
251         openKeychainRequiredTextView = findViewById(R.id.openkeychain_required_textview)
252         settingsFileLocationCardView = findViewById(R.id.settings_file_location_cardview)
253         settingsImportRadioButton = findViewById(R.id.settings_import_radiobutton)
254         val settingsExportRadioButton = findViewById<RadioButton>(R.id.settings_export_radiobutton)
255         settingsFileNameLinearLayout = findViewById(R.id.settings_file_name_linearlayout)
256         settingsFileNameEditText = findViewById(R.id.settings_file_name_edittext)
257         openKeychainImportInstructionsTextView = findViewById(R.id.openkeychain_import_instructions_textview)
258         settingsImportExportButton = findViewById(R.id.settings_import_export_button)
259         bookmarksImportRadioButton = findViewById(R.id.bookmarks_import_radiobutton)
260         bookmarksFileNameLinearLayout = findViewById(R.id.bookmarks_file_name_linearlayout)
261         bookmarksFileNameEditText = findViewById(R.id.bookmarks_file_name_edittext)
262         bookmarksImportExportButton = findViewById(R.id.bookmarks_import_export_button)
263
264         // Create an array adapter for the spinner.
265         val encryptionArrayAdapter = ArrayAdapter.createFromResource(this, R.array.encryption_type, R.layout.spinner_item)
266
267         // Set the drop down view resource on the spinner.
268         encryptionArrayAdapter.setDropDownViewResource(R.layout.spinner_dropdown_items)
269
270         // Set the array adapter for the spinner.
271         encryptionSpinner.adapter = encryptionArrayAdapter
272
273         // Update the UI when the spinner changes.
274         encryptionSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
275             override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
276                 when (position) {
277                     NO_ENCRYPTION -> {
278                         // Hide the unneeded layout items.
279                         encryptionPasswordTextInputLayout.visibility = View.GONE
280                         openKeychainRequiredTextView.visibility = View.GONE
281                         openKeychainImportInstructionsTextView.visibility = View.GONE
282
283                         // Show the file location card.
284                         settingsFileLocationCardView.visibility = View.VISIBLE
285
286                         // Show the file name linear layout if either import or export is checked.
287                         if (settingsImportRadioButton.isChecked || settingsExportRadioButton.isChecked)
288                             settingsFileNameLinearLayout.visibility = View.VISIBLE
289
290                         // Reset the text of the import button, which may have been changed to `Decrypt`.
291                         if (settingsImportRadioButton.isChecked)
292                             settingsImportExportButton.setText(R.string.import_button)
293
294                         // Clear the file name edit text.
295                         settingsFileNameEditText.text.clear()
296
297                         // Disable the import/export button.
298                         settingsImportExportButton.isEnabled = false
299                     }
300
301                     PASSWORD_ENCRYPTION -> {
302                         // Hide the OpenPGP layout items.
303                         openKeychainRequiredTextView.visibility = View.GONE
304                         openKeychainImportInstructionsTextView.visibility = View.GONE
305
306                         // Show the password encryption layout items.
307                         encryptionPasswordTextInputLayout.visibility = View.VISIBLE
308
309                         // Show the file location card.
310                         settingsFileLocationCardView.visibility = View.VISIBLE
311
312                         // Show the file name linear layout if either import or export is checked.
313                         if (settingsImportRadioButton.isChecked || settingsExportRadioButton.isChecked)
314                             settingsFileNameLinearLayout.visibility = View.VISIBLE
315
316                         // Reset the text of the import button, which may have been changed to `Decrypt`.
317                         if (settingsImportRadioButton.isChecked)
318                             settingsImportExportButton.setText(R.string.import_button)
319
320                         // Clear the file name edit text.
321                         settingsFileNameEditText.text.clear()
322
323                         // Disable the import/export button.
324                         settingsImportExportButton.isEnabled = false
325                     }
326
327                     OPENPGP_ENCRYPTION -> {
328                         // Hide the password encryption layout items.
329                         encryptionPasswordTextInputLayout.visibility = View.GONE
330
331                         // Updated items based on the installation status of OpenKeychain.
332                         if (openKeychainInstalled) {  // OpenKeychain is installed.
333                             // Show the file location card.
334                             settingsFileLocationCardView.visibility = View.VISIBLE
335
336                             // Update the layout based on the checked radio button.
337                             if (settingsImportRadioButton.isChecked) {
338                                 // Show the file name linear layout and the OpenKeychain import instructions.
339                                 settingsFileNameLinearLayout.visibility = View.VISIBLE
340                                 openKeychainImportInstructionsTextView.visibility = View.VISIBLE
341
342                                 // Set the text of the import button to be `Decrypt`.
343                                 settingsImportExportButton.setText(R.string.decrypt)
344
345                                 // Clear the file name edit text.
346                                 settingsFileNameEditText.text.clear()
347
348                                 // Disable the import/export button.
349                                 settingsImportExportButton.isEnabled = false
350                             } else if (settingsExportRadioButton.isChecked) {
351                                 // Hide the file name linear layout and the OpenKeychain import instructions.
352                                 settingsFileNameLinearLayout.visibility = View.GONE
353                                 openKeychainImportInstructionsTextView.visibility = View.GONE
354
355                                 // Enable the export button.
356                                 settingsImportExportButton.isEnabled = true
357                             }
358                         } else {  // OpenKeychain is not installed.
359                             // Show the OpenPGP required layout item.
360                             openKeychainRequiredTextView.visibility = View.VISIBLE
361
362                             // Hide the file location card.
363                             settingsFileLocationCardView.visibility = View.GONE
364                         }
365                     }
366                 }
367             }
368
369             override fun onNothingSelected(parent: AdapterView<*>?) {}
370         }
371
372         // Update the status of the import/export button when the password changes.
373         encryptionPasswordEditText.addTextChangedListener(object : TextWatcher {
374             override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
375                 // Do nothing.
376             }
377
378             override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
379                 // Do nothing.
380             }
381
382             override fun afterTextChanged(s: Editable) {
383                 // Enable the import/export button if both the file string and the password are populated.
384                 settingsImportExportButton.isEnabled = settingsFileNameEditText.text.toString().isNotEmpty() && encryptionPasswordEditText.text.toString().isNotEmpty()
385             }
386         })
387
388         // Update the UI when the settings file name edit text changes.
389         settingsFileNameEditText.addTextChangedListener(object : TextWatcher {
390             override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
391                 // Do nothing.
392             }
393
394             override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
395                 // Do nothing.
396             }
397
398             override fun afterTextChanged(s: Editable) {
399                 // Adjust the UI according to the encryption spinner position.
400                 if (encryptionSpinner.selectedItemPosition == PASSWORD_ENCRYPTION) {
401                     // Enable the settings import/export button if both the file name and the password are populated.
402                     settingsImportExportButton.isEnabled = settingsFileNameEditText.text.toString().isNotEmpty() && encryptionPasswordEditText.text.toString().isNotEmpty()
403                 } else {
404                     // Enable the settings import/export button if the file name is populated.
405                     settingsImportExportButton.isEnabled = settingsFileNameEditText.text.toString().isNotEmpty()
406                 }
407             }
408         })
409
410         // Update the UI when the bookmarks file name edit text changes.
411         bookmarksFileNameEditText.addTextChangedListener(object : TextWatcher {
412             override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
413                 // Do nothing.
414             }
415
416             override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
417                 // Do nothing.
418             }
419
420             override fun afterTextChanged(s: Editable) {
421                 // Enable the bookmarks import/export button if the file name is populated.
422                 bookmarksImportExportButton.isEnabled = bookmarksFileNameEditText.text.toString().isNotEmpty()
423             }
424         })
425
426         // Check to see if the activity has been restarted.
427         if (savedInstanceState == null) {  // The app has not been restarted.
428             // Initially hide the unneeded views.
429             encryptionPasswordTextInputLayout.visibility = View.GONE
430             openKeychainRequiredTextView.visibility = View.GONE
431             settingsFileNameLinearLayout.visibility = View.GONE
432             openKeychainImportInstructionsTextView.visibility = View.GONE
433             settingsImportExportButton.visibility = View.GONE
434             bookmarksFileNameLinearLayout.visibility = View.GONE
435             bookmarksImportExportButton.visibility = View.GONE
436         } else {  // The app has been restarted.
437             // Restore the visibility of the views.
438             encryptionPasswordTextInputLayout.visibility = savedInstanceState.getInt(ENCRYPTION_PASSWORD_TEXTINPUTLAYOUT_VISIBILITY)
439             openKeychainRequiredTextView.visibility = savedInstanceState.getInt(OPEN_KEYCHAIN_REQUIRED_TEXTVIEW_VISIBILITY)
440             settingsFileLocationCardView.visibility = savedInstanceState.getInt(SETTINGS_FILE_LOCATION_CARDVIEW_VISIBILITY)
441             settingsFileNameLinearLayout.visibility = savedInstanceState.getInt(SETTINGS_FILE_NAME_LINEARLAYOUT_VISIBILITY)
442             openKeychainImportInstructionsTextView.visibility = savedInstanceState.getInt(OPEN_KEYCHAIN_IMPORT_INSTRUCTIONS_TEXTVIEW_VISIBILITY)
443             settingsImportExportButton.visibility = savedInstanceState.getInt(SETTINGS_IMPORT_EXPORT_BUTTON_VISIBILITY)
444             bookmarksFileNameLinearLayout.visibility = savedInstanceState.getInt(BOOKMARKS_FILE_NAME_LINEARLAYOUT_VISIBILITY)
445             bookmarksImportExportButton.visibility = savedInstanceState.getInt(BOOKMARKS_IMPORT_EXPORT_BUTTON_VISIBILITY)
446
447             // Restore the text.
448             settingsFileNameEditText.post { settingsFileNameEditText.setText(savedInstanceState.getString(SETTINGS_FILE_NAME_TEXT)) }
449             settingsImportExportButton.text = savedInstanceState.getString(SETTINGS_IMPORT_EXPORT_BUTTON_TEXT)
450             bookmarksFileNameEditText.post { bookmarksFileNameEditText.setText(savedInstanceState.getString(BOOKMARKS_FILE_NAME_TEXT)) }
451             bookmarksImportExportButton.text = savedInstanceState.getString(BOOKMARKS_IMPORT_EXPORT_BUTTON_TEXT)
452         }
453     }
454
455     public override fun onSaveInstanceState(savedInstanceState: Bundle) {
456         // Run the default commands.
457         super.onSaveInstanceState(savedInstanceState)
458
459         // Save the visibility of the views.
460         savedInstanceState.putInt(ENCRYPTION_PASSWORD_TEXTINPUTLAYOUT_VISIBILITY, encryptionPasswordTextInputLayout.visibility)
461         savedInstanceState.putInt(OPEN_KEYCHAIN_REQUIRED_TEXTVIEW_VISIBILITY, openKeychainRequiredTextView.visibility)
462         savedInstanceState.putInt(SETTINGS_FILE_LOCATION_CARDVIEW_VISIBILITY, settingsFileLocationCardView.visibility)
463         savedInstanceState.putInt(SETTINGS_FILE_NAME_LINEARLAYOUT_VISIBILITY, settingsFileNameLinearLayout.visibility)
464         savedInstanceState.putInt(OPEN_KEYCHAIN_IMPORT_INSTRUCTIONS_TEXTVIEW_VISIBILITY, openKeychainImportInstructionsTextView.visibility)
465         savedInstanceState.putInt(SETTINGS_IMPORT_EXPORT_BUTTON_VISIBILITY, settingsImportExportButton.visibility)
466         savedInstanceState.putInt(BOOKMARKS_FILE_NAME_LINEARLAYOUT_VISIBILITY, bookmarksFileNameLinearLayout.visibility)
467         savedInstanceState.putInt(BOOKMARKS_IMPORT_EXPORT_BUTTON_VISIBILITY, bookmarksImportExportButton.visibility)
468
469         // Save the text.
470         savedInstanceState.putString(SETTINGS_FILE_NAME_TEXT, settingsFileNameEditText.text.toString())
471         savedInstanceState.putString(SETTINGS_IMPORT_EXPORT_BUTTON_TEXT, settingsImportExportButton.text.toString())
472         savedInstanceState.putString(BOOKMARKS_FILE_NAME_TEXT, bookmarksFileNameEditText.text.toString())
473         savedInstanceState.putString(BOOKMARKS_IMPORT_EXPORT_BUTTON_VISIBILITY, bookmarksImportExportButton.text.toString())
474     }
475
476     fun onClickBookmarksRadioButton(view: View) {
477         // Check to see if import or export was selected.
478         if (view.id == R.id.bookmarks_import_radiobutton) {  // The bookmarks import radio button was selected.
479             // Set the text on the bookmarks import/export button to be `Import`.
480             bookmarksImportExportButton.setText(R.string.import_button)
481         } else {  // The bookmarks export radio button was selected.
482             // Set the text on the bookmarks import/export button to be `Export`.
483             bookmarksImportExportButton.setText(R.string.export)
484         }
485
486         // Display the bookmarks views.
487         bookmarksFileNameLinearLayout.visibility = View.VISIBLE
488         bookmarksImportExportButton.visibility = View.VISIBLE
489
490         // Clear the bookmarks file name edit text.
491         bookmarksFileNameEditText.text.clear()
492
493         // Disable the bookmarks import/export button.
494         bookmarksImportExportButton.isEnabled = false
495
496         // Scroll to the bottom of the screen.
497         scrollView.post {
498             scrollView.scrollY = scrollView.height
499         }
500     }
501
502     fun onClickSettingsRadioButton(view: View) {
503         // Check to see if import or export was selected.
504         if (view.id == R.id.settings_import_radiobutton) {  // The settings import radio button was selected.
505             // Check to see if OpenPGP encryption is selected.
506             if (encryptionSpinner.selectedItemPosition == OPENPGP_ENCRYPTION) {  // OpenPGP encryption selected.
507                 // Show the OpenKeychain import instructions.
508                 openKeychainImportInstructionsTextView.visibility = View.VISIBLE
509
510                 // Set the text on the settings import/export button to be `Decrypt`.
511                 settingsImportExportButton.setText(R.string.decrypt)
512             } else {  // OpenPGP encryption not selected.
513                 // Hide the OpenKeychain import instructions.
514                 openKeychainImportInstructionsTextView.visibility = View.GONE
515
516                 // Set the text on the settings import/export button to be `Import`.
517                 settingsImportExportButton.setText(R.string.import_button)
518             }
519
520             // Display the views.
521             settingsFileNameLinearLayout.visibility = View.VISIBLE
522             settingsImportExportButton.visibility = View.VISIBLE
523
524             // Clear the settings file name edit text.
525             settingsFileNameEditText.text.clear()
526
527             // Disable the settings import/export button.
528             settingsImportExportButton.isEnabled = false
529         } else {  // The settings export radio button was selected.
530             // Hide the OpenKeychain import instructions.
531             openKeychainImportInstructionsTextView.visibility = View.GONE
532
533             // Set the text on the settings import/export button to be `Export`.
534             settingsImportExportButton.setText(R.string.export)
535
536             // Show the settings import/export button.
537             settingsImportExportButton.visibility = View.VISIBLE
538
539             // Check to see if OpenPGP encryption is selected.
540             if (encryptionSpinner.selectedItemPosition == OPENPGP_ENCRYPTION) {  // OpenPGP encryption is selected.
541                 // Hide the settings file name views.
542                 settingsFileNameLinearLayout.visibility = View.GONE
543
544                 // Enable the settings export button.
545                 settingsImportExportButton.isEnabled = true
546             } else {  // OpenPGP encryption is not selected.
547                 // Show the settings file name view.
548                 settingsFileNameLinearLayout.visibility = View.VISIBLE
549
550                 // Clear the settings file name edit text.
551                 settingsFileNameEditText.text.clear()
552
553                 // Disable the settings import/export button.
554                 settingsImportExportButton.isEnabled = false
555             }
556         }
557     }
558
559     fun settingsBrowse(@Suppress("UNUSED_PARAMETER") view: View) {
560         // Check to see if import or export is selected.
561         if (settingsImportRadioButton.isChecked) {  // Import is selected.
562             // Open the file picker.
563             settingsBrowseForImportActivityResultLauncher.launch("*/*")
564         } else {  // Export is selected
565             // Open the file picker with the export name according to the encryption type.
566             if (encryptionSpinner.selectedItemPosition == NO_ENCRYPTION)  // No encryption is selected.
567                 settingsBrowseForExportActivityResultLauncher.launch(getString(R.string.privacy_browser_settings_pbs, BuildConfig.VERSION_NAME, IMPORT_EXPORT_SCHEMA_VERSION))
568             else  // Password encryption is selected.
569                 settingsBrowseForExportActivityResultLauncher.launch(getString(R.string.privacy_browser_settings_pbs_aes, BuildConfig.VERSION_NAME, IMPORT_EXPORT_SCHEMA_VERSION))
570         }
571     }
572
573     fun bookmarksBrowse(@Suppress("UNUSED_PARAMETER") view: View) {
574         // Check to see if import or export is selected.
575         if (bookmarksImportRadioButton.isChecked) {  // Import is selected.
576             // Open the file picker.
577             bookmarksBrowseForImportActivityResultLauncher.launch("*/*")
578         } else {  // Export is selected.
579             // Open the file picker.
580             bookmarksBrowseForExportActivityResultLauncher.launch(getString(R.string.privacy_browser_bookmarks_html))
581         }
582     }
583
584     fun importExportBookmarks(@Suppress("UNUSED_PARAMETER") view: View) {
585         // Instantiate the import/export bookmarks helper.
586         val importExportBookmarksHelper = ImportExportBookmarksHelper()
587
588         // Get the file name string.
589         val fileNameString = bookmarksFileNameEditText.text.toString()
590
591         // Check to see if import or export is selected.
592         if (bookmarksImportRadioButton.isChecked) {  // Import is selected.
593             // Import the bookmarks.
594             importExportBookmarksHelper.importBookmarks(fileNameString, context = this, scrollView)
595
596             // Repopulate the bookmarks in the main WebView activity.
597             MainWebViewActivity.restartFromBookmarksActivity = true
598         } else {  // Export is selected.
599             // Export the bookmarks.
600             importExportBookmarksHelper.exportBookmarks(fileNameString, context = this, scrollView)
601         }
602     }
603
604     fun importExportSettings(@Suppress("UNUSED_PARAMETER") view: View) {
605         // Instantiate the import/export database helper.
606         val importExportDatabaseHelper = ImportExportDatabaseHelper()
607
608         // Check to see if import or export is selected.
609         if (settingsImportRadioButton.isChecked) {  // Import is selected.
610             // Initialize the import status string
611             var importStatus = ""
612
613             // Get the file name string.
614             val fileNameString = settingsFileNameEditText.text.toString()
615
616             // Import according to the encryption type.
617             when (encryptionSpinner.selectedItemPosition) {
618                 NO_ENCRYPTION -> {
619                     try {
620                         // Get an input stream for the file name.
621                         // A file may be opened directly once the minimum API >= 29.  <https://developer.android.com/reference/kotlin/android/content/ContentResolver#openfile>
622                         val inputStream = contentResolver.openInputStream(Uri.parse(fileNameString))!!
623
624                         // Import the unencrypted file.
625                         importStatus = importExportDatabaseHelper.importUnencrypted(inputStream, this)
626
627                         // Close the input stream.
628                         inputStream.close()
629                     } catch (exception: FileNotFoundException) {
630                         // Update the import status.
631                         importStatus = exception.toString()
632                     }
633
634                     // Restart Privacy Browser if successful.
635                     if (importStatus == IMPORT_SUCCESSFUL)
636                         restartPrivacyBrowser()
637                 }
638
639                 PASSWORD_ENCRYPTION -> {
640                     try {
641                         // Get the encryption password.
642                         val encryptionPasswordString = encryptionPasswordEditText.text.toString()
643
644                         // Get an input stream for the file name.
645                         val inputStream = contentResolver.openInputStream(Uri.parse(fileNameString))!!
646
647                         // Initialize a salt byte array.  Salt protects against rainbow table attacks.
648                         val saltByteArray = ByteArray(32)
649
650                         // Get the salt from the beginning of the import file.
651                         inputStream.read(saltByteArray)
652
653                         // Create an initialization vector.
654                         val initializationVector = ByteArray(12)
655
656                         // Get the initialization vector from the import file.
657                         inputStream.read(initializationVector)
658
659                         // Convert the encryption password to a byte array.
660                         val encryptionPasswordByteArray = encryptionPasswordString.toByteArray(StandardCharsets.UTF_8)
661
662                         // Create an encryption password with salt byte array.
663                         val encryptionPasswordWithSaltByteArray = ByteArray(encryptionPasswordByteArray.size + saltByteArray.size)
664
665                         // Populate the first part of the encryption password with salt byte array with the encryption password.
666                         System.arraycopy(encryptionPasswordByteArray, 0, encryptionPasswordWithSaltByteArray, 0, encryptionPasswordByteArray.size)
667
668                         // Populate the second part of the encryption password with salt byte array with the salt.
669                         System.arraycopy(saltByteArray, 0, encryptionPasswordWithSaltByteArray, encryptionPasswordByteArray.size, saltByteArray.size)
670
671                         // Get a SHA-512 message digest.
672                         val messageDigest = MessageDigest.getInstance("SHA-512")
673
674                         // Hash the salted encryption password.  Otherwise, any characters after the 32nd character in the password are ignored.
675                         val hashedEncryptionPasswordWithSaltByteArray = messageDigest.digest(encryptionPasswordWithSaltByteArray)
676
677                         // Truncate the encryption password byte array to 256 bits (32 bytes).
678                         val truncatedHashedEncryptionPasswordWithSaltByteArray = Arrays.copyOf(hashedEncryptionPasswordWithSaltByteArray, 32)
679
680                         // Create an AES secret key from the encryption password byte array.
681                         val secretKey = SecretKeySpec(truncatedHashedEncryptionPasswordWithSaltByteArray, "AES")
682
683                         // 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.
684                         val cipher = Cipher.getInstance("AES/GCM/NoPadding")
685
686                         // Set the GCM tag length to be 128 bits (the maximum) and apply the initialization vector.
687                         val gcmParameterSpec = GCMParameterSpec(128, initializationVector)
688
689                         // Initialize the cipher.
690                         cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec)
691
692                         // Create a cipher input stream.
693                         val cipherInputStream = CipherInputStream(inputStream, cipher)
694
695                         // 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.
696                         var numberOfBytesRead: Int
697                         val decryptedBytes = ByteArray(16)
698
699                         // Create a private temporary unencrypted import file.
700                         val temporaryUnencryptedImportFile = File.createTempFile("temporary_unencrypted_import_file", null, applicationContext.cacheDir)
701
702                         // Create an temporary unencrypted import file output stream.
703                         val temporaryUnencryptedImportFileOutputStream = FileOutputStream(temporaryUnencryptedImportFile)
704
705                         // Read up to 128 bits (16 bytes) of data from the cipher input stream.  `-1` will be returned when the end of the file is reached.
706                         while (cipherInputStream.read(decryptedBytes).also { numberOfBytesRead = it } != -1) {
707                             // Write the data to the temporary unencrypted import file output stream.
708                             temporaryUnencryptedImportFileOutputStream.write(decryptedBytes, 0, numberOfBytesRead)
709                         }
710
711                         // Flush the temporary unencrypted import file output stream.
712                         temporaryUnencryptedImportFileOutputStream.flush()
713
714                         // Close the streams.
715                         temporaryUnencryptedImportFileOutputStream.close()
716                         cipherInputStream.close()
717                         inputStream.close()
718
719                         // Wipe the encryption data from memory.
720                         Arrays.fill(saltByteArray, 0.toByte())
721                         Arrays.fill(initializationVector, 0.toByte())
722                         Arrays.fill(encryptionPasswordByteArray, 0.toByte())
723                         Arrays.fill(encryptionPasswordWithSaltByteArray, 0.toByte())
724                         Arrays.fill(hashedEncryptionPasswordWithSaltByteArray, 0.toByte())
725                         Arrays.fill(truncatedHashedEncryptionPasswordWithSaltByteArray, 0.toByte())
726                         Arrays.fill(decryptedBytes, 0.toByte())
727
728                         // Create a temporary unencrypted import file input stream.
729                         val temporaryUnencryptedImportFileInputStream = FileInputStream(temporaryUnencryptedImportFile)
730
731                         // Import the temporary unencrypted import file.
732                         importStatus = importExportDatabaseHelper.importUnencrypted(temporaryUnencryptedImportFileInputStream, this)
733
734                         // Close the temporary unencrypted import file input stream.
735                         temporaryUnencryptedImportFileInputStream.close()
736
737                         // Delete the temporary unencrypted import file.
738                         temporaryUnencryptedImportFile.delete()
739
740                         // Restart Privacy Browser if successful.
741                         if (importStatus == IMPORT_SUCCESSFUL)
742                             restartPrivacyBrowser()
743                     } catch (exception: Exception) {
744                         // Update the import status.
745                         importStatus = exception.toString()
746                     }
747                 }
748
749                 OPENPGP_ENCRYPTION -> {
750                     try {
751                         // Get a handle for the file provider directory.
752                         fileProviderDirectory = File(applicationContext.cacheDir.toString() + "/" + getString(R.string.file_provider_directory))
753
754                         // Create the file provider directory.  Any errors will be handled by the catch statement below.
755                         fileProviderDirectory.mkdir()
756
757                         // Set the temporary PGP encrypted import file.
758                         temporaryPgpEncryptedImportFile = File.createTempFile("temporary_pgp_encrypted_import_file", null, fileProviderDirectory)
759
760                         // Create a temporary PGP encrypted import file output stream.
761                         val temporaryPgpEncryptedImportFileOutputStream = FileOutputStream(temporaryPgpEncryptedImportFile)
762
763                         // Get an input stream for the file name.
764                         val inputStream = contentResolver.openInputStream(Uri.parse(fileNameString))!!
765
766                         // Create a transfer byte array.
767                         val transferByteArray = ByteArray(1024)
768
769                         // Create an integer to track the number of bytes read.
770                         var bytesRead: Int
771
772                         // Copy the input stream to the temporary PGP encrypted import file.
773                         while (inputStream.read(transferByteArray).also { bytesRead = it } > 0)
774                             temporaryPgpEncryptedImportFileOutputStream.write(transferByteArray, 0, bytesRead)
775
776                         // Flush the temporary PGP encrypted import file output stream.
777                         temporaryPgpEncryptedImportFileOutputStream.flush()
778
779                         // Close the streams.
780                         inputStream.close()
781                         temporaryPgpEncryptedImportFileOutputStream.close()
782
783                         // Create a decryption intent for OpenKeychain.
784                         val openKeychainDecryptIntent = Intent("org.sufficientlysecure.keychain.action.DECRYPT_DATA")
785
786                         // Include the URI to be decrypted.
787                         openKeychainDecryptIntent.data = FileProvider.getUriForFile(this, getString(R.string.file_provider), temporaryPgpEncryptedImportFile)
788
789                         // Allow OpenKeychain to read the file URI.
790                         openKeychainDecryptIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
791
792                         // Send the intent to the OpenKeychain package.
793                         openKeychainDecryptIntent.setPackage("org.sufficientlysecure.keychain")
794
795                         // Make it so.
796                         openKeychainDecryptActivityResultLauncher.launch(openKeychainDecryptIntent)
797
798                         // Update the import status.
799                         importStatus = IMPORT_SUCCESSFUL
800                     } catch (exception: Exception) {
801                         // Update the import status.
802                         importStatus = exception.toString()
803                     }
804                 }
805             }
806
807             // Display a snack bar with the import error if it was unsuccessful.
808             if (importStatus != IMPORT_SUCCESSFUL)
809                 Snackbar.make(settingsFileNameEditText, getString(R.string.import_failed, importStatus), Snackbar.LENGTH_INDEFINITE).show()
810         } else {  // Export is selected.
811             // Export according to the encryption type.
812             when (encryptionSpinner.selectedItemPosition) {
813                 NO_ENCRYPTION -> {
814                     // Get the file name string.
815                     val noEncryptionFileNameString = settingsFileNameEditText.text.toString()
816
817                     try {
818                         // Get the export file output stream, truncating any existing content.
819                         // A file may be opened directly once the minimum API >= 29.  <https://developer.android.com/reference/kotlin/android/content/ContentResolver#openfile>
820                         val exportFileOutputStream = contentResolver.openOutputStream(Uri.parse(noEncryptionFileNameString), "wt")!!
821
822                         // Export the unencrypted file.
823                         val noEncryptionExportStatus = importExportDatabaseHelper.exportUnencrypted(exportFileOutputStream, this)
824
825                         // Close the output stream.
826                         exportFileOutputStream.close()
827
828                         // Display an export disposition snackbar.
829                         if (noEncryptionExportStatus == EXPORT_SUCCESSFUL)
830                             Snackbar.make(settingsFileNameEditText, getString(R.string.export_successful), Snackbar.LENGTH_SHORT).show()
831                         else
832                             Snackbar.make(settingsFileNameEditText, getString(R.string.export_failed, noEncryptionExportStatus), Snackbar.LENGTH_INDEFINITE).show()
833                     } catch (fileNotFoundException: FileNotFoundException) {
834                         // Display a snackbar with the exception.
835                         Snackbar.make(settingsFileNameEditText, getString(R.string.export_failed, fileNotFoundException), Snackbar.LENGTH_INDEFINITE).show()
836                     }
837                 }
838
839                 PASSWORD_ENCRYPTION -> {
840                     try {
841                         // Create a temporary unencrypted export file.
842                         val temporaryUnencryptedExportFile = File.createTempFile("temporary_unencrypted_export_file", null, applicationContext.cacheDir)
843
844                         // Create a temporary unencrypted export output stream.
845                         val temporaryUnencryptedExportOutputStream = FileOutputStream(temporaryUnencryptedExportFile)
846
847                         // Populate the temporary unencrypted export.
848                         val passwordEncryptionExportStatus = importExportDatabaseHelper.exportUnencrypted(temporaryUnencryptedExportOutputStream, this)
849
850                         // Close the temporary unencrypted export output stream.
851                         temporaryUnencryptedExportOutputStream.close()
852
853                         // Create an unencrypted export file input stream.
854                         val unencryptedExportFileInputStream = FileInputStream(temporaryUnencryptedExportFile)
855
856                         // Get the encryption password.
857                         val encryptionPasswordString = encryptionPasswordEditText.text.toString()
858
859                         // Initialize a secure random number generator.
860                         val secureRandom = SecureRandom()
861
862                         // Initialize a salt byte array.  Salt protects against rainbow table attacks.
863                         val saltByteArray = ByteArray(32)
864
865                         // Get a 256 bit (32 byte) random salt.
866                         secureRandom.nextBytes(saltByteArray)
867
868                         // Convert the encryption password to a byte array.
869                         val encryptionPasswordByteArray = encryptionPasswordString.toByteArray(StandardCharsets.UTF_8)
870
871                         // Create an encryption password with salt byte array.
872                         val encryptionPasswordWithSaltByteArray = ByteArray(encryptionPasswordByteArray.size + saltByteArray.size)
873
874                         // Populate the first part of the encryption password with salt byte array with the encryption password.
875                         System.arraycopy(encryptionPasswordByteArray, 0, encryptionPasswordWithSaltByteArray, 0, encryptionPasswordByteArray.size)
876
877                         // Populate the second part of the encryption password with salt byte array with the salt.
878                         System.arraycopy(saltByteArray, 0, encryptionPasswordWithSaltByteArray, encryptionPasswordByteArray.size, saltByteArray.size)
879
880                         // Get a SHA-512 message digest.
881                         val messageDigest = MessageDigest.getInstance("SHA-512")
882
883                         // Hash the salted encryption password.  Otherwise, any characters after the 32nd character in the password are ignored.
884                         val hashedEncryptionPasswordWithSaltByteArray = messageDigest.digest(encryptionPasswordWithSaltByteArray)
885
886                         // Truncate the encryption password byte array to 256 bits (32 bytes).
887                         val truncatedHashedEncryptionPasswordWithSaltByteArray = Arrays.copyOf(hashedEncryptionPasswordWithSaltByteArray, 32)
888
889                         // Create an AES secret key from the encryption password byte array.
890                         val secretKey = SecretKeySpec(truncatedHashedEncryptionPasswordWithSaltByteArray, "AES")
891
892                         // Create an initialization vector.  According to NIST, a 12 byte initialization vector is more secure than a 16 byte one.
893                         val initializationVector = ByteArray(12)
894
895                         // Populate the initialization vector with random data.
896                         secureRandom.nextBytes(initializationVector)
897
898                         // 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.
899                         val cipher = Cipher.getInstance("AES/GCM/NoPadding")
900
901                         // Set the GCM tag length to be 128 bits (the maximum) and apply the initialization vector.
902                         val gcmParameterSpec = GCMParameterSpec(128, initializationVector)
903
904                         // Initialize the cipher.
905                         cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmParameterSpec)
906
907                         // Get the file name string.
908                         val passwordEncryptionFileNameString = settingsFileNameEditText.text.toString()
909
910                         // Get the export file output stream, truncating any existing content.
911                         val exportFileOutputStream = contentResolver.openOutputStream(Uri.parse(passwordEncryptionFileNameString), "wt")!!
912
913                         // Add the salt and the initialization vector to the export file output stream.
914                         exportFileOutputStream.write(saltByteArray)
915                         exportFileOutputStream.write(initializationVector)
916
917                         // Create a cipher output stream.
918                         val cipherOutputStream = CipherOutputStream(exportFileOutputStream, cipher)
919
920                         // 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.
921                         var numberOfBytesRead: Int
922                         val encryptedBytes = ByteArray(16)
923
924                         // 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.
925                         while (unencryptedExportFileInputStream.read(encryptedBytes).also { numberOfBytesRead = it } != -1)
926                         // Write the data to the cipher output stream.
927                             cipherOutputStream.write(encryptedBytes, 0, numberOfBytesRead)
928
929                         // Close the streams.
930                         cipherOutputStream.flush()
931                         cipherOutputStream.close()
932                         exportFileOutputStream.close()
933                         unencryptedExportFileInputStream.close()
934
935                         // Wipe the encryption data from memory.
936                         Arrays.fill(saltByteArray, 0.toByte())
937                         Arrays.fill(encryptionPasswordByteArray, 0.toByte())
938                         Arrays.fill(encryptionPasswordWithSaltByteArray, 0.toByte())
939                         Arrays.fill(hashedEncryptionPasswordWithSaltByteArray, 0.toByte())
940                         Arrays.fill(truncatedHashedEncryptionPasswordWithSaltByteArray, 0.toByte())
941                         Arrays.fill(initializationVector, 0.toByte())
942                         Arrays.fill(encryptedBytes, 0.toByte())
943
944                         // Delete the temporary unencrypted export file.
945                         temporaryUnencryptedExportFile.delete()
946
947                         // Display an export disposition snackbar.
948                         if (passwordEncryptionExportStatus == EXPORT_SUCCESSFUL)
949                             Snackbar.make(settingsFileNameEditText, getString(R.string.export_successful), Snackbar.LENGTH_SHORT).show()
950                         else
951                             Snackbar.make(settingsFileNameEditText, getString(R.string.export_failed, passwordEncryptionExportStatus), Snackbar.LENGTH_INDEFINITE).show()
952                     } catch (exception: Exception) {
953                         // Display a snackbar with the exception.
954                         Snackbar.make(settingsFileNameEditText, getString(R.string.export_failed, exception), Snackbar.LENGTH_INDEFINITE).show()
955                     }
956                 }
957
958                 OPENPGP_ENCRYPTION -> {
959                     try {
960                         // Get a handle for the file provider directory.
961                         fileProviderDirectory = File(applicationContext.cacheDir.toString() + "/" + getString(R.string.file_provider_directory))
962
963                         // Create the file provider directory.  Any errors will be handled by the catch statement below.
964                         fileProviderDirectory.mkdir()
965
966                         // Set the temporary pre-encrypted export file.
967                         temporaryPreEncryptedExportFile = File(fileProviderDirectory.toString() + "/" +
968                                 getString(R.string.privacy_browser_settings_pbs, BuildConfig.VERSION_NAME, IMPORT_EXPORT_SCHEMA_VERSION))
969
970                         // Delete the temporary pre-encrypted export file if it already exists.
971                         if (temporaryPreEncryptedExportFile.exists())
972                             temporaryPreEncryptedExportFile.delete()
973
974                         // Create a temporary pre-encrypted export output stream.
975                         val temporaryPreEncryptedExportOutputStream = FileOutputStream(temporaryPreEncryptedExportFile)
976
977                         // Populate the temporary pre-encrypted export file.
978                         val openpgpEncryptionExportStatus = importExportDatabaseHelper.exportUnencrypted(temporaryPreEncryptedExportOutputStream, this)
979
980                         // Flush the temporary pre-encryption export output stream.
981                         temporaryPreEncryptedExportOutputStream.flush()
982
983                         // Close the temporary pre-encryption export output stream.
984                         temporaryPreEncryptedExportOutputStream.close()
985
986                         // Display an export error snackbar if the temporary pre-encrypted export failed.
987                         if (openpgpEncryptionExportStatus != EXPORT_SUCCESSFUL)
988                             Snackbar.make(settingsFileNameEditText, getString(R.string.export_failed, openpgpEncryptionExportStatus), Snackbar.LENGTH_INDEFINITE).show()
989
990                         // Create an encryption intent for OpenKeychain.
991                         val openKeychainEncryptIntent = Intent("org.sufficientlysecure.keychain.action.ENCRYPT_DATA")
992
993                         // Include the temporary unencrypted export file URI.
994                         openKeychainEncryptIntent.data = FileProvider.getUriForFile(this, getString(R.string.file_provider), temporaryPreEncryptedExportFile)
995
996                         // Allow OpenKeychain to read the file URI.
997                         openKeychainEncryptIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
998
999                         // Send the intent to the OpenKeychain package.
1000                         openKeychainEncryptIntent.setPackage("org.sufficientlysecure.keychain")
1001
1002                         // Make it so.
1003                         openKeychainEncryptActivityResultLauncher.launch(openKeychainEncryptIntent)
1004                     } catch (exception: Exception) {
1005                         // Display a snackbar with the exception.
1006                         Snackbar.make(settingsFileNameEditText, getString(R.string.export_failed, exception), Snackbar.LENGTH_INDEFINITE).show()
1007                     }
1008                 }
1009             }
1010         }
1011     }
1012
1013     private fun restartPrivacyBrowser() {
1014         // Create an intent to restart Privacy Browser.
1015         val restartIntent = parentActivityIntent!!
1016
1017         // `Intent.FLAG_ACTIVITY_CLEAR_TASK` removes all activities from the stack.  It requires `Intent.FLAG_ACTIVITY_NEW_TASK`.
1018         restartIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
1019
1020         // Create a restart handler.
1021         val restartHandler = Handler(mainLooper)
1022
1023         // Create a restart runnable.
1024         val restartRunnable = Runnable {
1025
1026             // Restart Privacy Browser.
1027             startActivity(restartIntent)
1028
1029             // Kill this instance of Privacy Browser.  Otherwise, the app exhibits sporadic behavior after the restart.
1030             exitProcess(0)
1031         }
1032
1033         // Restart Privacy Browser after 150 milliseconds to allow enough time for the preferences to be saved.
1034         restartHandler.postDelayed(restartRunnable, 150)
1035     }
1036 }