]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/commitdiff
Add import and export of bookmarks to HTML file. https://redmine.stoutner.com/issues/91
authorSoren Stoutner <soren@stoutner.com>
Thu, 16 Nov 2023 22:09:45 +0000 (15:09 -0700)
committerSoren Stoutner <soren@stoutner.com>
Thu, 16 Nov 2023 22:09:45 +0000 (15:09 -0700)
19 files changed:
app/src/main/java/com/stoutner/privacybrowser/activities/BookmarksActivity.kt
app/src/main/java/com/stoutner/privacybrowser/activities/ImportExportActivity.kt
app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.kt
app/src/main/java/com/stoutner/privacybrowser/dialogs/EditBookmarkDialog.kt
app/src/main/java/com/stoutner/privacybrowser/helpers/BookmarksDatabaseHelper.kt
app/src/main/java/com/stoutner/privacybrowser/helpers/ImportExportBookmarksHelper.kt [new file with mode: 0644]
app/src/main/res/layout/import_export_bottom_appbar.xml
app/src/main/res/layout/import_export_top_appbar.xml
app/src/main/res/layout/logcat_bottom_appbar.xml
app/src/main/res/layout/logcat_top_appbar.xml
app/src/main/res/values-de/strings.xml
app/src/main/res/values-es/strings.xml
app/src/main/res/values-fr/strings.xml
app/src/main/res/values-it/strings.xml
app/src/main/res/values-pt-rBR/strings.xml
app/src/main/res/values-ru/strings.xml
app/src/main/res/values-tr/strings.xml
app/src/main/res/values-zh-rCN/strings.xml
app/src/main/res/values/strings.xml

index b5b3992b686d5f30813236e2ca7bd4044df40ab8..737d6393be75dadd936d94968a211f58ab41f52a 100644 (file)
@@ -90,6 +90,7 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookma
 
     // Define the class variables.
     private var bookmarksDeletedSnackbar: Snackbar? = null
+    private var checkingManyBookmarks = false
     private var closeActivityAfterDismissingSnackbar = false
     private var contextualActionMode: ActionMode? = null
 
@@ -250,46 +251,49 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookma
                 return true
             }
 
-            override fun onItemCheckedStateChanged(mode: ActionMode, position: Int, id: Long, checked: Boolean) {
-                // Get the number of selected bookmarks.
-                val numberOfSelectedBookmarks = bookmarksListView.checkedItemCount
-
-                // Only process commands if at least one bookmark is selected.  Otherwise, a context menu with 0 selected bookmarks is briefly displayed.
-                if (numberOfSelectedBookmarks > 0) {
-                    // Adjust the action mode and the menu according to the number of selected bookmarks.
-                    if (numberOfSelectedBookmarks == 1) {  // One bookmark is selected.
-                        // Show the applicable menu items.
-                        moveBookmarkUpMenuItem.isVisible = true
-                        moveBookmarkDownMenuItem.isVisible = true
-                        editBookmarkMenuItem.isVisible = true
-
-                        // Update the enabled status of the move icons.
-                        updateMoveIcons()
-                    } else {  // More than one bookmark is selected.
-                        // Hide non-applicable `MenuItems`.
-                        moveBookmarkUpMenuItem.isVisible = false
-                        moveBookmarkDownMenuItem.isVisible = false
-                        editBookmarkMenuItem.isVisible = false
-                    }
+            override fun onItemCheckedStateChanged(actionMode: ActionMode, position: Int, id: Long, checked: Boolean) {
+                // Only update the UI if not checking many bookmarks.  In that case, the flag will be reset on the last bookmark so the UI is only updated once.
+                if (!checkingManyBookmarks) {
+                    // Get the number of selected bookmarks.
+                    val numberOfSelectedBookmarks = bookmarksListView.checkedItemCount
+
+                    // Only process commands if at least one bookmark is selected.  Otherwise, a context menu with 0 selected bookmarks is briefly displayed.
+                    if (numberOfSelectedBookmarks > 0) {
+                        // Adjust the action mode and the menu according to the number of selected bookmarks.
+                        if (numberOfSelectedBookmarks == 1) {  // One bookmark is selected.
+                            // Show the applicable menu items.
+                            moveBookmarkUpMenuItem.isVisible = true
+                            moveBookmarkDownMenuItem.isVisible = true
+                            editBookmarkMenuItem.isVisible = true
+
+                            // Update the enabled status of the move icons.
+                            updateMoveIcons()
+                        } else {  // More than one bookmark is selected.
+                            // Hide non-applicable `MenuItems`.
+                            moveBookmarkUpMenuItem.isVisible = false
+                            moveBookmarkDownMenuItem.isVisible = false
+                            editBookmarkMenuItem.isVisible = false
+                        }
 
-                    // Display the move to folder menu item if at least one other folder exists.
-                    moveToFolderMenuItem.isVisible = bookmarksDatabaseHelper.hasFoldersExceptDatabaseId(bookmarksListView.checkedItemIds)
+                        // Display the move to folder menu item if at least one other folder exists.
+                        moveToFolderMenuItem.isVisible = bookmarksDatabaseHelper.hasFoldersExceptDatabaseId(bookmarksListView.checkedItemIds)
 
-                    // List the number of selected bookmarks in the subtitle.
-                    mode.subtitle = getString(R.string.selected, numberOfSelectedBookmarks)
+                        // List the number of selected bookmarks in the subtitle.
+                        actionMode.subtitle = getString(R.string.selected, numberOfSelectedBookmarks)
 
-                    // Show the select all menu item if all the bookmarks are not selected.
-                    selectAllBookmarksMenuItem.isVisible = (numberOfSelectedBookmarks != bookmarksListView.count)
+                        // Show the select all menu item if all the bookmarks are not selected.
+                        selectAllBookmarksMenuItem.isVisible = (numberOfSelectedBookmarks != bookmarksListView.count)
+                    }
                 }
             }
 
             override fun onActionItemClicked(actionMode: ActionMode, menuItem: MenuItem): Boolean {
                 // Declare the variables.
-                val selectedBookmarkNewPosition: Int
-                val selectedBookmarksPositionsSparseBooleanArray: SparseBooleanArray
+                val checkedBookmarkNewPosition: Int
+                val checkedBookmarksPositionsSparseBooleanArray: SparseBooleanArray
 
-                // Initialize the selected bookmark position.
-                var selectedBookmarkPosition = 0
+                // Initialize the checked bookmark position.
+                var checkedBookmarkPosition = 0
 
                 // Get the menu item ID.
                 val menuItemId = menuItem.itemId
@@ -297,30 +301,33 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookma
                 // Run the commands according to the selected action item.
                 if (menuItemId == R.id.move_bookmark_up) {  // Move the bookmark up.
                     // Get the array of checked bookmark positions.
-                    selectedBookmarksPositionsSparseBooleanArray = bookmarksListView.checkedItemPositions
+                    checkedBookmarksPositionsSparseBooleanArray = bookmarksListView.checkedItemPositions
 
                     // Get the position of the bookmark that is selected.  If other bookmarks have previously been selected they will be included in the sparse boolean array with a value of `false`.
-                    for (i in 0 until selectedBookmarksPositionsSparseBooleanArray.size()) {
+                    for (i in 0 until checkedBookmarksPositionsSparseBooleanArray.size()) {
                         // Check to see if the value for the bookmark is true, meaning it is currently selected.
-                        if (selectedBookmarksPositionsSparseBooleanArray.valueAt(i)) {
+                        if (checkedBookmarksPositionsSparseBooleanArray.valueAt(i)) {
                             // Only one bookmark should have a value of `true` when move bookmark up is enabled.
-                            selectedBookmarkPosition = selectedBookmarksPositionsSparseBooleanArray.keyAt(i)
+                            checkedBookmarkPosition = checkedBookmarksPositionsSparseBooleanArray.keyAt(i)
                         }
                     }
 
-                    // Calculate the new position of the selected bookmark.
-                    selectedBookmarkNewPosition = selectedBookmarkPosition - 1
+                    // Calculate the new position of the checked bookmark.
+                    checkedBookmarkNewPosition = checkedBookmarkPosition - 1
+
+                    // Get the bookmarks count.
+                    val bookmarksCount = bookmarksListView.count
 
                     // Iterate through the bookmarks.
-                    for (i in 0 until bookmarksListView.count) {
+                    for (i in 0 until bookmarksCount) {
                         // Get the database ID for the current bookmark.
                         val currentBookmarkDatabaseId = bookmarksListView.getItemIdAtPosition(i).toInt()
 
                         // Update the display order for the current bookmark.
-                        if (i == selectedBookmarkPosition) {  // The current bookmark is the selected bookmark.
+                        if (i == checkedBookmarkPosition) {  // The current bookmark is the selected bookmark.
                             // Move the current bookmark up one.
                             bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i - 1)
-                        } else if ((i + 1) == selectedBookmarkPosition) {  // The current bookmark is immediately above the selected bookmark.
+                        } else if ((i + 1) == checkedBookmarkPosition) {  // The current bookmark is immediately above the selected bookmark.
                             // Move the current bookmark down one.
                             bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i + 1)
                         } else {  // The current bookmark is not changing positions.
@@ -340,25 +347,25 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookma
                     bookmarksCursorAdapter.changeCursor(bookmarksCursor)
 
                     // Scroll to the new bookmark position.
-                    scrollBookmarks(selectedBookmarkNewPosition)
+                    scrollBookmarks(checkedBookmarkNewPosition)
 
                     // Update the enabled status of the move icons.
                     updateMoveIcons()
                 } else if (menuItemId == R.id.move_bookmark_down) {  // Move the bookmark down.
                     // Get the array of checked bookmark positions.
-                    selectedBookmarksPositionsSparseBooleanArray = bookmarksListView.checkedItemPositions
+                    checkedBookmarksPositionsSparseBooleanArray = bookmarksListView.checkedItemPositions
 
-                    // Get the position of the bookmark that is selected.  If other bookmarks have previously been selected they will be included in the sparse boolean array with a value of `false`.
-                    for (i in 0 until selectedBookmarksPositionsSparseBooleanArray.size()) {
+                    // Get the position of the bookmark that is selected.  If other bookmarks have previously been checked they will be included in the sparse boolean array with a value of `false`.
+                    for (i in 0 until checkedBookmarksPositionsSparseBooleanArray.size()) {
                         // Check to see if the value for the bookmark is true, meaning it is currently selected.
-                        if (selectedBookmarksPositionsSparseBooleanArray.valueAt(i)) {
+                        if (checkedBookmarksPositionsSparseBooleanArray.valueAt(i)) {
                             // Only one bookmark should have a value of `true` when move bookmark down is enabled.
-                            selectedBookmarkPosition = selectedBookmarksPositionsSparseBooleanArray.keyAt(i)
+                            checkedBookmarkPosition = checkedBookmarksPositionsSparseBooleanArray.keyAt(i)
                         }
                     }
 
-                    // Calculate the new position of the selected bookmark.
-                    selectedBookmarkNewPosition = selectedBookmarkPosition + 1
+                    // Calculate the new position of the checked bookmark.
+                    checkedBookmarkNewPosition = checkedBookmarkPosition + 1
 
                     // Iterate through the bookmarks.
                     for (i in 0 until bookmarksListView.count) {
@@ -366,10 +373,10 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookma
                         val currentBookmarkDatabaseId = bookmarksListView.getItemIdAtPosition(i).toInt()
 
                         // Update the display order for the current bookmark.
-                        if (i == selectedBookmarkPosition) {  // The current bookmark is the selected bookmark.
+                        if (i == checkedBookmarkPosition) {  // The current bookmark is the checked bookmark.
                             // Move the current bookmark down one.
                             bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i + 1)
-                        } else if (i - 1 == selectedBookmarkPosition) {  // The current bookmark is immediately below the selected bookmark.
+                        } else if ((i - 1) == checkedBookmarkPosition) {  // The current bookmark is immediately below the checked bookmark.
                             // Move the bookmark below the selected bookmark up one.
                             bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i - 1)
                         } else {  // The current bookmark is not changing positions.
@@ -390,7 +397,7 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookma
                     bookmarksCursorAdapter.changeCursor(bookmarksCursor)
 
                     // Scroll to the new bookmark position.
-                    scrollBookmarks(selectedBookmarkNewPosition)
+                    scrollBookmarks(checkedBookmarkNewPosition)
 
                     // Update the enabled status of the move icons.
                     updateMoveIcons()
@@ -402,19 +409,19 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookma
                     moveToFolderDialog.show(supportFragmentManager, resources.getString(R.string.move_to_folder))
                 } else if (menuItemId == R.id.edit_bookmark) {
                     // Get the array of checked bookmark positions.
-                    selectedBookmarksPositionsSparseBooleanArray = bookmarksListView.checkedItemPositions
+                    checkedBookmarksPositionsSparseBooleanArray = bookmarksListView.checkedItemPositions
 
                     // Get the position of the bookmark that is selected.  If other bookmarks have previously been selected they will be included in the sparse boolean array with a value of `false`.
-                    for (i in 0 until selectedBookmarksPositionsSparseBooleanArray.size()) {
+                    for (i in 0 until checkedBookmarksPositionsSparseBooleanArray.size()) {
                         // Check to see if the value for the bookmark is true, meaning it is currently selected.
-                        if (selectedBookmarksPositionsSparseBooleanArray.valueAt(i)) {
+                        if (checkedBookmarksPositionsSparseBooleanArray.valueAt(i)) {
                             // Only one bookmark should have a value of `true` when move edit bookmark is enabled.
-                            selectedBookmarkPosition = selectedBookmarksPositionsSparseBooleanArray.keyAt(i)
+                            checkedBookmarkPosition = checkedBookmarksPositionsSparseBooleanArray.keyAt(i)
                         }
                     }
 
                     // Move the cursor to the selected position.
-                    bookmarksCursor.moveToPosition(selectedBookmarkPosition)
+                    bookmarksCursor.moveToPosition(checkedBookmarkPosition)
 
                     // Get the selected bookmark database ID.
                     val databaseId = bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(ID))
@@ -437,14 +444,14 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookma
                     // Set the deleting bookmarks flag, which prevents the delete menu item from being enabled until the current process finishes.
                     deletingBookmarks = true
 
-                    // Get an array of the selected row IDs.
-                    val selectedBookmarksIdsLongArray = bookmarksListView.checkedItemIds
+                    // Get an array of the checked row IDs.
+                    val checkedBookmarksIdsLongArray = bookmarksListView.checkedItemIds
 
                     // Initialize a variable to count the number of bookmarks to delete.
                     var numberOfBookmarksToDelete = 0
 
                     // Count the number of bookmarks to delete.
-                    for (databaseIdLong in selectedBookmarksIdsLongArray) {
+                    for (databaseIdLong in checkedBookmarksIdsLongArray) {
                         // Convert the database ID long to an int.
                         val databaseIdInt = databaseIdLong.toInt()
 
@@ -459,10 +466,10 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookma
                     }
 
                     // Get an array of checked bookmarks.  `.clone()` makes a copy that won't change if the list view is reloaded, which is needed for re-selecting the bookmarks on undelete.
-                    selectedBookmarksPositionsSparseBooleanArray = bookmarksListView.checkedItemPositions.clone()
+                    checkedBookmarksPositionsSparseBooleanArray = bookmarksListView.checkedItemPositions.clone()
 
                     // Update the bookmarks cursor with the current contents of the bookmarks database except for the specified database IDs.
-                    bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrderExcept(selectedBookmarksIdsLongArray, currentFolderId)
+                    bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrderExcept(checkedBookmarksIdsLongArray, currentFolderId)
 
                     // Update the list view.
                     bookmarksCursorAdapter.changeCursor(bookmarksCursor)
@@ -479,12 +486,24 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookma
                                     // Update the list view.
                                     bookmarksCursorAdapter.changeCursor(bookmarksCursor)
 
-                                    // Re-select the previously selected bookmarks.
-                                    for (i in 0 until selectedBookmarksPositionsSparseBooleanArray.size())
-                                        bookmarksListView.setItemChecked(selectedBookmarksPositionsSparseBooleanArray.keyAt(i), true)
+                                    // Get the number of checked bookmarks.
+                                    val numberOfCheckedBookmarks = checkedBookmarksPositionsSparseBooleanArray.size()
+
+                                    // Set the checking many bookmarks flag.
+                                    checkingManyBookmarks = true
+
+                                    // Re-check the previously checked bookmarks.
+                                    for (i in 0 until numberOfCheckedBookmarks) {
+                                        // Reset the checking many bookmarks flag on the last bookmark so the UI is updated.
+                                        if (i == (numberOfCheckedBookmarks - 1))
+                                            checkingManyBookmarks = false
+
+                                        // Check the bookmark.
+                                        bookmarksListView.setItemChecked(checkedBookmarksPositionsSparseBooleanArray.keyAt(i), true)
+                                    }
                                 } else {  // The snackbar was dismissed without the undo button being pushed.
                                     // Delete each selected bookmark.
-                                    for (databaseIdLong in selectedBookmarksIdsLongArray) {
+                                    for (databaseIdLong in checkedBookmarksIdsLongArray) {
                                         // Convert the database long ID to an int.
                                         val databaseIdInt = databaseIdLong.toInt()
 
@@ -533,8 +552,16 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookma
                     // Get the total number of bookmarks.
                     val numberOfBookmarks = bookmarksListView.count
 
+                    // Set the checking many bookmarks flag.
+                    checkingManyBookmarks = true
+
                     // Select them all.
                     for (i in 0 until numberOfBookmarks) {
+                        // Reset the checking many bookmarks flag on the last bookmark so the UI is updated.
+                        if (i == (numberOfBookmarks - 1))
+                            checkingManyBookmarks = false
+
+                        // Check the bookmark.
                         bookmarksListView.setItemChecked(i, true)
                     }
                 }
@@ -651,8 +678,16 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookma
             // Get the total number of bookmarks.
             val numberOfBookmarks = bookmarksListView.count
 
+            // Set the checking many bookmarks flag.
+            checkingManyBookmarks = true
+
             // Select them all.
             for (i in 0 until numberOfBookmarks) {
+                // Reset the checking many bookmarks flag on the last bookmark so the UI is updated.
+                if (i == (numberOfBookmarks - 1))
+                    checkingManyBookmarks = false
+
+                // Check the bookmark.
                 bookmarksListView.setItemChecked(i, true)
             }
         } else if (menuItemId == R.id.bookmarks_database_view) {
@@ -747,11 +782,11 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookma
         // Move all the bookmarks down one in the display order.
         for (i in 0 until bookmarksListView.count) {
             val databaseId = bookmarksListView.getItemIdAtPosition(i).toInt()
-            bookmarksDatabaseHelper.updateDisplayOrder(databaseId, i + 1)
+            bookmarksDatabaseHelper.updateDisplayOrder(databaseId, displayOrder = i + 1)
         }
 
         // Create the folder, which will be placed at the top of the list view.
-        bookmarksDatabaseHelper.createFolder(folderNameString, currentFolderId, folderIconByteArray)
+        bookmarksDatabaseHelper.createFolder(folderNameString, currentFolderId, displayOrder = 0, folderIconByteArray)
 
         // Update the bookmarks cursor with the contents of the current folder.
         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderId)
@@ -907,7 +942,7 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookma
         val folderId = bookmarksDatabaseHelper.getFolderId(folderDatabaseId)
 
         // Get the contents of the folder.
-        val folderCursor = bookmarksDatabaseHelper.getBookmarkIds(folderId)
+        val folderCursor = bookmarksDatabaseHelper.getBookmarkAndFolderIds(folderId)
 
         // Initialize the bookmark counter.
         var bookmarkCounter = 0
@@ -937,7 +972,7 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookma
         val folderId = bookmarksDatabaseHelper.getFolderId(folderDatabaseId)
 
         // Get the contents of the folder.
-        val folderCursor = bookmarksDatabaseHelper.getBookmarkIds(folderId)
+        val folderCursor = bookmarksDatabaseHelper.getBookmarkAndFolderIds(folderId)
 
         // Delete each of the bookmarks in the folder.
         for (i in 0 until folderCursor.count) {
index 8e4fff48177ea464e21a27dd99d47386b83bb6d5..35f4321acf4fc54fd6f59e6d64b86157fed5070a 100644 (file)
@@ -34,6 +34,7 @@ import android.widget.Button
 import android.widget.EditText
 import android.widget.LinearLayout
 import android.widget.RadioButton
+import android.widget.ScrollView
 import android.widget.Spinner
 import android.widget.TextView
 
@@ -52,6 +53,7 @@ import com.stoutner.privacybrowser.BuildConfig
 import com.stoutner.privacybrowser.helpers.EXPORT_SUCCESSFUL
 import com.stoutner.privacybrowser.helpers.IMPORT_EXPORT_SCHEMA_VERSION
 import com.stoutner.privacybrowser.helpers.IMPORT_SUCCESSFUL
+import com.stoutner.privacybrowser.helpers.ImportExportBookmarksHelper
 import com.stoutner.privacybrowser.helpers.ImportExportDatabaseHelper
 
 import java.io.File
@@ -79,25 +81,34 @@ private const val OPENPGP_ENCRYPTION = 2
 // Define the saved instance state constants.
 private const val ENCRYPTION_PASSWORD_TEXTINPUTLAYOUT_VISIBILITY = "A"
 private const val OPEN_KEYCHAIN_REQUIRED_TEXTVIEW_VISIBILITY = "B"
-private const val FILE_LOCATION_CARD_VIEW = "C"
-private const val FILE_NAME_LINEARLAYOUT_VISIBILITY = "D"
+private const val SETTINGS_FILE_LOCATION_CARDVIEW_VISIBILITY = "C"
+private const val SETTINGS_FILE_NAME_LINEARLAYOUT_VISIBILITY = "D"
 private const val OPEN_KEYCHAIN_IMPORT_INSTRUCTIONS_TEXTVIEW_VISIBILITY = "E"
-private const val IMPORT_EXPORT_BUTTON_VISIBILITY = "F"
-private const val FILE_NAME_TEXT = "G"
-private const val IMPORT_EXPORT_BUTTON_TEXT = "H"
+private const val SETTINGS_IMPORT_EXPORT_BUTTON_VISIBILITY = "F"
+private const val SETTINGS_FILE_NAME_TEXT = "G"
+private const val SETTINGS_IMPORT_EXPORT_BUTTON_TEXT = "H"
+private const val BOOKMARKS_FILE_NAME_LINEARLAYOUT_VISIBILITY = "I"
+private const val BOOKMARKS_IMPORT_EXPORT_BUTTON_VISIBILITY = "J"
+private const val BOOKMARKS_FILE_NAME_TEXT = "K"
+private const val BOOKMARKS_IMPORT_EXPORT_BUTTON_TEXT = "L"
 
 class ImportExportActivity : AppCompatActivity() {
     // Define the class views.
+    private lateinit var scrollView: ScrollView
     private lateinit var encryptionSpinner: Spinner
     private lateinit var encryptionPasswordTextInputLayout: TextInputLayout
     private lateinit var encryptionPasswordEditText: EditText
     private lateinit var openKeychainRequiredTextView: TextView
-    private lateinit var fileLocationCardView: CardView
-    private lateinit var importRadioButton: RadioButton
-    private lateinit var fileNameLinearLayout: LinearLayout
-    private lateinit var fileNameEditText: EditText
+    private lateinit var settingsFileLocationCardView: CardView
+    private lateinit var settingsImportRadioButton: RadioButton
+    private lateinit var settingsFileNameLinearLayout: LinearLayout
+    private lateinit var settingsFileNameEditText: EditText
     private lateinit var openKeychainImportInstructionsTextView: TextView
-    private lateinit var importExportButton: Button
+    private lateinit var settingsImportExportButton: Button
+    private lateinit var bookmarksImportRadioButton: RadioButton
+    private lateinit var bookmarksFileNameLinearLayout: LinearLayout
+    private lateinit var bookmarksFileNameEditText: EditText
+    private lateinit var bookmarksImportExportButton: Button
 
     // Define the class variables.
     private lateinit var fileProviderDirectory: File
@@ -105,32 +116,70 @@ class ImportExportActivity : AppCompatActivity() {
     private lateinit var temporaryPgpEncryptedImportFile: File
     private lateinit var temporaryPreEncryptedExportFile: File
 
-    // Define the browse for import activity result launcher.  It must be defined before `onCreate()` is run or the app will crash.
-    private val browseForImportActivityResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { fileUri: Uri? ->
+    // Define the result launchers.  They must be defined before `onCreate()` is run or the app will crash.
+    private val settingsBrowseForImportActivityResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { fileUri: Uri? ->
         // Only do something if the user didn't press back from the file picker.
         if (fileUri != null) {
             // Get the file name string from the URI.
             val fileNameString = fileUri.toString()
 
-            // Set the file name name text.
-            fileNameEditText.setText(fileNameString)
+            // Set the settings file name text.
+            settingsFileNameEditText.setText(fileNameString)
 
             // Move the cursor to the end of the file name edit text.
-            fileNameEditText.setSelection(fileNameString.length)
+            settingsFileNameEditText.setSelection(fileNameString.length)
         }
     }
 
-    private val browseForExportActivityResultLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("*/*")) { fileUri: Uri? ->
+    private val settingsBrowseForExportActivityResultLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("*/*")) { fileUri: Uri? ->
         // Only do something if the user didn't press back from the file picker.
         if (fileUri != null) {
             // Get the file name string from the URI.
             val fileNameString = fileUri.toString()
 
-            // Set the file name name text.
-            fileNameEditText.setText(fileNameString)
+            // Set the settings file name text.
+            settingsFileNameEditText.setText(fileNameString)
 
             // Move the cursor to the end of the file name edit text.
-            fileNameEditText.setSelection(fileNameString.length)
+            settingsFileNameEditText.setSelection(fileNameString.length)
+        }
+    }
+
+    private val bookmarksBrowseForImportActivityResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { fileUri: Uri? ->
+        // Only do something if the user didn't press back from the file picker.
+        if (fileUri != null) {
+            // Get the file name string from the URI.
+            val fileNameString = fileUri.toString()
+
+            // Set the bookmarks file name text.
+            bookmarksFileNameEditText.setText(fileNameString)
+
+            // Move the cursor to the end of the file name edit text.
+            bookmarksFileNameEditText.setSelection(fileNameString.length)
+
+            // Scroll to the bottom.
+            scrollView.post {
+                scrollView.scrollY = scrollView.height
+            }
+        }
+    }
+
+    private val bookmarksBrowseForExportActivityResultLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("*/*")) { fileUri: Uri? ->
+        // Only do something if the user didn't press back from the file picker.
+        if (fileUri != null) {
+            // Get the file name string from the URI.
+            val fileNameString = fileUri.toString()
+
+            // Set the bookmarks file name text.
+            bookmarksFileNameEditText.setText(fileNameString)
+
+            // Move the cursor to the end of the file name edit text.
+            bookmarksFileNameEditText.setSelection(fileNameString.length)
+
+            // Scroll to the bottom.
+            scrollView.post {
+                scrollView.scrollY = scrollView.height
+            }
         }
     }
 
@@ -195,17 +244,22 @@ class ImportExportActivity : AppCompatActivity() {
         }
 
         // Get handles for the views.
+        scrollView = findViewById(R.id.scrollview)
         encryptionSpinner = findViewById(R.id.encryption_spinner)
         encryptionPasswordTextInputLayout = findViewById(R.id.encryption_password_textinputlayout)
         encryptionPasswordEditText = findViewById(R.id.encryption_password_edittext)
         openKeychainRequiredTextView = findViewById(R.id.openkeychain_required_textview)
-        fileLocationCardView = findViewById(R.id.file_location_cardview)
-        importRadioButton = findViewById(R.id.import_radiobutton)
-        val exportRadioButton = findViewById<RadioButton>(R.id.export_radiobutton)
-        fileNameLinearLayout = findViewById(R.id.file_name_linearlayout)
-        fileNameEditText = findViewById(R.id.file_name_edittext)
+        settingsFileLocationCardView = findViewById(R.id.settings_file_location_cardview)
+        settingsImportRadioButton = findViewById(R.id.settings_import_radiobutton)
+        val settingsExportRadioButton = findViewById<RadioButton>(R.id.settings_export_radiobutton)
+        settingsFileNameLinearLayout = findViewById(R.id.settings_file_name_linearlayout)
+        settingsFileNameEditText = findViewById(R.id.settings_file_name_edittext)
         openKeychainImportInstructionsTextView = findViewById(R.id.openkeychain_import_instructions_textview)
-        importExportButton = findViewById(R.id.import_export_button)
+        settingsImportExportButton = findViewById(R.id.settings_import_export_button)
+        bookmarksImportRadioButton = findViewById(R.id.bookmarks_import_radiobutton)
+        bookmarksFileNameLinearLayout = findViewById(R.id.bookmarks_file_name_linearlayout)
+        bookmarksFileNameEditText = findViewById(R.id.bookmarks_file_name_edittext)
+        bookmarksImportExportButton = findViewById(R.id.bookmarks_import_export_button)
 
         // Create an array adapter for the spinner.
         val encryptionArrayAdapter = ArrayAdapter.createFromResource(this, R.array.encryption_type, R.layout.spinner_item)
@@ -218,7 +272,7 @@ class ImportExportActivity : AppCompatActivity() {
 
         // Update the UI when the spinner changes.
         encryptionSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
-            override fun onItemSelected(parent: AdapterView<*>?, view: View, position: Int, id: Long) {
+            override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
                 when (position) {
                     NO_ENCRYPTION -> {
                         // Hide the unneeded layout items.
@@ -227,21 +281,21 @@ class ImportExportActivity : AppCompatActivity() {
                         openKeychainImportInstructionsTextView.visibility = View.GONE
 
                         // Show the file location card.
-                        fileLocationCardView.visibility = View.VISIBLE
+                        settingsFileLocationCardView.visibility = View.VISIBLE
 
                         // Show the file name linear layout if either import or export is checked.
-                        if (importRadioButton.isChecked || exportRadioButton.isChecked)
-                            fileNameLinearLayout.visibility = View.VISIBLE
+                        if (settingsImportRadioButton.isChecked || settingsExportRadioButton.isChecked)
+                            settingsFileNameLinearLayout.visibility = View.VISIBLE
 
                         // Reset the text of the import button, which may have been changed to `Decrypt`.
-                        if (importRadioButton.isChecked)
-                            importExportButton.setText(R.string.import_button)
+                        if (settingsImportRadioButton.isChecked)
+                            settingsImportExportButton.setText(R.string.import_button)
 
                         // Clear the file name edit text.
-                        fileNameEditText.text.clear()
+                        settingsFileNameEditText.text.clear()
 
                         // Disable the import/export button.
-                        importExportButton.isEnabled = false
+                        settingsImportExportButton.isEnabled = false
                     }
 
                     PASSWORD_ENCRYPTION -> {
@@ -253,21 +307,21 @@ class ImportExportActivity : AppCompatActivity() {
                         encryptionPasswordTextInputLayout.visibility = View.VISIBLE
 
                         // Show the file location card.
-                        fileLocationCardView.visibility = View.VISIBLE
+                        settingsFileLocationCardView.visibility = View.VISIBLE
 
                         // Show the file name linear layout if either import or export is checked.
-                        if (importRadioButton.isChecked || exportRadioButton.isChecked)
-                            fileNameLinearLayout.visibility = View.VISIBLE
+                        if (settingsImportRadioButton.isChecked || settingsExportRadioButton.isChecked)
+                            settingsFileNameLinearLayout.visibility = View.VISIBLE
 
                         // Reset the text of the import button, which may have been changed to `Decrypt`.
-                        if (importRadioButton.isChecked)
-                            importExportButton.setText(R.string.import_button)
+                        if (settingsImportRadioButton.isChecked)
+                            settingsImportExportButton.setText(R.string.import_button)
 
                         // Clear the file name edit text.
-                        fileNameEditText.text.clear()
+                        settingsFileNameEditText.text.clear()
 
                         // Disable the import/export button.
-                        importExportButton.isEnabled = false
+                        settingsImportExportButton.isEnabled = false
                     }
 
                     OPENPGP_ENCRYPTION -> {
@@ -277,36 +331,36 @@ class ImportExportActivity : AppCompatActivity() {
                         // Updated items based on the installation status of OpenKeychain.
                         if (openKeychainInstalled) {  // OpenKeychain is installed.
                             // Show the file location card.
-                            fileLocationCardView.visibility = View.VISIBLE
+                            settingsFileLocationCardView.visibility = View.VISIBLE
 
                             // Update the layout based on the checked radio button.
-                            if (importRadioButton.isChecked) {
+                            if (settingsImportRadioButton.isChecked) {
                                 // Show the file name linear layout and the OpenKeychain import instructions.
-                                fileNameLinearLayout.visibility = View.VISIBLE
+                                settingsFileNameLinearLayout.visibility = View.VISIBLE
                                 openKeychainImportInstructionsTextView.visibility = View.VISIBLE
 
                                 // Set the text of the import button to be `Decrypt`.
-                                importExportButton.setText(R.string.decrypt)
+                                settingsImportExportButton.setText(R.string.decrypt)
 
                                 // Clear the file name edit text.
-                                fileNameEditText.text.clear()
+                                settingsFileNameEditText.text.clear()
 
                                 // Disable the import/export button.
-                                importExportButton.isEnabled = false
-                            } else if (exportRadioButton.isChecked) {
+                                settingsImportExportButton.isEnabled = false
+                            } else if (settingsExportRadioButton.isChecked) {
                                 // Hide the file name linear layout and the OpenKeychain import instructions.
-                                fileNameLinearLayout.visibility = View.GONE
+                                settingsFileNameLinearLayout.visibility = View.GONE
                                 openKeychainImportInstructionsTextView.visibility = View.GONE
 
                                 // Enable the export button.
-                                importExportButton.isEnabled = true
+                                settingsImportExportButton.isEnabled = true
                             }
                         } else {  // OpenKeychain is not installed.
                             // Show the OpenPGP required layout item.
                             openKeychainRequiredTextView.visibility = View.VISIBLE
 
                             // Hide the file location card.
-                            fileLocationCardView.visibility = View.GONE
+                            settingsFileLocationCardView.visibility = View.GONE
                         }
                     }
                 }
@@ -327,12 +381,12 @@ class ImportExportActivity : AppCompatActivity() {
 
             override fun afterTextChanged(s: Editable) {
                 // Enable the import/export button if both the file string and the password are populated.
-                importExportButton.isEnabled = fileNameEditText.text.toString().isNotEmpty() && encryptionPasswordEditText.text.toString().isNotEmpty()
+                settingsImportExportButton.isEnabled = settingsFileNameEditText.text.toString().isNotEmpty() && encryptionPasswordEditText.text.toString().isNotEmpty()
             }
         })
 
-        // Update the UI when the file name edit text changes.
-        fileNameEditText.addTextChangedListener(object : TextWatcher {
+        // Update the UI when the settings file name edit text changes.
+        settingsFileNameEditText.addTextChangedListener(object : TextWatcher {
             override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
                 // Do nothing.
             }
@@ -344,35 +398,57 @@ class ImportExportActivity : AppCompatActivity() {
             override fun afterTextChanged(s: Editable) {
                 // Adjust the UI according to the encryption spinner position.
                 if (encryptionSpinner.selectedItemPosition == PASSWORD_ENCRYPTION) {
-                    // Enable the import/export button if both the file name and the password are populated.
-                    importExportButton.isEnabled = fileNameEditText.text.toString().isNotEmpty() && encryptionPasswordEditText.text.toString().isNotEmpty()
+                    // Enable the settings import/export button if both the file name and the password are populated.
+                    settingsImportExportButton.isEnabled = settingsFileNameEditText.text.toString().isNotEmpty() && encryptionPasswordEditText.text.toString().isNotEmpty()
                 } else {
-                    // Enable the export button if the file name is populated.
-                    importExportButton.isEnabled = fileNameEditText.text.toString().isNotEmpty()
+                    // Enable the settings import/export button if the file name is populated.
+                    settingsImportExportButton.isEnabled = settingsFileNameEditText.text.toString().isNotEmpty()
                 }
             }
         })
 
+        // Update the UI when the bookmarks file name edit text changes.
+        bookmarksFileNameEditText.addTextChangedListener(object : TextWatcher {
+            override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
+                // Do nothing.
+            }
+
+            override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
+                // Do nothing.
+            }
+
+            override fun afterTextChanged(s: Editable) {
+                // Enable the bookmarks import/export button if the file name is populated.
+                bookmarksImportExportButton.isEnabled = bookmarksFileNameEditText.text.toString().isNotEmpty()
+            }
+        })
+
         // Check to see if the activity has been restarted.
         if (savedInstanceState == null) {  // The app has not been restarted.
             // Initially hide the unneeded views.
             encryptionPasswordTextInputLayout.visibility = View.GONE
             openKeychainRequiredTextView.visibility = View.GONE
-            fileNameLinearLayout.visibility = View.GONE
+            settingsFileNameLinearLayout.visibility = View.GONE
             openKeychainImportInstructionsTextView.visibility = View.GONE
-            importExportButton.visibility = View.GONE
+            settingsImportExportButton.visibility = View.GONE
+            bookmarksFileNameLinearLayout.visibility = View.GONE
+            bookmarksImportExportButton.visibility = View.GONE
         } else {  // The app has been restarted.
             // Restore the visibility of the views.
             encryptionPasswordTextInputLayout.visibility = savedInstanceState.getInt(ENCRYPTION_PASSWORD_TEXTINPUTLAYOUT_VISIBILITY)
             openKeychainRequiredTextView.visibility = savedInstanceState.getInt(OPEN_KEYCHAIN_REQUIRED_TEXTVIEW_VISIBILITY)
-            fileLocationCardView.visibility = savedInstanceState.getInt(FILE_LOCATION_CARD_VIEW)
-            fileNameLinearLayout.visibility = savedInstanceState.getInt(FILE_NAME_LINEARLAYOUT_VISIBILITY)
+            settingsFileLocationCardView.visibility = savedInstanceState.getInt(SETTINGS_FILE_LOCATION_CARDVIEW_VISIBILITY)
+            settingsFileNameLinearLayout.visibility = savedInstanceState.getInt(SETTINGS_FILE_NAME_LINEARLAYOUT_VISIBILITY)
             openKeychainImportInstructionsTextView.visibility = savedInstanceState.getInt(OPEN_KEYCHAIN_IMPORT_INSTRUCTIONS_TEXTVIEW_VISIBILITY)
-            importExportButton.visibility = savedInstanceState.getInt(IMPORT_EXPORT_BUTTON_VISIBILITY)
+            settingsImportExportButton.visibility = savedInstanceState.getInt(SETTINGS_IMPORT_EXPORT_BUTTON_VISIBILITY)
+            bookmarksFileNameLinearLayout.visibility = savedInstanceState.getInt(BOOKMARKS_FILE_NAME_LINEARLAYOUT_VISIBILITY)
+            bookmarksImportExportButton.visibility = savedInstanceState.getInt(BOOKMARKS_IMPORT_EXPORT_BUTTON_VISIBILITY)
 
             // Restore the text.
-            fileNameEditText.post { fileNameEditText.setText(savedInstanceState.getString(FILE_NAME_TEXT)) }
-            importExportButton.text = savedInstanceState.getString(IMPORT_EXPORT_BUTTON_TEXT)
+            settingsFileNameEditText.post { settingsFileNameEditText.setText(savedInstanceState.getString(SETTINGS_FILE_NAME_TEXT)) }
+            settingsImportExportButton.text = savedInstanceState.getString(SETTINGS_IMPORT_EXPORT_BUTTON_TEXT)
+            bookmarksFileNameEditText.post { bookmarksFileNameEditText.setText(savedInstanceState.getString(BOOKMARKS_FILE_NAME_TEXT)) }
+            bookmarksImportExportButton.text = savedInstanceState.getString(BOOKMARKS_IMPORT_EXPORT_BUTTON_TEXT)
         }
     }
 
@@ -383,98 +459,159 @@ class ImportExportActivity : AppCompatActivity() {
         // Save the visibility of the views.
         savedInstanceState.putInt(ENCRYPTION_PASSWORD_TEXTINPUTLAYOUT_VISIBILITY, encryptionPasswordTextInputLayout.visibility)
         savedInstanceState.putInt(OPEN_KEYCHAIN_REQUIRED_TEXTVIEW_VISIBILITY, openKeychainRequiredTextView.visibility)
-        savedInstanceState.putInt(FILE_LOCATION_CARD_VIEW, fileLocationCardView.visibility)
-        savedInstanceState.putInt(FILE_NAME_LINEARLAYOUT_VISIBILITY, fileNameLinearLayout.visibility)
+        savedInstanceState.putInt(SETTINGS_FILE_LOCATION_CARDVIEW_VISIBILITY, settingsFileLocationCardView.visibility)
+        savedInstanceState.putInt(SETTINGS_FILE_NAME_LINEARLAYOUT_VISIBILITY, settingsFileNameLinearLayout.visibility)
         savedInstanceState.putInt(OPEN_KEYCHAIN_IMPORT_INSTRUCTIONS_TEXTVIEW_VISIBILITY, openKeychainImportInstructionsTextView.visibility)
-        savedInstanceState.putInt(IMPORT_EXPORT_BUTTON_VISIBILITY, importExportButton.visibility)
+        savedInstanceState.putInt(SETTINGS_IMPORT_EXPORT_BUTTON_VISIBILITY, settingsImportExportButton.visibility)
+        savedInstanceState.putInt(BOOKMARKS_FILE_NAME_LINEARLAYOUT_VISIBILITY, bookmarksFileNameLinearLayout.visibility)
+        savedInstanceState.putInt(BOOKMARKS_IMPORT_EXPORT_BUTTON_VISIBILITY, bookmarksImportExportButton.visibility)
 
         // Save the text.
-        savedInstanceState.putString(FILE_NAME_TEXT, fileNameEditText.text.toString())
-        savedInstanceState.putString(IMPORT_EXPORT_BUTTON_TEXT, importExportButton.text.toString())
+        savedInstanceState.putString(SETTINGS_FILE_NAME_TEXT, settingsFileNameEditText.text.toString())
+        savedInstanceState.putString(SETTINGS_IMPORT_EXPORT_BUTTON_TEXT, settingsImportExportButton.text.toString())
+        savedInstanceState.putString(BOOKMARKS_FILE_NAME_TEXT, bookmarksFileNameEditText.text.toString())
+        savedInstanceState.putString(BOOKMARKS_IMPORT_EXPORT_BUTTON_VISIBILITY, bookmarksImportExportButton.text.toString())
+    }
+
+    fun onClickBookmarksRadioButton(view: View) {
+        // Check to see if import or export was selected.
+        if (view.id == R.id.bookmarks_import_radiobutton) {  // The bookmarks import radio button was selected.
+            // Set the text on the bookmarks import/export button to be `Import`.
+            bookmarksImportExportButton.setText(R.string.import_button)
+        } else {  // The bookmarks export radio button was selected.
+            // Set the text on the bookmarks import/export button to be `Export`.
+            bookmarksImportExportButton.setText(R.string.export)
+        }
+
+        // Display the bookmarks views.
+        bookmarksFileNameLinearLayout.visibility = View.VISIBLE
+        bookmarksImportExportButton.visibility = View.VISIBLE
+
+        // Clear the bookmarks file name edit text.
+        bookmarksFileNameEditText.text.clear()
+
+        // Disable the bookmarks import/export button.
+        bookmarksImportExportButton.isEnabled = false
+
+        // Scroll to the bottom of the screen.
+        scrollView.post {
+            scrollView.scrollY = scrollView.height
+        }
     }
 
-    fun onClickRadioButton(view: View) {
+    fun onClickSettingsRadioButton(view: View) {
         // Check to see if import or export was selected.
-        if (view.id == R.id.import_radiobutton) {  // The import radio button is selected.
+        if (view.id == R.id.settings_import_radiobutton) {  // The settings import radio button was selected.
             // Check to see if OpenPGP encryption is selected.
             if (encryptionSpinner.selectedItemPosition == OPENPGP_ENCRYPTION) {  // OpenPGP encryption selected.
                 // Show the OpenKeychain import instructions.
                 openKeychainImportInstructionsTextView.visibility = View.VISIBLE
 
-                // Set the text on the import/export button to be `Decrypt`.
-                importExportButton.setText(R.string.decrypt)
+                // Set the text on the settings import/export button to be `Decrypt`.
+                settingsImportExportButton.setText(R.string.decrypt)
             } else {  // OpenPGP encryption not selected.
                 // Hide the OpenKeychain import instructions.
                 openKeychainImportInstructionsTextView.visibility = View.GONE
 
-                // Set the text on the import/export button to be `Import`.
-                importExportButton.setText(R.string.import_button)
+                // Set the text on the settings import/export button to be `Import`.
+                settingsImportExportButton.setText(R.string.import_button)
             }
 
-            // Display the file name views.
-            fileNameLinearLayout.visibility = View.VISIBLE
-            importExportButton.visibility = View.VISIBLE
+            // Display the views.
+            settingsFileNameLinearLayout.visibility = View.VISIBLE
+            settingsImportExportButton.visibility = View.VISIBLE
 
-            // Clear the file name edit text.
-            fileNameEditText.text.clear()
+            // Clear the settings file name edit text.
+            settingsFileNameEditText.text.clear()
 
-            // Disable the import/export button.
-            importExportButton.isEnabled = false
-        } else {  // The export radio button is selected.
+            // Disable the settings import/export button.
+            settingsImportExportButton.isEnabled = false
+        } else {  // The settings export radio button was selected.
             // Hide the OpenKeychain import instructions.
             openKeychainImportInstructionsTextView.visibility = View.GONE
 
-            // Set the text on the import/export button to be `Export`.
-            importExportButton.setText(R.string.export)
+            // Set the text on the settings import/export button to be `Export`.
+            settingsImportExportButton.setText(R.string.export)
 
-            // Show the import/export button.
-            importExportButton.visibility = View.VISIBLE
+            // Show the settings import/export button.
+            settingsImportExportButton.visibility = View.VISIBLE
 
             // Check to see if OpenPGP encryption is selected.
             if (encryptionSpinner.selectedItemPosition == OPENPGP_ENCRYPTION) {  // OpenPGP encryption is selected.
-                // Hide the file name views.
-                fileNameLinearLayout.visibility = View.GONE
+                // Hide the settings file name views.
+                settingsFileNameLinearLayout.visibility = View.GONE
 
-                // Enable the export button.
-                importExportButton.isEnabled = true
+                // Enable the settings export button.
+                settingsImportExportButton.isEnabled = true
             } else {  // OpenPGP encryption is not selected.
-                // Show the file name view.
-                fileNameLinearLayout.visibility = View.VISIBLE
+                // Show the settings file name view.
+                settingsFileNameLinearLayout.visibility = View.VISIBLE
 
-                // Clear the file name edit text.
-                fileNameEditText.text.clear()
+                // Clear the settings file name edit text.
+                settingsFileNameEditText.text.clear()
 
-                // Disable the import/export button.
-                importExportButton.isEnabled = false
+                // Disable the settings import/export button.
+                settingsImportExportButton.isEnabled = false
             }
         }
     }
 
-    fun browse(@Suppress("UNUSED_PARAMETER") view: View) {
+    fun settingsBrowse(@Suppress("UNUSED_PARAMETER") view: View) {
         // Check to see if import or export is selected.
-        if (importRadioButton.isChecked) {  // Import is selected.
+        if (settingsImportRadioButton.isChecked) {  // Import is selected.
             // Open the file picker.
-            browseForImportActivityResultLauncher.launch("*/*")
+            settingsBrowseForImportActivityResultLauncher.launch("*/*")
         } else {  // Export is selected
             // Open the file picker with the export name according to the encryption type.
             if (encryptionSpinner.selectedItemPosition == NO_ENCRYPTION)  // No encryption is selected.
-                browseForExportActivityResultLauncher.launch(getString(R.string.privacy_browser_settings_pbs, BuildConfig.VERSION_NAME, IMPORT_EXPORT_SCHEMA_VERSION))
+                settingsBrowseForExportActivityResultLauncher.launch(getString(R.string.privacy_browser_settings_pbs, BuildConfig.VERSION_NAME, IMPORT_EXPORT_SCHEMA_VERSION))
             else  // Password encryption is selected.
-                browseForExportActivityResultLauncher.launch(getString(R.string.privacy_browser_settings_pbs_aes, BuildConfig.VERSION_NAME, IMPORT_EXPORT_SCHEMA_VERSION))
+                settingsBrowseForExportActivityResultLauncher.launch(getString(R.string.privacy_browser_settings_pbs_aes, BuildConfig.VERSION_NAME, IMPORT_EXPORT_SCHEMA_VERSION))
+        }
+    }
+
+    fun bookmarksBrowse(@Suppress("UNUSED_PARAMETER") view: View) {
+        // Check to see if import or export is selected.
+        if (bookmarksImportRadioButton.isChecked) {  // Import is selected.
+            // Open the file picker.
+            bookmarksBrowseForImportActivityResultLauncher.launch("*/*")
+        } else {  // Export is selected.
+            // Open the file picker.
+            bookmarksBrowseForExportActivityResultLauncher.launch(getString(R.string.privacy_browser_bookmarks_html))
+        }
+    }
+
+    fun importExportBookmarks(@Suppress("UNUSED_PARAMETER") view: View) {
+        // Instantiate the import/export bookmarks helper.
+        val importExportBookmarksHelper = ImportExportBookmarksHelper()
+
+        // Get the file name string.
+        val fileNameString = bookmarksFileNameEditText.text.toString()
+
+        // Check to see if import or export is selected.
+        if (bookmarksImportRadioButton.isChecked) {  // Import is selected.
+            // Import the bookmarks.
+            importExportBookmarksHelper.importBookmarks(fileNameString, context = this, scrollView)
+
+            // Repopulate the bookmarks in the main WebView activity.
+            MainWebViewActivity.restartFromBookmarksActivity = true
+        } else {  // Export is selected.
+            // Export the bookmarks.
+            importExportBookmarksHelper.exportBookmarks(fileNameString, context = this, scrollView)
         }
     }
 
-    fun importExport(@Suppress("UNUSED_PARAMETER") view: View) {
-        // Instantiate the import export database helper.
+    fun importExportSettings(@Suppress("UNUSED_PARAMETER") view: View) {
+        // Instantiate the import/export database helper.
         val importExportDatabaseHelper = ImportExportDatabaseHelper()
 
         // Check to see if import or export is selected.
-        if (importRadioButton.isChecked) {  // Import is selected.
+        if (settingsImportRadioButton.isChecked) {  // Import is selected.
             // Initialize the import status string
             var importStatus = ""
 
             // Get the file name string.
-            val fileNameString = fileNameEditText.text.toString()
+            val fileNameString = settingsFileNameEditText.text.toString()
 
             // Import according to the encryption type.
             when (encryptionSpinner.selectedItemPosition) {
@@ -669,18 +806,18 @@ class ImportExportActivity : AppCompatActivity() {
 
             // Display a snack bar with the import error if it was unsuccessful.
             if (importStatus != IMPORT_SUCCESSFUL)
-                Snackbar.make(fileNameEditText, getString(R.string.import_failed, importStatus), Snackbar.LENGTH_INDEFINITE).show()
+                Snackbar.make(settingsFileNameEditText, getString(R.string.import_failed, importStatus), Snackbar.LENGTH_INDEFINITE).show()
         } else {  // Export is selected.
             // Export according to the encryption type.
             when (encryptionSpinner.selectedItemPosition) {
                 NO_ENCRYPTION -> {
                     // Get the file name string.
-                    val noEncryptionFileNameString = fileNameEditText.text.toString()
+                    val noEncryptionFileNameString = settingsFileNameEditText.text.toString()
 
                     try {
-                        // Get the export file output stream.
+                        // Get the export file output stream, truncating any existing content.
                         // A file may be opened directly once the minimum API >= 29.  <https://developer.android.com/reference/kotlin/android/content/ContentResolver#openfile>
-                        val exportFileOutputStream = contentResolver.openOutputStream(Uri.parse(noEncryptionFileNameString))!!
+                        val exportFileOutputStream = contentResolver.openOutputStream(Uri.parse(noEncryptionFileNameString), "wt")!!
 
                         // Export the unencrypted file.
                         val noEncryptionExportStatus = importExportDatabaseHelper.exportUnencrypted(exportFileOutputStream, this)
@@ -690,12 +827,12 @@ class ImportExportActivity : AppCompatActivity() {
 
                         // Display an export disposition snackbar.
                         if (noEncryptionExportStatus == EXPORT_SUCCESSFUL)
-                            Snackbar.make(fileNameEditText, getString(R.string.export_successful), Snackbar.LENGTH_SHORT).show()
+                            Snackbar.make(settingsFileNameEditText, getString(R.string.export_successful), Snackbar.LENGTH_SHORT).show()
                         else
-                            Snackbar.make(fileNameEditText, getString(R.string.export_failed, noEncryptionExportStatus), Snackbar.LENGTH_INDEFINITE).show()
+                            Snackbar.make(settingsFileNameEditText, getString(R.string.export_failed, noEncryptionExportStatus), Snackbar.LENGTH_INDEFINITE).show()
                     } catch (fileNotFoundException: FileNotFoundException) {
                         // Display a snackbar with the exception.
-                        Snackbar.make(fileNameEditText, getString(R.string.export_failed, fileNotFoundException), Snackbar.LENGTH_INDEFINITE).show()
+                        Snackbar.make(settingsFileNameEditText, getString(R.string.export_failed, fileNotFoundException), Snackbar.LENGTH_INDEFINITE).show()
                     }
                 }
 
@@ -768,10 +905,10 @@ class ImportExportActivity : AppCompatActivity() {
                         cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmParameterSpec)
 
                         // Get the file name string.
-                        val passwordEncryptionFileNameString = fileNameEditText.text.toString()
+                        val passwordEncryptionFileNameString = settingsFileNameEditText.text.toString()
 
-                        // Get the export file output stream.
-                        val exportFileOutputStream = contentResolver.openOutputStream(Uri.parse(passwordEncryptionFileNameString))!!
+                        // Get the export file output stream, truncating any existing content.
+                        val exportFileOutputStream = contentResolver.openOutputStream(Uri.parse(passwordEncryptionFileNameString), "wt")!!
 
                         // Add the salt and the initialization vector to the export file output stream.
                         exportFileOutputStream.write(saltByteArray)
@@ -809,12 +946,12 @@ class ImportExportActivity : AppCompatActivity() {
 
                         // Display an export disposition snackbar.
                         if (passwordEncryptionExportStatus == EXPORT_SUCCESSFUL)
-                            Snackbar.make(fileNameEditText, getString(R.string.export_successful), Snackbar.LENGTH_SHORT).show()
+                            Snackbar.make(settingsFileNameEditText, getString(R.string.export_successful), Snackbar.LENGTH_SHORT).show()
                         else
-                            Snackbar.make(fileNameEditText, getString(R.string.export_failed, passwordEncryptionExportStatus), Snackbar.LENGTH_INDEFINITE).show()
+                            Snackbar.make(settingsFileNameEditText, getString(R.string.export_failed, passwordEncryptionExportStatus), Snackbar.LENGTH_INDEFINITE).show()
                     } catch (exception: Exception) {
                         // Display a snackbar with the exception.
-                        Snackbar.make(fileNameEditText, getString(R.string.export_failed, exception), Snackbar.LENGTH_INDEFINITE).show()
+                        Snackbar.make(settingsFileNameEditText, getString(R.string.export_failed, exception), Snackbar.LENGTH_INDEFINITE).show()
                     }
                 }
 
@@ -848,7 +985,7 @@ class ImportExportActivity : AppCompatActivity() {
 
                         // Display an export error snackbar if the temporary pre-encrypted export failed.
                         if (openpgpEncryptionExportStatus != EXPORT_SUCCESSFUL)
-                            Snackbar.make(fileNameEditText, getString(R.string.export_failed, openpgpEncryptionExportStatus), Snackbar.LENGTH_INDEFINITE).show()
+                            Snackbar.make(settingsFileNameEditText, getString(R.string.export_failed, openpgpEncryptionExportStatus), Snackbar.LENGTH_INDEFINITE).show()
 
                         // Create an encryption intent for OpenKeychain.
                         val openKeychainEncryptIntent = Intent("org.sufficientlysecure.keychain.action.ENCRYPT_DATA")
@@ -866,7 +1003,7 @@ class ImportExportActivity : AppCompatActivity() {
                         openKeychainEncryptActivityResultLauncher.launch(openKeychainEncryptIntent)
                     } catch (exception: Exception) {
                         // Display a snackbar with the exception.
-                        Snackbar.make(fileNameEditText, getString(R.string.export_failed, exception), Snackbar.LENGTH_INDEFINITE).show()
+                        Snackbar.make(settingsFileNameEditText, getString(R.string.export_failed, exception), Snackbar.LENGTH_INDEFINITE).show()
                     }
                 }
             }
index 13ae3b784456fac7046c78968f27f677ac603287..8d2a7a07035c27f6df2651fd9d3f1ad649db297b 100644 (file)
@@ -2996,7 +2996,9 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
 
         // Scroll to the new tab position if moving to the new tab.
         if (moveToTab)
-            tabLayout.post { tabLayout.setScrollPosition(newTabPosition, 0F, false, false) }
+            tabLayout.post {
+                tabLayout.setScrollPosition(newTabPosition, 0F, false, false)
+            }
 
         // Show the app bar if it is at the bottom of the screen and the new tab is taking focus.
         if (bottomAppBar && moveToTab && appBarLayout.translationY != 0f) {
@@ -4100,11 +4102,11 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
             val databaseId = bookmarksListView.getItemIdAtPosition(i).toInt()
 
             // Move the bookmark down one slot.
-            bookmarksDatabaseHelper!!.updateDisplayOrder(databaseId, i + 1)
+            bookmarksDatabaseHelper!!.updateDisplayOrder(databaseId, displayOrder = i + 1)
         }
 
         // Create the folder, which will be placed at the top of the list view.
-        bookmarksDatabaseHelper!!.createFolder(folderNameString, currentBookmarksFolderId, folderIconByteArray)
+        bookmarksDatabaseHelper!!.createFolder(folderNameString, currentBookmarksFolderId, displayOrder = 0, folderIconByteArray)
 
         // Update the bookmarks cursor with the current contents of this folder.
         bookmarksCursor = bookmarksDatabaseHelper!!.getBookmarksByDisplayOrder(currentBookmarksFolderId)
@@ -5901,17 +5903,10 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                 // Request focus for the URL text box.
                 urlEditText.requestFocus()
 
-                // Create a display keyboard handler.
-                val displayKeyboardHandler = Handler(Looper.getMainLooper())
-
-                // Create a display keyboard runnable.
-                val displayKeyboardRunnable = Runnable {
-                    // Display the keyboard.
+                // Display the keyboard once the tab layout has settled.
+                tabLayout.post {
                     inputMethodManager.showSoftInput(urlEditText, 0)
                 }
-
-                // Display the keyboard after 100 milliseconds, which leaves enough time for the tab to transition.
-                displayKeyboardHandler.postDelayed(displayKeyboardRunnable, 100)
             }
         }
     }
@@ -6270,7 +6265,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                 urlRelativeLayout.background = AppCompatResources.getDrawable(this, R.color.transparent)
             }
         }  catch (exception: Exception) {
-            //  Try again in 100 milliseconds if the WebView has not yet been populated.
+            //  Try again in 50 milliseconds if the WebView has not yet been populated.
             // Create a handler to set the current WebView.
             val setCurrentWebViewHandler = Handler(Looper.getMainLooper())
 
index 4b0b2c276b6f1f991e427c40503f03ef659ccc4b..0a8be3ec4922b3cc3ef9990beb3ac0af52d28ed1 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2016-2023 Soren Stoutner <soren@stoutner.com>.
+ * Copyright 2016-2023 Soren Stoutner <soren@stoutner.com>.
  *
  * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
  *
@@ -308,4 +308,4 @@ class EditBookmarkDialog : DialogFragment() {
         // Update the enabled status of the save button.
         saveButton.isEnabled = iconChanged || nameChanged || urlChanged
     }
-}
\ No newline at end of file
+}
index 6241b49f77d9202d844ca6bab586e7043903dce4..1d1988408d03abfc7a0c202fb97949f86a966c14 100644 (file)
@@ -172,6 +172,16 @@ class BookmarksDatabaseHelper(context: Context) : SQLiteOpenHelper(context, BOOK
             return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE", null)
         }
 
+    // Get a cursor with just the database IDs of all the bookmarks and folders.  This is useful for counting the number of bookmarks imported.
+    val allBookmarkAndFolderIds: Cursor
+        get() {
+            // Get a readable database handle.
+            val bookmarksDatabase = this.readableDatabase
+
+            // Return a cursor with all the database IDs.  The cursor cannot be closed because it is used in the parent activity.
+            return bookmarksDatabase.rawQuery("SELECT $ID FROM $BOOKMARKS_TABLE", null)
+        }
+
     // Get a cursor for all bookmarks and folders ordered by display order.
     val allBookmarksByDisplayOrder: Cursor
         get() {
@@ -218,16 +228,19 @@ class BookmarksDatabaseHelper(context: Context) : SQLiteOpenHelper(context, BOOK
     }
 
     // Create a folder.
-    fun createFolder(folderName: String, parentFolderId: Long, favoriteIcon: ByteArray) {
+    fun createFolder(folderName: String, parentFolderId: Long, displayOrder: Int, favoriteIcon: ByteArray): Long {
         // Create a bookmark folder content values.
         val bookmarkFolderContentValues = ContentValues()
 
+        // Generate the folder ID.
+        val folderId = generateFolderId()
+
         // The ID is created automatically.  Folders are always created at the top of the list.
         bookmarkFolderContentValues.put(BOOKMARK_NAME, folderName)
         bookmarkFolderContentValues.put(PARENT_FOLDER_ID, parentFolderId)
-        bookmarkFolderContentValues.put(DISPLAY_ORDER, 0)
+        bookmarkFolderContentValues.put(DISPLAY_ORDER, displayOrder)
         bookmarkFolderContentValues.put(IS_FOLDER, true)
-        bookmarkFolderContentValues.put(FOLDER_ID, generateFolderId())
+        bookmarkFolderContentValues.put(FOLDER_ID, folderId)
         bookmarkFolderContentValues.put(FAVORITE_ICON, favoriteIcon)
 
         // Get a writable database handle.
@@ -238,6 +251,9 @@ class BookmarksDatabaseHelper(context: Context) : SQLiteOpenHelper(context, BOOK
 
         // Close the database handle.
         bookmarksDatabase.close()
+
+        // Return the new folder ID.
+        return folderId
     }
 
     // Delete one bookmark.
@@ -309,8 +325,8 @@ class BookmarksDatabaseHelper(context: Context) : SQLiteOpenHelper(context, BOOK
         return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $ID NOT IN ($idsNotToGetStringBuilder)", null)
     }
 
-    // Get a cursor with just database ID of bookmarks and folders in the specified folder.  This is useful for deleting folders with bookmarks that have favorite icons too large to fit in a cursor.
-    fun getBookmarkIds(parentFolderId: Long): Cursor {
+    // Get a cursor with just the database IDs of bookmarks and folders in the specified folder.  This is useful for deleting folders with bookmarks that have favorite icons too large to fit in a cursor.
+    fun getBookmarkAndFolderIds(parentFolderId: Long): Cursor {
         // Get a readable database handle.
         val bookmarksDatabase = this.readableDatabase
 
diff --git a/app/src/main/java/com/stoutner/privacybrowser/helpers/ImportExportBookmarksHelper.kt b/app/src/main/java/com/stoutner/privacybrowser/helpers/ImportExportBookmarksHelper.kt
new file mode 100644 (file)
index 0000000..92d4c7c
--- /dev/null
@@ -0,0 +1,307 @@
+/*
+ * Copyright 2023 Soren Stoutner <soren@stoutner.com>.
+ *
+ * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
+ *
+ * Privacy Browser Android is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Privacy Browser Android is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.stoutner.privacybrowser.helpers
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.drawable.BitmapDrawable
+import android.net.Uri
+import android.util.Base64
+import android.widget.ScrollView
+
+import androidx.appcompat.content.res.AppCompatResources
+
+import com.google.android.material.snackbar.Snackbar
+import com.stoutner.privacybrowser.BuildConfig
+
+import com.stoutner.privacybrowser.R
+
+import java.io.BufferedReader
+import java.io.ByteArrayOutputStream
+import java.io.InputStreamReader
+import java.lang.Exception
+import java.nio.charset.StandardCharsets
+
+class ImportExportBookmarksHelper {
+    // Define the class variables.
+    private var bookmarksAndFolderExported = 0
+
+    fun importBookmarks(fileNameString: String, context: Context, scrollView: ScrollView) {
+        try {
+            // Get an input stream for the file name.
+            val inputStream = context.contentResolver.openInputStream(Uri.parse(fileNameString))!!
+
+            // Load the bookmarks input stream into a buffered reader.
+            val bufferedReader = BufferedReader(InputStreamReader(inputStream))
+
+            // Instantiate the bookmarks database helper.
+            val bookmarksDatabaseHelper = BookmarksDatabaseHelper(context)
+
+            // Get the default icon drawables.
+            val defaultFavoriteIconDrawable = AppCompatResources.getDrawable(context, R.drawable.world)
+            val defaultFolderIconDrawable = AppCompatResources.getDrawable(context, R.drawable.folder_blue_bitmap)
+
+            // Cast the default icon drawables to bitmap drawables.
+            val defaultFavoriteIconBitmapDrawable = (defaultFavoriteIconDrawable as BitmapDrawable?)!!
+            val defaultFolderIconBitmapDrawable = (defaultFolderIconDrawable as BitmapDrawable)
+
+            // Get the default icon bitmaps.
+            val defaultFavoriteIconBitmap = defaultFavoriteIconBitmapDrawable.bitmap
+            val defaultFolderIconBitmap = defaultFolderIconBitmapDrawable.bitmap
+
+            // Create the default icon byte array output streams.
+            val defaultFavoriteIconByteArrayOutputStream = ByteArrayOutputStream()
+            val defaultFolderIconByteArrayOutputStream = ByteArrayOutputStream()
+
+            // Convert the default icon bitmaps to byte array streams.  `0` is for lossless compression (the only option for a PNG).
+            defaultFavoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, defaultFavoriteIconByteArrayOutputStream)
+            defaultFolderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, defaultFolderIconByteArrayOutputStream)
+
+            // Convert the icon byte array streams to byte array stream to byte arrays.
+            val defaultFavoriteIconByteArray = defaultFavoriteIconByteArrayOutputStream.toByteArray()
+            val defaultFolderIconByteArray = defaultFolderIconByteArrayOutputStream.toByteArray()
+
+            // Get a cursor with all the bookmarks.
+            val initialNumberOfBookmarksAndFoldersCursor = bookmarksDatabaseHelper.allBookmarkAndFolderIds
+
+            // Get an initial count of the folders and bookmarks.
+            val initialNumberOfFoldersAndBookmarks = initialNumberOfBookmarksAndFoldersCursor.count
+
+            // Close the cursor.
+            initialNumberOfBookmarksAndFoldersCursor.close()
+
+            // Get a cursor with the contents of the home folder.
+            val homeFolderContentCursor = bookmarksDatabaseHelper.getBookmarkAndFolderIds(0L)
+
+            // Initialize the variables.
+            var parentFolderId = 0L
+            var displayOrder = homeFolderContentCursor.count
+
+            // Close the cursor.
+            homeFolderContentCursor.close()
+
+            // Parse the bookmarks.
+            bufferedReader.forEachLine {
+                // Trim the string.
+                var line = it.trimStart()
+
+                // Only process interesting lines.
+                if (line.startsWith("<DT>")) {  // This is a bookmark or a folder.
+                    // Remove the initial `<DT>`
+                    line = line.substring(4)
+
+                    // Check to see if this is a bookmark or a folder.
+                    if (line.contains("HREF=\"")) {  // This is a bookmark.
+                        // Remove the text before the bookmark name.
+                        var bookmarkName = line.substring(line.indexOf(">") + 1)
+
+                        // Remove the text after the bookmark name.
+                        bookmarkName = bookmarkName.substring(0, bookmarkName.indexOf("<"))
+
+                        // Remove the text before the bookmark URL.
+                        var bookmarkUrl = line.substring(line.indexOf("HREF=\"") + 6)
+
+                        // Remove the text after the bookmark URL.
+                        bookmarkUrl = bookmarkUrl.substring(0, bookmarkUrl.indexOf("\""))
+
+                        // Initialize the favorite icon string.
+                        var favoriteIconString = ""
+
+                        // Populate the favorite icon string.
+                        if (line.contains("ICON=\"data:image/png;base64,")) {  // The `ICON` attribute contains a Base64 encoded icon.
+                            // Remove the text before the icon string.
+                            favoriteIconString = line.substring(line.indexOf("ICON=\"data:image/png;base64,") + 28)
+
+                            // Remove the text after the icon string.
+                            favoriteIconString = favoriteIconString.substring(0, favoriteIconString.indexOf("\""))
+                        } else if (line.contains("ICON_URI=\"data:image/png;base64,")) {  // The `ICON_URI` attribute contains a Base64 encoded icon.
+                            // Remove the text before the icon string.
+                            favoriteIconString = line.substring(line.indexOf("ICON_URI=\"data:image/png;base64,") + 32)
+
+                            // Remove the text after the icon string.
+                            favoriteIconString = favoriteIconString.substring(0, favoriteIconString.indexOf("\""))
+                        }
+
+                        // Populate the favorite icon byte array.
+                        val favoriteIconByteArray = if (favoriteIconString.isEmpty())  // The favorite icon string is empty.  Use the default favorite icon.
+                            defaultFavoriteIconByteArray
+                        else  // The favorite icon string is populated.  Decode it to a byte array.
+                            Base64.decode(favoriteIconString, Base64.DEFAULT)
+
+                        // Add the bookmark.
+                        bookmarksDatabaseHelper.createBookmark(bookmarkName, bookmarkUrl, parentFolderId, displayOrder, favoriteIconByteArray)
+
+                        // Increment the display order.
+                        ++displayOrder
+                    } else {  // This is a folder.  The following lines will be contain in this folder until a `</DL>` is encountered.
+                        // Remove the text before the folder name.
+                        var folderName = line.substring(line.indexOf(">") + 1)
+
+                        // Remove the text after the folder name.
+                        folderName = folderName.substring(0, folderName.indexOf("<"))
+
+                        // Add the folder and set it as the new parent folder ID.
+                        parentFolderId = bookmarksDatabaseHelper.createFolder(folderName, parentFolderId, displayOrder, defaultFolderIconByteArray)
+
+                        // Reset the display order.
+                        displayOrder = 0
+                    }
+                } else if (line.startsWith("</DL>")) {  // This is the end of a folder's contents.
+                    // Reset the parent folder id if it isn't 0.
+                    if (parentFolderId != 0L)
+                        parentFolderId = bookmarksDatabaseHelper.getParentFolderId(parentFolderId)
+
+                    // Get a cursor with the contents of the new parent folder.
+                    val folderContentCursor = bookmarksDatabaseHelper.getBookmarkAndFolderIds(parentFolderId)
+
+                    // Reset the display order.
+                    displayOrder = folderContentCursor.count
+
+                    // Close the cursor.
+                    folderContentCursor.close()
+                }
+            }
+
+            // Get a cursor with all the bookmarks.
+            val finalNumberOfBookmarksAndFoldersCursor = bookmarksDatabaseHelper.allBookmarkAndFolderIds
+
+            // Get the final count of the folders and bookmarks.
+            val finalNumberOfFoldersAndBookmarks = finalNumberOfBookmarksAndFoldersCursor.count
+
+            // Close the cursor.
+            finalNumberOfBookmarksAndFoldersCursor.close()
+
+            // Close the bookmarks database helper.
+            bookmarksDatabaseHelper.close()
+
+            // Close the input stream.
+            inputStream.close()
+
+            // Display a snackbar with the number of folders and bookmarks imported.
+            Snackbar.make(scrollView, context.getString(R.string.bookmarks_imported, finalNumberOfFoldersAndBookmarks - initialNumberOfFoldersAndBookmarks), Snackbar.LENGTH_LONG).show()
+        } catch (exception: Exception) {
+            // Display a snackbar with the error message.
+            Snackbar.make(scrollView, context.getString(R.string.error, exception), Snackbar.LENGTH_INDEFINITE).show()
+        }
+    }
+
+    fun exportBookmarks(fileNameString: String, context: Context, scrollView: ScrollView) {
+        // Create a bookmarks string builder
+        val bookmarksStringBuilder = StringBuilder()
+
+        // Populate the headers.
+        bookmarksStringBuilder.append("<!DOCTYPE NETSCAPE-Bookmark-file-1>")
+        bookmarksStringBuilder.append("\n\n")
+        bookmarksStringBuilder.append("<!-- These bookmarks were exported from Privacy Browser Android ${BuildConfig.VERSION_NAME}. -->")
+        bookmarksStringBuilder.append("\n\n")
+        bookmarksStringBuilder.append("<meta HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">")
+        bookmarksStringBuilder.append("\n\n")
+
+        // Begin the bookmarks.
+        bookmarksStringBuilder.append("<DL><p>")
+        bookmarksStringBuilder.append("\n")
+
+        // Instantiate the bookmarks database helper.
+        val bookmarksDatabaseHelper = BookmarksDatabaseHelper(context)
+
+        // Initialize the indent string.
+        val indentString = "    "
+
+        // Populate the bookmarks, starting with the home folder.
+        bookmarksStringBuilder.append(populateBookmarks(bookmarksDatabaseHelper, 0L, indentString))
+
+        // End the bookmarks.
+        bookmarksStringBuilder.append("</DL>")
+        bookmarksStringBuilder.append("\n")
+
+        try {
+            // Get an output stream for the file name, truncating any existing content.
+            val outputStream = context.contentResolver.openOutputStream(Uri.parse(fileNameString), "wt")!!
+
+            // Write the bookmarks string to the output stream.
+            outputStream.write(bookmarksStringBuilder.toString().toByteArray(StandardCharsets.UTF_8))
+
+            // Close the output stream.
+            outputStream.close()
+
+            // Display a snackbar with the number of folders and bookmarks exported.
+            Snackbar.make(scrollView, context.getString(R.string.bookmarks_exported, bookmarksAndFolderExported), Snackbar.LENGTH_LONG).show()
+        } catch (exception: Exception) {
+            // Display a snackbar with the error message.
+            Snackbar.make(scrollView, context.getString(R.string.error, exception), Snackbar.LENGTH_INDEFINITE).show()
+        }
+
+        // Close the bookmarks database helper.
+        bookmarksDatabaseHelper.close()
+    }
+
+    private fun populateBookmarks(bookmarksDatabaseHelper: BookmarksDatabaseHelper, folderId: Long, indentString: String): String {
+        // Create a bookmarks string builder.
+        val bookmarksStringBuilder = StringBuilder()
+
+        // Get all the bookmarks in the current folder.
+        val bookmarksCursor = bookmarksDatabaseHelper.getBookmarks(folderId)
+
+        // Process each bookmark.
+        while (bookmarksCursor.moveToNext()) {
+            if (bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(IS_FOLDER)) == 1) {  // This is a folder.
+                // Export the folder.
+                bookmarksStringBuilder.append(indentString)
+                bookmarksStringBuilder.append("<DT><H3>")
+                bookmarksStringBuilder.append(bookmarksCursor.getString(bookmarksCursor.getColumnIndexOrThrow(BOOKMARK_NAME)))
+                bookmarksStringBuilder.append("</H3>")
+                bookmarksStringBuilder.append("\n")
+
+                // Begin the folder contents.
+                bookmarksStringBuilder.append(indentString)
+                bookmarksStringBuilder.append("<DL><p>")
+                bookmarksStringBuilder.append("\n")
+
+                // Populate the folder contents.
+                bookmarksStringBuilder.append(populateBookmarks(bookmarksDatabaseHelper, bookmarksCursor.getLong(bookmarksCursor.getColumnIndexOrThrow(FOLDER_ID)), "    $indentString"))
+
+                // End the folder contents.
+                bookmarksStringBuilder.append(indentString)
+                bookmarksStringBuilder.append("</DL><p>")
+                bookmarksStringBuilder.append("\n")
+
+                // Increment the bookmarks and folders exported counter.
+                ++bookmarksAndFolderExported
+            } else {  // This is a bookmark.
+                // Export the bookmark.
+                bookmarksStringBuilder.append(indentString)
+                bookmarksStringBuilder.append("<DT><A HREF=\"")
+                bookmarksStringBuilder.append(bookmarksCursor.getString(bookmarksCursor.getColumnIndexOrThrow(BOOKMARK_URL)))
+                bookmarksStringBuilder.append("\" ICON=\"data:image/png;base64,")
+                bookmarksStringBuilder.append(Base64.encodeToString(bookmarksCursor.getBlob(bookmarksCursor.getColumnIndexOrThrow(FAVORITE_ICON)), Base64.NO_WRAP))
+                bookmarksStringBuilder.append("\">")
+                bookmarksStringBuilder.append(bookmarksCursor.getString(bookmarksCursor.getColumnIndexOrThrow(BOOKMARK_NAME)))
+                bookmarksStringBuilder.append("</A>")
+                bookmarksStringBuilder.append("\n")
+
+                // Increment the bookmarks and folders exported counter.
+                ++bookmarksAndFolderExported
+            }
+        }
+
+        // Return the bookmarks string.
+        return bookmarksStringBuilder.toString()
+    }
+}
index 2c5759c9ee757cdacaa32ae89ec7a0f1ed7aab9e..f72fe66158d61f1a48b8390b373fcc471ae6611e 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-  Copyright © 2018-2022 Soren Stoutner <soren@stoutner.com>.
+  Copyright 2018-2023 Soren Stoutner <soren@stoutner.com>.
 
   This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
 
     android:layout_height="match_parent"
     android:layout_width="match_parent" >
 
-    <!-- the linear layout with `orientation="vertical"` keeps the content above the app bar layout.  `app:layout_dodgeInsetEdges="bottom"` as a child of a coordinator layout moves the view above snackbars.-->
+    <!-- The linear layout with `orientation="vertical"` keeps the content above the app bar layout. -->
     <LinearLayout
         android:layout_height="match_parent"
         android:layout_width="match_parent"
-        android:orientation="vertical"
-        app:layout_dodgeInsetEdges="bottom" >
+        android:orientation="vertical" >
 
         <ScrollView
+            android:id="@+id/scrollview"
             android:layout_height="0dp"
             android:layout_width="match_parent"
             android:layout_weight="1" >
 
-            <!-- Align the cards vertically. -->
+            <!-- Align the content vertically. -->
             <LinearLayout
                 android:layout_height="wrap_content"
                 android:layout_width="match_parent"
-                android:orientation="vertical" >
+                android:orientation="vertical"
+                android:layout_marginTop="10dp" >
+
+                <!-- Settings and Bookmarks. -->
+                <TextView
+                    android:layout_height="wrap_content"
+                    android:layout_width="match_parent"
+                    android:gravity="center_horizontal"
+                    android:text="@string/bookmarks_and_settings"
+                    android:textSize="30sp"
+                    android:textStyle="bold"
+                    android:textColor="?android:textColorPrimary" />
+
+                <TextView
+                    android:layout_height="wrap_content"
+                    android:layout_width="match_parent"
+                    android:gravity="center_horizontal"
+                    android:layout_marginBottom="10dp"
+                    android:text="@string/sqlite_database_format"
+                    android:textSize="14sp" />
 
                 <!-- The encryption card. -->
                 <androidx.cardview.widget.CardView
                     android:layout_height="wrap_content"
                     android:layout_width="match_parent"
-                    android:layout_marginTop="10dp"
-                    android:layout_marginBottom="5dp"
                     android:layout_marginStart="10dp"
-                    android:layout_marginEnd="10dp" >
+                    android:layout_marginEnd="10dp"
+                    android:layout_marginBottom="5dp" >
 
                     <!-- Align the contents of the card vertically. -->
                     <LinearLayout
                     </LinearLayout>
                 </androidx.cardview.widget.CardView>
 
-                <!-- The file location card. -->
+                <!-- The bookmarks and settings file location card. -->
                 <androidx.cardview.widget.CardView
-                    android:id="@+id/file_location_cardview"
+                    android:id="@+id/settings_file_location_cardview"
                     android:layout_height="wrap_content"
                     android:layout_width="match_parent"
                     android:layout_marginTop="5dp"
                             android:orientation="horizontal" >
 
                             <RadioButton
-                                android:id="@+id/import_radiobutton"
+                                android:id="@+id/settings_import_radiobutton"
                                 android:layout_height="wrap_content"
                                 android:layout_width="wrap_content"
                                 android:text="@string/import_button"
                                 android:layout_marginEnd="10dp"
-                                android:onClick="onClickRadioButton" />
+                                android:onClick="onClickSettingsRadioButton" />
 
                             <RadioButton
-                                android:id="@+id/export_radiobutton"
+                                android:id="@+id/settings_export_radiobutton"
                                 android:layout_height="wrap_content"
                                 android:layout_width="wrap_content"
                                 android:text="@string/export"
-                                android:onClick="onClickRadioButton" />
+                                android:onClick="onClickSettingsRadioButton" />
                         </RadioGroup>
 
                         <!-- Align the edit text and the select file button horizontally. -->
                         <LinearLayout
-                            android:id="@+id/file_name_linearlayout"
+                            android:id="@+id/settings_file_name_linearlayout"
                             android:layout_height="wrap_content"
                             android:layout_width="match_parent"
                             android:orientation="horizontal"
 
                                 <!-- `android:inputType="textUri" disables spell check and places an `/` on the main keyboard. -->
                                 <com.google.android.material.textfield.TextInputEditText
-                                    android:id="@+id/file_name_edittext"
+                                    android:id="@+id/settings_file_name_edittext"
                                     android:layout_height="wrap_content"
                                     android:layout_width="match_parent"
                                     android:hint="@string/file_name"
                             </com.google.android.material.textfield.TextInputLayout>
 
                             <Button
-                                android:id="@+id/browse_button"
+                                android:id="@+id/settings_browse_button"
                                 android:layout_height="wrap_content"
                                 android:layout_width="wrap_content"
                                 android:layout_gravity="center_vertical"
                                 android:text="@string/browse"
-                                android:onClick="browse" />
+                                android:onClick="settingsBrowse" />
                         </LinearLayout>
 
                         <!-- OpenKeychain import instructions -->
                             android:textAlignment="center" />
 
                         <Button
-                            android:id="@+id/import_export_button"
+                            android:id="@+id/settings_import_export_button"
+                            android:layout_height="wrap_content"
+                            android:layout_width="wrap_content"
+                            android:layout_gravity="center_horizontal"
+                            android:layout_marginTop="10dp"
+                            android:text="@string/import_button"
+                            android:textSize="18sp"
+                            android:onClick="importExportSettings"
+                            app:backgroundTint="@color/button_background_selector"
+                            android:textColor="@color/button_text_selector" />
+                    </LinearLayout>
+                </androidx.cardview.widget.CardView>
+
+                <!-- Bookmarks. -->
+                <TextView
+                    android:layout_height="wrap_content"
+                    android:layout_width="match_parent"
+                    android:gravity="center_horizontal"
+                    android:layout_marginTop="30dp"
+                    android:text="@string/bookmarks"
+                    android:textSize="30sp"
+                    android:textStyle="bold"
+                    android:textColor="?android:textColorPrimary" />
+
+                <TextView
+                    android:layout_height="wrap_content"
+                    android:layout_width="match_parent"
+                    android:gravity="center_horizontal"
+                    android:layout_marginBottom="10dp"
+                    android:text="@string/html_format"
+                    android:textSize="14sp" />
+
+                <!-- The bookmarks file location card. -->
+                <androidx.cardview.widget.CardView
+                    android:layout_height="wrap_content"
+                    android:layout_width="match_parent"
+                    android:layout_marginStart="10dp"
+                    android:layout_marginEnd="10dp"
+                    android:layout_marginBottom="20dp" >
+
+                    <!-- Align the contents of the card vertically. -->
+                    <LinearLayout
+                        android:layout_height="match_parent"
+                        android:layout_width="match_parent"
+                        android:orientation="vertical"
+                        android:layout_marginTop="10dp"
+                        android:layout_marginBottom="20dp"
+                        android:layout_marginStart="10dp"
+                        android:layout_marginEnd="10dp" >
+
+                        <TextView
+                            android:layout_width="wrap_content"
+                            android:layout_height="wrap_content"
+                            android:layout_gravity="center_horizontal"
+                            android:layout_marginBottom="6dp"
+                            android:text="@string/file_location"
+                            android:textSize="25sp"
+                            android:textStyle="bold"
+                            android:textColor="?colorAccent" />
+
+                        <RadioGroup
+                            android:layout_height="wrap_content"
+                            android:layout_width="wrap_content"
+                            android:layout_gravity="center_horizontal"
+                            android:orientation="horizontal" >
+
+                            <RadioButton
+                                android:id="@+id/bookmarks_import_radiobutton"
+                                android:layout_height="wrap_content"
+                                android:layout_width="wrap_content"
+                                android:text="@string/import_button"
+                                android:layout_marginEnd="10dp"
+                                android:onClick="onClickBookmarksRadioButton" />
+
+                            <RadioButton
+                                android:layout_height="wrap_content"
+                                android:layout_width="wrap_content"
+                                android:text="@string/export"
+                                android:onClick="onClickBookmarksRadioButton" />
+                        </RadioGroup>
+
+                        <!-- Align the edit text and the select file button horizontally. -->
+                        <LinearLayout
+                            android:id="@+id/bookmarks_file_name_linearlayout"
+                            android:layout_height="wrap_content"
+                            android:layout_width="match_parent"
+                            android:orientation="horizontal"
+                            android:layout_marginTop="10dp">
+
+                            <!-- The text input layout makes the hint float above the edit text. -->
+                            <com.google.android.material.textfield.TextInputLayout
+                                android:layout_height="wrap_content"
+                                android:layout_width="0dp"
+                                android:layout_weight="1" >
+
+                                <!-- `android:inputType="textUri" disables spell check and places an `/` on the main keyboard. -->
+                                <com.google.android.material.textfield.TextInputEditText
+                                    android:id="@+id/bookmarks_file_name_edittext"
+                                    android:layout_height="wrap_content"
+                                    android:layout_width="match_parent"
+                                    android:hint="@string/file_name"
+                                    android:inputType="textMultiLine|textUri" />
+                            </com.google.android.material.textfield.TextInputLayout>
+
+                            <Button
+                                android:id="@+id/bookmarks_browse_button"
+                                android:layout_height="wrap_content"
+                                android:layout_width="wrap_content"
+                                android:layout_gravity="center_vertical"
+                                android:text="@string/browse"
+                                android:onClick="bookmarksBrowse" />
+                        </LinearLayout>
+
+                        <Button
+                            android:id="@+id/bookmarks_import_export_button"
                             android:layout_height="wrap_content"
                             android:layout_width="wrap_content"
                             android:layout_gravity="center_horizontal"
                             android:layout_marginTop="10dp"
                             android:text="@string/import_button"
                             android:textSize="18sp"
-                            android:onClick="importExport"
+                            android:onClick="importExportBookmarks"
                             app:backgroundTint="@color/button_background_selector"
                             android:textColor="@color/button_text_selector" />
                     </LinearLayout>
                 android:layout_width="match_parent" />
         </com.google.android.material.appbar.AppBarLayout>
     </LinearLayout>
-</androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
index 3140bd1dbcc89cf3730450daaafc626a741940cc..8bb586f44fc7ebcfbb28bea33836e18ca6b3728c 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-  Copyright © 2018-2022 Soren Stoutner <soren@stoutner.com>.
+  Copyright 2018-2023 Soren Stoutner <soren@stoutner.com>.
 
   This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
 
@@ -25,7 +25,7 @@
     android:layout_height="match_parent"
     android:layout_width="match_parent" >
 
-    <!-- the linear layout with `orientation="vertical"` moves the content below the app bar layout. -->
+    <!-- The linear layout with `orientation="vertical"` moves the content below the app bar layout. -->
     <LinearLayout
         android:layout_height="match_parent"
         android:layout_width="match_parent"
         </com.google.android.material.appbar.AppBarLayout>
 
         <ScrollView
+            android:id="@+id/scrollview"
             android:layout_height="match_parent"
             android:layout_width="match_parent" >
 
-            <!-- Align the cards vertically. -->
+            <!-- Align the content vertically. -->
             <LinearLayout
                 android:layout_height="wrap_content"
                 android:layout_width="match_parent"
-                android:orientation="vertical" >
+                android:orientation="vertical"
+                android:layout_marginTop="10dp" >
+
+                <!-- Settings and Bookmarks. -->
+                <TextView
+                    android:layout_height="wrap_content"
+                    android:layout_width="match_parent"
+                    android:gravity="center_horizontal"
+                    android:text="@string/bookmarks_and_settings"
+                    android:textSize="30sp"
+                    android:textStyle="bold"
+                    android:textColor="?android:textColorPrimary" />
+
+                <TextView
+                    android:layout_height="wrap_content"
+                    android:layout_width="match_parent"
+                    android:gravity="center_horizontal"
+                    android:layout_marginBottom="10dp"
+                    android:text="@string/sqlite_database_format"
+                    android:textSize="14sp" />
 
                 <!-- The encryption card. -->
                 <androidx.cardview.widget.CardView
                     android:layout_height="wrap_content"
                     android:layout_width="match_parent"
-                    android:layout_marginTop="10dp"
-                    android:layout_marginBottom="5dp"
                     android:layout_marginStart="10dp"
-                    android:layout_marginEnd="10dp" >
+                    android:layout_marginEnd="10dp"
+                    android:layout_marginBottom="5dp" >
 
                     <!-- Align the contents of the card vertically. -->
                     <LinearLayout
                     </LinearLayout>
                 </androidx.cardview.widget.CardView>
 
-                <!-- The file location card. -->
+                <!-- The bookmarks and settings file location card. -->
                 <androidx.cardview.widget.CardView
-                    android:id="@+id/file_location_cardview"
+                    android:id="@+id/settings_file_location_cardview"
                     android:layout_height="wrap_content"
                     android:layout_width="match_parent"
                     android:layout_marginTop="5dp"
                             android:orientation="horizontal" >
 
                             <RadioButton
-                                android:id="@+id/import_radiobutton"
+                                android:id="@+id/settings_import_radiobutton"
                                 android:layout_height="wrap_content"
                                 android:layout_width="wrap_content"
                                 android:text="@string/import_button"
                                 android:layout_marginEnd="10dp"
-                                android:onClick="onClickRadioButton" />
+                                android:onClick="onClickSettingsRadioButton" />
 
                             <RadioButton
-                                android:id="@+id/export_radiobutton"
+                                android:id="@+id/settings_export_radiobutton"
                                 android:layout_height="wrap_content"
                                 android:layout_width="wrap_content"
                                 android:text="@string/export"
-                                android:onClick="onClickRadioButton" />
+                                android:onClick="onClickSettingsRadioButton" />
                         </RadioGroup>
 
                         <!-- Align the edit text and the select file button horizontally. -->
                         <LinearLayout
-                            android:id="@+id/file_name_linearlayout"
+                            android:id="@+id/settings_file_name_linearlayout"
                             android:layout_height="wrap_content"
                             android:layout_width="match_parent"
                             android:orientation="horizontal"
 
                                 <!-- `android:inputType="textUri" disables spell check and places an `/` on the main keyboard. -->
                                 <com.google.android.material.textfield.TextInputEditText
-                                    android:id="@+id/file_name_edittext"
+                                    android:id="@+id/settings_file_name_edittext"
                                     android:layout_height="wrap_content"
                                     android:layout_width="match_parent"
                                     android:hint="@string/file_name"
                             </com.google.android.material.textfield.TextInputLayout>
 
                             <Button
-                                android:id="@+id/browse_button"
+                                android:id="@+id/settings_browse_button"
                                 android:layout_height="wrap_content"
                                 android:layout_width="wrap_content"
                                 android:layout_gravity="center_vertical"
                                 android:text="@string/browse"
-                                android:onClick="browse" />
+                                android:onClick="settingsBrowse" />
                         </LinearLayout>
 
                         <!-- OpenKeychain import instructions -->
                             android:textAlignment="center" />
 
                         <Button
-                            android:id="@+id/import_export_button"
+                            android:id="@+id/settings_import_export_button"
+                            android:layout_height="wrap_content"
+                            android:layout_width="wrap_content"
+                            android:layout_gravity="center_horizontal"
+                            android:layout_marginTop="10dp"
+                            android:text="@string/import_button"
+                            android:textSize="18sp"
+                            android:onClick="importExportSettings"
+                            app:backgroundTint="@color/button_background_selector"
+                            android:textColor="@color/button_text_selector" />
+                    </LinearLayout>
+                </androidx.cardview.widget.CardView>
+
+                <!-- Bookmarks. -->
+                <TextView
+                    android:layout_height="wrap_content"
+                    android:layout_width="match_parent"
+                    android:gravity="center_horizontal"
+                    android:layout_marginTop="30dp"
+                    android:text="@string/bookmarks"
+                    android:textSize="30sp"
+                    android:textStyle="bold"
+                    android:textColor="?android:textColorPrimary" />
+
+                <TextView
+                    android:layout_height="wrap_content"
+                    android:layout_width="match_parent"
+                    android:gravity="center_horizontal"
+                    android:layout_marginBottom="10dp"
+                    android:text="@string/html_format"
+                    android:textSize="14sp" />
+
+                <!-- The bookmarks file location card. -->
+                <androidx.cardview.widget.CardView
+                    android:layout_height="wrap_content"
+                    android:layout_width="match_parent"
+                    android:layout_marginStart="10dp"
+                    android:layout_marginEnd="10dp"
+                    android:layout_marginBottom="20dp" >
+
+                    <!-- Align the contents of the card vertically. -->
+                    <LinearLayout
+                        android:layout_height="match_parent"
+                        android:layout_width="match_parent"
+                        android:orientation="vertical"
+                        android:layout_marginTop="10dp"
+                        android:layout_marginBottom="20dp"
+                        android:layout_marginStart="10dp"
+                        android:layout_marginEnd="10dp" >
+
+                        <TextView
+                            android:layout_width="wrap_content"
+                            android:layout_height="wrap_content"
+                            android:layout_gravity="center_horizontal"
+                            android:layout_marginBottom="6dp"
+                            android:text="@string/file_location"
+                            android:textSize="25sp"
+                            android:textStyle="bold"
+                            android:textColor="?colorAccent" />
+
+                        <RadioGroup
+                            android:layout_height="wrap_content"
+                            android:layout_width="wrap_content"
+                            android:layout_gravity="center_horizontal"
+                            android:orientation="horizontal" >
+
+                            <RadioButton
+                                android:id="@+id/bookmarks_import_radiobutton"
+                                android:layout_height="wrap_content"
+                                android:layout_width="wrap_content"
+                                android:text="@string/import_button"
+                                android:layout_marginEnd="10dp"
+                                android:onClick="onClickBookmarksRadioButton" />
+
+                            <RadioButton
+                                android:layout_height="wrap_content"
+                                android:layout_width="wrap_content"
+                                android:text="@string/export"
+                                android:onClick="onClickBookmarksRadioButton" />
+                        </RadioGroup>
+
+                        <!-- Align the edit text and the select file button horizontally. -->
+                        <LinearLayout
+                            android:id="@+id/bookmarks_file_name_linearlayout"
+                            android:layout_height="wrap_content"
+                            android:layout_width="match_parent"
+                            android:orientation="horizontal"
+                            android:layout_marginTop="10dp">
+
+                            <!-- The text input layout makes the hint float above the edit text. -->
+                            <com.google.android.material.textfield.TextInputLayout
+                                android:layout_height="wrap_content"
+                                android:layout_width="0dp"
+                                android:layout_weight="1" >
+
+                                <!-- `android:inputType="textUri" disables spell check and places an `/` on the main keyboard. -->
+                                <com.google.android.material.textfield.TextInputEditText
+                                    android:id="@+id/bookmarks_file_name_edittext"
+                                    android:layout_height="wrap_content"
+                                    android:layout_width="match_parent"
+                                    android:hint="@string/file_name"
+                                    android:inputType="textMultiLine|textUri" />
+                            </com.google.android.material.textfield.TextInputLayout>
+
+                            <Button
+                                android:id="@+id/bookmarks_browse_button"
+                                android:layout_height="wrap_content"
+                                android:layout_width="wrap_content"
+                                android:layout_gravity="center_vertical"
+                                android:text="@string/browse"
+                                android:onClick="bookmarksBrowse" />
+                        </LinearLayout>
+
+                        <Button
+                            android:id="@+id/bookmarks_import_export_button"
                             android:layout_height="wrap_content"
                             android:layout_width="wrap_content"
                             android:layout_gravity="center_horizontal"
                             android:layout_marginTop="10dp"
                             android:text="@string/import_button"
                             android:textSize="18sp"
-                            android:onClick="importExport"
+                            android:onClick="importExportBookmarks"
                             app:backgroundTint="@color/button_background_selector"
                             android:textColor="@color/button_text_selector" />
                     </LinearLayout>
             </LinearLayout>
         </ScrollView>
     </LinearLayout>
-</androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
index dd73e11f0c2fd81dee2bde2dabbdbba33ce7391e..69b04087056f8b8f08517778e13392d44b0ce57f 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-  Copyright © 2018-2022 Soren Stoutner <soren@stoutner.com>.
+  Copyright 2018-2023 Soren Stoutner <soren@stoutner.com>.
 
   This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
 
@@ -64,4 +64,4 @@
                 android:layout_width="match_parent" />
         </com.google.android.material.appbar.AppBarLayout>
     </LinearLayout>
-</androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
index 1e73ccaa1a63e7eca1e6c7ff5f32829ff01df9c2..10f487d7f663bd11b108d9f8d259c538761989b9 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-  Copyright © 2018-2022 Soren Stoutner <soren@stoutner.com>.
+  Copyright 2018-2023 Soren Stoutner <soren@stoutner.com>.
 
   This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
 
@@ -61,4 +61,4 @@
             </ScrollView>
         </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
     </LinearLayout>
-</androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
index 283521e0a41dcfd47ed490625ddc77d516877392..cabfeb4759be8fc8e7c13b1bcca894522332cfe3 100644 (file)
     <string name="username">Benutzername</string>
     <string name="password">Passwort</string>
 
-    <!-- MainWebViewActivity Navigation Menu. -->
+    <!-- Main Navigation Menu. -->
     <string name="navigation_drawer">Navigationspanel</string>
     <string name="clear_and_exit">Leeren und verlassen</string>
     <string name="home">Startseite</string>
     <string name="back">Zurück</string>
     <string name="forward">Vorwärts</string>
+    <string name="scroll_to_bottom">Nach unten scrollen</string>
+        <string name="scroll_to_top">Nach oben scrollen</string>
     <string name="history">Verlauf</string>
         <string name="clear_history">Verlauf löschen</string>
     <string name="open">Öffnen</string>
             <string name="user_agent_firefox_on_windows">Firefox auf Windows</string>
             <string name="user_agent_chrome_on_windows">Chrome auf Windows</string>
             <string name="user_agent_edge_on_windows">Edge auf Windows</string>
-            <string name="user_agent_internet_explorer_on_windows">Internet Explorer auf Windows</string>
             <string name="user_agent_safari_on_macos">Safari auf macOS</string>
             <string name="user_agent_custom">Eigener</string>
         <string name="swipe_to_refresh_options_menu">Herunterziehen zum aktualisieren</string>
         <string name="blocked">blockiert</string>
         <string name="request_blocked">%1$d. blockiert</string>
         <string name="blocked_plural">blockiert</string>
+    <string name="filterlist">Filter-Liste</string>
     <string name="sublist">Unterliste</string>
+        <string name="main_allowlist">Haupt-Positiv-Liste</string>
+        <string name="final_allowlist">Finale Positiv-Liste</string>
+        <string name="domain_allowlist">Domain-Positiv-Liste</string>
+        <string name="domain_initial_allowlist">Domain-Beginn-Positiv-Liste</string>
+        <string name="domain_final_allowlist">Domain-Ende-Positiv-Liste</string>
+        <string name="third_party_allowlist">Drittanbieter-Positiv-Liste</string>
+        <string name="third_party_domain_allowlist">Drittanbieter-Domain-Positiv-Liste</string>
+        <string name="third_party_domain_initial_allowlist">Drittanbieter-Domain-Beginn-Positiv-Liste</string>
+        <string name="main_blocklist">Haupt-Negativ-Liste</string>
+        <string name="initial_blocklist">Beginn-Negativ-Liste</string>
+        <string name="final_blocklist">Finale Negativ-Liste</string>
+        <string name="domain_blocklist">Domain-Negativ-Liste</string>
+        <string name="domain_initial_blocklist">Domain-Beginn-Negativ-Liste</string>
+        <string name="domain_final_blocklist">Domain-Ende-Negativ-Liste</string>
+        <string name="domain_regular_expression_blocklist">Negativ-Liste für reguläre Ausdrücke in Domains</string>
+        <string name="third_party_blocklist">Drittanbieter-Negativ-Liste</string>
+        <string name="third_party_initial_blocklist">Drittanbieter-Beginn-Negativ-Liste</string>
+        <string name="third_party_domain_blocklist">Drittanbieter-Domain-Negativ-Liste</string>
+        <string name="third_party_domain_initial_blocklist">Drittanbieter-Domain-Beginn-Negativ-Liste</string>
+        <string name="third_party_regular_expression_blocklist">Negativ-Liste für reguläre Ausdrücke in Drittanbietern</string>
+        <string name="third_party_domain_regular_expression_blocklist">Negativ-Liste für reguläre Ausdrücke in Drittanbieter-Domains</string>
+        <string name="regular_expression_blocklist">Negativ-Liste für reguläre Ausdrücke</string>
+    <string name="filterlist_entries">Filter-Listen-Einträge</string>
+    <string name="filterlist_original_entry">Filter-Listen-Original-Eintrag</string>
 
     <!-- Domains. -->
     <string name="domains">Domains</string>
             Im Incognito-Modus schließt \'Zurück\'-Button den aktive Tab (oder die App, wenn nur ein Tab geöffnet wurde).</string>
         <string name="allow_screenshots">Screenshots zulassen</string>
         <string name="allow_screenshots_summary">Screenshots, Bildschirmvideos und Anzeige auf unsicheren Bildschirmen zulassen. Eine Änderung dieser Einstellung startet Privacy Browser neu.</string>
+    <string name="filterlists">Filter-Listen</string>
         <string name="easylist">EasyList</string>
+        <string name="easylist_summary">Haupt-Filter-Liste für Werbung.</string>
         <string name="easyprivacy">EasyPrivacy</string>
+        <string name="easyprivacy_summary">Haupt-Filter-Liste für Tracker.</string>
         <string name="fanboys_annoyance_list">Fanboy’s Annoyance Sperrliste</string>
+        <string name="fanboys_annoyance_list_summary">Filtert störende Popups und Links. Beinhaltet Fanboy’s Social Blocking Lists.</string>
         <string name="fanboys_social_blocking_list">Fanboy’s Social Blocking Sperrliste</string>
+        <string name="fanboys_social_blocking_list_summary">Filtert Social-Media-Inhalte von Drittanbietern.</string>
         <string name="ultralist">UltraList</string>
+        <string name="ultralist_summary">UltraList filtert Werbung, die von EasyList nicht gefiltert wird, da dies Internet-Seiten beeinträchtigen könnte.</string>
         <string name="ultraprivacy">UltraPrivacy</string>
+        <string name="ultraprivacy_summary">UltraPrivacy filtert Tracker, die von EasyPrivacy nicht gefiltert werden, da dies Internet-Seiten beeinträchtigen könnte.</string>
         <string name="block_all_third_party_requests">Alle Zugriffe auf Dritt-Anbieter-Inhalte blockieren</string>
         <string name="block_all_third_party_requests_summary">Alle Zugriffe auf Dritt-Anbieter-Inhalte zu blockieren verbessert die Privatsphäre, kann jedoch Webseiten verunstalten.</string>
     <string name="url_modification">URL-Bereinigung</string>
index 092feae5f0a89ffdc8731c046f5bceecd0be2dcb..b3458f6f48263fa56da6bbd10bb3e2cd8d546be6 100644 (file)
     <string name="username">Usuario</string>
     <string name="password">Contraseña</string>
 
-    <!-- MainWebViewActivity Navigation Menu. -->
+    <!-- Main Navigation Menu. -->
     <string name="navigation_drawer">Caja de navegación</string>
     <string name="clear_and_exit">Borrar y salir</string>
     <string name="home">Inicio</string>
     <string name="back">Atrás</string>
     <string name="forward">Adelante</string>
+    <string name="scroll_to_bottom">Desplazar hacia abajo</string>
+        <string name="scroll_to_top">Desplazar hacia arriba</string>
     <string name="history">Historial</string>
         <string name="clear_history">Borrar historial</string>
     <string name="open">Abrir</string>
             <string name="user_agent_firefox_on_windows">Firefox en Windows</string>
             <string name="user_agent_chrome_on_windows">Chrome en Windows</string>
             <string name="user_agent_edge_on_windows">Edge en Windows</string>
-            <string name="user_agent_internet_explorer_on_windows">Internet Explorer en Windows</string>
             <string name="user_agent_safari_on_macos">Safari en macOS</string>
             <string name="user_agent_custom">Personalizado</string>
         <string name="swipe_to_refresh_options_menu">Deslizar para actualizar</string>
index 35adcaf30d4fe2bd31d84f04ff1cafd290e95e3c..95a9589f6afb30b79a76b0eb92c44176e1bc43b3 100644 (file)
     <string name="username">Nom d\'utilisateur</string>
     <string name="password">Mot de passe</string>
 
-    <!-- MainWebViewActivity Navigation Menu. -->
+    <!-- Main Navigation Menu. -->
     <string name="navigation_drawer">Panneau de navigation</string>
     <string name="clear_and_exit">Nettoyer et quitter</string>
     <string name="home">Accueil</string>
             <string name="user_agent_firefox_on_windows">Firefox sous Windows</string>
             <string name="user_agent_chrome_on_windows">Chrome sous Windows</string>
             <string name="user_agent_edge_on_windows">Edge sous Windows</string>
-            <string name="user_agent_internet_explorer_on_windows">Internet Explorer sous Windows</string>
             <string name="user_agent_safari_on_macos">Safari sous macOS</string>
             <string name="user_agent_custom">Personnalisé</string>
         <string name="swipe_to_refresh_options_menu">Glisser pour actualiser</string>
index 2a105a73db85b0c7d183a0d3c35f97df0c6ba173..bb038abac8ced78d91c58749c3e6716505ee1aa8 100644 (file)
@@ -69,6 +69,8 @@
     <string name="unencrypted_website">Sito non criptato</string>
     <string name="no_ssl_certificate">La comunicazione con questo sito web non è criptata. Questo consente a terze parti la possibilità di intercettare le informazioni scambiate,
         di tracciare la navigazione e di inviare contenuto maligno.</string>
+    <string name="content_url">Content URL</string>
+    <string name="content_url_message">Le Content URL sono dati caricati sul proprio dispositivo da altre app.</string>
     <string name="ssl_certificate">Certificato SSL</string>
     <string name="close">Chiudi</string>
     <string name="domain">Dominio</string>
     <string name="username">Nome utente</string>
     <string name="password">Password</string>
 
-    <!-- MainWebViewActivity Navigation Menu. -->
+    <!-- Main Navigation Menu. -->
     <string name="navigation_drawer">Menù di navigazione</string>
     <string name="clear_and_exit">Elimina dati ed esci</string>
     <string name="home">Home</string>
     <string name="back">Indietro</string>
     <string name="forward">Avanti</string>
+    <string name="scroll_to_bottom">Vai alla fine</string>
+        <string name="scroll_to_top">Vai all\'inizio</string>
     <string name="history">Cronologia</string>
         <string name="clear_history">Elimina cronologia</string>
     <string name="open">Apri</string>
             <string name="user_agent_firefox_on_windows">Firefox su Windows</string>
             <string name="user_agent_chrome_on_windows">Chrome su Windows</string>
             <string name="user_agent_edge_on_windows">Edge su Windows</string>
-            <string name="user_agent_internet_explorer_on_windows">Internet Explorer su Windows</string>
             <string name="user_agent_safari_on_macos">Safari su macOS</string>
             <string name="user_agent_custom">Personalizzato</string>
         <string name="swipe_to_refresh_options_menu">Swipe per aggiornare</string>
index 01deebddd736907b2e54a961b460e94e701f3041..5247217ae8e8acaf4be344414703c537f39e8d77 100644 (file)
     <string name="username">Nome do usuário</string>
     <string name="password">Senha</string>
 
-    <!-- MainWebViewActivity Navigation Menu. -->
+    <!-- Main Navigation Menu. -->
     <string name="navigation_drawer">Caixa de Navegação</string>
     <string name="clear_and_exit">Limpar e fechar</string>
     <string name="home">Início</string>
             <string name="user_agent_firefox_on_windows">Firefox para Windows</string>
             <string name="user_agent_chrome_on_windows">Chrome para Windows</string>
             <string name="user_agent_edge_on_windows">Edge para Windows</string>
-            <string name="user_agent_internet_explorer_on_windows">Internet Explorer para Windows</string>
             <string name="user_agent_safari_on_macos">Safari para macOS</string>
             <string name="user_agent_custom">Personalizado</string>
         <string name="swipe_to_refresh_options_menu">Deslize para atualizar</string>
index 092b6613c2a92085c8692cb92035dcb93b9cc0f0..38df983f78ac5253e0f21003bdcaad044a1b9305 100644 (file)
     <string name="username">Имя пользователя</string>
     <string name="password">Пароль</string>
 
-    <!-- MainWebViewActivity Navigation Menu. -->
+    <!-- Main Navigation Menu. -->
     <string name="navigation_drawer">Навигационная панель</string>
     <string name="clear_and_exit">Очистить и выйти</string>
     <string name="home">Домой</string>
     <string name="back">Назад</string>
     <string name="forward">Вперед</string>
+    <string name="scroll_to_bottom">Прокрутить вниз</string>
+        <string name="scroll_to_top">Прокрутить вверх</string>
     <string name="history">История</string>
         <string name="clear_history">Очистить историю</string>
     <string name="open">Открыть</string>
             <string name="user_agent_firefox_on_windows">Firefox на Windows</string>
             <string name="user_agent_chrome_on_windows">Chrome на Windows</string>
             <string name="user_agent_edge_on_windows">Edge на Windows</string>
-            <string name="user_agent_internet_explorer_on_windows">Internet Explorer на Windows</string>
             <string name="user_agent_safari_on_macos">Safari на macOS</string>
             <string name="user_agent_custom">Настраиваемый</string>
         <string name="swipe_to_refresh_options_menu">Потянуть для обновления</string>
index 1b180023ae60eaee6fb00d2ac233c8867f302d02..352c2996f806575d03f02626e43ab994e87777aa 100644 (file)
     <string name="username">Kullanıcı Adı</string>
     <string name="password">Şifre</string>
 
-    <!-- MainWebViewActivity Navigation Menu. -->
+    <!-- Main Navigation Menu. -->
     <string name="navigation_drawer">Gezinti Menüsü</string>
     <string name="clear_and_exit">Temizle ve Çık</string>
     <string name="home">Anasayfa</string>
             <string name="user_agent_firefox_on_windows">Firefox - Windows</string>
             <string name="user_agent_chrome_on_windows">Chrome - Windows</string>
             <string name="user_agent_edge_on_windows">Microsoft Edge - Windows</string>
-            <string name="user_agent_internet_explorer_on_windows">İnternet Explorer - Windows</string>
             <string name="user_agent_safari_on_macos">Safari - macOS</string>
             <string name="user_agent_custom">Özel</string>
         <string name="swipe_to_refresh_options_menu">Yenilemek için kaydır</string>
index 82c58ee3879972fae6ee5900649de8a264114f67..09ef33e1353c8ea19ac218f2f1800704098b902e 100644 (file)
     <string name="username">用户名</string>
     <string name="password">密码</string>
 
-    <!-- MainWebViewActivity Navigation Menu. -->
+    <!-- Main Navigation Menu. -->
     <string name="navigation_drawer">导航栏</string>
     <string name="clear_and_exit">清除并退出</string>
     <string name="home">主页</string>
             <string name="user_agent_firefox_on_windows">Firefox on Windows</string>
             <string name="user_agent_chrome_on_windows">Chrome on Windows</string>
             <string name="user_agent_edge_on_windows">Edge on Windows</string>
-            <string name="user_agent_internet_explorer_on_windows">Internet Explorer on Windows</string>
             <string name="user_agent_safari_on_macos">Safari on macOS</string>
             <string name="user_agent_custom">自定义</string>
         <string name="swipe_to_refresh_options_menu">下拉刷新</string>
index d92634610974045704e79ef7c3ba004d9420b018..7e1dd5be92fa21518da9556876fe16c8cafccd5a 100644 (file)
     <string name="username">Username</string>
     <string name="password">Password</string>
 
-    <!-- MainWebViewActivity Navigation Menu. -->
+    <!-- Main Navigation Menu. -->
     <string name="navigation_drawer">Navigation Drawer</string>
     <string name="clear_and_exit">Clear and Exit</string>
     <string name="home">Home</string>
 
     <!-- Import/Export.  Android removes double spaces, but extra spaces can be manually specified with the Unicode `\u0020` formatting.
       The `%1$*` code inserts variables into the displayed text and should be preserved in translation.  <https://developer.android.com/reference/kotlin/java/util/Formatter> -->
+    <string name="bookmarks_and_settings">Bookmarks and Settings</string>
+    <string name="sqlite_database_format">SQLite database format</string>
+    <string name="html_format">HTML format</string>
     <string name="encryption">Encryption</string>
     <string-array name="encryption_type">
         <item>None</item>
     <string name="decrypt">Decrypt</string>
     <string name="privacy_browser_settings_pbs">Privacy Browser Android %1$s Settings - Schema %2$d.pbs</string>
     <string name="privacy_browser_settings_pbs_aes">Privacy Browser Android %1$s Settings - Schema %2$d.pbs.aes</string>
+    <string name="privacy_browser_bookmarks_html">Privacy Browser Bookmarks.html</string>
     <string name="export_successful">Export successful.</string>
     <string name="export_failed">Export failed:\u0020 %1$s</string>
     <string name="import_failed">Import failed:\u0020 %1$s</string>
+    <string name="bookmarks_imported">%1$d folders and bookmarks imported.</string>
+    <string name="bookmarks_exported">%1$d folders and bookmarks exported.</string>
 
     <!-- Logcat.  Android removes double spaces, but extra spaces can be manually specified with the Unicode `\u0020` formatting.
         The `%1$s` code inserts variables into the displayed text and should be preserved in translation.  <https://developer.android.com/reference/kotlin/java/util/Formatter> -->