2 * Copyright 2016-2023 Soren Stoutner <soren@stoutner.com>.
4 * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
6 * Privacy Browser Android is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
11 * Privacy Browser Android is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with Privacy Browser Android. If not, see <http://www.gnu.org/licenses/>.
20 package com.stoutner.privacybrowser.helpers
22 import android.content.ContentValues
23 import android.content.Context
24 import android.database.Cursor
25 import android.database.DatabaseUtils
26 import android.database.MatrixCursor
27 import android.database.MergeCursor
28 import android.database.sqlite.SQLiteDatabase
29 import android.database.sqlite.SQLiteOpenHelper
31 import com.stoutner.privacybrowser.activities.HOME_FOLDER_ID
35 // Define the class constants.
36 private const val SCHEMA_VERSION = 2
38 // Define the public database constants.
39 const val BOOKMARKS_DATABASE = "bookmarks.db"
40 const val BOOKMARKS_TABLE = "bookmarks"
42 // Define the public schema constants.
43 const val BOOKMARK_NAME = "bookmarkname"
44 const val BOOKMARK_URL = "bookmarkurl"
45 const val DISPLAY_ORDER = "displayorder"
46 const val FAVORITE_ICON = "favoriteicon"
47 const val FOLDER_ID = "folder_id"
48 const val IS_FOLDER = "isfolder"
49 const val PARENT_FOLDER_ID = "parent_folder_id"
51 // Define the public table creation constant.
52 const val CREATE_BOOKMARKS_TABLE = "CREATE TABLE $BOOKMARKS_TABLE (" +
53 "$ID INTEGER PRIMARY KEY, " +
54 "$BOOKMARK_NAME TEXT, " +
55 "$BOOKMARK_URL TEXT, " +
56 "$PARENT_FOLDER_ID INTEGER, " +
57 "$DISPLAY_ORDER INTEGER, " +
58 "$IS_FOLDER BOOLEAN, " +
59 "$FOLDER_ID INTEGER, " +
60 "$FAVORITE_ICON BLOB)"
62 class BookmarksDatabaseHelper(context: Context) : SQLiteOpenHelper(context, BOOKMARKS_DATABASE, null, SCHEMA_VERSION) {
63 override fun onCreate(bookmarksDatabase: SQLiteDatabase) {
64 // Create the bookmarks table.
65 bookmarksDatabase.execSQL(CREATE_BOOKMARKS_TABLE)
68 override fun onUpgrade(bookmarksDatabase: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
69 // Upgrade from schema version 1, first used in Privacy Browser 1.8, to schema version 2, first used in Privacy Browser 3.15.
71 // Add the folder ID column.
72 bookmarksDatabase.execSQL("ALTER TABLE $BOOKMARKS_TABLE ADD COLUMN $FOLDER_ID INTEGER")
74 // Get a cursor with all the folders.
75 val foldersCursor = bookmarksDatabase.rawQuery("SELECT $ID FROM $BOOKMARKS_TABLE WHERE $IS_FOLDER = 1", null)
77 // Get the folders cursor ID column index.
78 val foldersCursorIdColumnIndex = foldersCursor.getColumnIndexOrThrow(ID)
80 // Add a folder ID to each folder.
81 while(foldersCursor.moveToNext()) {
82 // Get the current folder database ID.
83 val databaseId = foldersCursor.getInt(foldersCursorIdColumnIndex)
85 // Generate a folder ID.
86 val folderId = Date().time
88 // Create a folder content values.
89 val folderContentValues = ContentValues()
91 // Store the new folder ID in the content values.
92 folderContentValues.put(FOLDER_ID, folderId)
94 // Update the folder with the new folder ID.
95 bookmarksDatabase.update(BOOKMARKS_TABLE, folderContentValues, "$ID = $databaseId", null)
97 // Wait 2 milliseconds to ensure that the next folder ID is unique.
101 // Close the folders cursor.
102 foldersCursor.close()
105 // Add the parent folder ID column.
106 bookmarksDatabase.execSQL("ALTER TABLE $BOOKMARKS_TABLE ADD COLUMN $PARENT_FOLDER_ID INTEGER")
108 // Get a cursor with all the bookmarks.
109 val bookmarksCursor = bookmarksDatabase.rawQuery("SELECT $ID, parentfolder FROM $BOOKMARKS_TABLE", null)
111 // Get the bookmarks cursor ID column index.
112 val bookmarksCursorIdColumnIndex = bookmarksCursor.getColumnIndexOrThrow(ID)
113 val bookmarksCursorParentFolderColumnIndex = bookmarksCursor.getColumnIndexOrThrow("parentfolder")
115 // Populate the parent folder ID for each bookmark.
116 while(bookmarksCursor.moveToNext()) {
117 // Get the information from the cursor.
118 val databaseId = bookmarksCursor.getInt(bookmarksCursorIdColumnIndex)
119 val oldParentFolderString = bookmarksCursor.getString(bookmarksCursorParentFolderColumnIndex)
121 // Initialize the new parent folder ID.
122 var newParentFolderId = HOME_FOLDER_ID
124 // Get the parent folder ID if the bookmark is not in the home folder.
125 if (oldParentFolderString.isNotEmpty()) {
126 // SQL escape the old parent folder string.
127 val sqlEscapedFolderName = DatabaseUtils.sqlEscapeString(oldParentFolderString)
129 // Get the parent folder cursor.
130 val parentFolderCursor = bookmarksDatabase.rawQuery("SELECT $FOLDER_ID FROM $BOOKMARKS_TABLE WHERE $BOOKMARK_NAME = $sqlEscapedFolderName AND $IS_FOLDER = 1", null)
132 // Get the new parent folder ID if it exists.
133 if (parentFolderCursor.count > 0) {
134 // Move to the first entry.
135 parentFolderCursor.moveToFirst()
137 // Get the new parent folder ID.
138 newParentFolderId = parentFolderCursor.getLong(parentFolderCursor.getColumnIndexOrThrow(FOLDER_ID))
141 // Close the parent folder cursor.
142 parentFolderCursor.close()
145 // Create a bookmark content values.
146 val bookmarkContentValues = ContentValues()
148 // Store the new parent folder ID in the content values.
149 bookmarkContentValues.put(PARENT_FOLDER_ID, newParentFolderId)
151 // Update the folder with the new folder ID.
152 bookmarksDatabase.update(BOOKMARKS_TABLE, bookmarkContentValues, "$ID = $databaseId", null)
155 // Close the bookmarks cursor.
156 bookmarksCursor.close()
158 // This upgrade removed the old `parentfolder` string column.
159 // SQLite amazingly only added a command to drop a column in version 3.35.0. <https://www.sqlite.org/changes.html>
160 // It will be a while before that is supported in Android. <https://developer.android.com/reference/android/database/sqlite/package-summary>
161 // 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.
165 // Get a cursor for all bookmarks and folders.
166 val allBookmarks: Cursor
168 // Get a readable database handle.
169 val bookmarksDatabase = this.readableDatabase
171 // Return a cursor with the entire contents of the bookmarks table. The cursor cannot be closed because it is used in the parent activity.
172 return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE", null)
175 // Get a cursor for all bookmarks and folders ordered by display order.
176 val allBookmarksByDisplayOrder: Cursor
178 // Get a readable database handle.
179 val bookmarksDatabase = this.readableDatabase
181 // Return a cursor with the entire contents of the bookmarks table ordered by the display order. The cursor cannot be closed because it is used in the parent activity.
182 return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE ORDER BY $DISPLAY_ORDER ASC", null)
185 // Create a bookmark.
186 fun createBookmark(bookmarkName: String, bookmarkUrl: String, parentFolderId: Long, displayOrder: Int, favoriteIcon: ByteArray) {
187 // Store the bookmark data in a content values.
188 val bookmarkContentValues = ContentValues()
190 // The ID is created automatically.
191 bookmarkContentValues.put(BOOKMARK_NAME, bookmarkName)
192 bookmarkContentValues.put(BOOKMARK_URL, bookmarkUrl)
193 bookmarkContentValues.put(PARENT_FOLDER_ID, parentFolderId)
194 bookmarkContentValues.put(DISPLAY_ORDER, displayOrder)
195 bookmarkContentValues.put(IS_FOLDER, false)
196 bookmarkContentValues.put(FAVORITE_ICON, favoriteIcon)
198 // Get a writable database handle.
199 val bookmarksDatabase = this.writableDatabase
202 bookmarksDatabase.insert(BOOKMARKS_TABLE, null, bookmarkContentValues)
204 // Close the database handle.
205 bookmarksDatabase.close()
208 // Create a bookmark from content values.
209 fun createBookmark(contentValues: ContentValues) {
210 // Get a writable database.
211 val bookmarksDatabase = this.writableDatabase
214 bookmarksDatabase.insert(BOOKMARKS_TABLE, null, contentValues)
216 // Close the database handle.
217 bookmarksDatabase.close()
221 fun createFolder(folderName: String, parentFolderId: Long, favoriteIcon: ByteArray) {
222 // Create a bookmark folder content values.
223 val bookmarkFolderContentValues = ContentValues()
225 // The ID is created automatically. Folders are always created at the top of the list.
226 bookmarkFolderContentValues.put(BOOKMARK_NAME, folderName)
227 bookmarkFolderContentValues.put(PARENT_FOLDER_ID, parentFolderId)
228 bookmarkFolderContentValues.put(DISPLAY_ORDER, 0)
229 bookmarkFolderContentValues.put(IS_FOLDER, true)
230 bookmarkFolderContentValues.put(FOLDER_ID, generateFolderId())
231 bookmarkFolderContentValues.put(FAVORITE_ICON, favoriteIcon)
233 // Get a writable database handle.
234 val bookmarksDatabase = this.writableDatabase
236 // Insert the new folder.
237 bookmarksDatabase.insert(BOOKMARKS_TABLE, null, bookmarkFolderContentValues)
239 // Close the database handle.
240 bookmarksDatabase.close()
243 // Delete one bookmark.
244 fun deleteBookmark(databaseId: Int) {
245 // Get a writable database handle.
246 val bookmarksDatabase = this.writableDatabase
248 // Deletes the row with the given database ID.
249 bookmarksDatabase.delete(BOOKMARKS_TABLE, "$ID = $databaseId", null)
251 // Close the database handle.
252 bookmarksDatabase.close()
255 // Get a cursor for the bookmark with the specified database ID.
256 fun getBookmark(databaseId: Int): Cursor {
257 // Get a readable database handle.
258 val bookmarksDatabase = this.readableDatabase
260 // Return the cursor for the database ID. The cursor can't be closed because it is used in the parent activity.
261 return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $ID = $databaseId", null)
264 // Get a cursor for all bookmarks and folders by display order except those with the specified IDs.
265 fun getAllBookmarksByDisplayOrderExcept(exceptIdLongArray: LongArray): Cursor {
266 // Get a readable database handle.
267 val bookmarksDatabase = this.readableDatabase
269 // Prepare a string builder to contain the comma-separated list of IDs not to get.
270 val idsNotToGetStringBuilder = StringBuilder()
272 // Extract the array of IDs not to get to the string builder.
273 for (databaseIdLong in exceptIdLongArray) {
274 // Check to see if there is already a number in the builder.
275 if (idsNotToGetStringBuilder.isNotEmpty()) {
276 // This is not the first number, so place a `,` before the new number.
277 idsNotToGetStringBuilder.append(",")
280 // Add the new number to the builder.
281 idsNotToGetStringBuilder.append(databaseIdLong)
284 // Return a cursor with all the bookmarks except those specified ordered by display order. The cursor cannot be closed because it is used in the parent activity.
285 return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $ID NOT IN ($idsNotToGetStringBuilder) ORDER BY $DISPLAY_ORDER ASC", null)
288 // Get a cursor for all bookmarks and folders except those with the specified IDs.
289 fun getAllBookmarksExcept(exceptIdLongArray: LongArray): Cursor {
290 // Get a readable database handle.
291 val bookmarksDatabase = this.readableDatabase
293 // Prepare a string builder to contain the comma-separated list of IDs not to get.
294 val idsNotToGetStringBuilder = StringBuilder()
296 // Extract the array of IDs not to get to the string builder.
297 for (databaseIdLong in exceptIdLongArray) {
298 // Check to see if there is already a number in the builder.
299 if (idsNotToGetStringBuilder.isNotEmpty()) {
300 // This is not the first number, so place a `,` before the new number.
301 idsNotToGetStringBuilder.append(",")
304 // Add the new number to the builder.
305 idsNotToGetStringBuilder.append(databaseIdLong)
308 // Return a cursor with all the bookmarks except those specified. The cursor cannot be closed because it is used in the parent activity.
309 return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $ID NOT IN ($idsNotToGetStringBuilder)", null)
312 // 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.
313 fun getBookmarkIds(parentFolderId: Long): Cursor {
314 // Get a readable database handle.
315 val bookmarksDatabase = this.readableDatabase
317 // Return a cursor with all the database IDs. The cursor cannot be closed because it is used in the parent activity.
318 return bookmarksDatabase.rawQuery("SELECT $ID FROM $BOOKMARKS_TABLE WHERE $PARENT_FOLDER_ID = $parentFolderId", null)
321 // Get a cursor for bookmarks and folders in the specified folder.
322 fun getBookmarks(parentFolderId: Long): Cursor {
323 // Get a readable database handle.
324 val bookmarksDatabase = this.readableDatabase
326 // Return a cursor with all the bookmarks in a specified folder. The cursor cannot be closed because it is used in the parent activity.
327 return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $PARENT_FOLDER_ID = $parentFolderId", null)
330 // Get a cursor for bookmarks and folders in the specified folder ordered by display order.
331 fun getBookmarksByDisplayOrder(parentFolderId: Long): Cursor {
332 // Get a readable database handle.
333 val bookmarksDatabase = this.readableDatabase
335 // 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.
336 return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $PARENT_FOLDER_ID = $parentFolderId ORDER BY $DISPLAY_ORDER ASC", null)
339 // Get a cursor for bookmarks and folders in the specified folder by display order except those with the specified IDs.
340 fun getBookmarksByDisplayOrderExcept(exceptIdLongArray: LongArray, parentFolderId: Long): Cursor {
341 // Get a readable database handle.
342 val bookmarksDatabase = this.readableDatabase
344 // Prepare a string builder to contain the comma-separated list of IDs not to get.
345 val idsNotToGetStringBuilder = StringBuilder()
347 // Extract the array of IDs not to get to the string builder.
348 for (databaseIdLong in exceptIdLongArray) {
349 // Check to see if there is already a number in the builder.
350 if (idsNotToGetStringBuilder.isNotEmpty()) {
351 // This is not the first number, so place a `,` before the new number.
352 idsNotToGetStringBuilder.append(",")
355 // Add the new number to the builder.
356 idsNotToGetStringBuilder.append(databaseIdLong)
359 // Return a cursor with all the bookmarks in the specified folder except for those database IDs specified ordered by display order.
360 // The cursor cannot be closed because it will be used in the parent activity.
361 return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $PARENT_FOLDER_ID = $parentFolderId AND $ID NOT IN ($idsNotToGetStringBuilder) ORDER BY $DISPLAY_ORDER ASC", null)
364 // Get a cursor for bookmarks and folders in the specified folder except those with the specified IDs.
365 fun getBookmarksExcept(exceptIdLongArray: LongArray, parentFolderId: Long): Cursor {
366 // Get a readable database handle.
367 val bookmarksDatabase = this.readableDatabase
369 // Prepare a string builder to contain the comma-separated list of IDs not to get.
370 val idsNotToGetStringBuilder = StringBuilder()
372 // Extract the array of IDs not to get to the string builder.
373 for (databaseIdLong in exceptIdLongArray) {
374 // Check to see if there is already a number in the builder.
375 if (idsNotToGetStringBuilder.isNotEmpty()) {
376 // This is not the first number, so place a `,` before the new number.
377 idsNotToGetStringBuilder.append(",")
380 // Add the new number to the builder.
381 idsNotToGetStringBuilder.append(databaseIdLong)
384 // 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.
385 return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $PARENT_FOLDER_ID = $parentFolderId AND $ID NOT IN ($idsNotToGetStringBuilder)", null)
388 fun getFolderBookmarks(parentFolderId: Long): Cursor {
389 // Get a readable database handle.
390 val bookmarksDatabase = this.readableDatabase
392 // Return a cursor with all the bookmarks in the folder. The cursor cannot be closed because it is used in the parent activity.
393 return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $PARENT_FOLDER_ID = $parentFolderId AND $IS_FOLDER = 0 ORDER BY $DISPLAY_ORDER ASC", null)
396 // Get the database ID for the specified folder name.
397 fun getFolderDatabaseId(folderId: Long): Int {
398 // Get a readable database handle.
399 val bookmarksDatabase = this.readableDatabase
401 // Initialize the database ID.
404 // Get the cursor for the folder with the specified name.
405 val folderCursor = bookmarksDatabase.rawQuery("SELECT $ID FROM $BOOKMARKS_TABLE WHERE $FOLDER_ID = $folderId", null)
407 // Get the database ID if it exists.
408 if (folderCursor.count > 0) {
409 // Move to the first record.
410 folderCursor.moveToFirst()
412 // Get the database ID.
413 databaseId = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(ID))
416 // Close the cursor and the database handle.
418 bookmarksDatabase.close()
420 // Return the database ID.
424 // Get the folder ID for the specified folder database ID.
425 fun getFolderId(folderDatabaseId: Int): Long {
426 // Get a readable database handle.
427 val bookmarksDatabase = this.readableDatabase
429 // Get the cursor for the folder with the specified database ID.
430 val folderCursor = bookmarksDatabase.rawQuery("SELECT $FOLDER_ID FROM $BOOKMARKS_TABLE WHERE $ID = $folderDatabaseId", null)
432 // Move to the first record.
433 folderCursor.moveToFirst()
435 // Get the folder ID.
436 val folderId = folderCursor.getLong(folderCursor.getColumnIndexOrThrow(FOLDER_ID))
438 // Close the cursor and the database handle.
440 bookmarksDatabase.close()
442 // Return the folder ID.
446 // Get the folder name for the specified folder ID.
447 fun getFolderName(folderId: Long): String {
448 // Get a readable database handle.
449 val bookmarksDatabase = this.readableDatabase
451 // Initialize the folder name.
454 // Get the cursor for the folder with the specified folder ID.
455 val folderCursor = bookmarksDatabase.rawQuery("SELECT $BOOKMARK_NAME FROM $BOOKMARKS_TABLE WHERE $FOLDER_ID = $folderId", null)
457 // Get the folder name if it exists.
458 if (folderCursor.count > 0) {
459 // Move to the first record.
460 folderCursor.moveToFirst()
462 // Get the folder name.
463 folderName = folderCursor.getString(folderCursor.getColumnIndexOrThrow(BOOKMARK_NAME))
466 // Close the cursor and the database handle.
468 bookmarksDatabase.close()
470 // Return the folder name.
474 // Get a cursor of all the folders except those specified.
475 fun getFoldersExcept(exceptFolderIdLongList: List<Long>): Cursor {
476 // Prepare a string builder to contain the comma-separated list of IDs not to get.
477 val folderIdsNotToGetStringBuilder = StringBuilder()
479 // Extract the array of IDs not to get to the string builder.
480 for (folderId in exceptFolderIdLongList) {
481 // Check to see if there is already a number in the builder.
482 if (folderIdsNotToGetStringBuilder.isNotEmpty()) {
483 // This is not the first number, so place a `,` before the new number.
484 folderIdsNotToGetStringBuilder.append(",")
487 // Add the new number to the builder.
488 folderIdsNotToGetStringBuilder.append(folderId)
491 // Get an array list with all of the requested subfolders.
492 val subfoldersCursorArrayList = getSubfoldersExcept(HOME_FOLDER_ID, folderIdsNotToGetStringBuilder.toString())
495 return if (subfoldersCursorArrayList.isEmpty()) { // There are no folders. Return an empty cursor.
496 // A matrix cursor requires the definition of at least one column.
497 MatrixCursor(arrayOf(ID))
498 } else { // There is at least one folder.
499 // Use a merge cursor to return the folders.
500 MergeCursor(subfoldersCursorArrayList.toTypedArray())
504 // Determine if any folders exist beside the specified database IDs. The array of database IDs can include both bookmarks and folders.
505 fun hasFoldersExceptDatabaseId(exceptDatabaseIdLongArray: LongArray): Boolean {
506 // Create a folder ID long list.
507 val folderIdLongList = mutableListOf<Long>()
509 // Populate the list.
510 for (databaseId in exceptDatabaseIdLongArray) {
511 // Convert the database ID to an Int.
512 val databaseIdInt = databaseId.toInt()
514 // Only process database IDs that are folders.
515 if (isFolder(databaseIdInt)) {
516 // Add the folder ID to the list.
517 folderIdLongList.add(getFolderId(databaseIdInt))
521 // Get a lit of all the folders except those specified and their subfolders.
522 val foldersCursor = getFoldersExcept(folderIdLongList)
524 // Determine if any other folders exists.
525 val hasFolder = (foldersCursor.count > 0)
528 foldersCursor.close()
530 // Return the folder status.
534 // Get the name of the parent folder
535 fun getParentFolderId(currentFolderId: Long): Long {
536 // Get a readable database handle.
537 val bookmarksDatabase = this.readableDatabase
539 // Get a cursor for the current folder.
540 val bookmarkCursor = bookmarksDatabase.rawQuery("SELECT $PARENT_FOLDER_ID FROM $BOOKMARKS_TABLE WHERE $FOLDER_ID = $currentFolderId", null)
542 // Move to the first record.
543 bookmarkCursor.moveToFirst()
545 // Store the parent folder ID.
546 val parentFolderId = bookmarkCursor.getLong(bookmarkCursor.getColumnIndexOrThrow(PARENT_FOLDER_ID))
548 // Close the cursor and the database.
549 bookmarkCursor.close()
550 bookmarksDatabase.close()
552 // Return the parent folder string ID.
553 return parentFolderId
556 // Get the name of the parent folder.
557 fun getParentFolderId(databaseId: Int): Long {
558 // Get a readable database handle.
559 val bookmarksDatabase = this.readableDatabase
561 // Get a cursor for the specified database ID.
562 val bookmarkCursor = bookmarksDatabase.rawQuery("SELECT $PARENT_FOLDER_ID FROM $BOOKMARKS_TABLE WHERE $ID = $databaseId", null)
564 // Move to the first record.
565 bookmarkCursor.moveToFirst()
567 // Store the name of the parent folder.
568 val parentFolderId = bookmarkCursor.getLong(bookmarkCursor.getColumnIndexOrThrow(PARENT_FOLDER_ID))
570 // Close the cursor and the database.
571 bookmarkCursor.close()
572 bookmarksDatabase.close()
574 // Return the parent folder string.
575 return parentFolderId
578 // Get a cursor with the names and folder IDs of all the subfolders of the specified folder.
579 fun getSubfolderNamesAndFolderIds(currentFolderId: Long): Cursor {
580 // Get a readable database handle.
581 val bookmarksDatabase = this.readableDatabase
583 // Return the cursor with the subfolders. The cursor can't be closed because it is used in the parent activity.
584 return bookmarksDatabase.rawQuery("SELECT $BOOKMARK_NAME, $FOLDER_ID FROM $BOOKMARKS_TABLE WHERE $PARENT_FOLDER_ID = $currentFolderId AND $IS_FOLDER = 1", null)
587 fun getSubfolderSpacer(folderId: Long): String {
588 // Create a spacer string
589 var spacerString = ""
591 // Get the parent folder ID.
592 val parentFolderId = getParentFolderId(folderId)
594 // Check to see if the parent folder is not in the home folder.
595 if (parentFolderId != HOME_FOLDER_ID) {
596 // Add two spaces to the spacer string.
599 // Check the parent folder recursively.
600 spacerString += getSubfolderSpacer(parentFolderId)
603 // Return the spacer string.
607 private fun getSubfoldersExcept(folderId: Long, exceptFolderIdString: String): ArrayList<Cursor> {
608 // Get a readable database handle.
609 val bookmarksDatabase = this.readableDatabase
611 // Create a cursor array list.
612 val cursorArrayList = ArrayList<Cursor>()
614 // Create a matrix cursor column names.
615 val matrixCursorColumnNames = arrayOf(ID, BOOKMARK_NAME, FAVORITE_ICON, PARENT_FOLDER_ID, FOLDER_ID)
617 // Get a cursor with the subfolders.
618 val subfolderCursor = bookmarksDatabase.rawQuery(
619 "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)
621 // Get the subfolder cursor column indexes.
622 val idColumnIndex = subfolderCursor.getColumnIndexOrThrow(ID)
623 val nameColumnIndex = subfolderCursor.getColumnIndexOrThrow(BOOKMARK_NAME)
624 val favoriteIconColumnIndex = subfolderCursor.getColumnIndexOrThrow(FAVORITE_ICON)
625 val parentFolderIdColumnIndex = subfolderCursor.getColumnIndexOrThrow(PARENT_FOLDER_ID)
626 val folderIdColumnIndex = subfolderCursor.getColumnIndexOrThrow(FOLDER_ID)
628 while (subfolderCursor.moveToNext()) {
629 // Create an array list.
630 val matrixCursor = MatrixCursor(matrixCursorColumnNames)
632 // Add the subfolder to the matrix cursor.
633 matrixCursor.addRow(arrayOf<Any>(subfolderCursor.getInt(idColumnIndex), subfolderCursor.getString(nameColumnIndex), subfolderCursor.getBlob(favoriteIconColumnIndex),
634 subfolderCursor.getLong(parentFolderIdColumnIndex), subfolderCursor.getLong(folderIdColumnIndex)))
636 // Add the matrix cursor to the array list.
637 cursorArrayList.add(matrixCursor)
639 // Get all the sub-subfolders recursively
640 cursorArrayList.addAll(getSubfoldersExcept(subfolderCursor.getLong(folderIdColumnIndex), exceptFolderIdString))
643 // Close the subfolder cursor.
644 subfolderCursor.close()
646 // Return the matrix cursor.
647 return cursorArrayList
650 // Check if a database ID is a folder.
651 fun isFolder(databaseId: Int): Boolean {
652 // Get a readable database handle.
653 val bookmarksDatabase = this.readableDatabase
655 // Get a cursor with the is folder field for the specified database ID.
656 val folderCursor = bookmarksDatabase.rawQuery("SELECT $IS_FOLDER FROM $BOOKMARKS_TABLE WHERE $ID = $databaseId", null)
658 // Move to the first record.
659 folderCursor.moveToFirst()
661 // Ascertain if this database ID is a folder.
662 val isFolder = (folderCursor.getInt(folderCursor.getColumnIndexOrThrow(IS_FOLDER)) == 1)
664 // Close the cursor and the database handle.
666 bookmarksDatabase.close()
668 // Return the folder status.
672 // Move one bookmark or folder to a new folder.
673 fun moveToFolder(databaseId: Int, newFolderId: Long) {
674 // Get a writable database handle.
675 val bookmarksDatabase = this.writableDatabase
677 // Get a cursor for all the bookmarks in the new folder ordered by display order.
678 val newFolderCursor = bookmarksDatabase.rawQuery("SELECT $DISPLAY_ORDER FROM $BOOKMARKS_TABLE WHERE $PARENT_FOLDER_ID = $newFolderId ORDER BY $DISPLAY_ORDER ASC", null)
680 // Set the new display order.
681 val displayOrder: Int = if (newFolderCursor.count > 0) { // There are already bookmarks in the folder.
682 // Move to the last bookmark.
683 newFolderCursor.moveToLast()
685 // Set the display order to be one greater that the last bookmark.
686 newFolderCursor.getInt(newFolderCursor.getColumnIndexOrThrow(DISPLAY_ORDER)) + 1
687 } else { // There are no bookmarks in the new folder.
688 // Set the display order to be `0`.
693 newFolderCursor.close()
695 // Create a content values.
696 val bookmarkContentValues = ContentValues()
698 // Store the new values.
699 bookmarkContentValues.put(DISPLAY_ORDER, displayOrder)
700 bookmarkContentValues.put(PARENT_FOLDER_ID, newFolderId)
702 // Update the database.
703 bookmarksDatabase.update(BOOKMARKS_TABLE, bookmarkContentValues, "$ID = $databaseId", null)
705 // Close the database handle.
706 bookmarksDatabase.close()
709 // Update the bookmark name and URL.
710 fun updateBookmark(databaseId: Int, bookmarkName: String, bookmarkUrl: String) {
711 // Initialize a content values.
712 val bookmarkContentValues = ContentValues()
714 // Store the updated values.
715 bookmarkContentValues.put(BOOKMARK_NAME, bookmarkName)
716 bookmarkContentValues.put(BOOKMARK_URL, bookmarkUrl)
718 // Get a writable database handle.
719 val bookmarksDatabase = this.writableDatabase
721 // Update the bookmark.
722 bookmarksDatabase.update(BOOKMARKS_TABLE, bookmarkContentValues, "$ID = $databaseId", null)
724 // Close the database handle.
725 bookmarksDatabase.close()
728 // Update the bookmark name, URL, parent folder, and display order.
729 fun updateBookmark(databaseId: Int, bookmarkName: String, bookmarkUrl: String, parentFolderId: Long, displayOrder: Int) {
730 // Initialize a content values.
731 val bookmarkContentValues = ContentValues()
733 // Store the updated values.
734 bookmarkContentValues.put(BOOKMARK_NAME, bookmarkName)
735 bookmarkContentValues.put(BOOKMARK_URL, bookmarkUrl)
736 bookmarkContentValues.put(PARENT_FOLDER_ID, parentFolderId)
737 bookmarkContentValues.put(DISPLAY_ORDER, displayOrder)
739 // Get a writable database handle.
740 val bookmarksDatabase = this.writableDatabase
742 // Update the bookmark.
743 bookmarksDatabase.update(BOOKMARKS_TABLE, bookmarkContentValues, "$ID = $databaseId", null)
745 // Close the database handle.
746 bookmarksDatabase.close()
749 // Update the bookmark name, URL, and favorite icon.
750 fun updateBookmark(databaseId: Int, bookmarkName: String, bookmarkUrl: String, favoriteIcon: ByteArray) {
751 // Initialize a content values.
752 val bookmarkContentValues = ContentValues()
754 // Store the updated values.
755 bookmarkContentValues.put(BOOKMARK_NAME, bookmarkName)
756 bookmarkContentValues.put(BOOKMARK_URL, bookmarkUrl)
757 bookmarkContentValues.put(FAVORITE_ICON, favoriteIcon)
759 // Get a writable database handle.
760 val bookmarksDatabase = this.writableDatabase
762 // Update the bookmark.
763 bookmarksDatabase.update(BOOKMARKS_TABLE, bookmarkContentValues, "$ID = $databaseId", null)
765 // Close the database handle.
766 bookmarksDatabase.close()
769 // Update the bookmark name, URL, parent folder, display order, and favorite icon.
770 fun updateBookmark(databaseId: Int, bookmarkName: String, bookmarkUrl: String, parentFolderId: Long, displayOrder: Int, favoriteIcon: ByteArray) {
771 // Initialize a content values.
772 val bookmarkContentValues = ContentValues()
774 // Store the updated values.
775 bookmarkContentValues.put(BOOKMARK_NAME, bookmarkName)
776 bookmarkContentValues.put(BOOKMARK_URL, bookmarkUrl)
777 bookmarkContentValues.put(PARENT_FOLDER_ID, parentFolderId)
778 bookmarkContentValues.put(DISPLAY_ORDER, displayOrder)
779 bookmarkContentValues.put(FAVORITE_ICON, favoriteIcon)
781 // Get a writable database handle.
782 val bookmarksDatabase = this.writableDatabase
784 // Update the bookmark.
785 bookmarksDatabase.update(BOOKMARKS_TABLE, bookmarkContentValues, "$ID = $databaseId", null)
787 // Close the database handle.
788 bookmarksDatabase.close()
791 // Update the display order for one bookmark or folder.
792 fun updateDisplayOrder(databaseId: Int, displayOrder: Int) {
793 // Get a writable database handle.
794 val bookmarksDatabase = this.writableDatabase
796 // Create a content values.
797 val bookmarkContentValues = ContentValues()
799 // Store the new display order.
800 bookmarkContentValues.put(DISPLAY_ORDER, displayOrder)
802 // Update the database.
803 bookmarksDatabase.update(BOOKMARKS_TABLE, bookmarkContentValues, "$ID = $databaseId", null)
805 // Close the database handle.
806 bookmarksDatabase.close()
809 // Update the folder name.
810 fun updateFolder(databaseId: Int, newFolderName: String) {
811 // Get a writable database handle.
812 val bookmarksDatabase = this.writableDatabase
814 // Create a folder content values.
815 val folderContentValues = ContentValues()
817 // Store the new folder name.
818 folderContentValues.put(BOOKMARK_NAME, newFolderName)
820 // Run the update on the folder.
821 bookmarksDatabase.update(BOOKMARKS_TABLE, folderContentValues, "$ID = $databaseId", null)
823 // Close the database handle.
824 bookmarksDatabase.close()
827 // Update the folder name, parent folder, and display order.
828 fun updateFolder(databaseId: Int, newFolderName: String, parentFolderId: Long, displayOrder: Int) {
829 // Get a writable database handle.
830 val bookmarksDatabase = this.writableDatabase
832 // Create a folder content values.
833 val folderContentValues = ContentValues()
835 // Store the new folder values.
836 folderContentValues.put(BOOKMARK_NAME, newFolderName)
837 folderContentValues.put(PARENT_FOLDER_ID, parentFolderId)
838 folderContentValues.put(DISPLAY_ORDER, displayOrder)
840 // Run the update on the folder.
841 bookmarksDatabase.update(BOOKMARKS_TABLE, folderContentValues, "$ID = $databaseId", null)
843 // Close the database handle.
844 bookmarksDatabase.close()
847 // Update the folder name and icon.
848 fun updateFolder(databaseId: Int, newFolderName: String, folderIcon: ByteArray) {
849 // Get a writable database handle.
850 val bookmarksDatabase = this.writableDatabase
852 // Create a folder content values.
853 val folderContentValues = ContentValues()
855 // Store the updated values.
856 folderContentValues.put(BOOKMARK_NAME, newFolderName)
857 folderContentValues.put(FAVORITE_ICON, folderIcon)
859 // Run the update on the folder.
860 bookmarksDatabase.update(BOOKMARKS_TABLE, folderContentValues, "$ID = $databaseId", null)
862 // Close the database handle.
863 bookmarksDatabase.close()
866 // Update the folder name and icon.
867 fun updateFolder(databaseId: Int, newFolderName: String, parentFolderId: Long, displayOrder: Int, folderIcon: ByteArray) {
868 // Get a writable database handle.
869 val bookmarksDatabase = this.writableDatabase
871 // Create a folder content values.
872 val folderContentValues = ContentValues()
874 // Store the updated values.
875 folderContentValues.put(BOOKMARK_NAME, newFolderName)
876 folderContentValues.put(PARENT_FOLDER_ID, parentFolderId)
877 folderContentValues.put(DISPLAY_ORDER, displayOrder)
878 folderContentValues.put(FAVORITE_ICON, folderIcon)
880 // Run the update on the folder.
881 bookmarksDatabase.update(BOOKMARKS_TABLE, folderContentValues, "$ID = $databaseId", null)
883 // Close the database handle.
884 bookmarksDatabase.close()
887 private fun generateFolderId(): Long {
888 // Get the current time in epoch format (in milliseconds).
889 val possibleFolderId = Date().time
891 // Get a readable database.
892 val bookmarksDatabase = this.readableDatabase
894 // Get a cursor with any folders that already have this folder ID.
895 val existingFolderCursor = bookmarksDatabase.rawQuery("SELECT $ID FROM $BOOKMARKS_TABLE WHERE $FOLDER_ID = $possibleFolderId", null)
897 // Check if the folder ID is unique.
898 val folderIdIsUnique = (existingFolderCursor.count == 0)
901 existingFolderCursor.close()
903 // Either return the folder ID or test a new one.
904 return if (folderIdIsUnique)