]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/commitdiff
Allow duplicate bookmark folders. https://redmine.stoutner.com/issues/199
authorSoren Stoutner <soren@stoutner.com>
Tue, 6 Jun 2023 19:11:10 +0000 (12:11 -0700)
committerSoren Stoutner <soren@stoutner.com>
Tue, 6 Jun 2023 19:11:10 +0000 (12:11 -0700)
33 files changed:
app/src/main/java/com/stoutner/privacybrowser/activities/BookmarksActivity.kt
app/src/main/java/com/stoutner/privacybrowser/activities/BookmarksDatabaseViewActivity.kt
app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.kt
app/src/main/java/com/stoutner/privacybrowser/dialogs/CreateBookmarkFolderDialog.kt
app/src/main/java/com/stoutner/privacybrowser/dialogs/EditBookmarkDatabaseViewDialog.kt
app/src/main/java/com/stoutner/privacybrowser/dialogs/EditBookmarkDialog.kt
app/src/main/java/com/stoutner/privacybrowser/dialogs/EditBookmarkFolderDatabaseViewDialog.kt
app/src/main/java/com/stoutner/privacybrowser/dialogs/EditBookmarkFolderDialog.kt
app/src/main/java/com/stoutner/privacybrowser/dialogs/MoveToFolderDialog.kt
app/src/main/java/com/stoutner/privacybrowser/helpers/BookmarksDatabaseHelper.kt
app/src/main/java/com/stoutner/privacybrowser/helpers/ImportExportDatabaseHelper.kt
app/src/main/res/layout/appbar_spinner_dropdown_item.xml [deleted file]
app/src/main/res/layout/appbar_spinner_item.xml [deleted file]
app/src/main/res/layout/bookmarks_databaseview_appbar_spinner_dropdown_item.xml [new file with mode: 0644]
app/src/main/res/layout/bookmarks_databaseview_appbar_spinner_item.xml [new file with mode: 0644]
app/src/main/res/layout/bookmarks_databaseview_item_linearlayout.xml
app/src/main/res/layout/create_bookmark_folder_dialog.xml
app/src/main/res/layout/databaseview_spinner_dropdown_items.xml
app/src/main/res/layout/databaseview_spinner_item.xml
app/src/main/res/layout/edit_bookmark_databaseview_dialog.xml
app/src/main/res/layout/edit_bookmark_folder_databaseview_dialog.xml
app/src/main/res/layout/edit_bookmark_folder_dialog.xml
app/src/main/res/layout/move_to_folder_dialog.xml
app/src/main/res/layout/move_to_folder_item_linearlayout.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 2353627eae2283fd7abcee67e7b4268746a899ff..b5b3992b686d5f30813236e2ca7bd4044df40ab8 100644 (file)
@@ -19,7 +19,6 @@
 
 package com.stoutner.privacybrowser.activities
 
-import android.annotation.SuppressLint
 import android.content.Context
 import android.content.Intent
 import android.database.Cursor
@@ -56,33 +55,36 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton
 import com.google.android.material.snackbar.Snackbar
 
 import com.stoutner.privacybrowser.R
-import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog.Companion.createBookmark
-import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog.CreateBookmarkListener
-import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog.Companion.createBookmarkFolder
-import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog.CreateBookmarkFolderListener
-import com.stoutner.privacybrowser.dialogs.EditBookmarkDialog.Companion.bookmarkDatabaseId
-import com.stoutner.privacybrowser.dialogs.EditBookmarkDialog.EditBookmarkListener
-import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDialog.Companion.folderDatabaseId
-import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDialog.EditBookmarkFolderListener
-import com.stoutner.privacybrowser.dialogs.MoveToFolderDialog.Companion.moveBookmarks
-import com.stoutner.privacybrowser.dialogs.MoveToFolderDialog.MoveToFolderListener
+import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog
+import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog
+import com.stoutner.privacybrowser.dialogs.EditBookmarkDialog
+import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDialog
+import com.stoutner.privacybrowser.dialogs.MoveToFolderDialog
+import com.stoutner.privacybrowser.helpers.BOOKMARK_NAME
+import com.stoutner.privacybrowser.helpers.DISPLAY_ORDER
+import com.stoutner.privacybrowser.helpers.FAVORITE_ICON
+import com.stoutner.privacybrowser.helpers.FOLDER_ID
+import com.stoutner.privacybrowser.helpers.ID
+import com.stoutner.privacybrowser.helpers.IS_FOLDER
 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper
 
 import java.io.ByteArrayOutputStream
 import java.util.function.Consumer
 
 // Define the public constants.
-const val CURRENT_FOLDER = "current_folder"
-const val CURRENT_TITLE = "current_title"
-const val CURRENT_FAVORITE_ICON_BYTE_ARRAY = "current_favorite_icon_byte_array"
+const val CURRENT_FAVORITE_ICON_BYTE_ARRAY = "A"
+const val CURRENT_FOLDER_ID = "B"
+const val CURRENT_TITLE = "C"
 
 // Define the private constants.
-private const val CHECKED_BOOKMARKS_ARRAY_LIST = "checked_bookmarks_array_list"
+private const val CHECKED_BOOKMARKS_ARRAY_LIST = "D"
+
+class BookmarksActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener, EditBookmarkDialog.EditBookmarkListener,
+    EditBookmarkFolderDialog.EditBookmarkFolderListener, MoveToFolderDialog.MoveToFolderListener {
 
-class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBookmarkFolderListener, EditBookmarkListener, EditBookmarkFolderListener, MoveToFolderListener {
     companion object {
         // Define the public static variables, which are accessed from the bookmarks database view activity.
-        var currentFolder: String = ""
+        var currentFolderId: Long = 0
         var restartFromBookmarksDatabaseViewActivity = false
     }
 
@@ -100,7 +102,7 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
     private lateinit var currentFavoriteIconByteArray: ByteArray
     private lateinit var moveBookmarkDownMenuItem: MenuItem
     private lateinit var moveBookmarkUpMenuItem: MenuItem
-    private lateinit var oldFolderNameString: String
+    private lateinit var moveToFolderMenuItem: MenuItem
 
     override fun onCreate(savedInstanceState: Bundle?) {
         // Get a handle for the shared preferences.
@@ -122,7 +124,7 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
         val launchingIntent = intent
 
         // Populate the variables from the launching intent.
-        currentFolder = launchingIntent.getStringExtra(CURRENT_FOLDER)!!
+        currentFolderId = launchingIntent.getLongExtra(CURRENT_FOLDER_ID, HOME_FOLDER_ID)
         val currentTitle = launchingIntent.getStringExtra(CURRENT_TITLE)!!
         val currentUrl = launchingIntent.getStringExtra(CURRENT_URL)!!
         currentFavoriteIconByteArray = launchingIntent.getByteArrayExtra(CURRENT_FAVORITE_ICON_BYTE_ARRAY)!!
@@ -171,7 +173,7 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
         // Initialize the database helper.
         bookmarksDatabaseHelper = BookmarksDatabaseHelper(this)
 
-        // Set a listener so that tapping a list item loads the URL or folder.
+        // Set a listener so that tapping a list item edits the bookmark or opens a folder.
         bookmarksListView.onItemClickListener = AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, _: Int, id: Long ->
             // Convert the id from long to int to match the format of the bookmarks database.
             val databaseId = id.toInt()
@@ -183,15 +185,15 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
             bookmarkCursor.moveToFirst()
 
             // Act upon the bookmark according to the type.
-            if (bookmarkCursor.getInt(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {  // The selected bookmark is a folder.
-                // Update the current folder.
-                currentFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME))
+            if (bookmarkCursor.getInt(bookmarkCursor.getColumnIndexOrThrow(IS_FOLDER)) == 1) {  // The selected bookmark is a folder.
+                // Update the current folder ID.
+                currentFolderId = bookmarkCursor.getLong(bookmarkCursor.getColumnIndexOrThrow(FOLDER_ID))
 
                 // Load the new folder.
                 loadFolder()
             } else {  // The selected bookmark is not a folder.
                 // Instantiate the edit bookmark dialog.
-                val editBookmarkDialog: DialogFragment = bookmarkDatabaseId(databaseId, currentFavoriteIconBitmap)
+                val editBookmarkDialog = EditBookmarkDialog.editBookmark(databaseId, currentFavoriteIconBitmap)
 
                 // Make it so.
                 editBookmarkDialog.show(supportFragmentManager, resources.getString(R.string.edit_bookmark))
@@ -216,15 +218,16 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
                 menuInflater.inflate(R.menu.bookmarks_context_menu, menu)
 
                 // Set the title.
-                if (currentFolder.isEmpty()) {  // Use `R.string.bookmarks` if in the home folder.
+                if (currentFolderId == HOME_FOLDER_ID) {  // The current folder is the home folder.
                     mode.setTitle(R.string.bookmarks)
                 } else {  // Use the current folder name as the title.
-                    mode.title = currentFolder
+                    mode.title = bookmarksDatabaseHelper.getFolderName(currentFolderId)
                 }
 
                 // Get handles for menu items that need to be selectively disabled.
                 moveBookmarkUpMenuItem = menu.findItem(R.id.move_bookmark_up)
                 moveBookmarkDownMenuItem = menu.findItem(R.id.move_bookmark_down)
+                moveToFolderMenuItem = menu.findItem(R.id.move_to_folder)
                 editBookmarkMenuItem = menu.findItem(R.id.edit_bookmark)
                 deleteBookmarksMenuItem = menu.findItem(R.id.delete_bookmark)
                 selectAllBookmarksMenuItem = menu.findItem(R.id.context_menu_select_all_bookmarks)
@@ -240,14 +243,8 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
             }
 
             override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
-                // Get a handle for the move to folder menu item.
-                val moveToFolderMenuItem = menu.findItem(R.id.move_to_folder)
-
-                // Get a Cursor with all of the folders.
-                val folderCursor = bookmarksDatabaseHelper.allFolders
-
-                // Display the move to folder menu item if at least one folder exists.
-                moveToFolderMenuItem.isVisible = folderCursor.count > 0
+                // Display the move to folder menu item if at least one other folder exists.
+                moveToFolderMenuItem.isVisible = bookmarksDatabaseHelper.hasFoldersExceptDatabaseId(bookmarksListView.checkedItemIds)
 
                 // Make it so.
                 return true
@@ -275,6 +272,9 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
                         editBookmarkMenuItem.isVisible = false
                     }
 
+                    // 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)
 
@@ -328,13 +328,13 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
                             bookmarksCursor.moveToPosition(i)
 
                             // Update the display order only if it is not correct in the database.  This fixes problems where the display order somehow got out of sync.
-                            if (bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.DISPLAY_ORDER)) != i)
+                            if (bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(DISPLAY_ORDER)) != i)
                                 bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i)
                         }
                     }
 
                     // Update the bookmarks cursor with the current contents of the bookmarks database.
-                    bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder)
+                    bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderId)
 
                     // Update the list view.
                     bookmarksCursorAdapter.changeCursor(bookmarksCursor)
@@ -377,14 +377,14 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
                             bookmarksCursor.moveToPosition(i)
 
                             // Update the display order only if it is not correct in the database.  This fixes problems where the display order somehow got out of sync.
-                            if (bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.DISPLAY_ORDER)) != i) {
+                            if (bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(DISPLAY_ORDER)) != i) {
                                 bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i)
                             }
                         }
                     }
 
                     // Update the bookmarks cursor with the current contents of the bookmarks database.
-                    bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder)
+                    bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderId)
 
                     // Update the list view.
                     bookmarksCursorAdapter.changeCursor(bookmarksCursor)
@@ -396,7 +396,7 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
                     updateMoveIcons()
                 } else if (menuItemId == R.id.move_to_folder) {  // Move to folder.
                     // Instantiate the move to folder alert dialog.
-                    val moveToFolderDialog: DialogFragment = moveBookmarks(currentFolder, bookmarksListView.checkedItemIds)
+                    val moveToFolderDialog = MoveToFolderDialog.moveBookmarks(currentFolderId, bookmarksListView.checkedItemIds)
 
                     // Show the move to folder alert dialog.
                     moveToFolderDialog.show(supportFragmentManager, resources.getString(R.string.move_to_folder))
@@ -417,21 +417,18 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
                     bookmarksCursor.moveToPosition(selectedBookmarkPosition)
 
                     // Get the selected bookmark database ID.
-                    val databaseId = bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.ID))
+                    val databaseId = bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(ID))
 
                     // Show the edit bookmark or edit bookmark folder dialog.
-                    if (bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {  // A folder is selected.
-                        // Save the current folder name, which is used in `onSaveBookmarkFolder()`.
-                        oldFolderNameString = bookmarksCursor.getString(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME))
-
+                    if (bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(IS_FOLDER)) == 1) {  // A folder is selected.
                         // Instantiate the edit bookmark folder dialog.
-                        val editFolderDialog: DialogFragment = folderDatabaseId(databaseId, currentFavoriteIconBitmap)
+                        val editFolderDialog = EditBookmarkFolderDialog.editFolder(databaseId, currentFavoriteIconBitmap)
 
                         // Make it so.
                         editFolderDialog.show(supportFragmentManager, resources.getString(R.string.edit_folder))
                     } else {  // A bookmark is selected.
                         // Instantiate the edit bookmark dialog.
-                        val editBookmarkDialog: DialogFragment = bookmarkDatabaseId(databaseId, currentFavoriteIconBitmap)
+                        val editBookmarkDialog = EditBookmarkDialog.editBookmark(databaseId, currentFavoriteIconBitmap)
 
                         // Make it so.
                         editBookmarkDialog.show(supportFragmentManager, resources.getString(R.string.edit_bookmark))
@@ -465,7 +462,7 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
                     selectedBookmarksPositionsSparseBooleanArray = 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, currentFolder)
+                    bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrderExcept(selectedBookmarksIdsLongArray, currentFolderId)
 
                     // Update the list view.
                     bookmarksCursorAdapter.changeCursor(bookmarksCursor)
@@ -474,11 +471,10 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
                     bookmarksDeletedSnackbar = Snackbar.make(findViewById(R.id.bookmarks_coordinatorlayout), getString(R.string.bookmarks_deleted, numberOfBookmarksToDelete), Snackbar.LENGTH_LONG)
                         .setAction(R.string.undo) { }  // Do nothing because everything will be handled by `onDismissed()` below.
                         .addCallback(object : Snackbar.Callback() {
-                            @SuppressLint("SwitchIntDef")  // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
                             override fun onDismissed(snackbar: Snackbar, event: Int) {
                                 if (event == DISMISS_EVENT_ACTION) {  // The user pushed the undo button.
                                     // Update the bookmarks cursor with the current contents of the bookmarks database, including the "deleted" bookmarks.
-                                    bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder)
+                                    bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderId)
 
                                     // Update the list view.
                                     bookmarksCursorAdapter.changeCursor(bookmarksCursor)
@@ -509,7 +505,7 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
                                         bookmarksCursor.moveToPosition(i)
 
                                         // Update the display order only if it is not correct in the database.
-                                        if (bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.DISPLAY_ORDER)) != i)
+                                        if (bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(DISPLAY_ORDER)) != i)
                                             bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i)
                                     }
                                 }
@@ -521,8 +517,13 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
                                 deleteBookmarksMenuItem.isEnabled = true
 
                                 // Close the activity if back has been pressed.
-                                if (closeActivityAfterDismissingSnackbar)
+                                if (closeActivityAfterDismissingSnackbar) {
+                                    // Close the bookmarks drawer and reload the bookmarks list view when returning to the main WebView activity.
+                                    MainWebViewActivity.restartFromBookmarksActivity = true
+
+                                    // Finish the activity.
                                     finish()
+                                }
                             }
                         })
 
@@ -550,7 +551,7 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
         // Set the create new bookmark folder FAB to display the alert dialog.
         createBookmarkFolderFab.setOnClickListener {
             // Create a create bookmark folder dialog.
-            val createBookmarkFolderDialog: DialogFragment = createBookmarkFolder(currentFavoriteIconBitmap)
+            val createBookmarkFolderDialog = CreateBookmarkFolderDialog.createBookmarkFolder(currentFavoriteIconBitmap)
 
             // Show the create bookmark folder dialog.
             createBookmarkFolderDialog.show(supportFragmentManager, getString(R.string.create_folder))
@@ -559,7 +560,7 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
         // Set the create new bookmark FAB to display the alert dialog.
         createBookmarkFab.setOnClickListener {
             // Instantiate the create bookmark dialog.
-            val createBookmarkDialog: DialogFragment = createBookmark(currentUrl, currentTitle, currentFavoriteIconBitmap)
+            val createBookmarkDialog = CreateBookmarkDialog.createBookmark(currentUrl, currentTitle, currentFavoriteIconBitmap)
 
             // Display the create bookmark dialog.
             createBookmarkDialog.show(supportFragmentManager, resources.getString(R.string.create_bookmark))
@@ -568,7 +569,7 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
         // Restore the state if the app has been restarted.
         if (savedInstanceState != null) {
             // Restore the current folder.
-            currentFolder = savedInstanceState.getString(CURRENT_FOLDER)!!
+            currentFolderId = savedInstanceState.getLong(CURRENT_FOLDER_ID, HOME_FOLDER_ID)
 
             // Update the bookmarks list view after it has loaded.
             bookmarksListView.post {
@@ -618,7 +619,7 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
         }
 
         // Store the variables in the saved instance state.
-        savedInstanceState.putString(CURRENT_FOLDER, currentFolder)
+        savedInstanceState.putLong(CURRENT_FOLDER_ID, currentFolderId)
         savedInstanceState.putIntegerArrayList(CHECKED_BOOKMARKS_ARRAY_LIST, checkedBookmarksArrayList)
     }
 
@@ -636,12 +637,12 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
 
         // Run the command according to the selected option.
         if (menuItemId == android.R.id.home) {  // Home.  The home arrow is identified as `android.R.id.home`, not just `R.id.home`.
-            if (currentFolder.isEmpty()) {  // Currently in the home folder.
+            if (currentFolderId == HOME_FOLDER_ID) {  // The current folder is the home folder.
                 // Prepare to finish the activity.
                 prepareFinish()
             } else {  // Currently in a subfolder.
                 // Set the former parent folder as the current folder.
-                currentFolder = bookmarksDatabaseHelper.getParentFolderName(currentFolder)
+                currentFolderId = bookmarksDatabaseHelper.getParentFolderId(currentFolderId)
 
                 // Load the new current folder.
                 loadFolder()
@@ -695,10 +696,10 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
         val newBookmarkDisplayOrder = bookmarksListView.count
 
         // Create the bookmark.
-        bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentFolder, newBookmarkDisplayOrder, favoriteIconByteArray)
+        bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentFolderId, newBookmarkDisplayOrder, favoriteIconByteArray)
 
         // Update the bookmarks cursor with the current contents of this folder.
-        bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder)
+        bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderId)
 
         // Update the list view.
         bookmarksCursorAdapter.changeCursor(bookmarksCursor)
@@ -750,10 +751,10 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
         }
 
         // Create the folder, which will be placed at the top of the list view.
-        bookmarksDatabaseHelper.createFolder(folderNameString, currentFolder, folderIconByteArray)
+        bookmarksDatabaseHelper.createFolder(folderNameString, currentFolderId, folderIconByteArray)
 
         // Update the bookmarks cursor with the contents of the current folder.
-        bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder)
+        bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderId)
 
         // Update the list view.
         bookmarksCursorAdapter.changeCursor(bookmarksCursor)
@@ -796,7 +797,7 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
         contextualActionMode?.finish()
 
         // Update the bookmarks cursor with the contents of the current folder.
-        bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder)
+        bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderId)
 
         // Update the list view.
         bookmarksCursorAdapter.changeCursor(bookmarksCursor)
@@ -813,12 +814,12 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
         val editFolderNameEditText = dialog.findViewById<EditText>(R.id.folder_name_edittext)
 
         // Get the new folder name.
-        val newFolderNameString = editFolderNameEditText.text.toString()
+        val newFolderName = editFolderNameEditText.text.toString()
 
         // Check if the favorite icon has changed.
         if (currentFolderIconRadioButton.isChecked) {  // Only the name has changed.
             // Update the name in the database.
-            bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString)
+            bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderName)
         } else {  // The icon has changed.
             // Populate the new folder icon bitmap.
             val folderIconBitmap: Bitmap = if (defaultFolderIconRadioButton.isChecked) {
@@ -845,14 +846,11 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
             val newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray()
 
             // Update the database.
-            if (!currentFolderIconRadioButton.isChecked && newFolderNameString == oldFolderNameString)  // Only the icon has changed.
-                bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderIconByteArray)
-            else  // The folder icon and the name have changed.
-                bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, newFolderIconByteArray)
+            bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderName, newFolderIconByteArray)
         }
 
         // Update the bookmarks cursor with the current contents of this folder.
-        bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder)
+        bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderId)
 
         // Update the list view.
         bookmarksCursorAdapter.changeCursor(bookmarksCursor)
@@ -875,13 +873,12 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
         val newFolderDatabaseId = newFolderLongArray[0].toInt()
 
         // Set the new folder name.
-        val newFolderName = if (newFolderDatabaseId == 0) {
-            // The new folder is the home folder, represented as `""` in the database.
-            ""
-        } else {
+        val newFolderId = if (newFolderDatabaseId == HOME_FOLDER_DATABASE_ID)
+            // The new folder is the home folder.
+            HOME_FOLDER_ID
+        else
             // Get the new folder name from the database.
-            bookmarksDatabaseHelper.getFolderName(newFolderDatabaseId)
-        }
+            bookmarksDatabaseHelper.getFolderId(newFolderDatabaseId)
 
         // Get a long array with the the database ID of the selected bookmarks.
         val selectedBookmarksLongArray = bookmarksListView.checkedItemIds
@@ -892,11 +889,11 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
             val databaseIdInt = databaseIdLong.toInt()
 
             // Move the selected bookmark to the new folder.
-            bookmarksDatabaseHelper.moveToFolder(databaseIdInt, newFolderName)
+            bookmarksDatabaseHelper.moveToFolder(databaseIdInt, newFolderId)
         }
 
         // Update the bookmarks cursor with the current contents of this folder.
-        bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder)
+        bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderId)
 
         // Update the list view.
         bookmarksCursorAdapter.changeCursor(bookmarksCursor)
@@ -906,11 +903,11 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
     }
 
     private fun countBookmarkFolderContents(folderDatabaseId: Int): Int {
-        // Get the name of the folder.
-        val folderName = bookmarksDatabaseHelper.getFolderName(folderDatabaseId)
+        // Get the folder ID.
+        val folderId = bookmarksDatabaseHelper.getFolderId(folderDatabaseId)
 
         // Get the contents of the folder.
-        val folderCursor = bookmarksDatabaseHelper.getBookmarkIds(folderName)
+        val folderCursor = bookmarksDatabaseHelper.getBookmarkIds(folderId)
 
         // Initialize the bookmark counter.
         var bookmarkCounter = 0
@@ -921,7 +918,7 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
             folderCursor.moveToPosition(i)
 
             // Get the database ID of the item.
-            val itemDatabaseId = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.ID))
+            val itemDatabaseId = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(ID))
 
             // If this is a folder, recursively count the contents first.
             if (bookmarksDatabaseHelper.isFolder(itemDatabaseId))
@@ -936,11 +933,11 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
     }
 
     private fun deleteBookmarkFolderContents(folderDatabaseId: Int) {
-        // Get the name of the folder.
-        val folderName = bookmarksDatabaseHelper.getFolderName(folderDatabaseId)
+        // Get the folder ID.
+        val folderId = bookmarksDatabaseHelper.getFolderId(folderDatabaseId)
 
         // Get the contents of the folder.
-        val folderCursor = bookmarksDatabaseHelper.getBookmarkIds(folderName)
+        val folderCursor = bookmarksDatabaseHelper.getBookmarkIds(folderId)
 
         // Delete each of the bookmarks in the folder.
         for (i in 0 until folderCursor.count) {
@@ -948,7 +945,7 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
             folderCursor.moveToPosition(i)
 
             // Get the database ID of the item.
-            val itemDatabaseId = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.ID))
+            val itemDatabaseId = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(ID))
 
             // If this is a folder, recursively delete the contents first.
             if (bookmarksDatabaseHelper.isFolder(itemDatabaseId))
@@ -969,7 +966,7 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
             bookmarksDeletedSnackbar!!.dismiss()
         } else {  // Go home immediately.
             // Update the bookmarks folder for the bookmarks drawer in the main WebView activity.
-            MainWebViewActivity.currentBookmarksFolder = currentFolder
+            MainWebViewActivity.currentBookmarksFolderId = currentFolderId
 
             // Close the bookmarks drawer and reload the bookmarks list view when returning to the main WebView activity.
             MainWebViewActivity.restartFromBookmarksActivity = true
@@ -1040,7 +1037,7 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
 
     private fun loadFolder() {
         // Update the bookmarks cursor with the contents of the bookmarks database for the current folder.
-        bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder)
+        bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderId)
 
         // Setup a cursor adapter.
         bookmarksCursorAdapter = object : CursorAdapter(this, bookmarksCursor, false) {
@@ -1055,7 +1052,7 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
                 val bookmarkNameTextView = view.findViewById<TextView>(R.id.bookmark_name)
 
                 // Get the favorite icon byte array from the cursor.
-                val favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.FAVORITE_ICON))
+                val favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(FAVORITE_ICON))
 
                 // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
                 val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
@@ -1064,13 +1061,13 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
                 bookmarkFavoriteIconImageView.setImageBitmap(favoriteIconBitmap)
 
                 // Get the bookmark name from the cursor.
-                val bookmarkNameString = cursor.getString(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME))
+                val bookmarkNameString = cursor.getString(cursor.getColumnIndexOrThrow(BOOKMARK_NAME))
 
                 // Display the bookmark name.
                 bookmarkNameTextView.text = bookmarkNameString
 
                 // Make the font bold for folders.
-                if (cursor.getInt(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.IS_FOLDER)) == 1)
+                if (cursor.getInt(cursor.getColumnIndexOrThrow(IS_FOLDER)) == 1)
                     bookmarkNameTextView.typeface = Typeface.DEFAULT_BOLD
                 else  // Reset the font to default for normal bookmarks.
                     bookmarkNameTextView.typeface = Typeface.DEFAULT
@@ -1081,10 +1078,10 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkListener, CreateBoo
         bookmarksListView.adapter = bookmarksCursorAdapter
 
         // Set the app bar title.
-        if (currentFolder.isEmpty())
+        if (currentFolderId == HOME_FOLDER_ID)  // The home folder is the current folder.
             appBar.setTitle(R.string.bookmarks)
         else
-            appBar.title = currentFolder
+            appBar.title = bookmarksDatabaseHelper.getFolderName(currentFolderId)
     }
 
     public override fun onDestroy() {
index ad408ecee8c691e1ada7a4ea2e437f1b588db746..227c4149846db099a471ad573ab39418dc98d277 100644 (file)
@@ -61,24 +61,30 @@ import com.stoutner.privacybrowser.dialogs.EditBookmarkDatabaseViewDialog.Compan
 import com.stoutner.privacybrowser.dialogs.EditBookmarkDatabaseViewDialog.EditBookmarkDatabaseViewListener
 import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDatabaseViewDialog.Companion.folderDatabaseId
 import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDatabaseViewDialog.EditBookmarkFolderDatabaseViewListener
+import com.stoutner.privacybrowser.helpers.BOOKMARK_NAME
+import com.stoutner.privacybrowser.helpers.BOOKMARK_URL
+import com.stoutner.privacybrowser.helpers.DISPLAY_ORDER
+import com.stoutner.privacybrowser.helpers.FAVORITE_ICON
+import com.stoutner.privacybrowser.helpers.FOLDER_ID
+import com.stoutner.privacybrowser.helpers.ID
+import com.stoutner.privacybrowser.helpers.IS_FOLDER
+import com.stoutner.privacybrowser.helpers.PARENT_FOLDER_ID
 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper
 
 import java.io.ByteArrayOutputStream
 
 import java.util.Arrays
 
+// Define the public class constants.
+const val HOME_FOLDER_DATABASE_ID = -1
+const val HOME_FOLDER_ID = 0L
+
 // Define the private class constants.
 private const val ALL_FOLDERS_DATABASE_ID = -2
-private const val CURRENT_FOLDER_DATABASE_ID = "current_folder_database_id"
-private const val CURRENT_FOLDER_NAME = "current_folder_name"
-private const val SORT_BY_DISPLAY_ORDER = "sort_by_display_order"
+private const val CURRENT_FOLDER_DATABASE_ID = "A"
+private const val SORT_BY_DISPLAY_ORDER = "B"
 
 class BookmarksDatabaseViewActivity : AppCompatActivity(), EditBookmarkDatabaseViewListener, EditBookmarkFolderDatabaseViewListener {
-    companion object {
-        // Define the public class constants.
-        const val HOME_FOLDER_DATABASE_ID = -1
-    }
-
     // Define the class variables.
     private var closeActivityAfterDismissingSnackbar = false
     private var currentFolderDatabaseId = 0
@@ -90,8 +96,6 @@ class BookmarksDatabaseViewActivity : AppCompatActivity(), EditBookmarkDatabaseV
     private lateinit var bookmarksCursor: Cursor
     private lateinit var bookmarksDatabaseHelper: BookmarksDatabaseHelper
     private lateinit var bookmarksListView: ListView
-    private lateinit var currentFolderName: String
-    private lateinit var oldFolderNameString: String
 
     public override fun onCreate(savedInstanceState: Bundle?) {
         // Get a handle for the shared preferences.
@@ -110,10 +114,10 @@ class BookmarksDatabaseViewActivity : AppCompatActivity(), EditBookmarkDatabaseV
         super.onCreate(savedInstanceState)
 
         // Get the favorite icon byte array.
-        val favoriteIconByteArray = intent.getByteArrayExtra(CURRENT_FAVORITE_ICON_BYTE_ARRAY)!!
+        val currentFavoriteIconByteArray = intent.getByteArrayExtra(CURRENT_FAVORITE_ICON_BYTE_ARRAY)!!
 
         // Convert the favorite icon byte array to a bitmap and store it in a class variable.
-        val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
+        val currentFavoriteIconBitmap = BitmapFactory.decodeByteArray(currentFavoriteIconByteArray, 0, currentFavoriteIconByteArray.size)
 
         // Set the view according to the theme.
         if (bottomAppBar) {
@@ -129,7 +133,7 @@ class BookmarksDatabaseViewActivity : AppCompatActivity(), EditBookmarkDatabaseV
 
         // Get a handle for the toolbar.
         val toolbar = findViewById<Toolbar>(R.id.bookmarks_databaseview_toolbar)
-        val bookmarksListView = findViewById<ListView>(R.id.bookmarks_databaseview_listview)
+        bookmarksListView = findViewById(R.id.bookmarks_databaseview_listview)
 
         // Set the support action bar.
         setSupportActionBar(toolbar)
@@ -158,16 +162,16 @@ class BookmarksDatabaseViewActivity : AppCompatActivity(), EditBookmarkDatabaseV
         bookmarksDatabaseHelper = BookmarksDatabaseHelper(this)
 
         // Create a matrix cursor column name string array.
-        val matrixCursorColumnNames = arrayOf(BookmarksDatabaseHelper.ID, BookmarksDatabaseHelper.BOOKMARK_NAME)
+        val matrixCursorColumnNames = arrayOf(ID, BOOKMARK_NAME, PARENT_FOLDER_ID)
 
         // Setup the matrix cursor.
         MatrixCursor(matrixCursorColumnNames).use { matrixCursor ->
             // Add "All Folders" and "Home Folder" to the matrix cursor.
-            matrixCursor.addRow(arrayOf<Any>(ALL_FOLDERS_DATABASE_ID, getString(R.string.all_folders)))
-            matrixCursor.addRow(arrayOf<Any>(HOME_FOLDER_DATABASE_ID, getString(R.string.home_folder)))
+            matrixCursor.addRow(arrayOf<Any>(ALL_FOLDERS_DATABASE_ID, getString(R.string.all_folders), HOME_FOLDER_ID))
+            matrixCursor.addRow(arrayOf<Any>(HOME_FOLDER_DATABASE_ID, getString(R.string.home_folder), HOME_FOLDER_ID))
 
             // Get a cursor with the list of all the folders.
-            val foldersCursor = bookmarksDatabaseHelper.allFolders
+            val foldersCursor = bookmarksDatabaseHelper.getFoldersExcept(listOf())
 
             // Combine the matrix cursor and the folders cursor.
             val foldersMergeCursor = MergeCursor(arrayOf(matrixCursor, foldersCursor))
@@ -182,11 +186,24 @@ class BookmarksDatabaseViewActivity : AppCompatActivity(), EditBookmarkDatabaseV
             val defaultFolderBitmap = defaultFolderBitmapDrawable.bitmap
 
             // Create a resource cursor adapter for the spinner.
-            val foldersCursorAdapter: ResourceCursorAdapter = object : ResourceCursorAdapter(this, R.layout.appbar_spinner_item, foldersMergeCursor, 0) {
+            val foldersCursorAdapter: ResourceCursorAdapter = object : ResourceCursorAdapter(this, R.layout.bookmarks_databaseview_appbar_spinner_item, foldersMergeCursor, 0) {
                 override fun bindView(view: View, context: Context, cursor: Cursor) {
                     // Get handles for the spinner views.
-                    val spinnerItemImageView = view.findViewById<ImageView>(R.id.spinner_item_imageview)
-                    val spinnerItemTextView = view.findViewById<TextView>(R.id.spinner_item_textview)
+                    val subfolderSpacerTextView = view.findViewById<TextView>(R.id.subfolder_spacer_textview)
+                    val folderIconImageView = view.findViewById<ImageView>(R.id.folder_icon_imageview)
+                    val folderNameTextView = view.findViewById<TextView>(R.id.folder_name_textview)
+
+                    // Populate the subfolder spacer if it is not null (the spinner is open).
+                    if (subfolderSpacerTextView != null) {
+                        // Indent subfolders.
+                        if (cursor.getLong(cursor.getColumnIndexOrThrow(PARENT_FOLDER_ID)) != HOME_FOLDER_ID) {  // The folder is not in the home folder.
+                            // Get the subfolder spacer.
+                            subfolderSpacerTextView.text = bookmarksDatabaseHelper.getSubfolderSpacer(cursor.getLong(cursor.getColumnIndexOrThrow(FOLDER_ID)))
+                        } else {  // The folder is in the home folder.
+                            // Reset the subfolder spacer.
+                            subfolderSpacerTextView.text = ""
+                        }
+                    }
 
                     // Set the folder icon according to the type.
                     if (foldersMergeCursor.position > 1) {  // Set a user folder icon.
@@ -200,7 +217,7 @@ class BookmarksDatabaseViewActivity : AppCompatActivity(), EditBookmarkDatabaseV
                         val defaultFolderIconByteArray = defaultFolderIconByteArrayOutputStream.toByteArray()
 
                         // Get the folder icon byte array from the cursor.
-                        val folderIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.FAVORITE_ICON))
+                        val folderIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(FAVORITE_ICON))
 
                         // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
                         val folderIconBitmap = BitmapFactory.decodeByteArray(folderIconByteArray, 0, folderIconByteArray.size)
@@ -208,23 +225,23 @@ class BookmarksDatabaseViewActivity : AppCompatActivity(), EditBookmarkDatabaseV
                         // Set the icon according to the type.
                         if (Arrays.equals(folderIconByteArray, defaultFolderIconByteArray)) {  // The default folder icon is used.
                             // Set a smaller and darker folder icon, which works well with the spinner.
-                            spinnerItemImageView.setImageDrawable(AppCompatResources.getDrawable(context, R.drawable.folder_dark_blue))
+                            folderIconImageView.setImageDrawable(AppCompatResources.getDrawable(context, R.drawable.folder_dark_blue))
                         } else {  // A custom folder icon is uses.
                             // Set the folder image stored in the cursor.
-                            spinnerItemImageView.setImageBitmap(folderIconBitmap)
+                            folderIconImageView.setImageBitmap(folderIconBitmap)
                         }
                     } else {  // Set the `All Folders` or `Home Folder` icon.
                         // Set the gray folder image.
-                        spinnerItemImageView.setImageDrawable(AppCompatResources.getDrawable(context, R.drawable.folder_gray))
+                        folderIconImageView.setImageDrawable(AppCompatResources.getDrawable(context, R.drawable.folder_gray))
                     }
 
-                    // Set the text view to display the folder name.
-                    spinnerItemTextView.text = cursor.getString(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME))
+                    // Set the folder name.
+                    folderNameTextView.text = cursor.getString(cursor.getColumnIndexOrThrow(BOOKMARK_NAME))
                 }
             }
 
             // Set the resource cursor adapter drop drown view resource.
-            foldersCursorAdapter.setDropDownViewResource(R.layout.appbar_spinner_dropdown_item)
+            foldersCursorAdapter.setDropDownViewResource(R.layout.bookmarks_databaseview_appbar_spinner_dropdown_item)
 
             // Get a handle for the folder spinner.
             val folderSpinner = findViewById<Spinner>(R.id.spinner)
@@ -240,12 +257,6 @@ class BookmarksDatabaseViewActivity : AppCompatActivity(), EditBookmarkDatabaseV
                         // Store the current folder database ID.
                         currentFolderDatabaseId = id.toInt()
 
-                        // Get a handle for the selected view.
-                        val selectedFolderTextView = findViewById<TextView>(R.id.spinner_item_textview)
-
-                        // Store the current folder name.
-                        currentFolderName = selectedFolderTextView.text.toString()
-
                         // Update the list view.
                         updateBookmarksListView()
                     }
@@ -263,7 +274,6 @@ class BookmarksDatabaseViewActivity : AppCompatActivity(), EditBookmarkDatabaseV
             } else {  // The activity was restarted.
                 // Restore the class variables from the saved instance state.
                 currentFolderDatabaseId = savedInstanceState.getInt(CURRENT_FOLDER_DATABASE_ID)
-                currentFolderName = savedInstanceState.getString(CURRENT_FOLDER_NAME)!!
                 sortByDisplayOrder = savedInstanceState.getBoolean(SORT_BY_DISPLAY_ORDER)
 
                 // Update the spinner if the home folder is selected.  Android handles this by default for the main cursor but not the matrix cursor.
@@ -284,66 +294,71 @@ class BookmarksDatabaseViewActivity : AppCompatActivity(), EditBookmarkDatabaseV
 
                 override fun bindView(view: View, context: Context, cursor: Cursor) {
                     // Get handles for the views.
-                    val bookmarkDatabaseIdTextView = view.findViewById<TextView>(R.id.bookmarks_databaseview_database_id)
-                    val bookmarkFavoriteIcon = view.findViewById<ImageView>(R.id.bookmarks_databaseview_favorite_icon)
-                    val bookmarkNameTextView = view.findViewById<TextView>(R.id.bookmarks_databaseview_bookmark_name)
-                    val bookmarkUrlTextView = view.findViewById<TextView>(R.id.bookmarks_databaseview_bookmark_url)
-                    val bookmarkDisplayOrderTextView = view.findViewById<TextView>(R.id.bookmarks_databaseview_display_order)
-                    val parentFolderImageView = view.findViewById<ImageView>(R.id.bookmarks_databaseview_parent_folder_icon)
-                    val bookmarkParentFolderTextView = view.findViewById<TextView>(R.id.bookmarks_databaseview_parent_folder)
+                    val databaseIdTextView = view.findViewById<TextView>(R.id.database_id_textview)
+                    val bookmarkFavoriteIconImageView = view.findViewById<ImageView>(R.id.favorite_icon_imageview)
+                    val nameTextView = view.findViewById<TextView>(R.id.name_textview)
+                    val folderIdTextView = view.findViewById<TextView>(R.id.folder_id_textview)
+                    val urlTextView = view.findViewById<TextView>(R.id.url_textview)
+                    val displayOrderTextView = view.findViewById<TextView>(R.id.display_order_textview)
+                    val parentFolderIconImageView = view.findViewById<ImageView>(R.id.parent_folder_icon_imageview)
+                    val parentFolderTextView = view.findViewById<TextView>(R.id.parent_folder_textview)
 
                     // Get the information from the cursor.
-                    val bookmarkDatabaseId = cursor.getInt(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.ID))
-                    val bookmarkFavoriteIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.FAVORITE_ICON))
-                    val bookmarkNameString = cursor.getString(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME))
-                    val bookmarkUrlString = cursor.getString(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_URL))
-                    val bookmarkDisplayOrder = cursor.getInt(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.DISPLAY_ORDER))
-                    val bookmarkParentFolder = cursor.getString(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.PARENT_FOLDER))
+                    val databaseId = cursor.getInt(cursor.getColumnIndexOrThrow(ID))
+                    val bookmarkFavoriteIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(FAVORITE_ICON))
+                    val nameString = cursor.getString(cursor.getColumnIndexOrThrow(BOOKMARK_NAME))
+                    val folderId = cursor.getLong(cursor.getColumnIndexOrThrow(FOLDER_ID))
+                    val urlString = cursor.getString(cursor.getColumnIndexOrThrow(BOOKMARK_URL))
+                    val displayOrder = cursor.getInt(cursor.getColumnIndexOrThrow(DISPLAY_ORDER))
+                    val parentFolderId = cursor.getLong(cursor.getColumnIndexOrThrow(PARENT_FOLDER_ID))
 
                     // Convert the byte array to a `Bitmap` beginning at the beginning at the first byte and ending at the last.
                     val bookmarkFavoriteIconBitmap = BitmapFactory.decodeByteArray(bookmarkFavoriteIconByteArray, 0, bookmarkFavoriteIconByteArray.size)
 
                     // Populate the views.
-                    bookmarkDatabaseIdTextView.text = bookmarkDatabaseId.toString()
-                    bookmarkFavoriteIcon.setImageBitmap(bookmarkFavoriteIconBitmap)
-                    bookmarkNameTextView.text = bookmarkNameString
-                    bookmarkUrlTextView.text = bookmarkUrlString
-                    bookmarkDisplayOrderTextView.text = bookmarkDisplayOrder.toString()
+                    databaseIdTextView.text = databaseId.toString()
+                    bookmarkFavoriteIconImageView.setImageBitmap(bookmarkFavoriteIconBitmap)
+                    nameTextView.text = nameString
+                    urlTextView.text = urlString
+                    displayOrderTextView.text = displayOrder.toString()
 
                     // Check to see if the bookmark is a folder.
-                    if (cursor.getInt(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {  // The bookmark is a folder.
+                    if (cursor.getInt(cursor.getColumnIndexOrThrow(IS_FOLDER)) == 1) {  // The bookmark is a folder.
                         // Make the font bold.  When the first argument is null the font is not changed.
-                        bookmarkNameTextView.setTypeface(null, Typeface.BOLD)
+                        nameTextView.setTypeface(null, Typeface.BOLD)
+
+                        // Display the folder ID.
+                        folderIdTextView.text = getString(R.string.folder_id_separator, folderId)
 
                         // Hide the URL.
-                        bookmarkUrlTextView.visibility = View.GONE
+                        urlTextView.visibility = View.GONE
                     } else {  // The bookmark is not a folder.
                         // Reset the font to default.
-                        bookmarkNameTextView.typeface = Typeface.DEFAULT
+                        nameTextView.typeface = Typeface.DEFAULT
 
                         // Show the URL.
-                        bookmarkUrlTextView.visibility = View.VISIBLE
+                        urlTextView.visibility = View.VISIBLE
                     }
 
                     // Make the folder name gray if it is the home folder.
-                    if (bookmarkParentFolder.isEmpty()) {  // The bookmark is in the home folder.
+                    if (parentFolderId == HOME_FOLDER_ID) {  // The bookmark is in the home folder.
                         // Get the home folder icon.
-                        parentFolderImageView.setImageDrawable(AppCompatResources.getDrawable(applicationContext, R.drawable.folder_gray))
+                        parentFolderIconImageView.setImageDrawable(AppCompatResources.getDrawable(applicationContext, R.drawable.folder_gray))
 
                         // Set the parent folder text to be `Home Folder`.
-                        bookmarkParentFolderTextView.setText(R.string.home_folder)
+                        parentFolderTextView.setText(R.string.home_folder)
 
                         // Set the home folder text to be gray.
-                        bookmarkParentFolderTextView.setTextColor(getColor(R.color.gray_500))
+                        parentFolderTextView.setTextColor(getColor(R.color.gray_500))
                     } else {  // The bookmark is in a subfolder.
-                        // Get the parent folder icon.
-                        parentFolderImageView.setImageDrawable(AppCompatResources.getDrawable(applicationContext, R.drawable.folder_dark_blue))
+                        // Set the parent folder icon.
+                        parentFolderIconImageView.setImageDrawable(AppCompatResources.getDrawable(applicationContext, R.drawable.folder_dark_blue))
 
                         // Set the parent folder name.
-                        bookmarkParentFolderTextView.text = bookmarkParentFolder
+                        parentFolderTextView.text = bookmarksDatabaseHelper.getFolderName(parentFolderId)
 
                         // Set the parent folder text color.
-                        bookmarkParentFolderTextView.setTextColor(getColor(R.color.parent_folder_text))
+                        parentFolderTextView.setTextColor(getColor(R.color.parent_folder_text))
                     }
                 }
             }
@@ -358,17 +373,14 @@ class BookmarksDatabaseViewActivity : AppCompatActivity(), EditBookmarkDatabaseV
 
                 // Show the edit bookmark or edit bookmark folder dialog.
                 if (bookmarksDatabaseHelper.isFolder(databaseId)) {
-                    // Save the current folder name, which is used in `onSaveBookmarkFolder()`.
-                    oldFolderNameString = bookmarksCursor.getString(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME))
-
                     // Instantiate the edit bookmark folder dialog.
-                    val editBookmarkFolderDatabaseViewDialog: DialogFragment = folderDatabaseId(databaseId, favoriteIconBitmap)
+                    val editBookmarkFolderDatabaseViewDialog = folderDatabaseId(databaseId, currentFavoriteIconBitmap)
 
                     // Make it so.
                     editBookmarkFolderDatabaseViewDialog.show(supportFragmentManager, resources.getString(R.string.edit_folder))
                 } else {
                     // Instantiate the edit bookmark dialog.
-                    val editBookmarkDatabaseViewDialog: DialogFragment = bookmarkDatabaseId(databaseId, favoriteIconBitmap)
+                    val editBookmarkDatabaseViewDialog = bookmarkDatabaseId(databaseId, currentFavoriteIconBitmap)
 
                     // Make it so.
                     editBookmarkDatabaseViewDialog.show(supportFragmentManager, resources.getString(R.string.edit_bookmark))
@@ -436,13 +448,13 @@ class BookmarksDatabaseViewActivity : AppCompatActivity(), EditBookmarkDatabaseV
 
                         // Do not allow a bookmark to be deselected if the folder is selected.
                         if (!checked) {
-                            // Get the folder name.
-                            val folderName = bookmarksDatabaseHelper.getParentFolderName(id.toInt())
+                            // Get the parent folder ID.
+                            val parentFolderId = bookmarksDatabaseHelper.getParentFolderId(databaseId)
 
                             // If the bookmark is not in the root folder, check to see if the folder is selected.
-                            if (folderName.isNotEmpty()) {
-                                // Get the database ID of the folder.
-                                val folderDatabaseId = bookmarksDatabaseHelper.getFolderDatabaseId(folderName)
+                            if (parentFolderId != HOME_FOLDER_ID) {
+                                // Get the folder database ID.
+                                val folderDatabaseId = bookmarksDatabaseHelper.getFolderDatabaseId(parentFolderId)
 
                                 // Move the bookmarks cursor to the first position.
                                 bookmarksCursor.moveToFirst()
@@ -453,7 +465,7 @@ class BookmarksDatabaseViewActivity : AppCompatActivity(), EditBookmarkDatabaseV
                                 // Get the position of the folder in the bookmarks cursor.
                                 while ((folderPosition < 0) && (bookmarksCursor.position < bookmarksCursor.count)) {
                                     // Check if the folder database ID matches the bookmark database ID.
-                                    if (folderDatabaseId == bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.ID))) {
+                                    if (folderDatabaseId == bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(ID))) {
                                         // Get the folder position.
                                         folderPosition = bookmarksCursor.position
 
@@ -501,26 +513,32 @@ class BookmarksDatabaseViewActivity : AppCompatActivity(), EditBookmarkDatabaseV
                         // Populate the bookmarks cursor.
                         bookmarksCursor = when (currentFolderDatabaseId) {
                             // Get all the bookmarks except the ones being deleted.
-                            ALL_FOLDERS_DATABASE_ID ->
+                            ALL_FOLDERS_DATABASE_ID -> {
                                 if (sortByDisplayOrder)
                                     bookmarksDatabaseHelper.getAllBookmarksByDisplayOrderExcept(selectedBookmarksIdsLongArray)
                                 else
                                     bookmarksDatabaseHelper.getAllBookmarksExcept(selectedBookmarksIdsLongArray)
+                            }
 
                             // Get the home folder bookmarks except the ones being deleted.
-                            HOME_FOLDER_DATABASE_ID ->
+                            HOME_FOLDER_DATABASE_ID -> {
                                 if (sortByDisplayOrder)
-                                    bookmarksDatabaseHelper.getBookmarksByDisplayOrderExcept(selectedBookmarksIdsLongArray, "")
+                                    bookmarksDatabaseHelper.getBookmarksByDisplayOrderExcept(selectedBookmarksIdsLongArray, HOME_FOLDER_ID)
                                 else
-                                    bookmarksDatabaseHelper.getBookmarksExcept(selectedBookmarksIdsLongArray, "")
-
+                                    bookmarksDatabaseHelper.getBookmarksExcept(selectedBookmarksIdsLongArray, HOME_FOLDER_ID)
+                            }
 
                             // Get the current folder bookmarks except the ones being deleted.
-                            else ->
+                            else -> {
+                                // Get the current folder ID.
+                                val currentFolderId = bookmarksDatabaseHelper.getFolderId(currentFolderDatabaseId)
+
+                                // Get the cursor.
                                 if (sortByDisplayOrder)
-                                    bookmarksDatabaseHelper.getBookmarksByDisplayOrderExcept(selectedBookmarksIdsLongArray, currentFolderName)
+                                    bookmarksDatabaseHelper.getBookmarksByDisplayOrderExcept(selectedBookmarksIdsLongArray, currentFolderId)
                                 else
-                                    bookmarksDatabaseHelper.getBookmarksExcept(selectedBookmarksIdsLongArray, currentFolderName)
+                                    bookmarksDatabaseHelper.getBookmarksExcept(selectedBookmarksIdsLongArray, currentFolderId)
+                            }
                         }
 
                         // Update the list view.
@@ -557,8 +575,13 @@ class BookmarksDatabaseViewActivity : AppCompatActivity(), EditBookmarkDatabaseV
                                     deleteMenuItem.isEnabled = true
 
                                     // Close the activity if back has been pressed.
-                                    if (closeActivityAfterDismissingSnackbar)
+                                    if (closeActivityAfterDismissingSnackbar) {
+                                        // Reload the bookmarks list view when returning to the bookmarks activity.
+                                        BookmarksActivity.restartFromBookmarksDatabaseViewActivity = true
+
+                                        // Finish the activity.
                                         finish()
+                                    }
                                 }
                             })
 
@@ -592,6 +615,15 @@ class BookmarksDatabaseViewActivity : AppCompatActivity(), EditBookmarkDatabaseV
         return true
     }
 
+    public override fun onDestroy() {
+        // Close the bookmarks cursor and database.
+        bookmarksCursor.close()
+        bookmarksDatabaseHelper.close()
+
+        // Run the default commands.
+        super.onDestroy()
+    }
+
     override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
         // Get the menu item ID.
         val menuItemId = menuItem.itemId
@@ -633,112 +665,10 @@ class BookmarksDatabaseViewActivity : AppCompatActivity(), EditBookmarkDatabaseV
 
         // Store the class variables in the bundle.
         savedInstanceState.putInt(CURRENT_FOLDER_DATABASE_ID, currentFolderDatabaseId)
-        savedInstanceState.putString(CURRENT_FOLDER_NAME, currentFolderName)
         savedInstanceState.putBoolean(SORT_BY_DISPLAY_ORDER, sortByDisplayOrder)
     }
 
-    private fun prepareFinish() {
-        // Check to see if a snackbar is currently displayed.  If so, it must be closed before existing so that a pending delete is completed before reloading the list view in the bookmarks activity.
-        if ((bookmarksDeletedSnackbar != null) && bookmarksDeletedSnackbar!!.isShown) { // Close the bookmarks deleted snackbar before going home.
-            // Set the close flag.
-            closeActivityAfterDismissingSnackbar = true
-
-            // Dismiss the snackbar.
-            bookmarksDeletedSnackbar!!.dismiss()
-        } else {  // Go home immediately.
-            // Update the current folder in the bookmarks activity.
-            if (currentFolderDatabaseId == ALL_FOLDERS_DATABASE_ID || currentFolderDatabaseId == HOME_FOLDER_DATABASE_ID) {  // All folders or the the home folder are currently displayed.
-                // Load the home folder.
-                BookmarksActivity.currentFolder = ""
-            } else {  // A subfolder is currently displayed.
-                // Load the current folder.
-                BookmarksActivity.currentFolder = currentFolderName
-            }
-
-            // Reload the bookmarks list view when returning to the bookmarks activity.
-            BookmarksActivity.restartFromBookmarksDatabaseViewActivity = true
-
-            // Exit the bookmarks database view activity.
-            finish()
-        }
-    }
-
-    private fun updateBookmarksListView() {
-        // Populate the bookmarks list view based on the spinner selection.
-        bookmarksCursor = when (currentFolderDatabaseId) {
-            // Get all the bookmarks.
-            ALL_FOLDERS_DATABASE_ID ->
-                if (sortByDisplayOrder)
-                    bookmarksDatabaseHelper.allBookmarksByDisplayOrder
-                else
-                    bookmarksDatabaseHelper.allBookmarks
-
-            // Get the bookmarks in the current folder.
-            HOME_FOLDER_DATABASE_ID ->
-                if (sortByDisplayOrder)
-                    bookmarksDatabaseHelper.getBookmarksByDisplayOrder("")
-                else
-                    bookmarksDatabaseHelper.getBookmarks("")
-
-            // Get the bookmarks in the specified folder.
-            else ->
-                if (sortByDisplayOrder)
-                    bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderName)
-                else
-                    bookmarksDatabaseHelper.getBookmarks(currentFolderName)
-        }
-
-        // Update the cursor adapter if it isn't null, which happens when the activity is restarted.
-        if (bookmarksCursorAdapter != null) {
-            bookmarksCursorAdapter!!.changeCursor(bookmarksCursor)
-        }
-    }
-
-    private fun selectAllBookmarksInFolder(folderId: Int) {
-        // Get the folder name.
-        val folderName = bookmarksDatabaseHelper.getFolderName(folderId)
-
-        // Get a cursor with the contents of the folder.
-        val folderCursor = bookmarksDatabaseHelper.getBookmarks(folderName)
-
-        // Move to the beginning of the cursor.
-        folderCursor.moveToFirst()
-
-        while (folderCursor.position < folderCursor.count) {
-            // Get the bookmark database ID.
-            val bookmarkId = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.ID))
-
-            // Move the bookmarks cursor to the first position.
-            bookmarksCursor.moveToFirst()
-
-            // Initialize the bookmark position variable.
-            var bookmarkPosition = -1
-
-            // Get the position of this bookmark in the bookmarks cursor.
-            while ((bookmarkPosition < 0) && (bookmarksCursor.position < bookmarksCursor.count)) {
-                // Check if the bookmark IDs match.
-                if (bookmarkId == bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.ID))) {
-                    // Get the bookmark position.
-                    bookmarkPosition = bookmarksCursor.position
-
-                    // If this bookmark is a folder, select all the bookmarks inside it.
-                    if (bookmarksDatabaseHelper.isFolder(bookmarkId))
-                        selectAllBookmarksInFolder(bookmarkId)
-
-                    // Select the bookmark.
-                    bookmarksListView.setItemChecked(bookmarkPosition, true)
-                }
-
-                // Increment the bookmarks cursor position.
-                bookmarksCursor.moveToNext()
-            }
-
-            // Move to the next position.
-            folderCursor.moveToNext()
-        }
-    }
-
-    override fun onSaveBookmark(dialogFragment: DialogFragment, selectedBookmarkDatabaseId: Int, favoriteIconBitmap: Bitmap) {
+    override fun saveBookmark(dialogFragment: DialogFragment, selectedBookmarkDatabaseId: Int, favoriteIconBitmap: Bitmap) {
         // Get the dialog from the dialog fragment.
         val dialog = dialogFragment.dialog!!
 
@@ -755,15 +685,15 @@ class BookmarksDatabaseViewActivity : AppCompatActivity(), EditBookmarkDatabaseV
         val folderDatabaseId = folderSpinner.selectedItemId.toInt()
         val displayOrderInt = displayOrderEditText.text.toString().toInt()
 
-        // Get the parent folder name.
-        val parentFolderNameString: String = if (folderDatabaseId == HOME_FOLDER_DATABASE_ID)  // The home folder is selected.  Use `""`.
-            ""
+        // Get the parent folder ID.
+        val parentFolderId: Long = if (folderDatabaseId == HOME_FOLDER_DATABASE_ID)  // The home folder is selected.
+            HOME_FOLDER_ID
         else  // Get the parent folder name from the database.
-            bookmarksDatabaseHelper.getFolderName(folderDatabaseId)
+            bookmarksDatabaseHelper.getFolderId(folderDatabaseId)
 
         // Update the bookmark.
         if (currentIconRadioButton.isChecked) {  // Update the bookmark without changing the favorite icon.
-            bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, parentFolderNameString, displayOrderInt)
+            bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, parentFolderId, displayOrderInt)
         } else {  // Update the bookmark using the `WebView` favorite icon.
             // Create a favorite icon byte array output stream.
             val newFavoriteIconByteArrayOutputStream = ByteArrayOutputStream()
@@ -775,14 +705,14 @@ class BookmarksDatabaseViewActivity : AppCompatActivity(), EditBookmarkDatabaseV
             val newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray()
 
             //  Update the bookmark and the favorite icon.
-            bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, parentFolderNameString, displayOrderInt, newFavoriteIconByteArray)
+            bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, parentFolderId, displayOrderInt, newFavoriteIconByteArray)
         }
 
         // Update the list view.
         updateBookmarksListView()
     }
 
-    override fun onSaveBookmarkFolder(dialogFragment: DialogFragment, selectedFolderDatabaseId: Int, favoriteIconBitmap: Bitmap) {
+    override fun saveBookmarkFolder(dialogFragment: DialogFragment, selectedFolderDatabaseId: Int, favoriteIconBitmap: Bitmap) {
         // Get the dialog from the dialog fragment.
         val dialog = dialogFragment.dialog!!
 
@@ -799,15 +729,15 @@ class BookmarksDatabaseViewActivity : AppCompatActivity(), EditBookmarkDatabaseV
         val parentFolderDatabaseId = parentFolderSpinner.selectedItemId.toInt()
         val displayOrderInt = displayOrderEditText.text.toString().toInt()
 
-        // Set the parent folder name.
-        val parentFolderNameString: String = if (parentFolderDatabaseId == HOME_FOLDER_DATABASE_ID)  // The home folder is selected.  Use `""`.
-            ""
+        // Set the parent folder ID.
+        val parentFolderId: Long = if (parentFolderDatabaseId == HOME_FOLDER_DATABASE_ID)  // The home folder is selected.
+            HOME_FOLDER_ID
         else  // Get the parent folder name from the database.
-            bookmarksDatabaseHelper.getFolderName(parentFolderDatabaseId)
+            bookmarksDatabaseHelper.getFolderId(parentFolderDatabaseId)
 
         // Update the folder.
         if (currentIconRadioButton.isChecked) {  // Update the folder without changing the favorite icon.
-            bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, parentFolderNameString, displayOrderInt)
+            bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderNameString, parentFolderId, displayOrderInt)
         } else {  // Update the folder and the icon.
             // Get the new folder icon bitmap.
             val folderIconBitmap = if (defaultIconRadioButton.isChecked) {
@@ -834,19 +764,120 @@ class BookmarksDatabaseViewActivity : AppCompatActivity(), EditBookmarkDatabaseV
             val newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray()
 
             //  Update the folder and the icon.
-            bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, parentFolderNameString, displayOrderInt, newFolderIconByteArray)
+            bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderNameString, parentFolderId, displayOrderInt, newFolderIconByteArray)
         }
 
         // Update the list view.
         updateBookmarksListView()
     }
 
-    public override fun onDestroy() {
-        // Close the bookmarks cursor and database.
-        bookmarksCursor.close()
-        bookmarksDatabaseHelper.close()
+    private fun updateBookmarksListView() {
+        // Populate the bookmarks list view based on the spinner selection.
+        bookmarksCursor = when (currentFolderDatabaseId) {
+            // Get all the bookmarks.
+            ALL_FOLDERS_DATABASE_ID -> {
+                if (sortByDisplayOrder)
+                    bookmarksDatabaseHelper.allBookmarksByDisplayOrder
+                else
+                    bookmarksDatabaseHelper.allBookmarks
+            }
 
-        // Run the default commands.
-        super.onDestroy()
+            // Get the bookmarks in the home folder.
+            HOME_FOLDER_DATABASE_ID -> {
+                if (sortByDisplayOrder)
+                    bookmarksDatabaseHelper.getBookmarksByDisplayOrder(HOME_FOLDER_ID)
+                else
+                    bookmarksDatabaseHelper.getBookmarks(HOME_FOLDER_ID)
+            }
+
+            // Get the bookmarks in the specified folder.
+            else -> {
+                // Get the current folder ID.
+                val currentFolderId = bookmarksDatabaseHelper.getFolderId(currentFolderDatabaseId)
+
+                if (sortByDisplayOrder)
+                    bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderId)
+                else
+                    bookmarksDatabaseHelper.getBookmarks(currentFolderId)
+            }
+        }
+
+        // Update the cursor adapter if it isn't null, which happens when the activity is restarted.
+        if (bookmarksCursorAdapter != null) {
+            bookmarksCursorAdapter!!.changeCursor(bookmarksCursor)
+        }
+    }
+
+    private fun prepareFinish() {
+        // Check to see if a snackbar is currently displayed.  If so, it must be closed before existing so that a pending delete is completed before reloading the list view in the bookmarks activity.
+        if ((bookmarksDeletedSnackbar != null) && bookmarksDeletedSnackbar!!.isShown) { // Close the bookmarks deleted snackbar before going home.
+            // Set the close flag.
+            closeActivityAfterDismissingSnackbar = true
+
+            // Dismiss the snackbar.
+            bookmarksDeletedSnackbar!!.dismiss()
+        } else {  // Go home immediately.
+            // Update the current folder in the bookmarks activity.
+            if ((currentFolderDatabaseId == ALL_FOLDERS_DATABASE_ID) || (currentFolderDatabaseId == HOME_FOLDER_DATABASE_ID)) {  // All folders or the the home folder are currently displayed.
+                // Load the home folder.
+                BookmarksActivity.currentFolderId = HOME_FOLDER_ID
+            } else {  // A subfolder is currently displayed.
+                // Load the current folder.
+                BookmarksActivity.currentFolderId = bookmarksDatabaseHelper.getFolderId(currentFolderDatabaseId)
+            }
+
+            // Reload the bookmarks list view when returning to the bookmarks activity.
+            BookmarksActivity.restartFromBookmarksDatabaseViewActivity = true
+
+            // Exit the bookmarks database view activity.
+            finish()
+        }
+    }
+
+    private fun selectAllBookmarksInFolder(folderDatabaseId: Int) {
+        // Get the folder ID.
+        val folderId = bookmarksDatabaseHelper.getFolderId(folderDatabaseId)
+
+        // Get a cursor with the contents of the folder.
+        val folderCursor = bookmarksDatabaseHelper.getBookmarks(folderId)
+
+        // Get the column indexes.
+        val idColumnIndex = bookmarksCursor.getColumnIndexOrThrow(ID)
+
+        // Move to the beginning of the cursor.
+        folderCursor.moveToFirst()
+
+        while (folderCursor.position < folderCursor.count) {
+            // Get the bookmark database ID.
+            val bookmarkId = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(ID))
+
+            // Move the bookmarks cursor to the first position.
+            bookmarksCursor.moveToFirst()
+
+            // Initialize the bookmark position variable.
+            var bookmarkPosition = -1
+
+            // Get the position of this bookmark in the bookmarks cursor.
+            while ((bookmarkPosition < 0) && (bookmarksCursor.position < bookmarksCursor.count)) {
+                // Check if the bookmark IDs match.
+                if (bookmarkId == bookmarksCursor.getInt(idColumnIndex)) {
+                    // Get the bookmark position.
+                    bookmarkPosition = bookmarksCursor.position
+
+                    // If this bookmark is a folder, select all the bookmarks inside it.
+                    if (bookmarksDatabaseHelper.isFolder(bookmarkId))
+                        selectAllBookmarksInFolder(bookmarkId)
+
+                    // Select the bookmark.
+                    bookmarksListView.setItemChecked(bookmarkPosition, true)
+                }
+
+                // Increment the bookmarks cursor position.
+                bookmarksCursor.moveToNext()
+            }
+
+            // Move to the next position.
+            folderCursor.moveToNext()
+        }
     }
 }
index 16b40d855ab46df94eab92f0e5f33589a5f7aaec..8614ec7b8969eeb049b0227d2a4c7a17c37a78eb 100644 (file)
@@ -137,6 +137,8 @@ import com.stoutner.privacybrowser.dialogs.UrlHistoryDialog
 import com.stoutner.privacybrowser.dialogs.ViewSslCertificateDialog
 import com.stoutner.privacybrowser.dialogs.WaitingForProxyDialog
 import com.stoutner.privacybrowser.fragments.WebViewTabFragment
+import com.stoutner.privacybrowser.helpers.BOOKMARK_NAME
+import com.stoutner.privacybrowser.helpers.BOOKMARK_URL
 import com.stoutner.privacybrowser.helpers.COOKIES
 import com.stoutner.privacybrowser.helpers.DARK_THEME
 import com.stoutner.privacybrowser.helpers.DISABLED
@@ -151,9 +153,12 @@ import com.stoutner.privacybrowser.helpers.ENABLE_FORM_DATA
 import com.stoutner.privacybrowser.helpers.ENABLE_JAVASCRIPT
 import com.stoutner.privacybrowser.helpers.ENABLE_ULTRAPRIVACY
 import com.stoutner.privacybrowser.helpers.ENABLED
+import com.stoutner.privacybrowser.helpers.FAVORITE_ICON
+import com.stoutner.privacybrowser.helpers.FOLDER_ID
 import com.stoutner.privacybrowser.helpers.FONT_SIZE
 import com.stoutner.privacybrowser.helpers.ID
 import com.stoutner.privacybrowser.helpers.IP_ADDRESSES
+import com.stoutner.privacybrowser.helpers.IS_FOLDER
 import com.stoutner.privacybrowser.helpers.LIGHT_THEME
 import com.stoutner.privacybrowser.helpers.PINNED_IP_ADDRESSES
 import com.stoutner.privacybrowser.helpers.PINNED_SSL_CERTIFICATE
@@ -234,7 +239,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
 
     companion object {
         // Define the public static variables.
-        var currentBookmarksFolder = ""
+        var currentBookmarksFolderId = 0L
         val executorService = Executors.newFixedThreadPool(4)!!
         var orbotStatus = "unknown"
         val pendingDialogsArrayList = ArrayList<PendingDialogDataClass>()
@@ -3518,12 +3523,12 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
 
     // The view parameter cannot be removed because it is called from the layout onClick.
     fun bookmarksBack(@Suppress("UNUSED_PARAMETER")view: View?) {
-        if (currentBookmarksFolder.isEmpty()) {  // The home folder is displayed.
+        if (currentBookmarksFolderId == HOME_FOLDER_ID) {  // The home folder is displayed.
             // close the bookmarks drawer.
             drawerLayout.closeDrawer(GravityCompat.END)
         } else {  // A subfolder is displayed.
             // Set the former parent folder as the current folder.
-            currentBookmarksFolder = bookmarksDatabaseHelper!!.getParentFolderName(currentBookmarksFolder)
+            currentBookmarksFolderId = bookmarksDatabaseHelper!!.getParentFolderId(currentBookmarksFolderId)
 
             // Load the new folder.
             loadBookmarksFolder()
@@ -3782,10 +3787,10 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
         val newBookmarkDisplayOrder = bookmarksListView.count
 
         // Create the bookmark.
-        bookmarksDatabaseHelper!!.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolder, newBookmarkDisplayOrder, favoriteIconByteArray)
+        bookmarksDatabaseHelper!!.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolderId, newBookmarkDisplayOrder, favoriteIconByteArray)
 
         // Update the bookmarks cursor with the current contents of this folder.
-        bookmarksCursor = bookmarksDatabaseHelper!!.getBookmarksByDisplayOrder(currentBookmarksFolder)
+        bookmarksCursor = bookmarksDatabaseHelper!!.getBookmarksByDisplayOrder(currentBookmarksFolderId)
 
         // Update the list view.
         bookmarksCursorAdapter.changeCursor(bookmarksCursor)
@@ -3840,10 +3845,10 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
         }
 
         // Create the folder, which will be placed at the top of the list view.
-        bookmarksDatabaseHelper!!.createFolder(folderNameString, currentBookmarksFolder, folderIconByteArray)
+        bookmarksDatabaseHelper!!.createFolder(folderNameString, currentBookmarksFolderId, folderIconByteArray)
 
         // Update the bookmarks cursor with the current contents of this folder.
-        bookmarksCursor = bookmarksDatabaseHelper!!.getBookmarksByDisplayOrder(currentBookmarksFolder)
+        bookmarksCursor = bookmarksDatabaseHelper!!.getBookmarksByDisplayOrder(currentBookmarksFolderId)
 
         // Update the list view.
         bookmarksCursorAdapter.changeCursor(bookmarksCursor)
@@ -4186,7 +4191,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
             val bookmarksIntent = Intent(applicationContext, BookmarksActivity::class.java)
 
             // Add the extra information to the intent.
-            bookmarksIntent.putExtra(CURRENT_FOLDER, currentBookmarksFolder)
+            bookmarksIntent.putExtra(CURRENT_FOLDER_ID, currentBookmarksFolderId)
             bookmarksIntent.putExtra(CURRENT_TITLE, currentWebView!!.title)
             bookmarksIntent.putExtra(CURRENT_URL, currentWebView!!.url)
             bookmarksIntent.putExtra(CURRENT_FAVORITE_ICON_BYTE_ARRAY, currentFavoriteIconByteArray)
@@ -4283,15 +4288,15 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
             bookmarkCursor.moveToFirst()
 
             // Act upon the bookmark according to the type.
-            if (bookmarkCursor.getInt(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {  // The selected bookmark is a folder.
-                // Store the folder name.
-                currentBookmarksFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME))
+            if (bookmarkCursor.getInt(bookmarkCursor.getColumnIndexOrThrow(IS_FOLDER)) == 1) {  // The selected bookmark is a folder.
+                // Store the folder ID.
+                currentBookmarksFolderId = bookmarkCursor.getLong(bookmarkCursor.getColumnIndexOrThrow(FOLDER_ID))
 
                 // Load the new folder.
                 loadBookmarksFolder()
             } else {  // The selected bookmark is not a folder.
                 // Load the bookmark URL.
-                loadUrl(currentWebView!!, bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_URL)))
+                loadUrl(currentWebView!!, bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BOOKMARK_URL)))
 
                 // Close the bookmarks drawer if it is not pinned.
                 if (!bookmarksDrawerPinned)
@@ -4309,8 +4314,11 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
 
             // Run the commands associated with the type.
             if (bookmarksDatabaseHelper!!.isFolder(databaseId)) {  // The bookmark is a folder.
+                // Get the folder ID.
+                val folderId = bookmarksDatabaseHelper!!.getFolderId(databaseId)
+
                 // Get a cursor of all the bookmarks in the folder.
-                val bookmarksCursor = bookmarksDatabaseHelper!!.getFolderBookmarks(databaseId)
+                val bookmarksCursor = bookmarksDatabaseHelper!!.getFolderBookmarks(folderId)
 
                 // Move to the first entry in the cursor.
                 bookmarksCursor.moveToFirst()
@@ -4318,7 +4326,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                 // Open each bookmark
                 for (i in 0 until bookmarksCursor.count) {
                     // Load the bookmark in a new tab, moving to the tab for the first bookmark if the drawer is not pinned.
-                    addNewTab(bookmarksCursor.getString(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_URL)), !bookmarksDrawerPinned && (i == 0))
+                    addNewTab(bookmarksCursor.getString(bookmarksCursor.getColumnIndexOrThrow(BOOKMARK_URL)), !bookmarksDrawerPinned && (i == 0))
 
                     // Move to the next bookmark.
                     bookmarksCursor.moveToNext()
@@ -4334,7 +4342,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                 bookmarkCursor.moveToFirst()
 
                 // Load the bookmark in a new tab and move to the tab if the drawer is not pinned.
-                addNewTab(bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_URL)), !bookmarksDrawerPinned)
+                addNewTab(bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BOOKMARK_URL)), !bookmarksDrawerPinned)
 
                 // Close the cursor.
                 bookmarkCursor.close()
@@ -5603,7 +5611,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
 
     private fun loadBookmarksFolder() {
         // Update the bookmarks cursor with the contents of the bookmarks database for the current folder.
-        bookmarksCursor = bookmarksDatabaseHelper!!.getBookmarksByDisplayOrder(currentBookmarksFolder)
+        bookmarksCursor = bookmarksDatabaseHelper!!.getBookmarksByDisplayOrder(currentBookmarksFolderId)
 
         // Populate the bookmarks cursor adapter.
         bookmarksCursorAdapter = object : CursorAdapter(this, bookmarksCursor, false) {
@@ -5618,7 +5626,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                 val bookmarkNameTextView = view.findViewById<TextView>(R.id.bookmark_name)
 
                 // Get the favorite icon byte array from the cursor.
-                val favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.FAVORITE_ICON))
+                val favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(FAVORITE_ICON))
 
                 // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
                 val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
@@ -5627,10 +5635,10 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                 bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap)
 
                 // Display the bookmark name from the cursor in the bookmark name text view.
-                bookmarkNameTextView.text = cursor.getString(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME))
+                bookmarkNameTextView.text = cursor.getString(cursor.getColumnIndexOrThrow(BOOKMARK_NAME))
 
                 // Make the font bold for folders.
-                if (cursor.getInt(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.IS_FOLDER)) == 1)
+                if (cursor.getInt(cursor.getColumnIndexOrThrow(IS_FOLDER)) == 1)
                     bookmarkNameTextView.typeface = Typeface.DEFAULT_BOLD
                 else  // Reset the font to default for normal bookmarks.
                     bookmarkNameTextView.typeface = Typeface.DEFAULT
@@ -5641,10 +5649,10 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
         bookmarksListView.adapter = bookmarksCursorAdapter
 
         // Set the bookmarks drawer title.
-        if (currentBookmarksFolder.isEmpty())
+        if (currentBookmarksFolderId == HOME_FOLDER_ID)  // The current bookmarks folder is the home folder.
             bookmarksTitleTextView.setText(R.string.bookmarks)
         else
-            bookmarksTitleTextView.text = currentBookmarksFolder
+            bookmarksTitleTextView.text = bookmarksDatabaseHelper!!.getFolderName(currentBookmarksFolderId)
     }
 
     private fun loadUrl(nestedScrollWebView: NestedScrollWebView, url: String) {
index 2f413b0bbf808fcf898639f4646bd1277e5238a5..04d7d250c7553cdfcbf3418f51296ecc997bb9de 100644 (file)
@@ -40,7 +40,6 @@ import androidx.fragment.app.DialogFragment
 import androidx.preference.PreferenceManager
 
 import com.stoutner.privacybrowser.R
-import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper
 
 import java.io.ByteArrayOutputStream
 
@@ -177,10 +176,7 @@ class CreateBookmarkFolderDialog : DialogFragment() {
             defaultIconRadioButton.isChecked = false
         }
 
-        // Initialize the database helper.
-        val bookmarksDatabaseHelper = BookmarksDatabaseHelper(requireContext())
-
-        // Enable the create button if the new folder name is unique.
+        // Enable the create button if the folder name is populated.
         folderNameEditText.addTextChangedListener(object: TextWatcher {
             override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
                 // Do nothing.
@@ -194,11 +190,8 @@ class CreateBookmarkFolderDialog : DialogFragment() {
                 // Convert the current text to a string.
                 val folderName = editable.toString()
 
-                // Check if a folder with the name already exists.
-                val folderExistsCursor = bookmarksDatabaseHelper.getFolder(folderName)
-
-                // Enable the create button if the new folder name is not empty and doesn't already exist.
-                createButton.isEnabled = folderName.isNotEmpty() && (folderExistsCursor.count == 0)
+                // Enable the create button if the new folder name is not empty.
+                createButton.isEnabled = folderName.isNotEmpty()
             }
         })
 
index 44c0f6e6744a21905ba895d0d2f31fe90c67e011..d5902f217b86a603afc4eaa2717c8884afc48115 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>.
  *
@@ -44,13 +44,21 @@ import android.widget.Spinner
 import android.widget.TextView
 
 import androidx.appcompat.app.AlertDialog
-import androidx.core.content.ContextCompat
+import androidx.appcompat.content.res.AppCompatResources
 import androidx.cursoradapter.widget.ResourceCursorAdapter
 import androidx.fragment.app.DialogFragment
 import androidx.preference.PreferenceManager
 
 import com.stoutner.privacybrowser.R
-import com.stoutner.privacybrowser.activities.BookmarksDatabaseViewActivity
+import com.stoutner.privacybrowser.activities.HOME_FOLDER_DATABASE_ID
+import com.stoutner.privacybrowser.activities.HOME_FOLDER_ID
+import com.stoutner.privacybrowser.helpers.BOOKMARK_NAME
+import com.stoutner.privacybrowser.helpers.BOOKMARK_URL
+import com.stoutner.privacybrowser.helpers.DISPLAY_ORDER
+import com.stoutner.privacybrowser.helpers.FAVORITE_ICON
+import com.stoutner.privacybrowser.helpers.FOLDER_ID
+import com.stoutner.privacybrowser.helpers.ID
+import com.stoutner.privacybrowser.helpers.PARENT_FOLDER_ID
 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper
 
 import java.io.ByteArrayOutputStream
@@ -102,7 +110,7 @@ class EditBookmarkDatabaseViewDialog : DialogFragment() {
 
     // The public interface is used to send information back to the parent activity.
     interface EditBookmarkDatabaseViewListener {
-        fun onSaveBookmark(dialogFragment: DialogFragment, selectedBookmarkDatabaseId: Int, favoriteIconBitmap: Bitmap)
+        fun saveBookmark(dialogFragment: DialogFragment, selectedBookmarkDatabaseId: Int, favoriteIconBitmap: Bitmap)
     }
 
     override fun onAttach(context: Context) {
@@ -148,7 +156,7 @@ class EditBookmarkDatabaseViewDialog : DialogFragment() {
         // Set the save button listener.
         dialogBuilder.setPositiveButton(R.string.save) { _: DialogInterface, _: Int ->
             // Return the dialog fragment to the parent activity on save.
-            editBookmarkDatabaseViewListener.onSaveBookmark(this, bookmarkDatabaseId, favoriteIconBitmap)
+            editBookmarkDatabaseViewListener.saveBookmark(this, bookmarkDatabaseId, favoriteIconBitmap)
         }
 
         // Create an alert dialog from the alert dialog builder.
@@ -183,15 +191,15 @@ class EditBookmarkDatabaseViewDialog : DialogFragment() {
         saveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
 
         // Store the current bookmark values.
-        val currentBookmarkName = bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME))
-        val currentUrl = bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_URL))
-        val currentDisplayOrder = bookmarkCursor.getInt(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.DISPLAY_ORDER))
+        val currentBookmarkName = bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BOOKMARK_NAME))
+        val currentUrl = bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BOOKMARK_URL))
+        val currentDisplayOrder = bookmarkCursor.getInt(bookmarkCursor.getColumnIndexOrThrow(DISPLAY_ORDER))
 
         // Set the database ID.
-        databaseIdTextView.text = bookmarkCursor.getInt(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.ID)).toString()
+        databaseIdTextView.text = bookmarkCursor.getInt(bookmarkCursor.getColumnIndexOrThrow(ID)).toString()
 
         // Get the current favorite icon byte array from the cursor.
-        val currentIconByteArray = bookmarkCursor.getBlob(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.FAVORITE_ICON))
+        val currentIconByteArray = bookmarkCursor.getBlob(bookmarkCursor.getColumnIndexOrThrow(FAVORITE_ICON))
 
         // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
         val currentIconBitmap = BitmapFactory.decodeByteArray(currentIconByteArray, 0, currentIconByteArray.size)
@@ -207,16 +215,16 @@ class EditBookmarkDatabaseViewDialog : DialogFragment() {
         urlEditText.setText(currentUrl)
 
         // Create an an array of column names for the matrix cursor comprised of the ID and the name.
-        val matrixCursorColumnNamesArray = arrayOf(BookmarksDatabaseHelper.ID, BookmarksDatabaseHelper.BOOKMARK_NAME)
+        val matrixCursorColumnNamesArray = arrayOf(ID, BOOKMARK_NAME, PARENT_FOLDER_ID)
 
         // Create a matrix cursor based on the column names array.
         val matrixCursor = MatrixCursor(matrixCursorColumnNamesArray)
 
         // Add `Home Folder` as the first entry in the matrix folder.
-        matrixCursor.addRow(arrayOf(BookmarksDatabaseViewActivity.HOME_FOLDER_DATABASE_ID, getString(R.string.home_folder)))
+        matrixCursor.addRow(arrayOf(HOME_FOLDER_DATABASE_ID, getString(R.string.home_folder), HOME_FOLDER_ID))
 
         // Get a cursor with the list of all the folders.
-        val foldersCursor = bookmarksDatabaseHelper.allFolders
+        val foldersCursor = bookmarksDatabaseHelper.getFoldersExcept(listOf())
 
         // Combine the matrix cursor and the folders cursor.
         val foldersMergeCursor = MergeCursor(arrayOf(matrixCursor, foldersCursor))
@@ -225,26 +233,39 @@ class EditBookmarkDatabaseViewDialog : DialogFragment() {
         val foldersCursorAdapter: ResourceCursorAdapter = object: ResourceCursorAdapter(context, R.layout.databaseview_spinner_item, foldersMergeCursor, 0) {
             override fun bindView(view: View, context: Context, cursor: Cursor) {
                 // Get handles for the spinner views.
-                val spinnerItemImageView = view.findViewById<ImageView>(R.id.spinner_item_imageview)
-                val spinnerItemTextView = view.findViewById<TextView>(R.id.spinner_item_textview)
+                val subfolderSpacerTextView = view.findViewById<TextView>(R.id.subfolder_spacer_textview)
+                val folderIconImageView = view.findViewById<ImageView>(R.id.folder_icon_imageview)
+                val folderNameTextView = view.findViewById<TextView>(R.id.folder_name_textview)
+
+                // Populate the subfolder spacer if it is not null (the spinner is open).
+                if (subfolderSpacerTextView != null) {
+                    // Indent subfolders.
+                    if (cursor.getLong(cursor.getColumnIndexOrThrow(PARENT_FOLDER_ID)) != HOME_FOLDER_ID) {  // The folder is not in the home folder.
+                        // Get the subfolder spacer.
+                        subfolderSpacerTextView.text = bookmarksDatabaseHelper.getSubfolderSpacer(cursor.getLong(cursor.getColumnIndexOrThrow(FOLDER_ID)))
+                    } else {  // The folder is in the home folder.
+                        // Reset the subfolder spacer.
+                        subfolderSpacerTextView.text = ""
+                    }
+                }
 
                 // Set the folder icon according to the type.
                 if (foldersMergeCursor.position == 0) {  // The home folder.
-                    // Set the gray folder image.  `ContextCompat` must be used until the minimum API >= 21.
-                    spinnerItemImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.folder_gray))
+                    // Set the gray folder image.
+                   folderIconImageView.setImageDrawable(AppCompatResources.getDrawable(context, R.drawable.folder_gray))
                 } else {  // A user folder
                     // Get the folder icon byte array.
-                    val folderIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.FAVORITE_ICON))
+                    val folderIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(FAVORITE_ICON))
 
                     // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
                     val folderIconBitmap = BitmapFactory.decodeByteArray(folderIconByteArray, 0, folderIconByteArray.size)
 
                     // Set the folder icon.
-                    spinnerItemImageView.setImageBitmap(folderIconBitmap)
+                    folderIconImageView.setImageBitmap(folderIconBitmap)
                 }
 
-                // Set the text view to display the folder name.
-                spinnerItemTextView.text = cursor.getString(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME))
+                // Set the folder name.
+                folderNameTextView.text = cursor.getString(cursor.getColumnIndexOrThrow(BOOKMARK_NAME))
             }
         }
 
@@ -255,20 +276,20 @@ class EditBookmarkDatabaseViewDialog : DialogFragment() {
         folderSpinner.adapter = foldersCursorAdapter
 
         // Get the parent folder name.
-        val parentFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.PARENT_FOLDER))
+        val parentFolderId = bookmarkCursor.getLong(bookmarkCursor.getColumnIndexOrThrow(PARENT_FOLDER_ID))
 
-        // Select the current folder in the spinner if the bookmark isn't in the home folder.
-        if (parentFolder != "") {
+        // Select the parent folder in the spinner if the bookmark isn't in the home folder.
+        if (parentFolderId != HOME_FOLDER_ID) {
             // Get the database ID of the parent folder.
-            val folderDatabaseId = bookmarksDatabaseHelper.getFolderDatabaseId(bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.PARENT_FOLDER)))
+            val parentFolderDatabaseId = bookmarksDatabaseHelper.getFolderDatabaseId(parentFolderId)
 
             // Initialize the parent folder position and the iteration variable.
             var parentFolderPosition = 0
             var i = 0
 
-            // Find the parent folder position in folders cursor adapter.
+            // Find the parent folder position in the folders cursor adapter.
             do {
-                if (foldersCursorAdapter.getItemId(i) == folderDatabaseId.toLong()) {
+                if (foldersCursorAdapter.getItemId(i) == parentFolderDatabaseId.toLong()) {
                     // Store the current position for the parent folder.
                     parentFolderPosition = i
                 } else {
@@ -286,7 +307,7 @@ class EditBookmarkDatabaseViewDialog : DialogFragment() {
         val currentFolderDatabaseId = folderSpinner.selectedItemId.toInt()
 
         // Populate the display order edit text.
-        displayOrderEditText.setText(bookmarkCursor.getInt(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.DISPLAY_ORDER)).toString())
+        displayOrderEditText.setText(bookmarkCursor.getInt(bookmarkCursor.getColumnIndexOrThrow(DISPLAY_ORDER)).toString())
 
         // Initially disable the save button.
         saveButton.isEnabled = false
@@ -387,7 +408,7 @@ class EditBookmarkDatabaseViewDialog : DialogFragment() {
             // Check the key code, event, and button status.
             if (keyEvent.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER && saveButton.isEnabled) {  // The enter key was pressed and the save button is enabled.
                 // Trigger the listener and return the dialog fragment to the parent activity.
-                editBookmarkDatabaseViewListener.onSaveBookmark(this, bookmarkDatabaseId, favoriteIconBitmap)
+                editBookmarkDatabaseViewListener.saveBookmark(this, bookmarkDatabaseId, favoriteIconBitmap)
 
                 // Manually dismiss the alert dialog.
                 alertDialog.dismiss()
@@ -404,7 +425,7 @@ class EditBookmarkDatabaseViewDialog : DialogFragment() {
             // Check the key code, event, and button status.
             if (keyEvent.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER && saveButton.isEnabled) {  // The enter key was pressed and the save button is enabled.
                 // Trigger the listener and return the dialog fragment to the parent activity.
-                editBookmarkDatabaseViewListener.onSaveBookmark(this, bookmarkDatabaseId, favoriteIconBitmap)
+                editBookmarkDatabaseViewListener.saveBookmark(this, bookmarkDatabaseId, favoriteIconBitmap)
 
                 // Manually dismiss the alert dialog.
                 alertDialog.dismiss()
@@ -421,7 +442,7 @@ class EditBookmarkDatabaseViewDialog : DialogFragment() {
             // Check the key code, event, and button status.
             if (keyEvent.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER && saveButton.isEnabled) {  // The enter key was pressed and the save button is enabled.
                 // Trigger the listener and return the dialog fragment to the parent activity.
-                editBookmarkDatabaseViewListener.onSaveBookmark(this, bookmarkDatabaseId, favoriteIconBitmap)
+                editBookmarkDatabaseViewListener.saveBookmark(this, bookmarkDatabaseId, favoriteIconBitmap)
 
                 // Manually dismiss the alert dialog.
                 alertDialog.dismiss()
@@ -465,4 +486,4 @@ class EditBookmarkDatabaseViewDialog : DialogFragment() {
         // Update the enabled status of the save button.
         saveButton.isEnabled = (iconChanged || nameChanged || urlChanged || folderChanged || displayOrderChanged) && displayOrderNotEmpty
     }
-}
\ No newline at end of file
+}
index 361fdd0ed6f79d6e206866409c62646732ff3d5a..4b0b2c276b6f1f991e427c40503f03ef659ccc4b 100644 (file)
@@ -41,6 +41,9 @@ import androidx.fragment.app.DialogFragment
 import androidx.preference.PreferenceManager
 
 import com.stoutner.privacybrowser.R
+import com.stoutner.privacybrowser.helpers.BOOKMARK_NAME
+import com.stoutner.privacybrowser.helpers.BOOKMARK_URL
+import com.stoutner.privacybrowser.helpers.FAVORITE_ICON
 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper
 
 import java.io.ByteArrayOutputStream
@@ -51,7 +54,7 @@ private const val FAVORITE_ICON_BYTE_ARRAY = "favorite_icon_byte_array"
 
 class EditBookmarkDialog : DialogFragment() {
     companion object {
-        fun bookmarkDatabaseId(databaseId: Int, favoriteIconBitmap: Bitmap): EditBookmarkDialog {
+        fun editBookmark(databaseId: Int, favoriteIconBitmap: Bitmap): EditBookmarkDialog {
             // Create a favorite icon byte array output stream.
             val favoriteIconByteArrayOutputStream = ByteArrayOutputStream()
 
@@ -168,7 +171,7 @@ class EditBookmarkDialog : DialogFragment() {
         saveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
 
         // Get the current favorite icon byte array from the cursor.
-        val currentIconByteArray = bookmarkCursor.getBlob(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.FAVORITE_ICON))
+        val currentIconByteArray = bookmarkCursor.getBlob(bookmarkCursor.getColumnIndexOrThrow(FAVORITE_ICON))
 
         // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
         val currentIconBitmap = BitmapFactory.decodeByteArray(currentIconByteArray, 0, currentIconByteArray.size)
@@ -180,8 +183,8 @@ class EditBookmarkDialog : DialogFragment() {
         webpageFavoriteIconImageView.setImageBitmap(favoriteIconBitmap)
 
         // Store the current bookmark name and URL.
-        val currentName = bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME))
-        val currentUrl = bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_URL))
+        val currentName = bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BOOKMARK_NAME))
+        val currentUrl = bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BOOKMARK_URL))
 
         // Populate the edit texts.
         nameEditText.setText(currentName)
index 85f919b4da7677651cf000ed152be3c00f27e7ae..edcf6e05e1f6fbfb8bb6d0a6c954e343f113dedf 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>.
  *
@@ -23,7 +23,6 @@ import android.app.Dialog
 import android.content.Context
 import android.content.DialogInterface
 import android.database.Cursor
-import android.database.DatabaseUtils
 import android.database.MatrixCursor
 import android.database.MergeCursor
 import android.graphics.Bitmap
@@ -50,7 +49,14 @@ import androidx.fragment.app.DialogFragment
 import androidx.preference.PreferenceManager
 
 import com.stoutner.privacybrowser.R
-import com.stoutner.privacybrowser.activities.BookmarksDatabaseViewActivity
+import com.stoutner.privacybrowser.activities.HOME_FOLDER_DATABASE_ID
+import com.stoutner.privacybrowser.activities.HOME_FOLDER_ID
+import com.stoutner.privacybrowser.helpers.BOOKMARK_NAME
+import com.stoutner.privacybrowser.helpers.DISPLAY_ORDER
+import com.stoutner.privacybrowser.helpers.FAVORITE_ICON
+import com.stoutner.privacybrowser.helpers.FOLDER_ID
+import com.stoutner.privacybrowser.helpers.ID
+import com.stoutner.privacybrowser.helpers.PARENT_FOLDER_ID
 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper
 
 import java.io.ByteArrayOutputStream
@@ -101,7 +107,7 @@ class EditBookmarkFolderDatabaseViewDialog : DialogFragment() {
 
     // The public interface is used to send information back to the parent activity.
     interface EditBookmarkFolderDatabaseViewListener {
-        fun onSaveBookmarkFolder(dialogFragment: DialogFragment, selectedFolderDatabaseId: Int, favoriteIconBitmap: Bitmap)
+        fun saveBookmarkFolder(dialogFragment: DialogFragment, selectedFolderDatabaseId: Int, favoriteIconBitmap: Bitmap)
     }
 
     override fun onAttach(context: Context) {
@@ -147,7 +153,7 @@ class EditBookmarkFolderDatabaseViewDialog : DialogFragment() {
         // Set the save button listener.
         dialogBuilder.setPositiveButton(R.string.save) { _: DialogInterface?, _: Int ->
             // Return the dialog fragment to the parent activity.
-            editBookmarkFolderDatabaseViewListener.onSaveBookmarkFolder(this, folderDatabaseId, favoriteIconBitmap)
+            editBookmarkFolderDatabaseViewListener.saveBookmarkFolder(this, folderDatabaseId, favoriteIconBitmap)
         }
 
         // Create an alert dialog from the alert dialog builder.
@@ -169,6 +175,7 @@ class EditBookmarkFolderDatabaseViewDialog : DialogFragment() {
 
         // Get handles for the views in the alert dialog.
         val databaseIdTextView = alertDialog.findViewById<TextView>(R.id.folder_database_id_textview)!!
+        val folderIdTextView = alertDialog.findViewById<TextView>(R.id.folder_id_textview)!!
         val currentIconLinearLayout = alertDialog.findViewById<LinearLayout>(R.id.current_icon_linearlayout)!!
         currentIconRadioButton = alertDialog.findViewById(R.id.current_icon_radiobutton)!!
         val currentIconImageView = alertDialog.findViewById<ImageView>(R.id.current_icon_imageview)!!
@@ -183,15 +190,19 @@ class EditBookmarkFolderDatabaseViewDialog : DialogFragment() {
         saveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
 
         // Store the current folder values.
-        val currentFolderName = folderCursor.getString(folderCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME))
-        val currentDisplayOrder = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.DISPLAY_ORDER))
-        val parentFolder = folderCursor.getString(folderCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.PARENT_FOLDER))
+        val currentFolderName = folderCursor.getString(folderCursor.getColumnIndexOrThrow(BOOKMARK_NAME))
+        val currentDisplayOrder = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(DISPLAY_ORDER))
+        val parentFolderId = folderCursor.getLong(folderCursor.getColumnIndexOrThrow(PARENT_FOLDER_ID))
+        val currentFolderId = folderCursor.getLong(folderCursor.getColumnIndexOrThrow(FOLDER_ID))
 
         // Populate the database ID text view.
-        databaseIdTextView.text = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.ID)).toString()
+        databaseIdTextView.text = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(ID)).toString()
+
+        // Populate the folder ID text view.
+        folderIdTextView.text = folderCursor.getLong(folderCursor.getColumnIndexOrThrow(FOLDER_ID)).toString()
 
         // Get the current favorite icon byte array from the cursor.
-        val currentIconByteArray = folderCursor.getBlob(folderCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.FAVORITE_ICON))
+        val currentIconByteArray = folderCursor.getBlob(folderCursor.getColumnIndexOrThrow(FAVORITE_ICON))
 
         // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
         val currentIconBitmap = BitmapFactory.decodeByteArray(currentIconByteArray, 0, currentIconByteArray.size)
@@ -206,19 +217,29 @@ class EditBookmarkFolderDatabaseViewDialog : DialogFragment() {
         nameEditText.setText(currentFolderName)
 
         // Define an array of matrix cursor column names.
-        val matrixCursorColumnNames = arrayOf(BookmarksDatabaseHelper.ID, BookmarksDatabaseHelper.BOOKMARK_NAME)
+        val matrixCursorColumnNames = arrayOf(ID, BOOKMARK_NAME, PARENT_FOLDER_ID)
 
         // Create a matrix cursor.
         val matrixCursor = MatrixCursor(matrixCursorColumnNames)
 
         // Add `Home Folder` to the matrix cursor.
-        matrixCursor.addRow(arrayOf(BookmarksDatabaseViewActivity.HOME_FOLDER_DATABASE_ID, getString(R.string.home_folder)))
+        matrixCursor.addRow(arrayOf(HOME_FOLDER_DATABASE_ID, getString(R.string.home_folder), HOME_FOLDER_ID))
+
+        // Create a list of folder IDs.
+        val currentAndSubfolderIds = mutableListOf<Long>()
+
+        // Add the current folder ID to the list.
+        currentAndSubfolderIds.add(currentFolderId)
 
-        // Get a string of the current folder and all subfolders.
-        val currentAndSubfolderString = getStringOfSubfolders(currentFolderName, bookmarksDatabaseHelper)
+        // Get a long array of all the subfolders IDs.
+        val subfolderIdLongList = getListOfSubfolderIds(currentFolderId, bookmarksDatabaseHelper)
 
-        // Get a cursor with the list of all the folders.
-        val foldersCursor = bookmarksDatabaseHelper.getFoldersExcept(currentAndSubfolderString)
+        // Add the subfolder IDs to the list.
+        for (subfolderId in subfolderIdLongList)
+            currentAndSubfolderIds.add(subfolderId)
+
+        // Get a cursor with the list of all the folders except for those specified..
+        val foldersCursor = bookmarksDatabaseHelper.getFoldersExcept(currentAndSubfolderIds)
 
         // Combine the matrix cursor and the folders cursor.
         val combinedFoldersCursor = MergeCursor(arrayOf(matrixCursor, foldersCursor))
@@ -227,26 +248,39 @@ class EditBookmarkFolderDatabaseViewDialog : DialogFragment() {
         val foldersCursorAdapter: ResourceCursorAdapter = object: ResourceCursorAdapter(context, R.layout.databaseview_spinner_item, combinedFoldersCursor, 0) {
             override fun bindView(view: View, context: Context, cursor: Cursor) {
                 // Get handles for the spinner views.
-                val spinnerItemImageView = view.findViewById<ImageView>(R.id.spinner_item_imageview)
-                val spinnerItemTextView = view.findViewById<TextView>(R.id.spinner_item_textview)
+                val subfolderSpacerTextView = view.findViewById<TextView>(R.id.subfolder_spacer_textview)
+                val folderIconImageView = view.findViewById<ImageView>(R.id.folder_icon_imageview)
+                val folderNameTextView = view.findViewById<TextView>(R.id.folder_name_textview)
+
+                // Populate the subfolder spacer if it is not null (the spinner is open).
+                if (subfolderSpacerTextView != null) {
+                    // Indent subfolders.
+                    if (cursor.getLong(cursor.getColumnIndexOrThrow(PARENT_FOLDER_ID)) != HOME_FOLDER_ID) {  // The folder is not in the home folder.
+                        // Get the subfolder spacer.
+                        subfolderSpacerTextView.text = bookmarksDatabaseHelper.getSubfolderSpacer(cursor.getLong(cursor.getColumnIndexOrThrow(FOLDER_ID)))
+                    } else {  // The folder is in the home folder.
+                        // Reset the subfolder spacer.
+                        subfolderSpacerTextView.text = ""
+                    }
+                }
 
                 // Set the folder icon according to the type.
                 if (combinedFoldersCursor.position == 0) {  // Set the `Home Folder` icon.
                     // Set the gray folder image.  `ContextCompat` must be used until the minimum API >= 21.
-                    spinnerItemImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.folder_gray))
+                    folderIconImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.folder_gray))
                 } else {  // Set a user folder icon.
                     // Get the folder icon byte array.
-                    val folderIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.FAVORITE_ICON))
+                    val folderIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(FAVORITE_ICON))
 
                     // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
                     val folderIconBitmap = BitmapFactory.decodeByteArray(folderIconByteArray, 0, folderIconByteArray.size)
 
                     // Set the folder icon.
-                    spinnerItemImageView.setImageBitmap(folderIconBitmap)
+                    folderIconImageView.setImageBitmap(folderIconBitmap)
                 }
 
-                // Set the text view to display the folder name.
-                spinnerItemTextView.text = cursor.getString(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME))
+                // Set the folder name.
+                folderNameTextView.text = cursor.getString(cursor.getColumnIndexOrThrow(BOOKMARK_NAME))
             }
         }
 
@@ -256,10 +290,10 @@ class EditBookmarkFolderDatabaseViewDialog : DialogFragment() {
         // Set the parent folder spinner adapter.
         parentFolderSpinner.adapter = foldersCursorAdapter
 
-        // Select the current folder in the spinner if the bookmark isn't in the "Home Folder".
-        if (parentFolder != "") {
+        // Select the current folder in the spinner if the bookmark isn't in the home folder.
+        if (parentFolderId != HOME_FOLDER_ID) {
             // Get the database ID of the parent folder as a long.
-            val parentFolderDatabaseId = bookmarksDatabaseHelper.getFolderDatabaseId(folderCursor.getString(folderCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.PARENT_FOLDER))).toLong()
+            val parentFolderDatabaseId = bookmarksDatabaseHelper.getFolderDatabaseId(parentFolderId).toLong()
 
             // Initialize the parent folder position and the iteration variable.
             var parentFolderPosition = 0
@@ -285,7 +319,7 @@ class EditBookmarkFolderDatabaseViewDialog : DialogFragment() {
         val currentParentFolderDatabaseId = parentFolderSpinner.selectedItemId.toInt()
 
         // Populate the display order edit text.
-        displayOrderEditText.setText(folderCursor.getInt(folderCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.DISPLAY_ORDER)).toString())
+        displayOrderEditText.setText(folderCursor.getInt(folderCursor.getColumnIndexOrThrow(DISPLAY_ORDER)).toString())
 
         // Initially disable the edit button.
         saveButton.isEnabled = false
@@ -305,7 +339,7 @@ class EditBookmarkFolderDatabaseViewDialog : DialogFragment() {
             webpageFavoriteIconRadioButton.isChecked = false
 
             // Update the save button.
-            updateSaveButton(bookmarksDatabaseHelper, currentFolderName, currentParentFolderDatabaseId, currentDisplayOrder)
+            updateSaveButton(currentFolderName, currentParentFolderDatabaseId, currentDisplayOrder)
         }
 
         // Set the default icon linear layout click listener.
@@ -318,7 +352,7 @@ class EditBookmarkFolderDatabaseViewDialog : DialogFragment() {
             webpageFavoriteIconRadioButton.isChecked = false
 
             // Update the save button.
-            updateSaveButton(bookmarksDatabaseHelper, currentFolderName, currentParentFolderDatabaseId, currentDisplayOrder)
+            updateSaveButton(currentFolderName, currentParentFolderDatabaseId, currentDisplayOrder)
         }
 
         // Set the webpage favorite icon linear layout click listener.
@@ -331,7 +365,7 @@ class EditBookmarkFolderDatabaseViewDialog : DialogFragment() {
             defaultIconRadioButton.isChecked = false
 
             // Update the save button.
-            updateSaveButton(bookmarksDatabaseHelper, currentFolderName, currentParentFolderDatabaseId, currentDisplayOrder)
+            updateSaveButton(currentFolderName, currentParentFolderDatabaseId, currentDisplayOrder)
         }
 
         // Update the save button if the bookmark name changes.
@@ -346,7 +380,7 @@ class EditBookmarkFolderDatabaseViewDialog : DialogFragment() {
 
             override fun afterTextChanged(s: Editable) {
                 // Update the save button.
-                updateSaveButton(bookmarksDatabaseHelper, currentFolderName, currentParentFolderDatabaseId, currentDisplayOrder)
+                updateSaveButton(currentFolderName, currentParentFolderDatabaseId, currentDisplayOrder)
             }
         })
 
@@ -356,7 +390,7 @@ class EditBookmarkFolderDatabaseViewDialog : DialogFragment() {
             parentFolderSpinner.onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
                 override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
                     // Update the save button.
-                    updateSaveButton(bookmarksDatabaseHelper, currentFolderName, currentParentFolderDatabaseId, currentDisplayOrder)
+                    updateSaveButton(currentFolderName, currentParentFolderDatabaseId, currentDisplayOrder)
                 }
 
                 override fun onNothingSelected(parent: AdapterView<*>) {
@@ -377,7 +411,7 @@ class EditBookmarkFolderDatabaseViewDialog : DialogFragment() {
 
             override fun afterTextChanged(s: Editable) {
                 // Update the save button.
-                updateSaveButton(bookmarksDatabaseHelper, currentFolderName, currentParentFolderDatabaseId, currentDisplayOrder)
+                updateSaveButton(currentFolderName, currentParentFolderDatabaseId, currentDisplayOrder)
             }
         })
 
@@ -386,7 +420,7 @@ class EditBookmarkFolderDatabaseViewDialog : DialogFragment() {
             // Check the key code, event, and button status.
             if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER && saveButton.isEnabled) {  // The enter key was pressed and the save button is enabled.
                 // Trigger the listener and return the dialog fragment to the parent activity.
-                editBookmarkFolderDatabaseViewListener.onSaveBookmarkFolder(this, folderDatabaseId, favoriteIconBitmap)
+                editBookmarkFolderDatabaseViewListener.saveBookmarkFolder(this, folderDatabaseId, favoriteIconBitmap)
 
                 // Manually dismiss the alert dialog.
                 alertDialog.dismiss()
@@ -403,7 +437,7 @@ class EditBookmarkFolderDatabaseViewDialog : DialogFragment() {
             // Check the key code, event, and button status.
             if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER && saveButton.isEnabled) {  // The enter key was pressed and the save button is enabled.
                 // Trigger the listener and return the dialog fragment to the parent activity.
-                editBookmarkFolderDatabaseViewListener.onSaveBookmarkFolder(this, folderDatabaseId, favoriteIconBitmap)
+                editBookmarkFolderDatabaseViewListener.saveBookmarkFolder(this, folderDatabaseId, favoriteIconBitmap)
 
                 // Manually dismiss the alert dialog.
                 alertDialog.dismiss()
@@ -419,26 +453,17 @@ class EditBookmarkFolderDatabaseViewDialog : DialogFragment() {
         return alertDialog
     }
 
-    private fun updateSaveButton(bookmarksDatabaseHelper: BookmarksDatabaseHelper, currentFolderName: String, currentParentFolderDatabaseId: Int, currentDisplayOrder: Int) {
+    private fun updateSaveButton(currentFolderName: String, currentParentFolderDatabaseId: Int, currentDisplayOrder: Int) {
         // Get the values from the views.
         val newFolderName = nameEditText.text.toString()
         val newParentFolderDatabaseId = parentFolderSpinner.selectedItemId.toInt()
         val newDisplayOrder = displayOrderEditText.text.toString()
 
-        // Get a cursor for the new folder name if it exists.
-        val folderExistsCursor = bookmarksDatabaseHelper.getFolder(newFolderName)
-
-        // Is the new folder name empty?
-        val folderNameNotEmpty = newFolderName.isNotEmpty()
-
-        // Does the folder name already exist?
-        val folderNameAlreadyExists = (newFolderName != currentFolderName) && folderExistsCursor.count > 0
-
         // Has the favorite icon changed?
         val iconChanged = !currentIconRadioButton.isChecked
 
         // Has the folder been renamed?
-        val folderRenamed = (newFolderName != currentFolderName) && !folderNameAlreadyExists
+        val folderRenamed = (newFolderName != currentFolderName)
 
         // Has the parent folder changed?
         val parentFolderChanged = newParentFolderDatabaseId != currentParentFolderDatabaseId
@@ -446,39 +471,37 @@ class EditBookmarkFolderDatabaseViewDialog : DialogFragment() {
         // Has the display order changed?
         val displayOrderChanged = newDisplayOrder != currentDisplayOrder.toString()
 
-        // Is the display order empty?
-        val displayOrderNotEmpty = newDisplayOrder.isNotEmpty()
-
         // Update the enabled status of the edit button.
-        saveButton.isEnabled = (iconChanged || folderRenamed || parentFolderChanged || displayOrderChanged) && folderNameNotEmpty && displayOrderNotEmpty
+        saveButton.isEnabled = (iconChanged || folderRenamed || parentFolderChanged || displayOrderChanged) && newFolderName.isNotBlank() && newDisplayOrder.isNotBlank()
     }
 
-    private fun getStringOfSubfolders(folderName: String, bookmarksDatabaseHelper: BookmarksDatabaseHelper): String {
-        // Get a cursor will all the immediate subfolders.
-        val subfoldersCursor = bookmarksDatabaseHelper.getSubfolders(folderName)
+    private fun getListOfSubfolderIds(folderId: Long, bookmarksDatabaseHelper: BookmarksDatabaseHelper): List<Long> {
+        // Create a subfolder long list.
+        val subfolderIdLongList = mutableListOf<Long>()
 
-        // Initialize the subfolder string builder and populate it with the current folder.
-        val currentAndSubfolderStringBuilder = StringBuilder(DatabaseUtils.sqlEscapeString(folderName))
+        // Get a cursor with all the immediate subfolders.
+        val subfoldersCursor = bookmarksDatabaseHelper.getSubfolderNamesAndFolderIds(folderId)
 
-        // Populate the subfolder string builder
+        // Populate the subfolder list.
         for (i in 0 until subfoldersCursor.count) {
             // Move the subfolder cursor to the current item.
             subfoldersCursor.moveToPosition(i)
 
-            // Get the name of the subfolder.
-            val subfolderName = subfoldersCursor.getString(subfoldersCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME))
+            // Get the subfolder ID.
+            val subfolderId = subfoldersCursor.getLong(subfoldersCursor.getColumnIndexOrThrow(FOLDER_ID))
 
-            // Add a comma to the end of the existing string.
-            currentAndSubfolderStringBuilder.append(",")
+            // Add the folder ID to the list.
+            subfolderIdLongList.add(subfolderId)
 
-            // Get the folder name and run the task for any subfolders.
-            val subfolderString = getStringOfSubfolders(subfolderName, bookmarksDatabaseHelper)
+            // Get a list of any subfolders of the subfolder.
+            val nestedSubfolderIdList = getListOfSubfolderIds(subfolderId, bookmarksDatabaseHelper)
 
-            // Add the folder name to the string builder.
-            currentAndSubfolderStringBuilder.append(subfolderString)
+            // Add each of the subfolder IDs to the list.
+            for (nestedSubfolderId in nestedSubfolderIdList)
+                subfolderIdLongList.add(nestedSubfolderId)
         }
 
-        // Return the string of folders.
-        return currentAndSubfolderStringBuilder.toString()
+        // Return the list of subfolder IDs.
+        return subfolderIdLongList
     }
-}
\ No newline at end of file
+}
index 6ca8ebcefd30667027f03cb2368760835f8c7e3c..7427173d9c87d7767a33dd5801f21c47fa6aa04c 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>.
  *
@@ -41,6 +41,8 @@ import androidx.fragment.app.DialogFragment
 import androidx.preference.PreferenceManager
 
 import com.stoutner.privacybrowser.R
+import com.stoutner.privacybrowser.helpers.BOOKMARK_NAME
+import com.stoutner.privacybrowser.helpers.FAVORITE_ICON
 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper
 
 import java.io.ByteArrayOutputStream
@@ -51,7 +53,7 @@ private const val FAVORITE_ICON_BYTE_ARRAY = "favorite_icon_byte_array"
 
 class EditBookmarkFolderDialog : DialogFragment() {
     companion object {
-        fun folderDatabaseId(databaseId: Int, favoriteIconBitmap: Bitmap): EditBookmarkFolderDialog {
+        fun editFolder(databaseId: Int, favoriteIconBitmap: Bitmap): EditBookmarkFolderDialog {
             // Create a favorite icon byte array output stream.
             val favoriteIconByteArrayOutputStream = ByteArrayOutputStream()
 
@@ -170,7 +172,7 @@ class EditBookmarkFolderDialog : DialogFragment() {
         saveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
 
         // Get the current favorite icon byte array from the cursor.
-        val currentIconByteArray = folderCursor.getBlob(folderCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.FAVORITE_ICON))
+        val currentIconByteArray = folderCursor.getBlob(folderCursor.getColumnIndexOrThrow(FAVORITE_ICON))
 
         // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
         val currentIconBitmap = BitmapFactory.decodeByteArray(currentIconByteArray, 0, currentIconByteArray.size)
@@ -182,7 +184,7 @@ class EditBookmarkFolderDialog : DialogFragment() {
         webpageFavoriteIconImageView.setImageBitmap(favoriteIconBitmap)
 
         // Get the current folder name.
-        currentFolderName = folderCursor.getString(folderCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME))
+        currentFolderName = folderCursor.getString(folderCursor.getColumnIndexOrThrow(BOOKMARK_NAME))
 
         // Display the current folder name.
         folderNameEditText.setText(currentFolderName)
@@ -275,22 +277,13 @@ class EditBookmarkFolderDialog : DialogFragment() {
         // Get the new folder name.
         val newFolderName = folderNameEditText.text.toString()
 
-        // Get a cursor for the new folder name if it exists.
-        val folderExistsCursor = bookmarksDatabaseHelper.getFolder(newFolderName)
-
-        // Is the new folder name empty?
-        val folderNameEmpty = newFolderName.isEmpty()
-
-        // Does the folder name already exist?
-        val folderNameAlreadyExists = (newFolderName != currentFolderName) && folderExistsCursor.count > 0
-
         // Has the folder been renamed?
-        val folderRenamed = (newFolderName != currentFolderName) && !folderNameAlreadyExists
+        val folderRenamed = (newFolderName != currentFolderName)
 
         // Has the favorite icon changed?
-        val iconChanged = !currentIconRadioButton.isChecked && !folderNameAlreadyExists
+        val iconChanged = !currentIconRadioButton.isChecked
 
-        // Enable the save button if something has been edited and the new folder name is valid.
-        saveButton.isEnabled = !folderNameEmpty && (folderRenamed || iconChanged)
+        // Enable the save button if something has been edited and the new folder name is not valid.
+        saveButton.isEnabled = newFolderName.isNotBlank() && (folderRenamed || iconChanged)
     }
-}
\ No newline at end of file
+}
index 37d01e324e44e37f1b25308aafe916d2c7312ca0..c6a26a6d5b69e73cb1b83ef4256b92b2f53d39ed 100644 (file)
@@ -23,7 +23,6 @@ import android.app.Dialog
 import android.content.Context
 import android.content.DialogInterface
 import android.database.Cursor
-import android.database.DatabaseUtils
 import android.database.MatrixCursor
 import android.database.MergeCursor
 import android.graphics.Bitmap
@@ -46,6 +45,12 @@ import androidx.fragment.app.DialogFragment
 import androidx.preference.PreferenceManager
 
 import com.stoutner.privacybrowser.R
+import com.stoutner.privacybrowser.activities.HOME_FOLDER_ID
+import com.stoutner.privacybrowser.helpers.BOOKMARK_NAME
+import com.stoutner.privacybrowser.helpers.FAVORITE_ICON
+import com.stoutner.privacybrowser.helpers.FOLDER_ID
+import com.stoutner.privacybrowser.helpers.ID
+import com.stoutner.privacybrowser.helpers.PARENT_FOLDER_ID
 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper
 
 import kotlinx.coroutines.CoroutineScope
@@ -54,20 +59,19 @@ import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
 import java.io.ByteArrayOutputStream
-import java.lang.StringBuilder
 
 // Define the class constants.
-private const val CURRENT_FOLDER = "current_folder"
-private const val SELECTED_BOOKMARKS_LONG_ARRAY = "selected_bookmarks_long_array"
+private const val CURRENT_FOLDER_ID = "A"
+private const val SELECTED_BOOKMARKS_LONG_ARRAY = "B"
 
 class MoveToFolderDialog : DialogFragment() {
     companion object {
-        fun moveBookmarks(currentFolder: String, selectedBookmarksLongArray: LongArray): MoveToFolderDialog {
+        fun moveBookmarks(currentFolderId: Long, selectedBookmarksLongArray: LongArray): MoveToFolderDialog {
             // Create an arguments bundle.
             val argumentsBundle = Bundle()
 
             // Store the arguments in the bundle.
-            argumentsBundle.putString(CURRENT_FOLDER, currentFolder)
+            argumentsBundle.putLong(CURRENT_FOLDER_ID, currentFolderId)
             argumentsBundle.putLongArray(SELECTED_BOOKMARKS_LONG_ARRAY, selectedBookmarksLongArray)
 
             // Create a new instance of the dialog.
@@ -84,7 +88,6 @@ class MoveToFolderDialog : DialogFragment() {
     // Declare the class variables.
     private lateinit var moveToFolderListener: MoveToFolderListener
     private lateinit var bookmarksDatabaseHelper: BookmarksDatabaseHelper
-    private lateinit var exceptFolders: StringBuilder
 
     // The public interface is used to send information back to the parent activity.
     interface MoveToFolderListener {
@@ -101,7 +104,7 @@ class MoveToFolderDialog : DialogFragment() {
 
     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
         // Get the data from the arguments.
-        val currentFolder = requireArguments().getString(CURRENT_FOLDER)!!
+        val currentFolderId = requireArguments().getLong(CURRENT_FOLDER_ID, HOME_FOLDER_ID)
         val selectedBookmarksLongArray = requireArguments().getLongArray(SELECTED_BOOKMARKS_LONG_ARRAY)!!
 
         // Initialize the database helper.
@@ -152,32 +155,40 @@ class MoveToFolderDialog : DialogFragment() {
         // Initially disable the positive button.
         moveButton.isEnabled = false
 
-        // Initialize the except folders string builder.
-        exceptFolders = StringBuilder()
+        // Create a list of folders not to display.
+        val folderIdsNotToDisplay = mutableListOf<Long>()
 
-        // Declare the cursor variables.
-        val foldersCursor: Cursor
-        val foldersCursorAdapter: CursorAdapter
+        // Add any selected folders and their subfolders to the list of folders not to display.
+        for (databaseIdLong in selectedBookmarksLongArray) {
+            // Get the database ID int for each selected bookmark.
+            val databaseIdInt = databaseIdLong.toInt()
 
-        // Check to see if the bookmark is currently in the home folder.
-        if (currentFolder.isEmpty()) {  // The bookmark is currently in the home folder.  Don't display `Home Folder` at the top of the list view.
-            // If a folder is selected, add it and all children to the list of folders not to display.
-            for (databaseIdLong in selectedBookmarksLongArray) {
-                // Get the database ID int for each selected bookmark.
-                val databaseIdInt = databaseIdLong.toInt()
-
-                // Check to see if the bookmark is a folder.
-                if (bookmarksDatabaseHelper.isFolder(databaseIdInt)) {
-                    // Add the folder to the list of folders not to display.
-                    addFolderToExceptFolders(databaseIdInt)
-                }
+            // Check to see if the bookmark is a folder.
+            if (bookmarksDatabaseHelper.isFolder(databaseIdInt)) {
+                // Add the folder to the list of folders not to display.
+                folderIdsNotToDisplay.add(bookmarksDatabaseHelper.getFolderId(databaseIdInt))
             }
+        }
 
+        // Check to see if the bookmark is currently in the home folder.
+        if (currentFolderId == HOME_FOLDER_ID) {  // The bookmark is currently in the home folder.  Don't display `Home Folder` at the top of the list view.
             // Get a cursor containing the folders to display.
-            foldersCursor = bookmarksDatabaseHelper.getFoldersExcept(exceptFolders.toString())
+            val foldersCursor = bookmarksDatabaseHelper.getFoldersExcept(folderIdsNotToDisplay)
 
             // Populate the folders cursor adapter.
-            foldersCursorAdapter = populateFoldersCursorAdapter(requireContext(), foldersCursor)
+            val foldersCursorAdapter = populateFoldersCursorAdapter(requireContext(), foldersCursor)
+
+            // Get a handle for the folders list view.
+            val foldersListView = alertDialog.findViewById<ListView>(R.id.move_to_folder_listview)!!
+
+            // Set the folder list view adapter.
+            foldersListView.adapter = foldersCursorAdapter
+
+            // Enable the move button when a folder is selected.
+            foldersListView.onItemClickListener = OnItemClickListener { _: AdapterView<*>?, _: View?, _: Int, _: Long ->
+                // Enable the move button.
+                moveButton.isEnabled = true
+            }
         } else {  // The current folder is not directly in the home folder.  Display `Home Folder` at the top of the list view.
             // Get the home folder icon drawable.
             val homeFolderIconDrawable = ContextCompat.getDrawable(requireActivity().applicationContext, R.drawable.folder_gray_bitmap)
@@ -196,101 +207,53 @@ class MoveToFolderDialog : DialogFragment() {
                 withContext(Dispatchers.Default) {
                     // Convert the home folder bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
                     homeFolderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, homeFolderIconByteArrayOutputStream)
-                }
-            }
 
-            // Convert the home folder icon byte array output stream to a byte array.
-            val homeFolderIconByteArray = homeFolderIconByteArrayOutputStream.toByteArray()
+                    // Convert the home folder icon byte array output stream to a byte array.
+                    val homeFolderIconByteArray = homeFolderIconByteArrayOutputStream.toByteArray()
 
-            // Setup the home folder matrix cursor column names.
-            val homeFolderMatrixCursorColumnNames = arrayOf(BookmarksDatabaseHelper.ID, BookmarksDatabaseHelper.BOOKMARK_NAME, BookmarksDatabaseHelper.FAVORITE_ICON)
+                    // Setup the home folder matrix cursor column names.
+                    val homeFolderMatrixCursorColumnNames = arrayOf(ID, BOOKMARK_NAME, FAVORITE_ICON, PARENT_FOLDER_ID)
 
-            // Setup a matrix cursor for the `Home Folder`.
-            val homeFolderMatrixCursor = MatrixCursor(homeFolderMatrixCursorColumnNames)
+                    // Setup a matrix cursor for the `Home Folder`.
+                    val homeFolderMatrixCursor = MatrixCursor(homeFolderMatrixCursorColumnNames)
 
-            // Add the home folder to the home folder matrix cursor.
-            homeFolderMatrixCursor.addRow(arrayOf<Any>(0, getString(R.string.home_folder), homeFolderIconByteArray))
+                    // Add the home folder to the home folder matrix cursor.
+                    homeFolderMatrixCursor.addRow(arrayOf<Any>(0, getString(R.string.home_folder), homeFolderIconByteArray, HOME_FOLDER_ID))
 
-            // Add the parent folder to the list of folders not to display.
-            exceptFolders.append(DatabaseUtils.sqlEscapeString(currentFolder))
+                    // Add the current folder to the list of folders not to display.
+                    folderIdsNotToDisplay.add(currentFolderId)
 
-            // If a folder is selected, add it and all children to the list of folders not to display.
-            for (databaseIdLong in selectedBookmarksLongArray) {
-                // Get the database ID int for each selected bookmark.
-                val databaseIdInt = databaseIdLong.toInt()
-
-                // Check to see if the bookmark is a folder.
-                if (bookmarksDatabaseHelper.isFolder(databaseIdInt)) {
-                    // Add the folder to the list of folders not to display.
-                    addFolderToExceptFolders(databaseIdInt)
-                }
-            }
+                    // Get a cursor containing the folders to display.
+                    val foldersCursor = bookmarksDatabaseHelper.getFoldersExcept(folderIdsNotToDisplay)
 
-            // Get a cursor containing the folders to display.
-            foldersCursor = bookmarksDatabaseHelper.getFoldersExcept(exceptFolders.toString())
+                    // Combine the home folder matrix cursor and the folders cursor.
+                    val foldersMergeCursor = MergeCursor(arrayOf(homeFolderMatrixCursor, foldersCursor))
 
-            // Combine the home folder matrix cursor and the folders cursor.
-            val foldersMergeCursor = MergeCursor(arrayOf(homeFolderMatrixCursor, foldersCursor))
+                    // Populate the folders cursor on the main thread.
+                    withContext(Dispatchers.Main) {
+                        // Populate the folders cursor adapter.
+                        val foldersCursorAdapter = populateFoldersCursorAdapter(requireContext(), foldersMergeCursor)
 
-            // Populate the folders cursor adapter.
-            foldersCursorAdapter = populateFoldersCursorAdapter(requireContext(), foldersMergeCursor)
-        }
+                        // Get a handle for the folders list view.
+                        val foldersListView = alertDialog.findViewById<ListView>(R.id.move_to_folder_listview)!!
 
-        // Get a handle for the folders list view.
-        val foldersListView = alertDialog.findViewById<ListView>(R.id.move_to_folder_listview)!!
+                        // Set the folder list view adapter.
+                        foldersListView.adapter = foldersCursorAdapter
 
-        // Set the folder list view adapter.
-        foldersListView.adapter = foldersCursorAdapter
-
-        // Enable the move button when a folder is selected.
-        foldersListView.onItemClickListener = OnItemClickListener { _: AdapterView<*>?, _: View?, _: Int, _: Long ->
-            // Enable the move button.
-            moveButton.isEnabled = true
+                        // Enable the move button when a folder is selected.
+                        foldersListView.onItemClickListener = OnItemClickListener { _: AdapterView<*>?, _: View?, _: Int, _: Long ->
+                            // Enable the move button.
+                            moveButton.isEnabled = true
+                        }
+                    }
+                }
+            }
         }
 
         // Return the alert dialog.
         return alertDialog
     }
 
-    private fun addFolderToExceptFolders(databaseIdInt: Int) {
-        // Get the name of the selected folder.
-        val folderName = bookmarksDatabaseHelper.getFolderName(databaseIdInt)
-
-        // Populate the list of folders not to get.
-        if (exceptFolders.isEmpty()) {
-            // Add the selected folder to the list of folders not to display.
-            exceptFolders.append(DatabaseUtils.sqlEscapeString(folderName))
-        } else {
-            // Add the selected folder to the end of the list of folders not to display.
-            exceptFolders.append(",")
-            exceptFolders.append(DatabaseUtils.sqlEscapeString(folderName))
-        }
-
-        // Add the selected folder's subfolders to the list of folders not to display.
-        addSubfoldersToExceptFolders(folderName)
-    }
-
-    private fun addSubfoldersToExceptFolders(folderName: String) {
-        // Get a cursor with all the immediate subfolders.
-        val subfoldersCursor = bookmarksDatabaseHelper.getSubfolders(folderName)
-
-        // Add each subfolder to the list of folders not to display.
-        for (i in 0 until subfoldersCursor.count) {
-            // Move the subfolder cursor to the current item.
-            subfoldersCursor.moveToPosition(i)
-
-            // Get the name of the subfolder.
-            val subfolderName = subfoldersCursor.getString(subfoldersCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME))
-
-            // Add the subfolder to except folders.
-            exceptFolders.append(",")
-            exceptFolders.append(DatabaseUtils.sqlEscapeString(subfolderName))
-
-            // Run the same tasks for any subfolders of the subfolder.
-            addSubfoldersToExceptFolders(subfolderName)
-        }
-    }
-
     private fun populateFoldersCursorAdapter(context: Context, cursor: Cursor): CursorAdapter {
         // Return the folders cursor adapter.
         return object : CursorAdapter(context, cursor, false) {
@@ -301,12 +264,22 @@ class MoveToFolderDialog : DialogFragment() {
 
             override fun bindView(view: View, context: Context, cursor: Cursor) {
                 // Get the data from the cursor.
-                val folderIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.FAVORITE_ICON))
-                val folderName = cursor.getString(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME))
+                val folderIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(FAVORITE_ICON))
+                val folderName = cursor.getString(cursor.getColumnIndexOrThrow(BOOKMARK_NAME))
 
                 // Get handles for the views.
-                val folderIconImageView = view.findViewById<ImageView>(R.id.move_to_folder_icon)
-                val folderNameTextView = view.findViewById<TextView>(R.id.move_to_folder_name_textview)
+                val subfolderSpacerTextView = view.findViewById<TextView>(R.id.subfolder_spacer_textview)
+                val folderIconImageView = view.findViewById<ImageView>(R.id.folder_icon_imageview)
+                val folderNameTextView = view.findViewById<TextView>(R.id.folder_name_textview)
+
+                // Populate the subfolder spacer.
+                if (cursor.getLong(cursor.getColumnIndexOrThrow(PARENT_FOLDER_ID)) != HOME_FOLDER_ID) {  // The folder is not in the home folder.
+                    // Get the subfolder spacer.
+                    subfolderSpacerTextView.text = bookmarksDatabaseHelper.getSubfolderSpacer(cursor.getLong(cursor.getColumnIndexOrThrow(FOLDER_ID)))
+                } else {  // The folder is in the home folder.
+                    // Reset the subfolder spacer.
+                    subfolderSpacerTextView.text = ""
+                }
 
                 // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
                 val folderIconBitmap = BitmapFactory.decodeByteArray(folderIconByteArray, 0, folderIconByteArray.size)
index bd90abceadc153643175632479e4299a14de67f3..c4c8ebb0a1e8f0de4ec973e27b5c9efe597f4e71 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright 2016-2022 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>.
  *
@@ -23,57 +23,144 @@ import android.content.ContentValues
 import android.content.Context
 import android.database.Cursor
 import android.database.DatabaseUtils
+import android.database.MatrixCursor
+import android.database.MergeCursor
 import android.database.sqlite.SQLiteDatabase
 import android.database.sqlite.SQLiteOpenHelper
 
+import com.stoutner.privacybrowser.activities.HOME_FOLDER_ID
+
+import java.util.Date
+
 // Define the class constants.
-private const val SCHEMA_VERSION = 1
+private const val SCHEMA_VERSION = 2
+
+// Define the public database constants.
+const val BOOKMARKS_DATABASE = "bookmarks.db"
+const val BOOKMARKS_TABLE = "bookmarks"
+
+// Define the public schema constants.
+const val BOOKMARK_NAME = "bookmarkname"
+const val BOOKMARK_URL = "bookmarkurl"
+const val DISPLAY_ORDER = "displayorder"
+const val FAVORITE_ICON = "favoriteicon"
+const val FOLDER_ID = "folder_id"
+const val IS_FOLDER = "isfolder"
+const val PARENT_FOLDER_ID = "parent_folder_id"
+
+// Define the public table creation constant.
+const val CREATE_BOOKMARKS_TABLE = "CREATE TABLE $BOOKMARKS_TABLE (" +
+        "$ID INTEGER PRIMARY KEY, " +
+        "$BOOKMARK_NAME TEXT, " +
+        "$BOOKMARK_URL TEXT, " +
+        "$PARENT_FOLDER_ID INTEGER, " +
+        "$DISPLAY_ORDER INTEGER, " +
+        "$IS_FOLDER BOOLEAN, " +
+        "$FOLDER_ID INTEGER, " +
+        "$FAVORITE_ICON BLOB)"
 
 class BookmarksDatabaseHelper(context: Context) : SQLiteOpenHelper(context, BOOKMARKS_DATABASE, null, SCHEMA_VERSION) {
-    // Define the public companion object constants.  These can be moved to public class constants once the entire project has migrated to Kotlin.
-    companion object {
-        // Define the public database constants.
-        const val BOOKMARKS_DATABASE = "bookmarks.db"
-        const val BOOKMARKS_TABLE = "bookmarks"
-
-        // Define the public schema constants.
-        const val ID = "_id"
-        const val BOOKMARK_NAME = "bookmarkname"
-        const val BOOKMARK_URL = "bookmarkurl"
-        const val PARENT_FOLDER = "parentfolder"
-        const val DISPLAY_ORDER = "displayorder"
-        const val IS_FOLDER = "isfolder"
-        const val FAVORITE_ICON = "favoriteicon"
-
-        // Define the public table creation constant.
-        const val CREATE_BOOKMARKS_TABLE = "CREATE TABLE $BOOKMARKS_TABLE (" +
-                "$ID INTEGER PRIMARY KEY, " +
-                "$BOOKMARK_NAME TEXT, " +
-                "$BOOKMARK_URL TEXT, " +
-                "$PARENT_FOLDER TEXT, " +
-                "$DISPLAY_ORDER INTEGER, " +
-                "$IS_FOLDER BOOLEAN, " +
-                "$FAVORITE_ICON BLOB)"
-    }
-
     override fun onCreate(bookmarksDatabase: SQLiteDatabase) {
         // Create the bookmarks table.
         bookmarksDatabase.execSQL(CREATE_BOOKMARKS_TABLE)
     }
 
     override fun onUpgrade(bookmarksDatabase: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
-        // Code for upgrading the database will be added here when the schema version > 1.
-    }
+        // Upgrade from schema version 1, first used in Privacy Browser 1.8, to schema version 2, first used in Privacy Browser 3.15.
+        if (oldVersion < 2) {
+            // Add the folder ID column.
+            bookmarksDatabase.execSQL("ALTER TABLE $BOOKMARKS_TABLE ADD COLUMN $FOLDER_ID INTEGER")
 
-    // Get a cursor of all the folders.
-    val allFolders: Cursor
-        get() {
-            // Get a readable database handle.
-            val bookmarksDatabase = this.readableDatabase
+            // Get a cursor with all the folders.
+            val foldersCursor = bookmarksDatabase.rawQuery("SELECT $ID FROM $BOOKMARKS_TABLE WHERE $IS_FOLDER = 1", null)
+
+            // Get the folders cursor ID column index.
+            val foldersCursorIdColumnIndex = foldersCursor.getColumnIndexOrThrow(ID)
+
+            // Add a folder ID to each folder.
+            while(foldersCursor.moveToNext()) {
+                // Get the current folder database ID.
+                val databaseId = foldersCursor.getInt(foldersCursorIdColumnIndex)
+
+                // Generate a folder ID.
+                val folderId = Date().time
+
+                // Create a folder content values.
+                val folderContentValues = ContentValues()
+
+                // Store the new folder ID in the content values.
+                folderContentValues.put(FOLDER_ID, folderId)
+
+                // Update the folder with the new folder ID.
+                bookmarksDatabase.update(BOOKMARKS_TABLE, folderContentValues, "$ID = $databaseId", null)
+
+                // Wait 2 milliseconds to ensure that the next folder ID is unique.
+                Thread.sleep(2)
+            }
+
+            // Close the folders cursor.
+            foldersCursor.close()
+
+
+            // Add the parent folder ID column.
+            bookmarksDatabase.execSQL("ALTER TABLE $BOOKMARKS_TABLE ADD COLUMN $PARENT_FOLDER_ID INTEGER")
+
+            // Get a cursor with all the bookmarks.
+            val bookmarksCursor = bookmarksDatabase.rawQuery("SELECT $ID, parentfolder FROM $BOOKMARKS_TABLE", null)
 
-            // Return the cursor with the all the folders.  The cursor cannot be closed because it is used in the parent activity.
-            return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $IS_FOLDER = 1 ORDER BY $BOOKMARK_NAME ASC", null)
+            // Get the bookmarks cursor ID column index.
+            val bookmarksCursorIdColumnIndex = bookmarksCursor.getColumnIndexOrThrow(ID)
+            val bookmarksCursorParentFolderColumnIndex = bookmarksCursor.getColumnIndexOrThrow("parentfolder")
+
+            // Populate the parent folder ID for each bookmark.
+            while(bookmarksCursor.moveToNext()) {
+                // Get the information from the cursor.
+                val databaseId = bookmarksCursor.getInt(bookmarksCursorIdColumnIndex)
+                val oldParentFolderString = bookmarksCursor.getString(bookmarksCursorParentFolderColumnIndex)
+
+                // Initialize the new parent folder ID.
+                var newParentFolderId = HOME_FOLDER_ID
+
+                // Get the parent folder ID if the bookmark is not in the home folder.
+                if (oldParentFolderString.isNotEmpty()) {
+                    // SQL escape the old parent folder string.
+                    val sqlEscapedFolderName = DatabaseUtils.sqlEscapeString(oldParentFolderString)
+
+                    // Get the parent folder cursor.
+                    val parentFolderCursor = bookmarksDatabase.rawQuery("SELECT $FOLDER_ID FROM $BOOKMARKS_TABLE WHERE $BOOKMARK_NAME = $sqlEscapedFolderName AND $IS_FOLDER = 1", null)
+
+                    // Get the new parent folder ID if it exists.
+                    if (parentFolderCursor.count > 0) {
+                        // Move to the first entry.
+                        parentFolderCursor.moveToFirst()
+
+                        // Get the new parent folder ID.
+                        newParentFolderId = parentFolderCursor.getLong(parentFolderCursor.getColumnIndexOrThrow(FOLDER_ID))
+                    }
+
+                    // Close the parent folder cursor.
+                    parentFolderCursor.close()
+                }
+
+                // Create a bookmark content values.
+                val bookmarkContentValues = ContentValues()
+
+                // Store the new parent folder ID in the content values.
+                bookmarkContentValues.put(PARENT_FOLDER_ID, newParentFolderId)
+
+                // Update the folder with the new folder ID.
+                bookmarksDatabase.update(BOOKMARKS_TABLE, bookmarkContentValues, "$ID = $databaseId", null)
+            }
+
+            // Close the bookmarks cursor.
+            bookmarksCursor.close()
+
+            // This upgrade removed the old `parentfolder` string column.
+            // SQLite amazingly only added a command to drop a column in version 3.35.0.  <https://www.sqlite.org/changes.html>
+            // It will be a while before that is supported in Android.  <https://developer.android.com/reference/android/database/sqlite/package-summary>
+            // Although a new table could be created and all the data copied to it, I think I will just leave the old parent folder column.  It will be wiped out the next time an import is run.
         }
+    }
 
     // Get a cursor for all bookmarks and folders.
     val allBookmarks: Cursor
@@ -96,14 +183,14 @@ class BookmarksDatabaseHelper(context: Context) : SQLiteOpenHelper(context, BOOK
         }
 
     // Create a bookmark.
-    fun createBookmark(bookmarkName: String, bookmarkURL: String, parentFolder: String, displayOrder: Int, favoriteIcon: ByteArray) {
+    fun createBookmark(bookmarkName: String, bookmarkUrl: String, parentFolderId: Long, displayOrder: Int, favoriteIcon: ByteArray) {
         // Store the bookmark data in a content values.
         val bookmarkContentValues = ContentValues()
 
         // The ID is created automatically.
         bookmarkContentValues.put(BOOKMARK_NAME, bookmarkName)
-        bookmarkContentValues.put(BOOKMARK_URL, bookmarkURL)
-        bookmarkContentValues.put(PARENT_FOLDER, parentFolder)
+        bookmarkContentValues.put(BOOKMARK_URL, bookmarkUrl)
+        bookmarkContentValues.put(PARENT_FOLDER_ID, parentFolderId)
         bookmarkContentValues.put(DISPLAY_ORDER, displayOrder)
         bookmarkContentValues.put(IS_FOLDER, false)
         bookmarkContentValues.put(FAVORITE_ICON, favoriteIcon)
@@ -131,15 +218,16 @@ class BookmarksDatabaseHelper(context: Context) : SQLiteOpenHelper(context, BOOK
     }
 
     // Create a folder.
-    fun createFolder(folderName: String, parentFolder: String, favoriteIcon: ByteArray) {
-        // Store the bookmark folder data in a content values.
+    fun createFolder(folderName: String, parentFolderId: Long, favoriteIcon: ByteArray) {
+        // Create a bookmark folder content values.
         val bookmarkFolderContentValues = ContentValues()
 
         // The ID is created automatically.  Folders are always created at the top of the list.
         bookmarkFolderContentValues.put(BOOKMARK_NAME, folderName)
-        bookmarkFolderContentValues.put(PARENT_FOLDER, parentFolder)
+        bookmarkFolderContentValues.put(PARENT_FOLDER_ID, parentFolderId)
         bookmarkFolderContentValues.put(DISPLAY_ORDER, 0)
         bookmarkFolderContentValues.put(IS_FOLDER, true)
+        bookmarkFolderContentValues.put(FOLDER_ID, generateFolderId())
         bookmarkFolderContentValues.put(FAVORITE_ICON, favoriteIcon)
 
         // Get a writable database handle.
@@ -222,43 +310,34 @@ class BookmarksDatabaseHelper(context: Context) : SQLiteOpenHelper(context, BOOK
     }
 
     // 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(folderName: String): Cursor {
+    fun getBookmarkIds(parentFolderId: Long): Cursor {
         // Get a readable database handle.
         val bookmarksDatabase = this.readableDatabase
 
-        // SQL escape the folder name.
-        val sqlEscapedFolderName = DatabaseUtils.sqlEscapeString(folderName)
-
         // 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 WHERE $PARENT_FOLDER = $sqlEscapedFolderName", null)
+        return bookmarksDatabase.rawQuery("SELECT $ID FROM $BOOKMARKS_TABLE WHERE $PARENT_FOLDER_ID = $parentFolderId", null)
     }
 
     // Get a cursor for bookmarks and folders in the specified folder.
-    fun getBookmarks(folderName: String): Cursor {
+    fun getBookmarks(parentFolderId: Long): Cursor {
         // Get a readable database handle.
         val bookmarksDatabase = this.readableDatabase
 
-        // SQL escape the folder name.
-        val sqlEscapedFolderName = DatabaseUtils.sqlEscapeString(folderName)
-
         // Return a cursor with all the bookmarks in a specified folder.  The cursor cannot be closed because it is used in the parent activity.
-        return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $PARENT_FOLDER = $sqlEscapedFolderName", null)
+        return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $PARENT_FOLDER_ID = $parentFolderId", null)
     }
 
     // Get a cursor for bookmarks and folders in the specified folder ordered by display order.
-    fun getBookmarksByDisplayOrder(folderName: String): Cursor {
+    fun getBookmarksByDisplayOrder(parentFolderId: Long): Cursor {
         // Get a readable database handle.
         val bookmarksDatabase = this.readableDatabase
 
-        // SQL escape the folder name.
-        val sqlEscapedFolderName = DatabaseUtils.sqlEscapeString(folderName)
-
         // Return a cursor with all the bookmarks in the specified folder ordered by display order.  The cursor cannot be closed because it is used in the parent activity.
-        return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $PARENT_FOLDER = $sqlEscapedFolderName ORDER BY $DISPLAY_ORDER ASC", null)
+        return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $PARENT_FOLDER_ID = $parentFolderId ORDER BY $DISPLAY_ORDER ASC", null)
     }
 
     // Get a cursor for bookmarks and folders in the specified folder by display order except those with the specified IDs.
-    fun getBookmarksByDisplayOrderExcept(exceptIdLongArray: LongArray, folderName: String): Cursor {
+    fun getBookmarksByDisplayOrderExcept(exceptIdLongArray: LongArray, parentFolderId: Long): Cursor {
         // Get a readable database handle.
         val bookmarksDatabase = this.readableDatabase
 
@@ -277,17 +356,13 @@ class BookmarksDatabaseHelper(context: Context) : SQLiteOpenHelper(context, BOOK
             idsNotToGetStringBuilder.append(databaseIdLong)
         }
 
-        // SQL escape the folder name.
-        val sqlEscapedFolderName = DatabaseUtils.sqlEscapeString(folderName)
-
         // Return a cursor with all the bookmarks in the specified folder except for those database IDs specified ordered by display order.
         // The cursor cannot be closed because it will be used in the parent activity.
-        return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $PARENT_FOLDER = $sqlEscapedFolderName AND $ID NOT IN ($idsNotToGetStringBuilder) ORDER BY $DISPLAY_ORDER ASC",
-            null)
+        return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $PARENT_FOLDER_ID = $parentFolderId AND $ID NOT IN ($idsNotToGetStringBuilder) ORDER BY $DISPLAY_ORDER ASC", null)
     }
 
     // Get a cursor for bookmarks and folders in the specified folder except those with the specified IDs.
-    fun getBookmarksExcept(exceptIdLongArray: LongArray, folderName: String): Cursor {
+    fun getBookmarksExcept(exceptIdLongArray: LongArray, parentFolderId: Long): Cursor {
         // Get a readable database handle.
         val bookmarksDatabase = this.readableDatabase
 
@@ -306,55 +381,37 @@ class BookmarksDatabaseHelper(context: Context) : SQLiteOpenHelper(context, BOOK
             idsNotToGetStringBuilder.append(databaseIdLong)
         }
 
-        // SQL escape the folder name.
-        val sqlEscapedFolderName = DatabaseUtils.sqlEscapeString(folderName)
-
         // Return a cursor with all the bookmarks in the specified folder except for those database IDs specified.  The cursor cannot be closed because it is used in the parent activity.
-        return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $PARENT_FOLDER = $sqlEscapedFolderName AND $ID NOT IN ($idsNotToGetStringBuilder)", null)
+        return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $PARENT_FOLDER_ID = $parentFolderId AND $ID NOT IN ($idsNotToGetStringBuilder)", null)
     }
 
-    // Get a cursor for the specified folder name.
-    fun getFolder(folderName: String): Cursor {
-        // Get a readable database handle.
-        val bookmarksDatabase = this.readableDatabase
-
-        // SQL escape the folder name.
-        val sqlEscapedFolderName = DatabaseUtils.sqlEscapeString(folderName)
-
-        // Return the cursor for the specified folder.  The cursor can't be closed because it is used in the parent activity.
-        return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $BOOKMARK_NAME = $sqlEscapedFolderName AND $IS_FOLDER = 1", null)
-    }
-
-    fun getFolderBookmarks(folderDatabaseId: Int): Cursor {
-        // Get the folder name.
-        val folderName = getFolderName(folderDatabaseId)
-
-        // SQL escape the folder name.
-        val sqlEscapedFolderName = DatabaseUtils.sqlEscapeString(folderName)
-
+    fun getFolderBookmarks(parentFolderId: Long): Cursor {
         // Get a readable database handle.
         val bookmarksDatabase = this.readableDatabase
 
         // Return a cursor with all the bookmarks in the folder.  The cursor cannot be closed because it is used in the parent activity.
-        return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $PARENT_FOLDER = $sqlEscapedFolderName AND $IS_FOLDER = 0 ORDER BY $DISPLAY_ORDER ASC", null)
+        return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $PARENT_FOLDER_ID = $parentFolderId AND $IS_FOLDER = 0 ORDER BY $DISPLAY_ORDER ASC", null)
     }
 
     // Get the database ID for the specified folder name.
-    fun getFolderDatabaseId(folderName: String): Int {
+    fun getFolderDatabaseId(folderId: Long): Int {
         // Get a readable database handle.
         val bookmarksDatabase = this.readableDatabase
 
-        // SQL escape the folder name.
-        val sqlEscapedFolderName = DatabaseUtils.sqlEscapeString(folderName)
+        // Initialize the database ID.
+        var databaseId = 0
 
         // Get the cursor for the folder with the specified name.
-        val folderCursor = bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $BOOKMARK_NAME = $sqlEscapedFolderName AND $IS_FOLDER = 1", null)
+        val folderCursor = bookmarksDatabase.rawQuery("SELECT $ID FROM $BOOKMARKS_TABLE WHERE $FOLDER_ID = $folderId", null)
 
-        // Move to the first record.
-        folderCursor.moveToFirst()
+        // Get the database ID if it exists.
+        if (folderCursor.count > 0) {
+            // Move to the first record.
+            folderCursor.moveToFirst()
 
-        // Get the database ID.
-        val databaseId = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(ID))
+            // Get the database ID.
+            databaseId = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(ID))
+        }
 
         // Close the cursor and the database handle.
         folderCursor.close()
@@ -364,19 +421,47 @@ class BookmarksDatabaseHelper(context: Context) : SQLiteOpenHelper(context, BOOK
         return databaseId
     }
 
-    // Get the folder name for the specified database ID.
-    fun getFolderName(databaseId: Int): String {
+    // Get the folder ID for the specified folder database ID.
+    fun getFolderId(folderDatabaseId: Int): Long {
         // Get a readable database handle.
         val bookmarksDatabase = this.readableDatabase
 
         // Get the cursor for the folder with the specified database ID.
-        val folderCursor = bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $ID = $databaseId", null)
+        val folderCursor = bookmarksDatabase.rawQuery("SELECT $FOLDER_ID FROM $BOOKMARKS_TABLE WHERE $ID = $folderDatabaseId", null)
 
         // Move to the first record.
         folderCursor.moveToFirst()
 
-        // Get the folder name.
-        val folderName = folderCursor.getString(folderCursor.getColumnIndexOrThrow(BOOKMARK_NAME))
+        // Get the folder ID.
+        val folderId = folderCursor.getLong(folderCursor.getColumnIndexOrThrow(FOLDER_ID))
+
+        // Close the cursor and the database handle.
+        folderCursor.close()
+        bookmarksDatabase.close()
+
+        // Return the folder ID.
+        return folderId
+    }
+
+    // Get the folder name for the specified folder ID.
+    fun getFolderName(folderId: Long): String {
+        // Get a readable database handle.
+        val bookmarksDatabase = this.readableDatabase
+
+        // Initialize the folder name.
+        var folderName = ""
+
+        // Get the cursor for the folder with the specified folder ID.
+        val folderCursor = bookmarksDatabase.rawQuery("SELECT $BOOKMARK_NAME FROM $BOOKMARKS_TABLE WHERE $FOLDER_ID = $folderId", null)
+
+        // Get the folder name if it exists.
+        if (folderCursor.count > 0) {
+            // Move to the first record.
+            folderCursor.moveToFirst()
+
+            // Get the folder name.
+            folderName = folderCursor.getString(folderCursor.getColumnIndexOrThrow(BOOKMARK_NAME))
+        }
 
         // Close the cursor and the database handle.
         folderCursor.close()
@@ -387,71 +472,179 @@ class BookmarksDatabaseHelper(context: Context) : SQLiteOpenHelper(context, BOOK
     }
 
     // Get a cursor of all the folders except those specified.
-    fun getFoldersExcept(exceptFolders: String): Cursor {
-        // Get a readable database handle.
-        val bookmarksDatabase = this.readableDatabase
+    fun getFoldersExcept(exceptFolderIdLongList: List<Long>): Cursor {
+        // Prepare a string builder to contain the comma-separated list of IDs not to get.
+        val folderIdsNotToGetStringBuilder = StringBuilder()
 
-        // Return the cursor of all folders except those specified.  Each individual folder in the list has already been SQL escaped.  The cursor can't be closed because it is used in the parent activity.
-        return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $IS_FOLDER = 1 AND $BOOKMARK_NAME NOT IN ($exceptFolders) ORDER BY $BOOKMARK_NAME ASC", null)
+        // Extract the array of IDs not to get to the string builder.
+        for (folderId in exceptFolderIdLongList) {
+            // Check to see if there is already a number in the builder.
+            if (folderIdsNotToGetStringBuilder.isNotEmpty()) {
+                // This is not the first number, so place a `,` before the new number.
+                folderIdsNotToGetStringBuilder.append(",")
+            }
+
+            // Add the new number to the builder.
+            folderIdsNotToGetStringBuilder.append(folderId)
+        }
+
+        // Get an array list with all of the requested subfolders.
+        val subfoldersCursorArrayList = getSubfoldersExcept(HOME_FOLDER_ID, folderIdsNotToGetStringBuilder.toString())
+
+        // Return a cursor.
+        return if (subfoldersCursorArrayList.isEmpty()) {  // There are no folders.  Return an empty cursor.
+            // A matrix cursor requires the definition of at least one column.
+            MatrixCursor(arrayOf(ID))
+        } else {  // There is at least one folder.
+            // Use a merge cursor to return the folders.
+            MergeCursor(subfoldersCursorArrayList.toTypedArray())
+        }
     }
 
-    // Get the name of the parent folder.
-    fun getParentFolderName(currentFolder: String): String {
+    // Determine if any folders exist beside the specified database IDs.  The array of database IDs can include both bookmarks and folders.
+    fun hasFoldersExceptDatabaseId(exceptDatabaseIdLongArray: LongArray): Boolean {
+        // Create a folder ID long list.
+        val folderIdLongList = mutableListOf<Long>()
+
+        // Populate the list.
+        for (databaseId in exceptDatabaseIdLongArray) {
+            // Convert the database ID to an Int.
+            val databaseIdInt = databaseId.toInt()
+
+            // Only process database IDs that are folders.
+            if (isFolder(databaseIdInt)) {
+                // Add the folder ID to the list.
+                folderIdLongList.add(getFolderId(databaseIdInt))
+            }
+        }
+
+        // Get a lit of all the folders except those specified and their subfolders.
+        val foldersCursor = getFoldersExcept(folderIdLongList)
+
+        // Determine if any other folders exists.
+        val hasFolder = (foldersCursor.count > 0)
+
+        // Close the cursor.
+        foldersCursor.close()
+
+        // Return the folder status.
+        return hasFolder
+    }
+
+    // Get the name of the parent folder
+    fun getParentFolderId(currentFolderId: Long): Long {
         // Get a readable database handle.
         val bookmarksDatabase = this.readableDatabase
 
-        // SQL escape the current folder.
-        val sqlEscapedCurrentFolder = DatabaseUtils.sqlEscapeString(currentFolder)
-
         // Get a cursor for the current folder.
-        val bookmarkCursor = bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $IS_FOLDER = 1 AND $BOOKMARK_NAME = $sqlEscapedCurrentFolder", null)
+        val bookmarkCursor = bookmarksDatabase.rawQuery("SELECT $PARENT_FOLDER_ID FROM $BOOKMARKS_TABLE WHERE $FOLDER_ID = $currentFolderId", null)
 
         // Move to the first record.
         bookmarkCursor.moveToFirst()
 
-        // Store the name of the parent folder.
-        val parentFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(PARENT_FOLDER))
+        // Store the parent folder ID.
+        val parentFolderId = bookmarkCursor.getLong(bookmarkCursor.getColumnIndexOrThrow(PARENT_FOLDER_ID))
 
         // Close the cursor and the database.
         bookmarkCursor.close()
         bookmarksDatabase.close()
 
-        // Return the parent folder string.
-        return parentFolder
+        // Return the parent folder string ID.
+        return parentFolderId
     }
 
     // Get the name of the parent folder.
-    fun getParentFolderName(databaseId: Int): String {
+    fun getParentFolderId(databaseId: Int): Long {
         // Get a readable database handle.
         val bookmarksDatabase = this.readableDatabase
 
         // Get a cursor for the specified database ID.
-        val bookmarkCursor = bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $ID = $databaseId", null)
+        val bookmarkCursor = bookmarksDatabase.rawQuery("SELECT $PARENT_FOLDER_ID FROM $BOOKMARKS_TABLE WHERE $ID = $databaseId", null)
 
         // Move to the first record.
         bookmarkCursor.moveToFirst()
 
         // Store the name of the parent folder.
-        val parentFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(PARENT_FOLDER))
+        val parentFolderId = bookmarkCursor.getLong(bookmarkCursor.getColumnIndexOrThrow(PARENT_FOLDER_ID))
 
         // Close the cursor and the database.
         bookmarkCursor.close()
         bookmarksDatabase.close()
 
         // Return the parent folder string.
-        return parentFolder
+        return parentFolderId
     }
 
-    // Get a cursor with all the subfolders of the specified folder.
-    fun getSubfolders(currentFolder: String): Cursor {
+    // Get a cursor with the names and folder IDs of all the subfolders of the specified folder.
+    fun getSubfolderNamesAndFolderIds(currentFolderId: Long): Cursor {
         // Get a readable database handle.
         val bookmarksDatabase = this.readableDatabase
 
-        // SQL escape the current folder.
-        val sqlEscapedCurrentFolder = DatabaseUtils.sqlEscapeString(currentFolder)
-
         // Return the cursor with the subfolders.  The cursor can't be closed because it is used in the parent activity.
-        return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $PARENT_FOLDER = $sqlEscapedCurrentFolder AND $IS_FOLDER = 1", null)
+        return bookmarksDatabase.rawQuery("SELECT $BOOKMARK_NAME, $FOLDER_ID FROM $BOOKMARKS_TABLE WHERE $PARENT_FOLDER_ID = $currentFolderId AND $IS_FOLDER = 1", null)
+    }
+
+    fun getSubfolderSpacer(folderId: Long): String {
+        // Create a spacer string
+        var spacerString = ""
+
+        // Get the parent folder ID.
+        val parentFolderId = getParentFolderId(folderId)
+
+        // Check to see if the parent folder is not in the home folder.
+        if (parentFolderId != HOME_FOLDER_ID) {
+            // Add two spaces to the spacer string.
+            spacerString += "  "
+
+            // Check the parent folder recursively.
+            spacerString += getSubfolderSpacer(parentFolderId)
+        }
+
+        // Return the spacer string.
+        return spacerString
+    }
+
+    private fun getSubfoldersExcept(folderId: Long, exceptFolderIdString: String): ArrayList<Cursor> {
+        // Get a readable database handle.
+        val bookmarksDatabase = this.readableDatabase
+
+        // Create a cursor array list.
+        val cursorArrayList = ArrayList<Cursor>()
+
+        // Create a matrix cursor column names.
+        val matrixCursorColumnNames = arrayOf(ID, BOOKMARK_NAME, FAVORITE_ICON, PARENT_FOLDER_ID, FOLDER_ID)
+
+        // Get a cursor with the subfolders.
+        val subfolderCursor = bookmarksDatabase.rawQuery(
+            "SELECT * FROM $BOOKMARKS_TABLE WHERE $IS_FOLDER = 1 AND $PARENT_FOLDER_ID = $folderId AND $FOLDER_ID NOT IN ($exceptFolderIdString) ORDER BY $DISPLAY_ORDER ASC", null)
+
+        // Get the subfolder cursor column indexes.
+        val idColumnIndex = subfolderCursor.getColumnIndexOrThrow(ID)
+        val nameColumnIndex = subfolderCursor.getColumnIndexOrThrow(BOOKMARK_NAME)
+        val favoriteIconColumnIndex = subfolderCursor.getColumnIndexOrThrow(FAVORITE_ICON)
+        val parentFolderIdColumnIndex = subfolderCursor.getColumnIndexOrThrow(PARENT_FOLDER_ID)
+        val folderIdColumnIndex = subfolderCursor.getColumnIndexOrThrow(FOLDER_ID)
+
+        while (subfolderCursor.moveToNext()) {
+            // Create an array list.
+            val matrixCursor = MatrixCursor(matrixCursorColumnNames)
+
+            // Add the subfolder to the matrix cursor.
+            matrixCursor.addRow(arrayOf<Any>(subfolderCursor.getInt(idColumnIndex), subfolderCursor.getString(nameColumnIndex), subfolderCursor.getBlob(favoriteIconColumnIndex),
+                subfolderCursor.getLong(parentFolderIdColumnIndex), subfolderCursor.getLong(folderIdColumnIndex)))
+
+            // Add the matrix cursor to the array list.
+            cursorArrayList.add(matrixCursor)
+
+            // Get all the sub-subfolders recursively
+            cursorArrayList.addAll(getSubfoldersExcept(subfolderCursor.getLong(folderIdColumnIndex), exceptFolderIdString))
+        }
+
+        // Close the subfolder cursor.
+        subfolderCursor.close()
+
+        // Return the matrix cursor.
+        return cursorArrayList
     }
 
     // Check if a database ID is a folder.
@@ -466,7 +659,7 @@ class BookmarksDatabaseHelper(context: Context) : SQLiteOpenHelper(context, BOOK
         folderCursor.moveToFirst()
 
         // Ascertain if this database ID is a folder.
-        val isFolder = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(IS_FOLDER)) == 1
+        val isFolder = (folderCursor.getInt(folderCursor.getColumnIndexOrThrow(IS_FOLDER)) == 1)
 
         // Close the cursor and the database handle.
         folderCursor.close()
@@ -477,15 +670,12 @@ class BookmarksDatabaseHelper(context: Context) : SQLiteOpenHelper(context, BOOK
     }
 
     // Move one bookmark or folder to a new folder.
-    fun moveToFolder(databaseId: Int, newFolder: String) {
+    fun moveToFolder(databaseId: Int, newFolderId: Long) {
         // Get a writable database handle.
         val bookmarksDatabase = this.writableDatabase
 
-        // SQL escape the new folder name.
-        val sqlEscapedNewFolder = DatabaseUtils.sqlEscapeString(newFolder)
-
         // Get a cursor for all the bookmarks in the new folder ordered by display order.
-        val newFolderCursor = bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $PARENT_FOLDER = $sqlEscapedNewFolder ORDER BY $DISPLAY_ORDER ASC", null)
+        val newFolderCursor = bookmarksDatabase.rawQuery("SELECT $DISPLAY_ORDER FROM $BOOKMARKS_TABLE WHERE $PARENT_FOLDER_ID = $newFolderId ORDER BY $DISPLAY_ORDER ASC", null)
 
         // Set the new display order.
         val displayOrder: Int = if (newFolderCursor.count > 0) {  // There are already bookmarks in the folder.
@@ -507,7 +697,7 @@ class BookmarksDatabaseHelper(context: Context) : SQLiteOpenHelper(context, BOOK
 
         // Store the new values.
         bookmarkContentValues.put(DISPLAY_ORDER, displayOrder)
-        bookmarkContentValues.put(PARENT_FOLDER, newFolder)
+        bookmarkContentValues.put(PARENT_FOLDER_ID, newFolderId)
 
         // Update the database.
         bookmarksDatabase.update(BOOKMARKS_TABLE, bookmarkContentValues, "$ID = $databaseId", null)
@@ -536,14 +726,14 @@ class BookmarksDatabaseHelper(context: Context) : SQLiteOpenHelper(context, BOOK
     }
 
     // Update the bookmark name, URL, parent folder, and display order.
-    fun updateBookmark(databaseId: Int, bookmarkName: String, bookmarkUrl: String, parentFolder: String, displayOrder: Int) {
+    fun updateBookmark(databaseId: Int, bookmarkName: String, bookmarkUrl: String, parentFolderId: Long, displayOrder: Int) {
         // Initialize a content values.
         val bookmarkContentValues = ContentValues()
 
         // Store the updated values.
         bookmarkContentValues.put(BOOKMARK_NAME, bookmarkName)
         bookmarkContentValues.put(BOOKMARK_URL, bookmarkUrl)
-        bookmarkContentValues.put(PARENT_FOLDER, parentFolder)
+        bookmarkContentValues.put(PARENT_FOLDER_ID, parentFolderId)
         bookmarkContentValues.put(DISPLAY_ORDER, displayOrder)
 
         // Get a writable database handle.
@@ -577,14 +767,14 @@ class BookmarksDatabaseHelper(context: Context) : SQLiteOpenHelper(context, BOOK
     }
 
     // Update the bookmark name, URL, parent folder, display order, and favorite icon.
-    fun updateBookmark(databaseId: Int, bookmarkName: String, bookmarkUrl: String, parentFolder: String, displayOrder: Int, favoriteIcon: ByteArray) {
+    fun updateBookmark(databaseId: Int, bookmarkName: String, bookmarkUrl: String, parentFolderId: Long, displayOrder: Int, favoriteIcon: ByteArray) {
         // Initialize a content values.
         val bookmarkContentValues = ContentValues()
 
         // Store the updated values.
         bookmarkContentValues.put(BOOKMARK_NAME, bookmarkName)
         bookmarkContentValues.put(BOOKMARK_URL, bookmarkUrl)
-        bookmarkContentValues.put(PARENT_FOLDER, parentFolder)
+        bookmarkContentValues.put(PARENT_FOLDER_ID, parentFolderId)
         bookmarkContentValues.put(DISPLAY_ORDER, displayOrder)
         bookmarkContentValues.put(FAVORITE_ICON, favoriteIcon)
 
@@ -617,7 +807,7 @@ class BookmarksDatabaseHelper(context: Context) : SQLiteOpenHelper(context, BOOK
     }
 
     // Update the folder name.
-    fun updateFolder(databaseId: Int, oldFolderName: String, newFolderName: String) {
+    fun updateFolder(databaseId: Int, newFolderName: String) {
         // Get a writable database handle.
         val bookmarksDatabase = this.writableDatabase
 
@@ -630,42 +820,12 @@ class BookmarksDatabaseHelper(context: Context) : SQLiteOpenHelper(context, BOOK
         // Run the update on the folder.
         bookmarksDatabase.update(BOOKMARKS_TABLE, folderContentValues, "$ID = $databaseId", null)
 
-        // Create a bookmark content values.
-        val bookmarkContentValues = ContentValues()
-
-        // Store the new parent folder name.
-        bookmarkContentValues.put(PARENT_FOLDER, newFolderName)
-
-        // SQL escape the old folder name.
-        val sqlEscapedOldFolderName = DatabaseUtils.sqlEscapeString(oldFolderName)
-
-        // Run the update on all the bookmarks that currently list the old folder name as their parent folder.
-        bookmarksDatabase.update(BOOKMARKS_TABLE, bookmarkContentValues, "$PARENT_FOLDER = $sqlEscapedOldFolderName", null)
-
-        // Close the database handle.
-        bookmarksDatabase.close()
-    }
-
-    // Update the folder icon.
-    fun updateFolder(databaseId: Int, folderIcon: ByteArray) {
-        // Get a writable database handle.
-        val bookmarksDatabase = this.writableDatabase
-
-        // Create a content values.
-        val folderContentValues = ContentValues()
-
-        // Store the updated icon.
-        folderContentValues.put(FAVORITE_ICON, folderIcon)
-
-        // Run the update on the folder.
-        bookmarksDatabase.update(BOOKMARKS_TABLE, folderContentValues, "$ID = $databaseId", null)
-
         // Close the database handle.
         bookmarksDatabase.close()
     }
 
     // Update the folder name, parent folder, and display order.
-    fun updateFolder(databaseId: Int, oldFolderName: String, newFolderName: String, parentFolder: String, displayOrder: Int) {
+    fun updateFolder(databaseId: Int, newFolderName: String, parentFolderId: Long, displayOrder: Int) {
         // Get a writable database handle.
         val bookmarksDatabase = this.writableDatabase
 
@@ -674,30 +834,18 @@ class BookmarksDatabaseHelper(context: Context) : SQLiteOpenHelper(context, BOOK
 
         // Store the new folder values.
         folderContentValues.put(BOOKMARK_NAME, newFolderName)
-        folderContentValues.put(PARENT_FOLDER, parentFolder)
+        folderContentValues.put(PARENT_FOLDER_ID, parentFolderId)
         folderContentValues.put(DISPLAY_ORDER, displayOrder)
 
         // Run the update on the folder.
         bookmarksDatabase.update(BOOKMARKS_TABLE, folderContentValues, "$ID = $databaseId", null)
 
-        // Create a bookmark content values.
-        val bookmarkContentValues = ContentValues()
-
-        // Store the new parent folder name.
-        bookmarkContentValues.put(PARENT_FOLDER, newFolderName)
-
-        // SQL escape the old folder name.
-        val sqlEscapedOldFolderName = DatabaseUtils.sqlEscapeString(oldFolderName)
-
-        // Run the update on all the bookmarks that currently list the old folder name as their parent folder.
-        bookmarksDatabase.update(BOOKMARKS_TABLE, bookmarkContentValues, "$PARENT_FOLDER = $sqlEscapedOldFolderName", null)
-
         // Close the database handle.
         bookmarksDatabase.close()
     }
 
     // Update the folder name and icon.
-    fun updateFolder(databaseId: Int, oldFolderName: String, newFolderName: String, folderIcon: ByteArray) {
+    fun updateFolder(databaseId: Int, newFolderName: String, folderIcon: ByteArray) {
         // Get a writable database handle.
         val bookmarksDatabase = this.writableDatabase
 
@@ -711,24 +859,12 @@ class BookmarksDatabaseHelper(context: Context) : SQLiteOpenHelper(context, BOOK
         // Run the update on the folder.
         bookmarksDatabase.update(BOOKMARKS_TABLE, folderContentValues, "$ID = $databaseId", null)
 
-        // Create a bookmark content values.
-        val bookmarkContentValues = ContentValues()
-
-        // Store the new parent folder name.
-        bookmarkContentValues.put(PARENT_FOLDER, newFolderName)
-
-        // SQL escape the old folder name.
-        val sqlEscapedOldFolderName = DatabaseUtils.sqlEscapeString(oldFolderName)
-
-        // Run the update on all the bookmarks that currently list the old folder name as their parent folder.
-        bookmarksDatabase.update(BOOKMARKS_TABLE, bookmarkContentValues, "$PARENT_FOLDER = $sqlEscapedOldFolderName", null)
-
         // Close the database handle.
         bookmarksDatabase.close()
     }
 
     // Update the folder name and icon.
-    fun updateFolder(databaseId: Int, oldFolderName: String, newFolderName: String, parentFolder: String, displayOrder: Int, folderIcon: ByteArray) {
+    fun updateFolder(databaseId: Int, newFolderName: String, parentFolderId: Long, displayOrder: Int, folderIcon: ByteArray) {
         // Get a writable database handle.
         val bookmarksDatabase = this.writableDatabase
 
@@ -737,26 +873,37 @@ class BookmarksDatabaseHelper(context: Context) : SQLiteOpenHelper(context, BOOK
 
         // Store the updated values.
         folderContentValues.put(BOOKMARK_NAME, newFolderName)
-        folderContentValues.put(PARENT_FOLDER, parentFolder)
+        folderContentValues.put(PARENT_FOLDER_ID, parentFolderId)
         folderContentValues.put(DISPLAY_ORDER, displayOrder)
         folderContentValues.put(FAVORITE_ICON, folderIcon)
 
         // Run the update on the folder.
         bookmarksDatabase.update(BOOKMARKS_TABLE, folderContentValues, "$ID = $databaseId", null)
 
-        // Create a bookmark content values.
-        val bookmarkContentValues = ContentValues()
+        // Close the database handle.
+        bookmarksDatabase.close()
+    }
 
-        // Store the new parent folder name.
-        bookmarkContentValues.put(PARENT_FOLDER, newFolderName)
+    private fun generateFolderId(): Long {
+        // Get the current time in epoch format.
+        val possibleFolderId = Date().time
 
-        // SQL escape the old folder name.
-        val sqlEscapedOldFolderName = DatabaseUtils.sqlEscapeString(oldFolderName)
+        // Get a readable database.
+        val bookmarksDatabase = this.readableDatabase
 
-        // Run the update on all the bookmarks that currently list the old folder name as their parent folder.
-        bookmarksDatabase.update(BOOKMARKS_TABLE, bookmarkContentValues, "$PARENT_FOLDER = $sqlEscapedOldFolderName", null)
+        // Get a cursor with any folders that already have this folder ID.
+        val existingFolderCursor = bookmarksDatabase.rawQuery("SELECT $ID FROM $BOOKMARKS_TABLE WHERE $FOLDER_ID = $possibleFolderId", null)
 
-        // Close the database handle.
-        bookmarksDatabase.close()
+        // Check if the folder ID is unique.
+        val folderIdIsUnique = (existingFolderCursor.count == 0)
+
+        // Close the cursor.
+        existingFolderCursor.close()
+
+        // Either return the folder ID or test a new one.
+        return if (folderIdIsUnique)
+            possibleFolderId
+        else
+            generateFolderId()
     }
 }
index 55729ff2a6286e8aee24a2c5d2649fcc1ea1f8c7..259a5b744793fec7083d1d9485897e15c5ade726 100644 (file)
@@ -27,6 +27,7 @@ import android.database.sqlite.SQLiteDatabase
 import androidx.preference.PreferenceManager
 
 import com.stoutner.privacybrowser.R
+import com.stoutner.privacybrowser.activities.HOME_FOLDER_ID
 
 import java.io.File
 import java.io.FileInputStream
@@ -34,10 +35,12 @@ import java.io.FileOutputStream
 import java.io.InputStream
 import java.io.OutputStream
 
+import java.util.Date
+
 // Define the public constants.
+const val IMPORT_EXPORT_SCHEMA_VERSION = 17
 const val EXPORT_SUCCESSFUL = "A"
 const val IMPORT_SUCCESSFUL = "B"
-const val IMPORT_EXPORT_SCHEMA_VERSION = 17
 
 // Define the private class constants.
 private const val ALLOW_SCREENSHOTS = "allow_screenshots"
@@ -395,11 +398,104 @@ class ImportExportDatabaseHelper {
             // This upgrade removed the `x_requested_with_header` from the domains and preferences tables.
             // There is no need to delete the columns as they will simply be ignored by the import.
 
+            // Upgrade from schema version 16, first used in Privacy Browser 3.12, to schema version 17, first used in Privacy Browser 3.15.
+            if (importDatabaseVersion < 17) {
+                // Add the folder ID column.
+                importDatabase.execSQL("ALTER TABLE $BOOKMARKS_TABLE ADD COLUMN $FOLDER_ID INTEGER")
+
+                // Get a cursor with all the folders.
+                val foldersCursor = importDatabase.rawQuery("SELECT $ID FROM $BOOKMARKS_TABLE WHERE $IS_FOLDER = 1", null)
+
+                // Get the folders cursor ID column index.
+                val foldersCursorIdColumnIndex = foldersCursor.getColumnIndexOrThrow(ID)
+
+                // Add a folder ID to each folder.
+                while(foldersCursor.moveToNext()) {
+                    // Get the current folder database ID.
+                    val databaseId = foldersCursor.getInt(foldersCursorIdColumnIndex)
+
+                    // Generate a folder ID.
+                    val folderId = generateFolderId(importDatabase)
+
+                    // Create a folder content values.
+                    val folderContentValues = ContentValues()
+
+                    // Store the new folder ID in the content values.
+                    folderContentValues.put(FOLDER_ID, folderId)
+
+                    // Update the folder with the new folder ID.
+                    importDatabase.update(BOOKMARKS_TABLE, folderContentValues, "$ID = $databaseId", null)
+                }
+
+                // Close the folders cursor.
+                foldersCursor.close()
+
+
+                // Add the parent folder ID column.
+                importDatabase.execSQL("ALTER TABLE $BOOKMARKS_TABLE ADD COLUMN $PARENT_FOLDER_ID INTEGER")
+
+                // Get a cursor with all the bookmarks.
+                val bookmarksCursor = importDatabase.rawQuery("SELECT $ID, parentfolder FROM $BOOKMARKS_TABLE", null)
+
+                // Get the bookmarks cursor ID column index.
+                val bookmarksCursorIdColumnIndex = bookmarksCursor.getColumnIndexOrThrow(ID)
+                val bookmarksCursorParentFolderColumnIndex = bookmarksCursor.getColumnIndexOrThrow("parentfolder")
+
+                // Populate the parent folder ID for each bookmark.
+                while(bookmarksCursor.moveToNext()) {
+                    // Get the information from the cursor.
+                    val databaseId = bookmarksCursor.getInt(bookmarksCursorIdColumnIndex)
+                    val oldParentFolderString = bookmarksCursor.getString(bookmarksCursorParentFolderColumnIndex)
+
+                    // Initialize the new parent folder ID.
+                    var newParentFolderId = HOME_FOLDER_ID
+
+                    // Get the parent folder ID if the bookmark is not in the home folder.
+                    if (oldParentFolderString.isNotEmpty()) {
+                        // SQL escape the old parent folder string.
+                        val sqlEscapedFolderName = DatabaseUtils.sqlEscapeString(oldParentFolderString)
+
+                        // Get the parent folder cursor.
+                        val parentFolderCursor = importDatabase.rawQuery("SELECT $FOLDER_ID FROM $BOOKMARKS_TABLE WHERE $BOOKMARK_NAME = $sqlEscapedFolderName AND $IS_FOLDER = 1", null)
+
+                        // Get the new parent folder ID if it exists.
+                        if (parentFolderCursor.count > 0) {
+                            // Move to the first entry.
+                            parentFolderCursor.moveToFirst()
+
+                            // Get the new parent folder ID.
+                            newParentFolderId = parentFolderCursor.getLong(parentFolderCursor.getColumnIndexOrThrow(FOLDER_ID))
+                        }
+
+                        // Close the parent folder cursor.
+                        parentFolderCursor.close()
+                    }
+
+                    // Create a bookmark content values.
+                    val bookmarkContentValues = ContentValues()
+
+                    // Store the new parent folder ID in the content values.
+                    bookmarkContentValues.put(PARENT_FOLDER_ID, newParentFolderId)
+
+                    // Update the folder with the new folder ID.
+                    importDatabase.update(BOOKMARKS_TABLE, bookmarkContentValues, "$ID = $databaseId", null)
+                }
+
+                // Close the bookmarks cursor.
+                bookmarksCursor.close()
+
+                // This upgrade removed the old `parentfolder` string column.
+                // SQLite amazingly only added a command to drop a column in version 3.35.0.  <https://www.sqlite.org/changes.html>
+                // It will be a while before that is supported in Android.  <https://developer.android.com/reference/android/database/sqlite/package-summary>
+                // Although a new table could be created and all the data copied to it, I think I will just leave the old parent folder column.  It will be wiped out the next time an import is run.
+            }
+
+
             // Get a cursor for the bookmarks table.
-            val importBookmarksCursor = importDatabase.rawQuery("SELECT * FROM ${BookmarksDatabaseHelper.BOOKMARKS_TABLE}", null)
+            val importBookmarksCursor = importDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE", null)
 
             // Delete the current bookmarks database.
-            context.deleteDatabase(BookmarksDatabaseHelper.BOOKMARKS_DATABASE)
+            context.deleteDatabase(BOOKMARKS_DATABASE)
 
             // Create a new bookmarks database.
             val bookmarksDatabaseHelper = BookmarksDatabaseHelper(context)
@@ -407,18 +503,28 @@ class ImportExportDatabaseHelper {
             // Move to the first record.
             importBookmarksCursor.moveToFirst()
 
+            // Get the bookmarks colum indexes.
+            val bookmarkNameColumnIndex = importBookmarksCursor.getColumnIndexOrThrow(BOOKMARK_NAME)
+            val bookmarkUrlColumnIndex = importBookmarksCursor.getColumnIndexOrThrow(BOOKMARK_URL)
+            val bookmarkParentFolderIdColumnIndex = importBookmarksCursor.getColumnIndexOrThrow(PARENT_FOLDER_ID)
+            val bookmarkDisplayOrderColumnIndex = importBookmarksCursor.getColumnIndexOrThrow(DISPLAY_ORDER)
+            val bookmarkIsFolderColumnIndex = importBookmarksCursor.getColumnIndexOrThrow(IS_FOLDER)
+            val bookmarkFolderIdColumnIndex = importBookmarksCursor.getColumnIndexOrThrow(FOLDER_ID)
+            val bookmarkFavoriteIconColumnIndex = importBookmarksCursor.getColumnIndexOrThrow(FAVORITE_ICON)
+
             // Copy the data from the import bookmarks cursor into the bookmarks database.
             for (i in 0 until importBookmarksCursor.count) {
                 // Create a bookmark content values.
                 val bookmarkContentValues = ContentValues()
 
                 // Add the information for this bookmark to the content values.
-                bookmarkContentValues.put(BookmarksDatabaseHelper.BOOKMARK_NAME, importBookmarksCursor.getString(importBookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME)))
-                bookmarkContentValues.put(BookmarksDatabaseHelper.BOOKMARK_URL, importBookmarksCursor.getString(importBookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_URL)))
-                bookmarkContentValues.put(BookmarksDatabaseHelper.PARENT_FOLDER, importBookmarksCursor.getString(importBookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.PARENT_FOLDER)))
-                bookmarkContentValues.put(BookmarksDatabaseHelper.DISPLAY_ORDER, importBookmarksCursor.getInt(importBookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.DISPLAY_ORDER)))
-                bookmarkContentValues.put(BookmarksDatabaseHelper.IS_FOLDER, importBookmarksCursor.getInt(importBookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.IS_FOLDER)))
-                bookmarkContentValues.put(BookmarksDatabaseHelper.FAVORITE_ICON, importBookmarksCursor.getBlob(importBookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.FAVORITE_ICON)))
+                bookmarkContentValues.put(BOOKMARK_NAME, importBookmarksCursor.getString(bookmarkNameColumnIndex))
+                bookmarkContentValues.put(BOOKMARK_URL, importBookmarksCursor.getString(bookmarkUrlColumnIndex))
+                bookmarkContentValues.put(PARENT_FOLDER_ID, importBookmarksCursor.getLong(bookmarkParentFolderIdColumnIndex))
+                bookmarkContentValues.put(DISPLAY_ORDER, importBookmarksCursor.getInt(bookmarkDisplayOrderColumnIndex))
+                bookmarkContentValues.put(IS_FOLDER, importBookmarksCursor.getInt(bookmarkIsFolderColumnIndex))
+                bookmarkContentValues.put(FOLDER_ID, importBookmarksCursor.getLong(bookmarkFolderIdColumnIndex))
+                bookmarkContentValues.put(FAVORITE_ICON, importBookmarksCursor.getBlob(bookmarkFavoriteIconColumnIndex))
 
                 // Insert the content values into the bookmarks database.
                 bookmarksDatabaseHelper.createBookmark(bookmarkContentValues)
@@ -520,41 +626,72 @@ class ImportExportDatabaseHelper {
             // Move to the first record.
             importDomainsCursor.moveToFirst()
 
+            // Get the domain column indexes.
+            val domainNameColumnIndex = importDomainsCursor.getColumnIndexOrThrow(DOMAIN_NAME)
+            val domainJavaScriptColumnIndex = importDomainsCursor.getColumnIndexOrThrow(ENABLE_JAVASCRIPT)
+            val domainCookiesColumnIndex = importDomainsCursor.getColumnIndexOrThrow(COOKIES)
+            val domainDomStorageColumnIndex = importDomainsCursor.getColumnIndexOrThrow(ENABLE_DOM_STORAGE)
+            val domainFormDataColumnIndex = importDomainsCursor.getColumnIndexOrThrow(ENABLE_FORM_DATA)  // Form data can be removed once the minimum API >= 26.
+            val domainEasyListColumnIndex = importDomainsCursor.getColumnIndexOrThrow(ENABLE_EASYLIST)
+            val domainEasyPrivacyColumnIndex = importDomainsCursor.getColumnIndexOrThrow(ENABLE_EASYPRIVACY)
+            val domainFanboysAnnoyanceListColumnIndex = importDomainsCursor.getColumnIndexOrThrow(ENABLE_FANBOYS_ANNOYANCE_LIST)
+            val domainFanboysSocialBlockingListColumnIndex = importDomainsCursor.getColumnIndexOrThrow(ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)
+            val domainUltraListColumnIndex = importDomainsCursor.getColumnIndexOrThrow(ULTRALIST)
+            val domainUltraPrivacyColumnIndex = importDomainsCursor.getColumnIndexOrThrow(ENABLE_EASYPRIVACY)
+            val domainBlockAllThirdPartyRequestsColumnIndex = importDomainsCursor.getColumnIndexOrThrow(BLOCK_ALL_THIRD_PARTY_REQUESTS)
+            val domainUserAgentColumnIndex = importDomainsCursor.getColumnIndexOrThrow(USER_AGENT)
+            val domainFontSizeColumnIndex = importDomainsCursor.getColumnIndexOrThrow(FONT_SIZE)
+            val domainSwipeToRefreshColumnIndex = importDomainsCursor.getColumnIndexOrThrow(SWIPE_TO_REFRESH)
+            val domainWebViewThemeColumnIndex = importDomainsCursor.getColumnIndexOrThrow(WEBVIEW_THEME)
+            val domainWideViewportColumnIndex = importDomainsCursor.getColumnIndexOrThrow(WIDE_VIEWPORT)
+            val domainDisplayImagesColumnIndex = importDomainsCursor.getColumnIndexOrThrow(DISPLAY_IMAGES)
+            val domainPinnedSslCertificateColumnIndex = importDomainsCursor.getColumnIndexOrThrow(PINNED_SSL_CERTIFICATE)
+            val domainSslIssuedToCommonNameColumnIndex = importDomainsCursor.getColumnIndexOrThrow(SSL_ISSUED_TO_COMMON_NAME)
+            val domainSslIssuedToOrganizationColumnIndex = importDomainsCursor.getColumnIndexOrThrow(SSL_ISSUED_TO_ORGANIZATION)
+            val domainSslIssuedToOrganizationalUnitColumnIndex = importDomainsCursor.getColumnIndexOrThrow(SSL_ISSUED_TO_ORGANIZATIONAL_UNIT)
+            val domainSslIssuedByCommonNameColumnIndex = importDomainsCursor.getColumnIndexOrThrow(SSL_ISSUED_BY_COMMON_NAME)
+            val domainSslIssuedByOrganizationColumnIndex = importDomainsCursor.getColumnIndexOrThrow(SSL_ISSUED_BY_ORGANIZATION)
+            val domainSslIssuedByOrganizationalUnitColumnIndex = importDomainsCursor.getColumnIndexOrThrow(SSL_ISSUED_BY_ORGANIZATIONAL_UNIT)
+            val domainSslStartDateColumnIndex = importDomainsCursor.getColumnIndexOrThrow(SSL_START_DATE)
+            val domainSslEndDateColumnIndex = importDomainsCursor.getColumnIndexOrThrow(SSL_END_DATE)
+            val domainPinnedIpAddressesColumnIndex = importDomainsCursor.getColumnIndexOrThrow(PINNED_IP_ADDRESSES)
+            val domainIpAddressesColumnIndex = importDomainsCursor.getColumnIndexOrThrow(IP_ADDRESSES)
+
             // Copy the data from the import domains cursor into the domains database.
             for (i in 0 until importDomainsCursor.count) {
                 // Create a domain content values.
                 val domainContentValues = ContentValues()
 
                 // Populate the domain content values.
-                domainContentValues.put(DOMAIN_NAME, importDomainsCursor.getString(importDomainsCursor.getColumnIndexOrThrow(DOMAIN_NAME)))
-                domainContentValues.put(ENABLE_JAVASCRIPT, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(ENABLE_JAVASCRIPT)))
-                domainContentValues.put(COOKIES, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(COOKIES)))
-                domainContentValues.put(ENABLE_DOM_STORAGE, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(ENABLE_DOM_STORAGE)))
-                domainContentValues.put(ENABLE_FORM_DATA, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(ENABLE_FORM_DATA)))
-                domainContentValues.put(ENABLE_EASYLIST, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(ENABLE_EASYLIST)))
-                domainContentValues.put(ENABLE_EASYPRIVACY, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(ENABLE_EASYPRIVACY)))
-                domainContentValues.put(ENABLE_FANBOYS_ANNOYANCE_LIST, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(ENABLE_FANBOYS_ANNOYANCE_LIST)))
-                domainContentValues.put(ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)))
-                domainContentValues.put(ULTRALIST, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(ULTRALIST)))
-                domainContentValues.put(ENABLE_ULTRAPRIVACY, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(ENABLE_ULTRAPRIVACY)))
-                domainContentValues.put(BLOCK_ALL_THIRD_PARTY_REQUESTS, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(BLOCK_ALL_THIRD_PARTY_REQUESTS)))
-                domainContentValues.put(USER_AGENT, importDomainsCursor.getString(importDomainsCursor.getColumnIndexOrThrow(USER_AGENT)))
-                domainContentValues.put(FONT_SIZE, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(FONT_SIZE)))
-                domainContentValues.put(SWIPE_TO_REFRESH, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(SWIPE_TO_REFRESH)))
-                domainContentValues.put(WEBVIEW_THEME, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(WEBVIEW_THEME)))
-                domainContentValues.put(WIDE_VIEWPORT, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(WIDE_VIEWPORT)))
-                domainContentValues.put(DISPLAY_IMAGES, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DISPLAY_IMAGES)))
-                domainContentValues.put(PINNED_SSL_CERTIFICATE, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(PINNED_SSL_CERTIFICATE)))
-                domainContentValues.put(SSL_ISSUED_TO_COMMON_NAME, importDomainsCursor.getString(importDomainsCursor.getColumnIndexOrThrow(SSL_ISSUED_TO_COMMON_NAME)))
-                domainContentValues.put(SSL_ISSUED_TO_ORGANIZATION, importDomainsCursor.getString(importDomainsCursor.getColumnIndexOrThrow(SSL_ISSUED_TO_ORGANIZATION)))
-                domainContentValues.put(SSL_ISSUED_TO_ORGANIZATIONAL_UNIT, importDomainsCursor.getString(importDomainsCursor.getColumnIndexOrThrow(SSL_ISSUED_TO_ORGANIZATIONAL_UNIT)))
-                domainContentValues.put(SSL_ISSUED_BY_COMMON_NAME, importDomainsCursor.getString(importDomainsCursor.getColumnIndexOrThrow(SSL_ISSUED_BY_COMMON_NAME)))
-                domainContentValues.put(SSL_ISSUED_BY_ORGANIZATION, importDomainsCursor.getString(importDomainsCursor.getColumnIndexOrThrow(SSL_ISSUED_BY_ORGANIZATION)))
-                domainContentValues.put(SSL_ISSUED_BY_ORGANIZATIONAL_UNIT, importDomainsCursor.getString(importDomainsCursor.getColumnIndexOrThrow(SSL_ISSUED_BY_ORGANIZATIONAL_UNIT)))
-                domainContentValues.put(SSL_START_DATE, importDomainsCursor.getLong(importDomainsCursor.getColumnIndexOrThrow(SSL_START_DATE)))
-                domainContentValues.put(SSL_END_DATE, importDomainsCursor.getLong(importDomainsCursor.getColumnIndexOrThrow(SSL_END_DATE)))
-                domainContentValues.put(PINNED_IP_ADDRESSES, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(PINNED_IP_ADDRESSES)))
-                domainContentValues.put(IP_ADDRESSES, importDomainsCursor.getString(importDomainsCursor.getColumnIndexOrThrow(IP_ADDRESSES)))
+                domainContentValues.put(DOMAIN_NAME, importDomainsCursor.getString(domainNameColumnIndex))
+                domainContentValues.put(ENABLE_JAVASCRIPT, importDomainsCursor.getInt(domainJavaScriptColumnIndex))
+                domainContentValues.put(COOKIES, importDomainsCursor.getInt(domainCookiesColumnIndex))
+                domainContentValues.put(ENABLE_DOM_STORAGE, importDomainsCursor.getInt(domainDomStorageColumnIndex))
+                domainContentValues.put(ENABLE_FORM_DATA, importDomainsCursor.getInt(domainFormDataColumnIndex))  // Form data can be removed once the minimum API >= 26.
+                domainContentValues.put(ENABLE_EASYLIST, importDomainsCursor.getInt(domainEasyListColumnIndex))
+                domainContentValues.put(ENABLE_EASYPRIVACY, importDomainsCursor.getInt(domainEasyPrivacyColumnIndex))
+                domainContentValues.put(ENABLE_FANBOYS_ANNOYANCE_LIST, importDomainsCursor.getInt(domainFanboysAnnoyanceListColumnIndex))
+                domainContentValues.put(ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST, importDomainsCursor.getInt(domainFanboysSocialBlockingListColumnIndex))
+                domainContentValues.put(ULTRALIST, importDomainsCursor.getInt(domainUltraListColumnIndex))
+                domainContentValues.put(ENABLE_ULTRAPRIVACY, importDomainsCursor.getInt(domainUltraPrivacyColumnIndex))
+                domainContentValues.put(BLOCK_ALL_THIRD_PARTY_REQUESTS, importDomainsCursor.getInt(domainBlockAllThirdPartyRequestsColumnIndex))
+                domainContentValues.put(USER_AGENT, importDomainsCursor.getString(domainUserAgentColumnIndex))
+                domainContentValues.put(FONT_SIZE, importDomainsCursor.getInt(domainFontSizeColumnIndex))
+                domainContentValues.put(SWIPE_TO_REFRESH, importDomainsCursor.getInt(domainSwipeToRefreshColumnIndex))
+                domainContentValues.put(WEBVIEW_THEME, importDomainsCursor.getInt(domainWebViewThemeColumnIndex))
+                domainContentValues.put(WIDE_VIEWPORT, importDomainsCursor.getInt(domainWideViewportColumnIndex))
+                domainContentValues.put(DISPLAY_IMAGES, importDomainsCursor.getInt(domainDisplayImagesColumnIndex))
+                domainContentValues.put(PINNED_SSL_CERTIFICATE, importDomainsCursor.getInt(domainPinnedSslCertificateColumnIndex))
+                domainContentValues.put(SSL_ISSUED_TO_COMMON_NAME, importDomainsCursor.getString(domainSslIssuedToCommonNameColumnIndex))
+                domainContentValues.put(SSL_ISSUED_TO_ORGANIZATION, importDomainsCursor.getString(domainSslIssuedToOrganizationColumnIndex))
+                domainContentValues.put(SSL_ISSUED_TO_ORGANIZATIONAL_UNIT, importDomainsCursor.getString(domainSslIssuedToOrganizationalUnitColumnIndex))
+                domainContentValues.put(SSL_ISSUED_BY_COMMON_NAME, importDomainsCursor.getString(domainSslIssuedByCommonNameColumnIndex))
+                domainContentValues.put(SSL_ISSUED_BY_ORGANIZATION, importDomainsCursor.getString(domainSslIssuedByOrganizationColumnIndex))
+                domainContentValues.put(SSL_ISSUED_BY_ORGANIZATIONAL_UNIT, importDomainsCursor.getString(domainSslIssuedByOrganizationalUnitColumnIndex))
+                domainContentValues.put(SSL_START_DATE, importDomainsCursor.getLong(domainSslStartDateColumnIndex))
+                domainContentValues.put(SSL_END_DATE, importDomainsCursor.getLong(domainSslEndDateColumnIndex))
+                domainContentValues.put(PINNED_IP_ADDRESSES, importDomainsCursor.getInt(domainPinnedIpAddressesColumnIndex))
+                domainContentValues.put(IP_ADDRESSES, importDomainsCursor.getString(domainIpAddressesColumnIndex))
 
                 // Insert the content values into the domains database.
                 domainsDatabaseHelper.addDomain(domainContentValues)
@@ -647,7 +784,7 @@ class ImportExportDatabaseHelper {
 
 
             // Create the temporary export database bookmarks table.
-            temporaryExportDatabase.execSQL(BookmarksDatabaseHelper.CREATE_BOOKMARKS_TABLE)
+            temporaryExportDatabase.execSQL(CREATE_BOOKMARKS_TABLE)
 
             // Open the bookmarks database.
             val bookmarksDatabaseHelper = BookmarksDatabaseHelper(context)
@@ -658,21 +795,31 @@ class ImportExportDatabaseHelper {
             // Move to the first record.
             bookmarksCursor.moveToFirst()
 
+            // Get the bookmarks colum indexes.
+            val bookmarkNameColumnIndex = bookmarksCursor.getColumnIndexOrThrow(BOOKMARK_NAME)
+            val bookmarkUrlColumnIndex = bookmarksCursor.getColumnIndexOrThrow(BOOKMARK_URL)
+            val bookmarkParentFolderIdColumnIndex = bookmarksCursor.getColumnIndexOrThrow(PARENT_FOLDER_ID)
+            val bookmarkDisplayOrderColumnIndex = bookmarksCursor.getColumnIndexOrThrow(DISPLAY_ORDER)
+            val bookmarkIsFolderColumnIndex = bookmarksCursor.getColumnIndexOrThrow(IS_FOLDER)
+            val bookmarkFolderIdColumnIndex = bookmarksCursor.getColumnIndexOrThrow(FOLDER_ID)
+            val bookmarkFavoriteIconColumnIndex = bookmarksCursor.getColumnIndexOrThrow(FAVORITE_ICON)
+
             // Copy the data from the bookmarks cursor into the export database.
             for (i in 0 until bookmarksCursor.count) {
                 // Create a bookmark content values.
                 val bookmarkContentValues = ContentValues()
 
                 // Populate the bookmark content values.
-                bookmarkContentValues.put(BookmarksDatabaseHelper.BOOKMARK_NAME, bookmarksCursor.getString(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME)))
-                bookmarkContentValues.put(BookmarksDatabaseHelper.BOOKMARK_URL, bookmarksCursor.getString(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_URL)))
-                bookmarkContentValues.put(BookmarksDatabaseHelper.PARENT_FOLDER, bookmarksCursor.getString(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.PARENT_FOLDER)))
-                bookmarkContentValues.put(BookmarksDatabaseHelper.DISPLAY_ORDER, bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.DISPLAY_ORDER)))
-                bookmarkContentValues.put(BookmarksDatabaseHelper.IS_FOLDER, bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.IS_FOLDER)))
-                bookmarkContentValues.put(BookmarksDatabaseHelper.FAVORITE_ICON, bookmarksCursor.getBlob(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.FAVORITE_ICON)))
+                bookmarkContentValues.put(BOOKMARK_NAME, bookmarksCursor.getString(bookmarkNameColumnIndex))
+                bookmarkContentValues.put(BOOKMARK_URL, bookmarksCursor.getString(bookmarkUrlColumnIndex))
+                bookmarkContentValues.put(PARENT_FOLDER_ID, bookmarksCursor.getLong(bookmarkParentFolderIdColumnIndex))
+                bookmarkContentValues.put(DISPLAY_ORDER, bookmarksCursor.getInt(bookmarkDisplayOrderColumnIndex))
+                bookmarkContentValues.put(IS_FOLDER, bookmarksCursor.getInt(bookmarkIsFolderColumnIndex))
+                bookmarkContentValues.put(FOLDER_ID, bookmarksCursor.getLong(bookmarkFolderIdColumnIndex))
+                bookmarkContentValues.put(FAVORITE_ICON, bookmarksCursor.getBlob(bookmarkFavoriteIconColumnIndex))
 
                 // Insert the content values into the temporary export database.
-                temporaryExportDatabase.insert(BookmarksDatabaseHelper.BOOKMARKS_TABLE, null, bookmarkContentValues)
+                temporaryExportDatabase.insert(BOOKMARKS_TABLE, null, bookmarkContentValues)
 
                 // Advance to the next record.
                 bookmarksCursor.moveToNext()
@@ -695,41 +842,72 @@ class ImportExportDatabaseHelper {
             // Move to the first record.
             domainsCursor.moveToFirst()
 
+            // Get the domain column indexes.
+            val domainNameColumnIndex = domainsCursor.getColumnIndexOrThrow(DOMAIN_NAME)
+            val domainJavaScriptColumnIndex = domainsCursor.getColumnIndexOrThrow(ENABLE_JAVASCRIPT)
+            val domainCookiesColumnIndex = domainsCursor.getColumnIndexOrThrow(COOKIES)
+            val domainDomStorageColumnIndex = domainsCursor.getColumnIndexOrThrow(ENABLE_DOM_STORAGE)
+            val domainFormDataColumnIndex = domainsCursor.getColumnIndexOrThrow(ENABLE_FORM_DATA)  // Form data can be removed once the minimum API >= 26.
+            val domainEasyListColumnIndex = domainsCursor.getColumnIndexOrThrow(ENABLE_EASYLIST)
+            val domainEasyPrivacyColumnIndex = domainsCursor.getColumnIndexOrThrow(ENABLE_EASYPRIVACY)
+            val domainFanboysAnnoyanceListColumnIndex = domainsCursor.getColumnIndexOrThrow(ENABLE_FANBOYS_ANNOYANCE_LIST)
+            val domainFanboysSocialBlockingListColumnIndex = domainsCursor.getColumnIndexOrThrow(ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)
+            val domainUltraListColumnIndex = domainsCursor.getColumnIndexOrThrow(ULTRALIST)
+            val domainUltraPrivacyColumnIndex = domainsCursor.getColumnIndexOrThrow(ENABLE_EASYPRIVACY)
+            val domainBlockAllThirdPartyRequestsColumnIndex = domainsCursor.getColumnIndexOrThrow(BLOCK_ALL_THIRD_PARTY_REQUESTS)
+            val domainUserAgentColumnIndex = domainsCursor.getColumnIndexOrThrow(USER_AGENT)
+            val domainFontSizeColumnIndex = domainsCursor.getColumnIndexOrThrow(FONT_SIZE)
+            val domainSwipeToRefreshColumnIndex = domainsCursor.getColumnIndexOrThrow(SWIPE_TO_REFRESH)
+            val domainWebViewThemeColumnIndex = domainsCursor.getColumnIndexOrThrow(WEBVIEW_THEME)
+            val domainWideViewportColumnIndex = domainsCursor.getColumnIndexOrThrow(WIDE_VIEWPORT)
+            val domainDisplayImagesColumnIndex = domainsCursor.getColumnIndexOrThrow(DISPLAY_IMAGES)
+            val domainPinnedSslCertificateColumnIndex = domainsCursor.getColumnIndexOrThrow(PINNED_SSL_CERTIFICATE)
+            val domainSslIssuedToCommonNameColumnIndex = domainsCursor.getColumnIndexOrThrow(SSL_ISSUED_TO_COMMON_NAME)
+            val domainSslIssuedToOrganizationColumnIndex = domainsCursor.getColumnIndexOrThrow(SSL_ISSUED_TO_ORGANIZATION)
+            val domainSslIssuedToOrganizationalUnitColumnIndex = domainsCursor.getColumnIndexOrThrow(SSL_ISSUED_TO_ORGANIZATIONAL_UNIT)
+            val domainSslIssuedByCommonNameColumnIndex = domainsCursor.getColumnIndexOrThrow(SSL_ISSUED_BY_COMMON_NAME)
+            val domainSslIssuedByOrganizationColumnIndex = domainsCursor.getColumnIndexOrThrow(SSL_ISSUED_BY_ORGANIZATION)
+            val domainSslIssuedByOrganizationalUnitColumnIndex = domainsCursor.getColumnIndexOrThrow(SSL_ISSUED_BY_ORGANIZATIONAL_UNIT)
+            val domainSslStartDateColumnIndex = domainsCursor.getColumnIndexOrThrow(SSL_START_DATE)
+            val domainSslEndDateColumnIndex = domainsCursor.getColumnIndexOrThrow(SSL_END_DATE)
+            val domainPinnedIpAddressesColumnIndex = domainsCursor.getColumnIndexOrThrow(PINNED_IP_ADDRESSES)
+            val domainIpAddressesColumnIndex = domainsCursor.getColumnIndexOrThrow(IP_ADDRESSES)
+
             // Copy the data from the domains cursor into the export database.
             for (i in 0 until domainsCursor.count) {
                 // Create a domain content values.
                 val domainContentValues = ContentValues()
 
                 // Populate the domain content values.
-                domainContentValues.put(DOMAIN_NAME, domainsCursor.getString(domainsCursor.getColumnIndexOrThrow(DOMAIN_NAME)))
-                domainContentValues.put(ENABLE_JAVASCRIPT, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(ENABLE_JAVASCRIPT)))
-                domainContentValues.put(COOKIES, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(COOKIES)))
-                domainContentValues.put(ENABLE_DOM_STORAGE, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(ENABLE_DOM_STORAGE)))
-                domainContentValues.put(ENABLE_FORM_DATA, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(ENABLE_FORM_DATA)))
-                domainContentValues.put(ENABLE_EASYLIST, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(ENABLE_EASYLIST)))
-                domainContentValues.put(ENABLE_EASYPRIVACY, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(ENABLE_EASYPRIVACY)))
-                domainContentValues.put(ENABLE_FANBOYS_ANNOYANCE_LIST, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(ENABLE_FANBOYS_ANNOYANCE_LIST)))
-                domainContentValues.put(ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)))
-                domainContentValues.put(ULTRALIST, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(ULTRALIST)))
-                domainContentValues.put(ENABLE_ULTRAPRIVACY, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(ENABLE_ULTRAPRIVACY)))
-                domainContentValues.put(BLOCK_ALL_THIRD_PARTY_REQUESTS, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(BLOCK_ALL_THIRD_PARTY_REQUESTS)))
-                domainContentValues.put(USER_AGENT, domainsCursor.getString(domainsCursor.getColumnIndexOrThrow(USER_AGENT)))
-                domainContentValues.put(FONT_SIZE, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(FONT_SIZE)))
-                domainContentValues.put(SWIPE_TO_REFRESH, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(SWIPE_TO_REFRESH)))
-                domainContentValues.put(WEBVIEW_THEME, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(WEBVIEW_THEME)))
-                domainContentValues.put(WIDE_VIEWPORT, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(WIDE_VIEWPORT)))
-                domainContentValues.put(DISPLAY_IMAGES, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DISPLAY_IMAGES)))
-                domainContentValues.put(PINNED_SSL_CERTIFICATE, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(PINNED_SSL_CERTIFICATE)))
-                domainContentValues.put(SSL_ISSUED_TO_COMMON_NAME, domainsCursor.getString(domainsCursor.getColumnIndexOrThrow(SSL_ISSUED_TO_COMMON_NAME)))
-                domainContentValues.put(SSL_ISSUED_TO_ORGANIZATION, domainsCursor.getString(domainsCursor.getColumnIndexOrThrow(SSL_ISSUED_TO_ORGANIZATION)))
-                domainContentValues.put(SSL_ISSUED_TO_ORGANIZATIONAL_UNIT, domainsCursor.getString(domainsCursor.getColumnIndexOrThrow(SSL_ISSUED_TO_ORGANIZATIONAL_UNIT)))
-                domainContentValues.put(SSL_ISSUED_BY_COMMON_NAME, domainsCursor.getString(domainsCursor.getColumnIndexOrThrow(SSL_ISSUED_BY_COMMON_NAME)))
-                domainContentValues.put(SSL_ISSUED_BY_ORGANIZATION, domainsCursor.getString(domainsCursor.getColumnIndexOrThrow(SSL_ISSUED_BY_ORGANIZATION)))
-                domainContentValues.put(SSL_ISSUED_BY_ORGANIZATIONAL_UNIT, domainsCursor.getString(domainsCursor.getColumnIndexOrThrow(SSL_ISSUED_BY_ORGANIZATIONAL_UNIT)))
-                domainContentValues.put(SSL_START_DATE, domainsCursor.getLong(domainsCursor.getColumnIndexOrThrow(SSL_START_DATE)))
-                domainContentValues.put(SSL_END_DATE, domainsCursor.getLong(domainsCursor.getColumnIndexOrThrow(SSL_END_DATE)))
-                domainContentValues.put(PINNED_IP_ADDRESSES, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(PINNED_IP_ADDRESSES)))
-                domainContentValues.put(IP_ADDRESSES, domainsCursor.getString(domainsCursor.getColumnIndexOrThrow(IP_ADDRESSES)))
+                domainContentValues.put(DOMAIN_NAME, domainsCursor.getString(domainNameColumnIndex))
+                domainContentValues.put(ENABLE_JAVASCRIPT, domainsCursor.getInt(domainJavaScriptColumnIndex))
+                domainContentValues.put(COOKIES, domainsCursor.getInt(domainCookiesColumnIndex))
+                domainContentValues.put(ENABLE_DOM_STORAGE, domainsCursor.getInt(domainDomStorageColumnIndex))
+                domainContentValues.put(ENABLE_FORM_DATA, domainsCursor.getInt(domainFormDataColumnIndex))  // Form data can be removed once the minimum API >= 26.
+                domainContentValues.put(ENABLE_EASYLIST, domainsCursor.getInt(domainEasyListColumnIndex))
+                domainContentValues.put(ENABLE_EASYPRIVACY, domainsCursor.getInt(domainEasyPrivacyColumnIndex))
+                domainContentValues.put(ENABLE_FANBOYS_ANNOYANCE_LIST, domainsCursor.getInt(domainFanboysAnnoyanceListColumnIndex))
+                domainContentValues.put(ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST, domainsCursor.getInt(domainFanboysSocialBlockingListColumnIndex))
+                domainContentValues.put(ULTRALIST, domainsCursor.getInt(domainUltraListColumnIndex))
+                domainContentValues.put(ENABLE_ULTRAPRIVACY, domainsCursor.getInt(domainUltraPrivacyColumnIndex))
+                domainContentValues.put(BLOCK_ALL_THIRD_PARTY_REQUESTS, domainsCursor.getInt(domainBlockAllThirdPartyRequestsColumnIndex))
+                domainContentValues.put(USER_AGENT, domainsCursor.getString(domainUserAgentColumnIndex))
+                domainContentValues.put(FONT_SIZE, domainsCursor.getInt(domainFontSizeColumnIndex))
+                domainContentValues.put(SWIPE_TO_REFRESH, domainsCursor.getInt(domainSwipeToRefreshColumnIndex))
+                domainContentValues.put(WEBVIEW_THEME, domainsCursor.getInt(domainWebViewThemeColumnIndex))
+                domainContentValues.put(WIDE_VIEWPORT, domainsCursor.getInt(domainWideViewportColumnIndex))
+                domainContentValues.put(DISPLAY_IMAGES, domainsCursor.getInt(domainDisplayImagesColumnIndex))
+                domainContentValues.put(PINNED_SSL_CERTIFICATE, domainsCursor.getInt(domainPinnedSslCertificateColumnIndex))
+                domainContentValues.put(SSL_ISSUED_TO_COMMON_NAME, domainsCursor.getString(domainSslIssuedToCommonNameColumnIndex))
+                domainContentValues.put(SSL_ISSUED_TO_ORGANIZATION, domainsCursor.getString(domainSslIssuedToOrganizationColumnIndex))
+                domainContentValues.put(SSL_ISSUED_TO_ORGANIZATIONAL_UNIT, domainsCursor.getString(domainSslIssuedToOrganizationalUnitColumnIndex))
+                domainContentValues.put(SSL_ISSUED_BY_COMMON_NAME, domainsCursor.getString(domainSslIssuedByCommonNameColumnIndex))
+                domainContentValues.put(SSL_ISSUED_BY_ORGANIZATION, domainsCursor.getString(domainSslIssuedByOrganizationColumnIndex))
+                domainContentValues.put(SSL_ISSUED_BY_ORGANIZATIONAL_UNIT, domainsCursor.getString(domainSslIssuedByOrganizationalUnitColumnIndex))
+                domainContentValues.put(SSL_START_DATE, domainsCursor.getLong(domainSslStartDateColumnIndex))
+                domainContentValues.put(SSL_END_DATE, domainsCursor.getLong(domainSslEndDateColumnIndex))
+                domainContentValues.put(PINNED_IP_ADDRESSES, domainsCursor.getInt(domainPinnedIpAddressesColumnIndex))
+                domainContentValues.put(IP_ADDRESSES, domainsCursor.getString(domainIpAddressesColumnIndex))
 
                 // Insert the content values into the temporary export database.
                 temporaryExportDatabase.insert(DOMAINS_TABLE, null, domainContentValues)
@@ -895,4 +1073,24 @@ class ImportExportDatabaseHelper {
         else  // The switch is currently enabled and that is not the system default.
             ENABLED
     }
+
+    private fun generateFolderId(database: SQLiteDatabase): Long {
+        // Get the current time in epoch format.
+        val possibleFolderId = Date().time
+
+        // Get a cursor with any folders that already have this folder ID.
+        val existingFolderCursor = database.rawQuery("SELECT $ID FROM $BOOKMARKS_TABLE WHERE $FOLDER_ID = $possibleFolderId", null)
+
+        // Check if the folder ID is unique.
+        val folderIdIsUnique = (existingFolderCursor.count == 0)
+
+        // Close the cursor.
+        existingFolderCursor.close()
+
+        // Either return the folder ID or test a new one.
+        return if (folderIdIsUnique)
+            possibleFolderId
+        else
+            generateFolderId(database)
+    }
 }
diff --git a/app/src/main/res/layout/appbar_spinner_dropdown_item.xml b/app/src/main/res/layout/appbar_spinner_dropdown_item.xml
deleted file mode 100644 (file)
index f282449..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-
-<!--
-  Copyright © 2017-2020,2022 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/>. -->
-
-<com.stoutner.privacybrowser.views.CheckedLinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:layout_height="wrap_content"
-    android:layout_width="match_parent"
-    android:orientation="horizontal" >
-
-    <ImageView
-        android:id="@+id/spinner_item_imageview"
-        android:layout_height="30dp"
-        android:layout_width="30dp"
-        android:layout_gravity="center_vertical"
-        android:layout_marginStart="10dp"
-        tools:ignore="contentDescription" />
-
-    <!-- A checked text view allows the color of the text to be changed when it is selected (checked). -->
-    <CheckedTextView
-        android:id="@+id/spinner_item_textview"
-        android:layout_height="wrap_content"
-        android:layout_width="match_parent"
-        android:lines="1"
-        android:ellipsize="end"
-        android:paddingStart="20dp"
-        android:paddingEnd="20dp"
-        android:paddingTop="8dp"
-        android:paddingBottom="8dp"
-        android:textSize="18sp"
-        android:textColor="@color/checked_text_selector" />
-</com.stoutner.privacybrowser.views.CheckedLinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/appbar_spinner_item.xml b/app/src/main/res/layout/appbar_spinner_item.xml
deleted file mode 100644 (file)
index 6b3cdd4..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-
-<!--
-  Copyright © 2017-2020,2022 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/>. -->
-
-<LinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:layout_height="wrap_content"
-    android:layout_width="match_parent"
-    android:orientation="horizontal"
-    tools:ignore="UseCompoundDrawables" >
-
-    <ImageView
-        android:id="@+id/spinner_item_imageview"
-        android:layout_height="30dp"
-        android:layout_width="30dp"
-        android:layout_gravity="center_vertical"
-        android:layout_marginStart="10dp"
-        tools:ignore="contentDescription" />
-
-    <TextView
-        android:id="@+id/spinner_item_textview"
-        android:layout_height="wrap_content"
-        android:layout_width="match_parent"
-        android:lines="1"
-        android:ellipsize="end"
-        android:paddingStart="10dp"
-        android:paddingEnd="10dp"
-        android:textSize="18sp"
-        android:textColor="?android:attr/textColorPrimary" />
-</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/bookmarks_databaseview_appbar_spinner_dropdown_item.xml b/app/src/main/res/layout/bookmarks_databaseview_appbar_spinner_dropdown_item.xml
new file mode 100644 (file)
index 0000000..4bfac14
--- /dev/null
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+  Copyright 2017-2020,2022-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/>. -->
+
+<com.stoutner.privacybrowser.views.CheckedLinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_height="wrap_content"
+    android:layout_width="match_parent"
+    android:orientation="horizontal" >
+
+    <!-- Subfolder spacer. -->
+    <TextView
+        android:id="@+id/subfolder_spacer_textview"
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content"
+        android:layout_marginStart="10dp" />
+
+    <!-- Folder icon. -->
+    <ImageView
+        android:id="@+id/folder_icon_imageview"
+        android:layout_height="30dp"
+        android:layout_width="30dp"
+        android:layout_gravity="center_vertical"
+        android:layout_marginStart="10dp"
+        tools:ignore="contentDescription" />
+
+    <!-- Folder name.  A checked text view allows the color of the text to be changed when it is selected (checked). -->
+    <CheckedTextView
+        android:id="@+id/folder_name_textview"
+        android:layout_height="wrap_content"
+        android:layout_width="match_parent"
+        android:lines="1"
+        android:ellipsize="end"
+        android:paddingStart="20dp"
+        android:paddingEnd="20dp"
+        android:paddingTop="8dp"
+        android:paddingBottom="8dp"
+        android:textSize="18sp"
+        android:textColor="@color/checked_text_selector" />
+</com.stoutner.privacybrowser.views.CheckedLinearLayout>
diff --git a/app/src/main/res/layout/bookmarks_databaseview_appbar_spinner_item.xml b/app/src/main/res/layout/bookmarks_databaseview_appbar_spinner_item.xml
new file mode 100644 (file)
index 0000000..a896542
--- /dev/null
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+  Copyright © 2017-2020,2022-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/>. -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_height="wrap_content"
+    android:layout_width="match_parent"
+    android:orientation="horizontal"
+    tools:ignore="UseCompoundDrawables" >
+
+    <!-- Folder icon. -->
+    <ImageView
+        android:id="@+id/folder_icon_imageview"
+        android:layout_height="30dp"
+        android:layout_width="30dp"
+        android:layout_gravity="center_vertical"
+        android:layout_marginStart="10dp"
+        tools:ignore="contentDescription" />
+
+    <!-- Folder name. -->
+    <TextView
+        android:id="@+id/folder_name_textview"
+        android:layout_height="wrap_content"
+        android:layout_width="match_parent"
+        android:lines="1"
+        android:ellipsize="end"
+        android:paddingStart="10dp"
+        android:paddingEnd="10dp"
+        android:textSize="18sp"
+        android:textColor="?android:attr/textColorPrimary" />
+</LinearLayout>
index 171a1f4d39973023e1dc6ce91a782fdbb00082fd..548b56bd60c8c8f7d74e02de5c3c66ae90ae5a77 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-  Copyright © 2016-2017,2019,2022 Soren Stoutner <soren@stoutner.com>.
+  Copyright © 2016-2017,2019,2022-2023 Soren Stoutner <soren@stoutner.com>.
 
   This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
 
@@ -19,7 +19,6 @@
   along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>. -->
 
 <LinearLayout
-    android:id="@+id/bookmarks_databaseview_item_linearlayout"
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_height="wrap_content"
@@ -36,8 +35,9 @@
         android:layout_marginEnd="10dp"
         android:orientation="horizontal" >
 
+        <!-- Database ID. -->
         <TextView
-            android:id="@+id/bookmarks_databaseview_database_id"
+            android:id="@+id/database_id_textview"
             android:layout_height="wrap_content"
             android:layout_width="50dp"
             android:layout_marginEnd="10dp"
             android:textColor="@color/gray_500"
             android:textSize="22sp" />
 
+        <!-- Favorite icon. -->
         <ImageView
-            android:id="@+id/bookmarks_databaseview_favorite_icon"
+            android:id="@+id/favorite_icon_imageview"
             android:layout_width="30dp"
             android:layout_height="30dp"
             android:layout_gravity="center_vertical"
             android:layout_marginEnd="10dp"
             tools:ignore="ContentDescription" />
 
+        <!-- Name. -->
         <TextView
-            android:id='@+id/bookmarks_databaseview_bookmark_name'
+            android:id="@+id/name_textview"
             android:layout_height="wrap_content"
             android:layout_width="wrap_content"
             android:textColor="?android:attr/textColorPrimary"
             android:textSize="22sp"
             android:ellipsize="end"
             android:lines="1" />
+
+        <!-- Folder ID. -->
+        <TextView
+            android:id="@+id/folder_id_textview"
+            android:layout_height="wrap_content"
+            android:layout_width="wrap_content"
+            android:textColor="@color/gray_500"
+            android:textSize="22sp"
+            android:ellipsize="end"
+            android:lines="1" />
     </LinearLayout>
 
-    <!-- Second row. -->
+    <!-- Second row - URL. -->
     <TextView
-        android:id="@+id/bookmarks_databaseview_bookmark_url"
+        android:id="@+id/url_textview"
         android:layout_height="wrap_content"
         android:layout_width="wrap_content"
         android:layout_marginStart="13dp"
@@ -84,8 +96,9 @@
         android:layout_marginBottom="10dp"
         android:orientation="horizontal" >
 
+        <!-- Display order. -->
         <TextView
-            android:id="@+id/bookmarks_databaseview_display_order"
+            android:id="@+id/display_order_textview"
             android:layout_height="wrap_content"
             android:layout_width="50dp"
             android:layout_marginEnd="10dp"
             android:textColor="?android:attr/textColorPrimary"
             android:textSize="22sp" />
 
+        <!-- Parent folder icon. -->
         <ImageView
-            android:id="@+id/bookmarks_databaseview_parent_folder_icon"
+            android:id="@+id/parent_folder_icon_imageview"
             android:layout_height="30dp"
             android:layout_width="30dp"
             android:layout_gravity="center_vertical"
             android:src="@drawable/folder_gray"
             tools:ignore="ContentDescription" />
 
+        <!-- Parent folder name. -->
         <TextView
-            android:id="@+id/bookmarks_databaseview_parent_folder"
+            android:id="@+id/parent_folder_textview"
             android:layout_height="wrap_content"
             android:layout_width="wrap_content"
             android:textSize="22sp"
             android:ellipsize="end"
             android:lines="1" />
     </LinearLayout>
-</LinearLayout>
\ No newline at end of file
+</LinearLayout>
index 92fbc4baa8909f3605b1854589277c041ea32f12..adacea94af78777aad2731404834b0d308070b55 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-  Copyright © 2016-2019,2021-2022 Soren Stoutner <soren@stoutner.com>.
+  Copyright 2016-2019,2021-2023 Soren Stoutner <soren@stoutner.com>.
 
   This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
 
                 <requestFocus />
             </com.google.android.material.textfield.TextInputEditText>
         </com.google.android.material.textfield.TextInputLayout>
-
-        <TextView
-            android:layout_height="wrap_content"
-            android:layout_width="match_parent"
-            android:gravity="center_horizontal"
-            android:text="@string/folder_names_must_be_unique"
-            android:layout_marginStart="7dp"
-            android:layout_marginEnd="7dp" />
     </LinearLayout>
-</ScrollView>
\ No newline at end of file
+</ScrollView>
index 87c16a5206a70179d2df6d791b945059221774df..9c95b7cddced35208a5c35e00f836044a835ac71 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-  Copyright © 2017-2018,2020,2022 Soren Stoutner <soren@stoutner.com>.
+  Copyright 2017-2018,2020,2022-2023 Soren Stoutner <soren@stoutner.com>.
 
   This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
 
     android:layout_width="match_parent"
     android:orientation="horizontal" >
 
+    <!-- Subfolder spacer. -->
+    <TextView
+        android:id="@+id/subfolder_spacer_textview"
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content"
+        android:layout_marginStart="10dp" />
+
+    <!-- Folder icon. -->
     <ImageView
-        android:id="@+id/spinner_item_imageview"
+        android:id="@+id/folder_icon_imageview"
         android:layout_height="30dp"
         android:layout_width="30dp"
         android:layout_gravity="center_vertical"
-        android:layout_marginStart="10dp"
         tools:ignore="contentDescription" />
 
     <!-- A `CheckedTextView` allows the color of the text to be changed when it is selected (checked). -->
     <CheckedTextView
-        android:id="@+id/spinner_item_textview"
+        android:id="@+id/folder_name_textview"
         android:layout_height="wrap_content"
         android:layout_width="match_parent"
         android:lines="1"
@@ -46,4 +53,4 @@
         android:paddingBottom="8dp"
         android:textSize="18sp"
         android:textColor="@color/checked_text_selector" />
-</com.stoutner.privacybrowser.views.CheckedLinearLayout>
\ No newline at end of file
+</com.stoutner.privacybrowser.views.CheckedLinearLayout>
index 98cebe21638983f8c15dc71cd088d1c9b8491d1e..bb0dc74d62af64cc23ee0b0e14987a982d15c1b0 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-  Copyright © 2017-2019,2022 Soren Stoutner <soren@stoutner.com>.
+  Copyright 2017-2019,2022-2023 Soren Stoutner <soren@stoutner.com>.
 
   This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
 
     android:orientation="horizontal"
     tools:ignore="UseCompoundDrawables" >
 
+    <!-- Folder icon. -->
     <ImageView
-        android:id="@+id/spinner_item_imageview"
+        android:id="@+id/folder_icon_imageview"
         android:layout_height="30dp"
         android:layout_width="30dp"
         android:layout_gravity="center_vertical"
         android:layout_marginStart="10dp"
         tools:ignore="contentDescription" />
 
+    <!-- Folder name. -->
     <TextView
-        android:id="@+id/spinner_item_textview"
+        android:id="@+id/folder_name_textview"
         android:layout_height="wrap_content"
         android:layout_width="match_parent"
         android:lines="1"
@@ -44,4 +46,4 @@
         android:paddingEnd="10dp"
         android:textSize="18sp"
         android:textColor="?android:textColorPrimary" />
-</LinearLayout>
\ No newline at end of file
+</LinearLayout>
index 97a42f6bdc210c14b74e5d0a716f9e7271cf4c0c..3be08cd9809966e3125dd8d0df8705afe222c9bb 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-  Copyright © 2016-2022 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>.
 
                 tools:targetApi="26" />
         </LinearLayout>
     </LinearLayout>
-</ScrollView>
\ No newline at end of file
+</ScrollView>
index abcb98c2caeb631f09e8f8cbd99ff70c2bab4ffd..cffc03d9e38fb24361c7a4ae8f389850481ea5fd 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-  Copyright © 2016-2022 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>.
 
                 android:textColor="@color/gray_500" />
         </LinearLayout>
 
+        <!-- Folder ID. -->
+        <LinearLayout
+            android:layout_height="wrap_content"
+            android:layout_width="match_parent"
+            android:layout_marginTop="6dp"
+            android:layout_marginBottom="6dp"
+            android:layout_marginStart="7dp"
+            android:layout_marginEnd="7dp" >
+
+            <TextView
+                android:layout_height="wrap_content"
+                android:layout_width="wrap_content"
+                android:text="@string/folder_id"
+                android:textSize="18sp"
+                android:textColor="?android:textColorPrimary"
+                android:layout_marginEnd="8dp" />
+
+            <TextView
+                android:id="@+id/folder_id_textview"
+                android:layout_height="wrap_content"
+                android:layout_width="wrap_content"
+                android:textSize="18sp"
+                android:textColor="@color/gray_500" />
+        </LinearLayout>
+
         <!-- Current icon. -->
         <LinearLayout
             android:id="@+id/current_icon_linearlayout"
                 android:importantForAutofill="no"
                 tools:targetApi="26" />
         </LinearLayout>
-
-        <TextView
-            android:layout_height="wrap_content"
-            android:layout_width="match_parent"
-            android:gravity="center_horizontal"
-            android:text="@string/folder_names_must_be_unique"
-            android:layout_marginStart="7dp"
-            android:layout_marginEnd="7dp" />
     </LinearLayout>
-</ScrollView>
\ No newline at end of file
+</ScrollView>
index 5ea5bc85a4ca0e799fd7b5138aa115e2d3bb8270..7bf352f5fb754a7bbf6046408105d66199e512bb 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-  Copyright © 2016-2017,2019-2022 Soren Stoutner <soren@stoutner.com>.
+  Copyright 2016-2017,2019-2023 Soren Stoutner <soren@stoutner.com>.
 
   This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
 
                 android:inputType="textUri"
                 android:selectAllOnFocus="true" />
         </com.google.android.material.textfield.TextInputLayout>
-
-        <TextView
-            android:layout_height="wrap_content"
-            android:layout_width="match_parent"
-            android:gravity="center_horizontal"
-            android:text="@string/folder_names_must_be_unique"
-            android:layout_marginStart="7dp"
-            android:layout_marginEnd="7dp" />
     </LinearLayout>
-</ScrollView>
\ No newline at end of file
+</ScrollView>
index c8cc7b6e4b908cc6480f2984317e3e2765ad60d6..f96c715d41f02c95d9c389b4532b2d32c0dd63bc 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-  Copyright © 2016-2017,2022 Soren Stoutner <soren@stoutner.com>.
+  Copyright 2016-2017,2022-2023 Soren Stoutner <soren@stoutner.com>.
 
   This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
 
@@ -26,4 +26,5 @@
     android:layout_width="match_parent"
     android:choiceMode="singleChoice"
     android:divider="@color/white"
-    android:dividerHeight="0dp" />
\ No newline at end of file
+    android:dividerHeight="0dp"
+    android:paddingTop = "5dp" />
index 5fe04790c4676f1660ff87e2f9213621e75bf8d9..144b0ac2a86bc462e336046301044bd3fff8d938 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-  Copyright © 2016-2017,2022 Soren Stoutner <soren@stoutner.com>.
+  Copyright 2016-2017,2022-2023 Soren Stoutner <soren@stoutner.com>.
 
   This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
 
@@ -19,7 +19,6 @@
   along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>. -->
 
 <LinearLayout
-    android:id="@+id/move_to_folder_item_linearlayout"
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_height="wrap_content"
     android:background="?attr/listSelectorDrawable"
     tools:ignore="UseCompoundDrawables">
 
+    <!-- Subfolder spacer. -->
+    <TextView
+        android:id="@+id/subfolder_spacer_textview"
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content"
+        android:layout_marginStart="10dp" />
+
+    <!-- Folder icon. -->
     <ImageView
-        android:id="@+id/move_to_folder_icon"
+        android:id="@+id/folder_icon_imageview"
         android:layout_height="30dp"
         android:layout_width="30dp"
         android:layout_gravity="center_vertical"
-        android:layout_marginStart="10dp"
         tools:ignore="ContentDescription" />
 
+    <!-- Folder name. -->
     <TextView
-        android:id="@+id/move_to_folder_name_textview"
+        android:id="@+id/folder_name_textview"
         android:layout_height="wrap_content"
         android:layout_width="wrap_content"
         android:layout_margin="10dp"
         android:lines="1"
         android:textColor="?android:attr/textColorPrimary"
         android:textSize="22sp" />
-</LinearLayout>
\ No newline at end of file
+</LinearLayout>
index 4ddcffad8e81448aa122b7a9576a1de4a4b5a7ba..8799d33a9b58d457429b9640fb03a267161e8579 100644 (file)
     <string name="saving_file">Speichere Datei:\u0020 %1$d%% - %2$s</string>
     <string name="saving_file_progress">Speichere Datei:\u0020 %1$s Bytes - %2$s</string>
     <string name="saving_file_percentage_progress">Speichere Datei:\u0020 %1$d%% - %2$s Bytes / %3$s Bytes - %4$s</string>
+    <string name="saved">%1$s gespeichert.</string>
+    <string name="download_cancelled">Download abgebrochen.</string>
     <string name="processing_image">Bild wird bearbeitet:\u0020 %1$s</string>
     <string name="error_saving_file">Fehler beim Speichern der Datei %1$s:\u0020 %2$s</string>
     <string name="unknown_error">Unbekannter Fehler</string>
     <string name="bookmark_name">Name des Lesezeichens</string>
     <string name="folder_name">Name des Ordners</string>
     <string name="bookmark_url">URL für das Lesezeichen</string>
-    <string name="folder_names_must_be_unique">Ordnernamen müssen einmalig sein</string>
     <string name="edit_bookmark">Lesezeichen bearbeiten</string>
     <string name="edit_folder">Ordner bearbeiten</string>
     <string name="move_to_folder">In Ordner verschieben</string>
     <string name="domain_name">Domainname</string>
     <string name="domain_deleted">Domain gelöscht</string>
     <string name="domain_name_instructions">*. kann als Wildcard-Subdomain verwendet werden (z.B. *.stoutner.com)</string>
+    <string-array name="javascript_array">
+        <item>Systemeinstellung</item>
+        <item>JavaScript aktiviert</item>
+        <item>JavaScript deaktiviert</item>
+    </string-array>
+    <string-array name="cookies_array">
+        <item>Systemeinstellung</item>
+        <item>Cookies aktiviert</item>
+        <item>Cookies deaktiviert</item>
+    </string-array>
+    <string-array name="dom_storage_array">
+        <item>Systemeinstellung</item>
+        <item>DOM-Speicher aktiviert</item>
+        <item>DOM-Speicher deaktiviert</item>
+    </string-array>
+    <string-array name="form_data_array">  <!-- Form data can be removed once the minimum API >= 26. -->
+        <item>Systemeinstellung</item>
+        <item>Formulardaten aktiviert</item>
+        <item>Formulardaten deaktiviert</item>
+    </string-array>
+    <string-array name="easylist_array">
+        <item>Systemeinstellung</item>
+        <item>EasyList aktiviert</item>
+        <item>EasyList deaktiviert</item>
+    </string-array>
+    <string-array name="easyprivacy_array">
+        <item>Systemeinstellung</item>
+        <item>EasyPrivacy aktiviert</item>
+        <item>EasyPrivacy deaktiviert</item>
+    </string-array>
+    <string-array name="fanboys_annoyance_list_array">
+        <item>Systemeinstellung</item>
+        <item>Fanboy’s Annoyance Sperrliste aktiviert</item>
+        <item>Fanboy’s Annoyance Sperrliste deaktiviert</item>
+    </string-array>
+    <string-array name="fanboys_social_blocking_list_array">
+        <item>Systemeinstellung</item>
+        <item>Fanboy’s Social Blocking Sperrliste aktiviert</item>
+        <item>Fanboy’s Social Blocking Sperrliste deaktiviert</item>
+    </string-array>
+    <string-array name="ultralist_array">
+        <item>Systemeinstellung</item>
+        <item>UltraList aktiviert</item>
+        <item>UltraList deaktiviert</item>
+    </string-array>
+    <string-array name="ultraprivacy_array">
+        <item>Systemeinstellung</item>
+        <item>UltraPrivacy aktiviert</item>
+        <item>UltraPrivacy deaktiviert</item>
+    </string-array>
+    <string-array name="block_all_third_party_requests_array">
+        <item>Systemeinstellung</item>
+        <item>Alle Drittanbieter-Anfragen blockieren</item>
+        <item>Alle Drittanbieter-Anfragen zulassen</item>
+    </string-array>
     <string-array name="font_size_array">
-        <item>System-Einstellung</item>
+        <item>Systemeinstellung</item>
         <item>Benutzerdefinierte Schriftgröße</item>
     </string-array>
     <string-array name="swipe_to_refresh_array">
-        <item>System-Einstellung</item>
+        <item>Systemeinstellung</item>
         <item>Herunterziehen zum aktualisieren aktiviert</item>
         <item>Herunterziehen zum aktualisieren deaktiviert</item>
     </string-array>
     <string-array name="webview_theme_array">
-        <item>System-Einstellung</item>
+        <item>Systemeinstellung</item>
         <item>Helles WebView-Erscheinungsbild</item>
         <item>Dunkles WebView-Erscheinungsbild</item>
     </string-array>
     <string-array name="wide_viewport_array">
-        <item>System-Einstellung</item>
+        <item>Systemeinstellung</item>
         <item>Breiter Anzeigebereich aktiviert</item>
         <item>Breiter Anzeigebereich deaktiviert</item>
     </string-array>
     <string-array name="display_webpage_images_array">
-        <item>System-Einstellung</item>
+        <item>Systemeinstellung</item>
         <item>Grafiken aktiviert</item>
         <item>Grafiken deaktiviert</item>
     </string-array>
     <string name="clear">leeren</string>
     <string name="logcat_copied">Logcat kopiert.</string>
     <string name="privacy_browser_logcat_txt">Privacy Browser Android %1$s Logcat.txt</string>
-    <string name="saved">%1$s gespeichert.</string>
     <string name="error_saving_logcat">Fehler beim Speichern von Logcat:\u0020 %1$s</string>
 
     <!-- Guide. -->
index 545b18a59acf6eedf77cfa550bedca031b884fb9..aeafdde697af1cb025354318eedef71f6a9f0651 100644 (file)
     <string name="saving_file">Guardando archivo:\u0020 %1$d%% - %2$s</string>
     <string name="saving_file_progress">Guardando archivo:\u0020 %1$s bytes - %2$s</string>
     <string name="saving_file_percentage_progress">Guardando archivo:\u0020 %1$d%% - %2$s bytes / %3$s bytes - %4$s</string>
+    <string name="saved">%1$s guardado.</string>
+    <string name="download_cancelled">Descarga cancelada.</string>
     <string name="processing_image">Procesando imagen:\u0020 %1$s</string>
     <string name="error_saving_file">Error al guardar %1$s:\u0020 %2$s</string>
     <string name="unknown_error">Error desconocido</string>
     <string name="bookmark_name">Nombre de favorito</string>
     <string name="folder_name">Nombre de carpeta</string>
     <string name="bookmark_url">Favorito URL</string>
-    <string name="folder_names_must_be_unique">Los nombres de carpetas deben ser únicos</string>
     <string name="edit_bookmark">Editar favorito</string>
     <string name="edit_folder">Editar carpeta</string>
     <string name="move_to_folder">Mover a carpeta</string>
     <string name="clear">Borrar</string>
     <string name="logcat_copied">Logcat copiado.</string>
     <string name="privacy_browser_logcat_txt">Navegador Privado Android %1$s Logcat.txt</string>
-    <string name="saved">%1$s guardado.</string>
     <string name="error_saving_logcat">Error guardando logcat:\u0020 %1$s</string>
 
     <!-- Guide. -->
index 648024b7f41768d44d00623930a7ca472ff9f13c..9ca8d80fa45daeea369a1d6d3963dd0b73197eb9 100644 (file)
     <string name="saving_file">Enregistrement du fichier : %1$d%% - %2$s</string>
     <string name="saving_file_progress">Enregistrement du fichier : %1$s octets - %2$s</string>
     <string name="saving_file_percentage_progress">Enregistrement du fichier : %1$d%% - %2$s octets / %3$s octets - %4$s</string>
+    <string name="saved">%1$s sauvegardé.</string>
     <string name="processing_image">Traitement de l\'image : %1$s</string>
     <string name="error_saving_file">Erreur lors de l\'enregistrement de %1$s : %2$s</string>
     <string name="unknown_error">Erreur inconnue</string>
     <string name="bookmark_name">Nom du favori</string>
     <string name="folder_name">Nom du dossier</string>
     <string name="bookmark_url">Ajouter aux favoris</string>
-    <string name="folder_names_must_be_unique">Les noms de dossiers doivent être uniques</string>
     <string name="edit_bookmark">Editer favori</string>
     <string name="edit_folder">Editer dossier</string>
     <string name="move_to_folder">Déplacer vers dossier</string>
     <string name="clear">Vider</string>
     <string name="logcat_copied">Journal système copié.</string>
     <string name="privacy_browser_logcat_txt">Privacy Browser Android %1$s Logcat.txt</string>
-    <string name="saved">%1$s sauvegardé.</string>
     <string name="error_saving_logcat">Erreur de sauvegarde du logcat : %1$s</string>
 
     <!-- Guide. -->
index 22db320c8506c06ce857b789420ff5e2e6648eeb..e313dfef2f0d463c01105c4797f8b5bb37ca2bbb 100644 (file)
     <string name="saving_file">Salvataggio file:\u0020 %1$d%% - %2$s</string>
     <string name="saving_file_progress">Salvataggio file:\u0020 %1$s byte - %2$s</string>
     <string name="saving_file_percentage_progress">Salvataggio file:\u0020 %1$d%% - %2$s byte / %3$s byte - %4$s</string>
+    <string name="saved">%1$s salvato.</string>
+    <string name="download_cancelled">Download cancellato.</string>
     <string name="processing_image">Creazione immagine:\u0020 %1$s</string>
     <string name="error_saving_file">Error di salvataggio di %1$s:\u0020 %2$s</string>
     <string name="unknown_error">Errore sconosciuto</string>
     <string name="bookmark_name">Nome Segnalibro</string>
     <string name="folder_name">Nome Cartella</string>
     <string name="bookmark_url">URL Segnalibro</string>
-    <string name="folder_names_must_be_unique">I nomi delle cartelle devono essere unici</string>
     <string name="edit_bookmark">Modifica Segnalibro</string>
     <string name="edit_folder">Modifica Cartella</string>
     <string name="move_to_folder">Sposta nella Cartella</string>
     <string name="clear">Cancella</string>
     <string name="logcat_copied">Logcat copiato.</string>
     <string name="privacy_browser_logcat_txt">Privacy Browser Android %1$s Logcat.txt</string>
-    <string name="saved">%1$s salvato.</string>
     <string name="error_saving_logcat">Errore salvataggio logcat:\u0020 %1$s</string>
 
     <!-- Guide. -->
index 7176644ef5d5bc559c895c3a396d0556ccbfbbd2..2ddd613fea9ab90c062a9f7295172a5995421421 100644 (file)
     <string name="saving_file">Salvando file:\u0020 %1$d%% - %2$s</string>
     <string name="saving_file_progress">Salvando file:\u0020 %1$s bytes - %2$s</string>
     <string name="saving_file_percentage_progress">Salvando file:\u0020 %1$d%% - %2$s bytes / %3$s bytes - %4$s</string>
+    <string name="saved">%1$s salvo.</string>
     <string name="processing_image">Processando imagem:\u0020 %1$s</string>
     <string name="error_saving_file">Erro ao salvar %1$s:\u0020 %2$s</string>
     <string name="unknown_error">Erro desconhecido</string>
     <string name="bookmark_name">Nome do marcador</string>
     <string name="folder_name">Nome da pasta</string>
     <string name="bookmark_url">URL do favorito</string>
-    <string name="folder_names_must_be_unique">Os nomes das pastas devem ser únicos</string>
     <string name="edit_bookmark">Editar favorito</string>
     <string name="edit_folder">Editar Pasta</string>
     <string name="move_to_folder">Mover para a pasta</string>
     <string name="clear">Limpar</string>
     <string name="logcat_copied">Logcat copiado.</string>
     <string name="privacy_browser_logcat_txt">Privacy Browser Android %1$s Logcat.txt</string>
-    <string name="saved">%1$s salvo.</string>
     <string name="error_saving_logcat">Erro ao salvar logcat:\u0020 %1$s</string>
 
     <!-- Guide. -->
index c2e16c8a8f62cda4326f3c2c352d4e9c9bfa5bf0..f3b9481667cee98b18a8ac460de472d704e7b9ed 100644 (file)
     <string name="saving_file">Сохранение файла:\u0020 %1$d%% - %2$s</string>
     <string name="saving_file_progress">Сохранение файла:\u0020 %1$s байтов - %2$s</string>
     <string name="saving_file_percentage_progress">Сохранение файла:\u0020 %1$d%% - %2$s байтов / %3$s байтов - %4$s</string>
+    <string name="saved">%1$s сохранен.</string>
+    <string name="download_cancelled">Загрузка отменена.</string>
     <string name="processing_image">Обработка изображения:\u0020 %1$s</string>
     <string name="error_saving_file">Ошибка сохранения %1$s:\u0020 %2$s</string>
     <string name="unknown_error">Неизвестная ошибка</string>
     <string name="bookmark_name">Имя закладки</string>
     <string name="folder_name">Имя папки</string>
     <string name="bookmark_url">URL-адрес закладки</string>
-    <string name="folder_names_must_be_unique">Имена папок должны быть уникальными</string>
     <string name="edit_bookmark">Изменить закладку</string>
     <string name="edit_folder">Изменить папку</string>
     <string name="move_to_folder">Переместить в папку</string>
     <string name="clear">Очистить</string>
     <string name="logcat_copied">Logcat скопирован.</string>
     <string name="privacy_browser_logcat_txt">Privacy Browser Android %1$s Logcat.txt</string>
-    <string name="saved">%1$s сохранен.</string>
     <string name="error_saving_logcat">Ошибка сохранения logcat:\u0020 %1$s</string>
 
     <!-- Guide. -->
index 8640a15983376d9cf622255d322218e868f02461..409afe7224b55292e8d4c0cacdc8bfa44710bd54 100644 (file)
     <string name="bookmark_name">Yer imi adı</string>
     <string name="folder_name">Klasör adı</string>
     <string name="bookmark_url">Yer imi URL\'si</string>
-    <string name="folder_names_must_be_unique">Klasör adları özgün olmalı</string>
     <string name="edit_bookmark">Yer imi düzenle</string>
     <string name="edit_folder">Klasörü düzenle</string>
     <string name="move_to_folder">Klasöre taşı</string>
index 95f94bee5ec5e479b268c56986ce9ed26827123a..e2a9e08daccfac0059ba04c6463df4bfe2f97c09 100644 (file)
     <string name="bookmark_name">书签名</string>
     <string name="folder_name">文件夹名</string>
     <string name="bookmark_url">书签链接</string>
-    <string name="folder_names_must_be_unique">文件夹明不能重复</string>
     <string name="edit_bookmark">编辑书签</string>
     <string name="edit_folder">编辑文件夹</string>
     <string name="move_to_folder">移动到文件夹</string>
index 62c7f948521b38d38786dda73bf7fc15873953f7..04513d49e9e4ec6a4b86511aa5c4242f08c157a1 100644 (file)
     <string name="bookmark_name">Bookmark name</string>
     <string name="folder_name">Folder name</string>
     <string name="bookmark_url">Bookmark URL</string>
-    <string name="folder_names_must_be_unique">Folder names must be unique</string>
     <string name="edit_bookmark">Edit Bookmark</string>
     <string name="edit_folder">Edit Folder</string>
     <string name="move_to_folder">Move to Folder</string>
     <string name="bookmarks_deleted">Bookmarks Deleted:\u0020 %1$d</string>
     <string name="undo">Undo</string>
 
-    <!-- Bookmarks Database View. -->
+    <!-- Bookmarks Database View.  Android removes initial spaces, but they can be manually specified with the Unicode `\u0020` formatting.
+        The `%1$d` 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_database_view">Bookmarks Database View</string>
     <string name="all_folders">All Folders</string>
     <string name="home_folder">Home Folder</string>
         <string name="sorted_by_database_id">Sorted by database ID.</string>
         <string name="sorted_by_display_order">Sorted by display order.</string>
     <string name="database_id">Database ID:</string>
+    <string name="folder_id">Folder ID:</string>
+    <string name="folder_id_separator">\u0020 – %1$d</string>
     <string name="folder">Folder:</string>
     <string name="parent_folder">Parent folder:</string>
     <string name="display_order">Display order:</string>