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 with just the database IDs of all the bookmarks and folders. This is useful for counting the number of bookmarks imported.
176 val allBookmarkAndFolderIds: Cursor
178 // Get a readable database handle.
179 val bookmarksDatabase = this.readableDatabase
181 // Return a cursor with all the database IDs. The cursor cannot be closed because it is used in the parent activity.
182 return bookmarksDatabase.rawQuery("SELECT $ID FROM $BOOKMARKS_TABLE", null)
185 // Get a cursor for all bookmarks and folders ordered by display order.
186 val allBookmarksByDisplayOrder: Cursor
188 // Get a readable database handle.
189 val bookmarksDatabase = this.readableDatabase
191 // 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.
192 return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE ORDER BY $DISPLAY_ORDER ASC", null)
195 // Create a bookmark.
196 fun createBookmark(bookmarkName: String, bookmarkUrl: String, parentFolderId: Long, displayOrder: Int, favoriteIcon: ByteArray) {
197 // Store the bookmark data in a content values.
198 val bookmarkContentValues = ContentValues()
200 // The ID is created automatically.
201 bookmarkContentValues.put(BOOKMARK_NAME, bookmarkName)
202 bookmarkContentValues.put(BOOKMARK_URL, bookmarkUrl)
203 bookmarkContentValues.put(PARENT_FOLDER_ID, parentFolderId)
204 bookmarkContentValues.put(DISPLAY_ORDER, displayOrder)
205 bookmarkContentValues.put(IS_FOLDER, false)
206 bookmarkContentValues.put(FAVORITE_ICON, favoriteIcon)
208 // Get a writable database handle.
209 val bookmarksDatabase = this.writableDatabase
212 bookmarksDatabase.insert(BOOKMARKS_TABLE, null, bookmarkContentValues)
214 // Close the database handle.
215 bookmarksDatabase.close()
218 // Create a bookmark from content values.
219 fun createBookmark(contentValues: ContentValues) {
220 // Get a writable database.
221 val bookmarksDatabase = this.writableDatabase
224 bookmarksDatabase.insert(BOOKMARKS_TABLE, null, contentValues)
226 // Close the database handle.
227 bookmarksDatabase.close()
231 fun createFolder(folderName: String, parentFolderId: Long, displayOrder: Int, favoriteIcon: ByteArray): Long {
232 // Create a bookmark folder content values.
233 val bookmarkFolderContentValues = ContentValues()
235 // Generate the folder ID.
236 val folderId = generateFolderId()
238 // The ID is created automatically. Folders are always created at the top of the list.
239 bookmarkFolderContentValues.put(BOOKMARK_NAME, folderName)
240 bookmarkFolderContentValues.put(PARENT_FOLDER_ID, parentFolderId)
241 bookmarkFolderContentValues.put(DISPLAY_ORDER, displayOrder)
242 bookmarkFolderContentValues.put(IS_FOLDER, true)
243 bookmarkFolderContentValues.put(FOLDER_ID, folderId)
244 bookmarkFolderContentValues.put(FAVORITE_ICON, favoriteIcon)
246 // Get a writable database handle.
247 val bookmarksDatabase = this.writableDatabase
249 // Insert the new folder.
250 bookmarksDatabase.insert(BOOKMARKS_TABLE, null, bookmarkFolderContentValues)
252 // Close the database handle.
253 bookmarksDatabase.close()
255 // Return the new folder ID.
259 // Delete one bookmark.
260 fun deleteBookmark(databaseId: Int) {
261 // Get a writable database handle.
262 val bookmarksDatabase = this.writableDatabase
264 // Deletes the row with the given database ID.
265 bookmarksDatabase.delete(BOOKMARKS_TABLE, "$ID = $databaseId", null)
267 // Close the database handle.
268 bookmarksDatabase.close()
271 // Get a cursor for the bookmark with the specified database ID.
272 fun getBookmark(databaseId: Int): Cursor {
273 // Get a readable database handle.
274 val bookmarksDatabase = this.readableDatabase
276 // Return the cursor for the database ID. The cursor can't be closed because it is used in the parent activity.
277 return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $ID = $databaseId", null)
280 // Get a cursor for all bookmarks and folders by display order except those with the specified IDs.
281 fun getAllBookmarksByDisplayOrderExcept(exceptIdLongArray: LongArray): Cursor {
282 // Get a readable database handle.
283 val bookmarksDatabase = this.readableDatabase
285 // Prepare a string builder to contain the comma-separated list of IDs not to get.
286 val idsNotToGetStringBuilder = StringBuilder()
288 // Extract the array of IDs not to get to the string builder.
289 for (databaseIdLong in exceptIdLongArray) {
290 // Check to see if there is already a number in the builder.
291 if (idsNotToGetStringBuilder.isNotEmpty()) {
292 // This is not the first number, so place a `,` before the new number.
293 idsNotToGetStringBuilder.append(",")
296 // Add the new number to the builder.
297 idsNotToGetStringBuilder.append(databaseIdLong)
300 // 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.
301 return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $ID NOT IN ($idsNotToGetStringBuilder) ORDER BY $DISPLAY_ORDER ASC", null)
304 // Get a cursor for all bookmarks and folders except those with the specified IDs.
305 fun getAllBookmarksExcept(exceptIdLongArray: LongArray): Cursor {
306 // Get a readable database handle.
307 val bookmarksDatabase = this.readableDatabase
309 // Prepare a string builder to contain the comma-separated list of IDs not to get.
310 val idsNotToGetStringBuilder = StringBuilder()
312 // Extract the array of IDs not to get to the string builder.
313 for (databaseIdLong in exceptIdLongArray) {
314 // Check to see if there is already a number in the builder.
315 if (idsNotToGetStringBuilder.isNotEmpty()) {
316 // This is not the first number, so place a `,` before the new number.
317 idsNotToGetStringBuilder.append(",")
320 // Add the new number to the builder.
321 idsNotToGetStringBuilder.append(databaseIdLong)
324 // Return a cursor with all the bookmarks except those specified. The cursor cannot be closed because it is used in the parent activity.
325 return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $ID NOT IN ($idsNotToGetStringBuilder)", null)
328 // Get a cursor with just the database IDs of bookmarks and folders in the specified folder. This is useful for deleting folders with bookmarks that have favorite icons too large to fit in a cursor.
329 fun getBookmarkAndFolderIds(parentFolderId: Long): Cursor {
330 // Get a readable database handle.
331 val bookmarksDatabase = this.readableDatabase
333 // Return a cursor with all the database IDs. The cursor cannot be closed because it is used in the parent activity.
334 return bookmarksDatabase.rawQuery("SELECT $ID FROM $BOOKMARKS_TABLE WHERE $PARENT_FOLDER_ID = $parentFolderId", null)
337 // Get a cursor for bookmarks and folders in the specified folder.
338 fun getBookmarks(parentFolderId: Long): Cursor {
339 // Get a readable database handle.
340 val bookmarksDatabase = this.readableDatabase
342 // Return a cursor with all the bookmarks in a specified folder. The cursor cannot be closed because it is used in the parent activity.
343 return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $PARENT_FOLDER_ID = $parentFolderId", null)
346 // Get a cursor for bookmarks and folders in the specified folder ordered by display order.
347 fun getBookmarksByDisplayOrder(parentFolderId: Long): Cursor {
348 // Get a readable database handle.
349 val bookmarksDatabase = this.readableDatabase
351 // 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.
352 return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $PARENT_FOLDER_ID = $parentFolderId ORDER BY $DISPLAY_ORDER ASC", null)
355 // Get a cursor for bookmarks and folders in the specified folder by display order except those with the specified IDs.
356 fun getBookmarksByDisplayOrderExcept(exceptIdLongArray: LongArray, parentFolderId: Long): Cursor {
357 // Get a readable database handle.
358 val bookmarksDatabase = this.readableDatabase
360 // Prepare a string builder to contain the comma-separated list of IDs not to get.
361 val idsNotToGetStringBuilder = StringBuilder()
363 // Extract the array of IDs not to get to the string builder.
364 for (databaseIdLong in exceptIdLongArray) {
365 // Check to see if there is already a number in the builder.
366 if (idsNotToGetStringBuilder.isNotEmpty()) {
367 // This is not the first number, so place a `,` before the new number.
368 idsNotToGetStringBuilder.append(",")
371 // Add the new number to the builder.
372 idsNotToGetStringBuilder.append(databaseIdLong)
375 // Return a cursor with all the bookmarks in the specified folder except for those database IDs specified ordered by display order.
376 // The cursor cannot be closed because it will be used in the parent activity.
377 return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $PARENT_FOLDER_ID = $parentFolderId AND $ID NOT IN ($idsNotToGetStringBuilder) ORDER BY $DISPLAY_ORDER ASC", null)
380 // Get a cursor for bookmarks and folders in the specified folder except those with the specified IDs.
381 fun getBookmarksExcept(exceptIdLongArray: LongArray, parentFolderId: Long): Cursor {
382 // Get a readable database handle.
383 val bookmarksDatabase = this.readableDatabase
385 // Prepare a string builder to contain the comma-separated list of IDs not to get.
386 val idsNotToGetStringBuilder = StringBuilder()
388 // Extract the array of IDs not to get to the string builder.
389 for (databaseIdLong in exceptIdLongArray) {
390 // Check to see if there is already a number in the builder.
391 if (idsNotToGetStringBuilder.isNotEmpty()) {
392 // This is not the first number, so place a `,` before the new number.
393 idsNotToGetStringBuilder.append(",")
396 // Add the new number to the builder.
397 idsNotToGetStringBuilder.append(databaseIdLong)
400 // 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.
401 return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $PARENT_FOLDER_ID = $parentFolderId AND $ID NOT IN ($idsNotToGetStringBuilder)", null)
404 fun getFolderBookmarks(parentFolderId: Long): Cursor {
405 // Get a readable database handle.
406 val bookmarksDatabase = this.readableDatabase
408 // Return a cursor with all the bookmarks in the folder. The cursor cannot be closed because it is used in the parent activity.
409 return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $PARENT_FOLDER_ID = $parentFolderId AND $IS_FOLDER = 0 ORDER BY $DISPLAY_ORDER ASC", null)
412 // Get the database ID for the specified folder name.
413 fun getFolderDatabaseId(folderId: Long): Int {
414 // Get a readable database handle.
415 val bookmarksDatabase = this.readableDatabase
417 // Initialize the database ID.
420 // Get the cursor for the folder with the specified name.
421 val folderCursor = bookmarksDatabase.rawQuery("SELECT $ID FROM $BOOKMARKS_TABLE WHERE $FOLDER_ID = $folderId", null)
423 // Get the database ID if it exists.
424 if (folderCursor.count > 0) {
425 // Move to the first record.
426 folderCursor.moveToFirst()
428 // Get the database ID.
429 databaseId = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(ID))
432 // Close the cursor and the database handle.
434 bookmarksDatabase.close()
436 // Return the database ID.
440 // Get the folder ID for the specified folder database ID.
441 fun getFolderId(folderDatabaseId: Int): Long {
442 // Get a readable database handle.
443 val bookmarksDatabase = this.readableDatabase
445 // Get the cursor for the folder with the specified database ID.
446 val folderCursor = bookmarksDatabase.rawQuery("SELECT $FOLDER_ID FROM $BOOKMARKS_TABLE WHERE $ID = $folderDatabaseId", null)
448 // Move to the first record.
449 folderCursor.moveToFirst()
451 // Get the folder ID.
452 val folderId = folderCursor.getLong(folderCursor.getColumnIndexOrThrow(FOLDER_ID))
454 // Close the cursor and the database handle.
456 bookmarksDatabase.close()
458 // Return the folder ID.
462 // Get the folder name for the specified folder ID.
463 fun getFolderName(folderId: Long): String {
464 // Get a readable database handle.
465 val bookmarksDatabase = this.readableDatabase
467 // Initialize the folder name.
470 // Get the cursor for the folder with the specified folder ID.
471 val folderCursor = bookmarksDatabase.rawQuery("SELECT $BOOKMARK_NAME FROM $BOOKMARKS_TABLE WHERE $FOLDER_ID = $folderId", null)
473 // Get the folder name if it exists.
474 if (folderCursor.count > 0) {
475 // Move to the first record.
476 folderCursor.moveToFirst()
478 // Get the folder name.
479 folderName = folderCursor.getString(folderCursor.getColumnIndexOrThrow(BOOKMARK_NAME))
482 // Close the cursor and the database handle.
484 bookmarksDatabase.close()
486 // Return the folder name.
490 // Get a cursor of all the folders except those specified.
491 fun getFoldersExcept(exceptFolderIdLongList: List<Long>): Cursor {
492 // Prepare a string builder to contain the comma-separated list of IDs not to get.
493 val folderIdsNotToGetStringBuilder = StringBuilder()
495 // Extract the array of IDs not to get to the string builder.
496 for (folderId in exceptFolderIdLongList) {
497 // Check to see if there is already a number in the builder.
498 if (folderIdsNotToGetStringBuilder.isNotEmpty()) {
499 // This is not the first number, so place a `,` before the new number.
500 folderIdsNotToGetStringBuilder.append(",")
503 // Add the new number to the builder.
504 folderIdsNotToGetStringBuilder.append(folderId)
507 // Get an array list with all of the requested subfolders.
508 val subfoldersCursorArrayList = getSubfoldersExcept(HOME_FOLDER_ID, folderIdsNotToGetStringBuilder.toString())
511 return if (subfoldersCursorArrayList.isEmpty()) { // There are no folders. Return an empty cursor.
512 // A matrix cursor requires the definition of at least one column.
513 MatrixCursor(arrayOf(ID))
514 } else { // There is at least one folder.
515 // Use a merge cursor to return the folders.
516 MergeCursor(subfoldersCursorArrayList.toTypedArray())
520 // Determine if any folders exist beside the specified database IDs. The array of database IDs can include both bookmarks and folders.
521 fun hasFoldersExceptDatabaseId(exceptDatabaseIdLongArray: LongArray): Boolean {
522 // Create a folder ID long list.
523 val folderIdLongList = mutableListOf<Long>()
525 // Populate the list.
526 for (databaseId in exceptDatabaseIdLongArray) {
527 // Convert the database ID to an Int.
528 val databaseIdInt = databaseId.toInt()
530 // Only process database IDs that are folders.
531 if (isFolder(databaseIdInt)) {
532 // Add the folder ID to the list.
533 folderIdLongList.add(getFolderId(databaseIdInt))
537 // Get a lit of all the folders except those specified and their subfolders.
538 val foldersCursor = getFoldersExcept(folderIdLongList)
540 // Determine if any other folders exists.
541 val hasFolder = (foldersCursor.count > 0)
544 foldersCursor.close()
546 // Return the folder status.
550 // Get the name of the parent folder
551 fun getParentFolderId(currentFolderId: Long): Long {
552 // Get a readable database handle.
553 val bookmarksDatabase = this.readableDatabase
555 // Get a cursor for the current folder.
556 val bookmarkCursor = bookmarksDatabase.rawQuery("SELECT $PARENT_FOLDER_ID FROM $BOOKMARKS_TABLE WHERE $FOLDER_ID = $currentFolderId", null)
558 // Move to the first record.
559 bookmarkCursor.moveToFirst()
561 // Store the parent folder ID.
562 val parentFolderId = bookmarkCursor.getLong(bookmarkCursor.getColumnIndexOrThrow(PARENT_FOLDER_ID))
564 // Close the cursor and the database.
565 bookmarkCursor.close()
566 bookmarksDatabase.close()
568 // Return the parent folder string ID.
569 return parentFolderId
572 // Get the name of the parent folder.
573 fun getParentFolderId(databaseId: Int): Long {
574 // Get a readable database handle.
575 val bookmarksDatabase = this.readableDatabase
577 // Get a cursor for the specified database ID.
578 val bookmarkCursor = bookmarksDatabase.rawQuery("SELECT $PARENT_FOLDER_ID FROM $BOOKMARKS_TABLE WHERE $ID = $databaseId", null)
580 // Move to the first record.
581 bookmarkCursor.moveToFirst()
583 // Store the name of the parent folder.
584 val parentFolderId = bookmarkCursor.getLong(bookmarkCursor.getColumnIndexOrThrow(PARENT_FOLDER_ID))
586 // Close the cursor and the database.
587 bookmarkCursor.close()
588 bookmarksDatabase.close()
590 // Return the parent folder string.
591 return parentFolderId
594 // Get a cursor with the names and folder IDs of all the subfolders of the specified folder.
595 fun getSubfolderNamesAndFolderIds(currentFolderId: Long): Cursor {
596 // Get a readable database handle.
597 val bookmarksDatabase = this.readableDatabase
599 // Return the cursor with the subfolders. The cursor can't be closed because it is used in the parent activity.
600 return bookmarksDatabase.rawQuery("SELECT $BOOKMARK_NAME, $FOLDER_ID FROM $BOOKMARKS_TABLE WHERE $PARENT_FOLDER_ID = $currentFolderId AND $IS_FOLDER = 1", null)
603 fun getSubfolderSpacer(folderId: Long): String {
604 // Create a spacer string
605 var spacerString = ""
607 // Get the parent folder ID.
608 val parentFolderId = getParentFolderId(folderId)
610 // Check to see if the parent folder is not in the home folder.
611 if (parentFolderId != HOME_FOLDER_ID) {
612 // Add two spaces to the spacer string.
615 // Check the parent folder recursively.
616 spacerString += getSubfolderSpacer(parentFolderId)
619 // Return the spacer string.
623 private fun getSubfoldersExcept(folderId: Long, exceptFolderIdString: String): ArrayList<Cursor> {
624 // Get a readable database handle.
625 val bookmarksDatabase = this.readableDatabase
627 // Create a cursor array list.
628 val cursorArrayList = ArrayList<Cursor>()
630 // Create a matrix cursor column names.
631 val matrixCursorColumnNames = arrayOf(ID, BOOKMARK_NAME, FAVORITE_ICON, PARENT_FOLDER_ID, FOLDER_ID)
633 // Get a cursor with the subfolders.
634 val subfolderCursor = bookmarksDatabase.rawQuery(
635 "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)
637 // Get the subfolder cursor column indexes.
638 val idColumnIndex = subfolderCursor.getColumnIndexOrThrow(ID)
639 val nameColumnIndex = subfolderCursor.getColumnIndexOrThrow(BOOKMARK_NAME)
640 val favoriteIconColumnIndex = subfolderCursor.getColumnIndexOrThrow(FAVORITE_ICON)
641 val parentFolderIdColumnIndex = subfolderCursor.getColumnIndexOrThrow(PARENT_FOLDER_ID)
642 val folderIdColumnIndex = subfolderCursor.getColumnIndexOrThrow(FOLDER_ID)
644 while (subfolderCursor.moveToNext()) {
645 // Create an array list.
646 val matrixCursor = MatrixCursor(matrixCursorColumnNames)
648 // Add the subfolder to the matrix cursor.
649 matrixCursor.addRow(arrayOf<Any>(subfolderCursor.getInt(idColumnIndex), subfolderCursor.getString(nameColumnIndex), subfolderCursor.getBlob(favoriteIconColumnIndex),
650 subfolderCursor.getLong(parentFolderIdColumnIndex), subfolderCursor.getLong(folderIdColumnIndex)))
652 // Add the matrix cursor to the array list.
653 cursorArrayList.add(matrixCursor)
655 // Get all the sub-subfolders recursively
656 cursorArrayList.addAll(getSubfoldersExcept(subfolderCursor.getLong(folderIdColumnIndex), exceptFolderIdString))
659 // Close the subfolder cursor.
660 subfolderCursor.close()
662 // Return the matrix cursor.
663 return cursorArrayList
666 // Check if a database ID is a folder.
667 fun isFolder(databaseId: Int): Boolean {
668 // Get a readable database handle.
669 val bookmarksDatabase = this.readableDatabase
671 // Get a cursor with the is folder field for the specified database ID.
672 val folderCursor = bookmarksDatabase.rawQuery("SELECT $IS_FOLDER FROM $BOOKMARKS_TABLE WHERE $ID = $databaseId", null)
674 // Move to the first record.
675 folderCursor.moveToFirst()
677 // Ascertain if this database ID is a folder.
678 val isFolder = (folderCursor.getInt(folderCursor.getColumnIndexOrThrow(IS_FOLDER)) == 1)
680 // Close the cursor and the database handle.
682 bookmarksDatabase.close()
684 // Return the folder status.
688 // Move one bookmark or folder to a new folder.
689 fun moveToFolder(databaseId: Int, newFolderId: Long) {
690 // Get a writable database handle.
691 val bookmarksDatabase = this.writableDatabase
693 // Get a cursor for all the bookmarks in the new folder ordered by display order.
694 val newFolderCursor = bookmarksDatabase.rawQuery("SELECT $DISPLAY_ORDER FROM $BOOKMARKS_TABLE WHERE $PARENT_FOLDER_ID = $newFolderId ORDER BY $DISPLAY_ORDER ASC", null)
696 // Set the new display order.
697 val displayOrder: Int = if (newFolderCursor.count > 0) { // There are already bookmarks in the folder.
698 // Move to the last bookmark.
699 newFolderCursor.moveToLast()
701 // Set the display order to be one greater that the last bookmark.
702 newFolderCursor.getInt(newFolderCursor.getColumnIndexOrThrow(DISPLAY_ORDER)) + 1
703 } else { // There are no bookmarks in the new folder.
704 // Set the display order to be `0`.
709 newFolderCursor.close()
711 // Create a content values.
712 val bookmarkContentValues = ContentValues()
714 // Store the new values.
715 bookmarkContentValues.put(DISPLAY_ORDER, displayOrder)
716 bookmarkContentValues.put(PARENT_FOLDER_ID, newFolderId)
718 // Update the database.
719 bookmarksDatabase.update(BOOKMARKS_TABLE, bookmarkContentValues, "$ID = $databaseId", null)
721 // Close the database handle.
722 bookmarksDatabase.close()
725 // Update the bookmark name and URL.
726 fun updateBookmark(databaseId: Int, bookmarkName: String, bookmarkUrl: String) {
727 // Initialize a content values.
728 val bookmarkContentValues = ContentValues()
730 // Store the updated values.
731 bookmarkContentValues.put(BOOKMARK_NAME, bookmarkName)
732 bookmarkContentValues.put(BOOKMARK_URL, bookmarkUrl)
734 // Get a writable database handle.
735 val bookmarksDatabase = this.writableDatabase
737 // Update the bookmark.
738 bookmarksDatabase.update(BOOKMARKS_TABLE, bookmarkContentValues, "$ID = $databaseId", null)
740 // Close the database handle.
741 bookmarksDatabase.close()
744 // Update the bookmark name, URL, parent folder, and display order.
745 fun updateBookmark(databaseId: Int, bookmarkName: String, bookmarkUrl: String, parentFolderId: Long, displayOrder: Int) {
746 // Initialize a content values.
747 val bookmarkContentValues = ContentValues()
749 // Store the updated values.
750 bookmarkContentValues.put(BOOKMARK_NAME, bookmarkName)
751 bookmarkContentValues.put(BOOKMARK_URL, bookmarkUrl)
752 bookmarkContentValues.put(PARENT_FOLDER_ID, parentFolderId)
753 bookmarkContentValues.put(DISPLAY_ORDER, displayOrder)
755 // Get a writable database handle.
756 val bookmarksDatabase = this.writableDatabase
758 // Update the bookmark.
759 bookmarksDatabase.update(BOOKMARKS_TABLE, bookmarkContentValues, "$ID = $databaseId", null)
761 // Close the database handle.
762 bookmarksDatabase.close()
765 // Update the bookmark name, URL, and favorite icon.
766 fun updateBookmark(databaseId: Int, bookmarkName: String, bookmarkUrl: String, favoriteIcon: ByteArray) {
767 // Initialize a content values.
768 val bookmarkContentValues = ContentValues()
770 // Store the updated values.
771 bookmarkContentValues.put(BOOKMARK_NAME, bookmarkName)
772 bookmarkContentValues.put(BOOKMARK_URL, bookmarkUrl)
773 bookmarkContentValues.put(FAVORITE_ICON, favoriteIcon)
775 // Get a writable database handle.
776 val bookmarksDatabase = this.writableDatabase
778 // Update the bookmark.
779 bookmarksDatabase.update(BOOKMARKS_TABLE, bookmarkContentValues, "$ID = $databaseId", null)
781 // Close the database handle.
782 bookmarksDatabase.close()
785 // Update the bookmark name, URL, parent folder, display order, and favorite icon.
786 fun updateBookmark(databaseId: Int, bookmarkName: String, bookmarkUrl: String, parentFolderId: Long, displayOrder: Int, favoriteIcon: ByteArray) {
787 // Initialize a content values.
788 val bookmarkContentValues = ContentValues()
790 // Store the updated values.
791 bookmarkContentValues.put(BOOKMARK_NAME, bookmarkName)
792 bookmarkContentValues.put(BOOKMARK_URL, bookmarkUrl)
793 bookmarkContentValues.put(PARENT_FOLDER_ID, parentFolderId)
794 bookmarkContentValues.put(DISPLAY_ORDER, displayOrder)
795 bookmarkContentValues.put(FAVORITE_ICON, favoriteIcon)
797 // Get a writable database handle.
798 val bookmarksDatabase = this.writableDatabase
800 // Update the bookmark.
801 bookmarksDatabase.update(BOOKMARKS_TABLE, bookmarkContentValues, "$ID = $databaseId", null)
803 // Close the database handle.
804 bookmarksDatabase.close()
807 // Update the display order for one bookmark or folder.
808 fun updateDisplayOrder(databaseId: Int, displayOrder: Int) {
809 // Get a writable database handle.
810 val bookmarksDatabase = this.writableDatabase
812 // Create a content values.
813 val bookmarkContentValues = ContentValues()
815 // Store the new display order.
816 bookmarkContentValues.put(DISPLAY_ORDER, displayOrder)
818 // Update the database.
819 bookmarksDatabase.update(BOOKMARKS_TABLE, bookmarkContentValues, "$ID = $databaseId", null)
821 // Close the database handle.
822 bookmarksDatabase.close()
825 // Update the folder name.
826 fun updateFolder(databaseId: Int, newFolderName: String) {
827 // Get a writable database handle.
828 val bookmarksDatabase = this.writableDatabase
830 // Create a folder content values.
831 val folderContentValues = ContentValues()
833 // Store the new folder name.
834 folderContentValues.put(BOOKMARK_NAME, newFolderName)
836 // Run the update on the folder.
837 bookmarksDatabase.update(BOOKMARKS_TABLE, folderContentValues, "$ID = $databaseId", null)
839 // Close the database handle.
840 bookmarksDatabase.close()
843 // Update the folder name, parent folder, and display order.
844 fun updateFolder(databaseId: Int, newFolderName: String, parentFolderId: Long, displayOrder: Int) {
845 // Get a writable database handle.
846 val bookmarksDatabase = this.writableDatabase
848 // Create a folder content values.
849 val folderContentValues = ContentValues()
851 // Store the new folder values.
852 folderContentValues.put(BOOKMARK_NAME, newFolderName)
853 folderContentValues.put(PARENT_FOLDER_ID, parentFolderId)
854 folderContentValues.put(DISPLAY_ORDER, displayOrder)
856 // Run the update on the folder.
857 bookmarksDatabase.update(BOOKMARKS_TABLE, folderContentValues, "$ID = $databaseId", null)
859 // Close the database handle.
860 bookmarksDatabase.close()
863 // Update the folder name and icon.
864 fun updateFolder(databaseId: Int, newFolderName: String, folderIcon: ByteArray) {
865 // Get a writable database handle.
866 val bookmarksDatabase = this.writableDatabase
868 // Create a folder content values.
869 val folderContentValues = ContentValues()
871 // Store the updated values.
872 folderContentValues.put(BOOKMARK_NAME, newFolderName)
873 folderContentValues.put(FAVORITE_ICON, folderIcon)
875 // Run the update on the folder.
876 bookmarksDatabase.update(BOOKMARKS_TABLE, folderContentValues, "$ID = $databaseId", null)
878 // Close the database handle.
879 bookmarksDatabase.close()
882 // Update the folder name and icon.
883 fun updateFolder(databaseId: Int, newFolderName: String, parentFolderId: Long, displayOrder: Int, folderIcon: ByteArray) {
884 // Get a writable database handle.
885 val bookmarksDatabase = this.writableDatabase
887 // Create a folder content values.
888 val folderContentValues = ContentValues()
890 // Store the updated values.
891 folderContentValues.put(BOOKMARK_NAME, newFolderName)
892 folderContentValues.put(PARENT_FOLDER_ID, parentFolderId)
893 folderContentValues.put(DISPLAY_ORDER, displayOrder)
894 folderContentValues.put(FAVORITE_ICON, folderIcon)
896 // Run the update on the folder.
897 bookmarksDatabase.update(BOOKMARKS_TABLE, folderContentValues, "$ID = $databaseId", null)
899 // Close the database handle.
900 bookmarksDatabase.close()
903 private fun generateFolderId(): Long {
904 // Get the current time in epoch format (in milliseconds).
905 val possibleFolderId = Date().time
907 // Get a readable database.
908 val bookmarksDatabase = this.readableDatabase
910 // Get a cursor with any folders that already have this folder ID.
911 val existingFolderCursor = bookmarksDatabase.rawQuery("SELECT $ID FROM $BOOKMARKS_TABLE WHERE $FOLDER_ID = $possibleFolderId", null)
913 // Check if the folder ID is unique.
914 val folderIdIsUnique = (existingFolderCursor.count == 0)
917 existingFolderCursor.close()
919 // Either return the folder ID or test a new one.
920 return if (folderIdIsUnique)