]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/helpers/BookmarksDatabaseHelper.kt
First wrong button text in View Headers in night theme. https://redmine.stoutner...
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / helpers / BookmarksDatabaseHelper.kt
1 /*
2  * Copyright 2016-2023 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
5  *
6  * Privacy Browser Android is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * Privacy Browser Android is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 package com.stoutner.privacybrowser.helpers
21
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
30
31 import com.stoutner.privacybrowser.activities.HOME_FOLDER_ID
32
33 import java.util.Date
34
35 // Define the class constants.
36 private const val SCHEMA_VERSION = 2
37
38 // Define the public database constants.
39 const val BOOKMARKS_DATABASE = "bookmarks.db"
40 const val BOOKMARKS_TABLE = "bookmarks"
41
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"
50
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)"
61
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)
66     }
67
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.
70         if (oldVersion < 2) {
71             // Add the folder ID column.
72             bookmarksDatabase.execSQL("ALTER TABLE $BOOKMARKS_TABLE ADD COLUMN $FOLDER_ID INTEGER")
73
74             // Get a cursor with all the folders.
75             val foldersCursor = bookmarksDatabase.rawQuery("SELECT $ID FROM $BOOKMARKS_TABLE WHERE $IS_FOLDER = 1", null)
76
77             // Get the folders cursor ID column index.
78             val foldersCursorIdColumnIndex = foldersCursor.getColumnIndexOrThrow(ID)
79
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)
84
85                 // Generate a folder ID.
86                 val folderId = Date().time
87
88                 // Create a folder content values.
89                 val folderContentValues = ContentValues()
90
91                 // Store the new folder ID in the content values.
92                 folderContentValues.put(FOLDER_ID, folderId)
93
94                 // Update the folder with the new folder ID.
95                 bookmarksDatabase.update(BOOKMARKS_TABLE, folderContentValues, "$ID = $databaseId", null)
96
97                 // Wait 2 milliseconds to ensure that the next folder ID is unique.
98                 Thread.sleep(2)
99             }
100
101             // Close the folders cursor.
102             foldersCursor.close()
103
104
105             // Add the parent folder ID column.
106             bookmarksDatabase.execSQL("ALTER TABLE $BOOKMARKS_TABLE ADD COLUMN $PARENT_FOLDER_ID INTEGER")
107
108             // Get a cursor with all the bookmarks.
109             val bookmarksCursor = bookmarksDatabase.rawQuery("SELECT $ID, parentfolder FROM $BOOKMARKS_TABLE", null)
110
111             // Get the bookmarks cursor ID column index.
112             val bookmarksCursorIdColumnIndex = bookmarksCursor.getColumnIndexOrThrow(ID)
113             val bookmarksCursorParentFolderColumnIndex = bookmarksCursor.getColumnIndexOrThrow("parentfolder")
114
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)
120
121                 // Initialize the new parent folder ID.
122                 var newParentFolderId = HOME_FOLDER_ID
123
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)
128
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)
131
132                     // Get the new parent folder ID if it exists.
133                     if (parentFolderCursor.count > 0) {
134                         // Move to the first entry.
135                         parentFolderCursor.moveToFirst()
136
137                         // Get the new parent folder ID.
138                         newParentFolderId = parentFolderCursor.getLong(parentFolderCursor.getColumnIndexOrThrow(FOLDER_ID))
139                     }
140
141                     // Close the parent folder cursor.
142                     parentFolderCursor.close()
143                 }
144
145                 // Create a bookmark content values.
146                 val bookmarkContentValues = ContentValues()
147
148                 // Store the new parent folder ID in the content values.
149                 bookmarkContentValues.put(PARENT_FOLDER_ID, newParentFolderId)
150
151                 // Update the folder with the new folder ID.
152                 bookmarksDatabase.update(BOOKMARKS_TABLE, bookmarkContentValues, "$ID = $databaseId", null)
153             }
154
155             // Close the bookmarks cursor.
156             bookmarksCursor.close()
157
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.
162         }
163     }
164
165     // Get a cursor for all bookmarks and folders.
166     val allBookmarks: Cursor
167         get() {
168             // Get a readable database handle.
169             val bookmarksDatabase = this.readableDatabase
170
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)
173         }
174
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
177         get() {
178             // Get a readable database handle.
179             val bookmarksDatabase = this.readableDatabase
180
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)
183         }
184
185     // Get a cursor for all bookmarks and folders ordered by display order.
186     val allBookmarksByDisplayOrder: Cursor
187         get() {
188             // Get a readable database handle.
189             val bookmarksDatabase = this.readableDatabase
190
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)
193         }
194
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()
199
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)
207
208         // Get a writable database handle.
209         val bookmarksDatabase = this.writableDatabase
210
211         // Insert a new row.
212         bookmarksDatabase.insert(BOOKMARKS_TABLE, null, bookmarkContentValues)
213
214         // Close the database handle.
215         bookmarksDatabase.close()
216     }
217
218     // Create a bookmark from content values.
219     fun createBookmark(contentValues: ContentValues) {
220         // Get a writable database.
221         val bookmarksDatabase = this.writableDatabase
222
223         // Insert a new row.
224         bookmarksDatabase.insert(BOOKMARKS_TABLE, null, contentValues)
225
226         // Close the database handle.
227         bookmarksDatabase.close()
228     }
229
230     // Create a folder.
231     fun createFolder(folderName: String, parentFolderId: Long, displayOrder: Int, favoriteIcon: ByteArray): Long {
232         // Create a bookmark folder content values.
233         val bookmarkFolderContentValues = ContentValues()
234
235         // Generate the folder ID.
236         val folderId = generateFolderId()
237
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)
245
246         // Get a writable database handle.
247         val bookmarksDatabase = this.writableDatabase
248
249         // Insert the new folder.
250         bookmarksDatabase.insert(BOOKMARKS_TABLE, null, bookmarkFolderContentValues)
251
252         // Close the database handle.
253         bookmarksDatabase.close()
254
255         // Return the new folder ID.
256         return folderId
257     }
258
259     // Delete one bookmark.
260     fun deleteBookmark(databaseId: Int) {
261         // Get a writable database handle.
262         val bookmarksDatabase = this.writableDatabase
263
264         // Deletes the row with the given database ID.
265         bookmarksDatabase.delete(BOOKMARKS_TABLE, "$ID = $databaseId", null)
266
267         // Close the database handle.
268         bookmarksDatabase.close()
269     }
270
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
275
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)
278     }
279
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
284
285         // Prepare a string builder to contain the comma-separated list of IDs not to get.
286         val idsNotToGetStringBuilder = StringBuilder()
287
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(",")
294             }
295
296             // Add the new number to the builder.
297             idsNotToGetStringBuilder.append(databaseIdLong)
298         }
299
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)
302     }
303
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
308
309         // Prepare a string builder to contain the comma-separated list of IDs not to get.
310         val idsNotToGetStringBuilder = StringBuilder()
311
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(",")
318             }
319
320             // Add the new number to the builder.
321             idsNotToGetStringBuilder.append(databaseIdLong)
322         }
323
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)
326     }
327
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
332
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)
335     }
336
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
341
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)
344     }
345
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
350
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)
353     }
354
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
359
360         // Prepare a string builder to contain the comma-separated list of IDs not to get.
361         val idsNotToGetStringBuilder = StringBuilder()
362
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(",")
369             }
370
371             // Add the new number to the builder.
372             idsNotToGetStringBuilder.append(databaseIdLong)
373         }
374
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)
378     }
379
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
384
385         // Prepare a string builder to contain the comma-separated list of IDs not to get.
386         val idsNotToGetStringBuilder = StringBuilder()
387
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(",")
394             }
395
396             // Add the new number to the builder.
397             idsNotToGetStringBuilder.append(databaseIdLong)
398         }
399
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)
402     }
403
404     fun getFolderBookmarks(parentFolderId: Long): Cursor {
405         // Get a readable database handle.
406         val bookmarksDatabase = this.readableDatabase
407
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)
410     }
411
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
416
417         // Initialize the database ID.
418         var databaseId = 0
419
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)
422
423         // Get the database ID if it exists.
424         if (folderCursor.count > 0) {
425             // Move to the first record.
426             folderCursor.moveToFirst()
427
428             // Get the database ID.
429             databaseId = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(ID))
430         }
431
432         // Close the cursor and the database handle.
433         folderCursor.close()
434         bookmarksDatabase.close()
435
436         // Return the database ID.
437         return databaseId
438     }
439
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
444
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)
447
448         // Move to the first record.
449         folderCursor.moveToFirst()
450
451         // Get the folder ID.
452         val folderId = folderCursor.getLong(folderCursor.getColumnIndexOrThrow(FOLDER_ID))
453
454         // Close the cursor and the database handle.
455         folderCursor.close()
456         bookmarksDatabase.close()
457
458         // Return the folder ID.
459         return folderId
460     }
461
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
466
467         // Initialize the folder name.
468         var folderName = ""
469
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)
472
473         // Get the folder name if it exists.
474         if (folderCursor.count > 0) {
475             // Move to the first record.
476             folderCursor.moveToFirst()
477
478             // Get the folder name.
479             folderName = folderCursor.getString(folderCursor.getColumnIndexOrThrow(BOOKMARK_NAME))
480         }
481
482         // Close the cursor and the database handle.
483         folderCursor.close()
484         bookmarksDatabase.close()
485
486         // Return the folder name.
487         return folderName
488     }
489
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()
494
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(",")
501             }
502
503             // Add the new number to the builder.
504             folderIdsNotToGetStringBuilder.append(folderId)
505         }
506
507         // Get an array list with all of the requested subfolders.
508         val subfoldersCursorArrayList = getSubfoldersExcept(HOME_FOLDER_ID, folderIdsNotToGetStringBuilder.toString())
509
510         // Return a cursor.
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())
517         }
518     }
519
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>()
524
525         // Populate the list.
526         for (databaseId in exceptDatabaseIdLongArray) {
527             // Convert the database ID to an Int.
528             val databaseIdInt = databaseId.toInt()
529
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))
534             }
535         }
536
537         // Get a lit of all the folders except those specified and their subfolders.
538         val foldersCursor = getFoldersExcept(folderIdLongList)
539
540         // Determine if any other folders exists.
541         val hasFolder = (foldersCursor.count > 0)
542
543         // Close the cursor.
544         foldersCursor.close()
545
546         // Return the folder status.
547         return hasFolder
548     }
549
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
554
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)
557
558         // Move to the first record.
559         bookmarkCursor.moveToFirst()
560
561         // Store the parent folder ID.
562         val parentFolderId = bookmarkCursor.getLong(bookmarkCursor.getColumnIndexOrThrow(PARENT_FOLDER_ID))
563
564         // Close the cursor and the database.
565         bookmarkCursor.close()
566         bookmarksDatabase.close()
567
568         // Return the parent folder string ID.
569         return parentFolderId
570     }
571
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
576
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)
579
580         // Move to the first record.
581         bookmarkCursor.moveToFirst()
582
583         // Store the name of the parent folder.
584         val parentFolderId = bookmarkCursor.getLong(bookmarkCursor.getColumnIndexOrThrow(PARENT_FOLDER_ID))
585
586         // Close the cursor and the database.
587         bookmarkCursor.close()
588         bookmarksDatabase.close()
589
590         // Return the parent folder string.
591         return parentFolderId
592     }
593
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
598
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)
601     }
602
603     fun getSubfolderSpacer(folderId: Long): String {
604         // Create a spacer string
605         var spacerString = ""
606
607         // Get the parent folder ID.
608         val parentFolderId = getParentFolderId(folderId)
609
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.
613             spacerString += "  "
614
615             // Check the parent folder recursively.
616             spacerString += getSubfolderSpacer(parentFolderId)
617         }
618
619         // Return the spacer string.
620         return spacerString
621     }
622
623     private fun getSubfoldersExcept(folderId: Long, exceptFolderIdString: String): ArrayList<Cursor> {
624         // Get a readable database handle.
625         val bookmarksDatabase = this.readableDatabase
626
627         // Create a cursor array list.
628         val cursorArrayList = ArrayList<Cursor>()
629
630         // Create a matrix cursor column names.
631         val matrixCursorColumnNames = arrayOf(ID, BOOKMARK_NAME, FAVORITE_ICON, PARENT_FOLDER_ID, FOLDER_ID)
632
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)
636
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)
643
644         while (subfolderCursor.moveToNext()) {
645             // Create an array list.
646             val matrixCursor = MatrixCursor(matrixCursorColumnNames)
647
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)))
651
652             // Add the matrix cursor to the array list.
653             cursorArrayList.add(matrixCursor)
654
655             // Get all the sub-subfolders recursively
656             cursorArrayList.addAll(getSubfoldersExcept(subfolderCursor.getLong(folderIdColumnIndex), exceptFolderIdString))
657         }
658
659         // Close the subfolder cursor.
660         subfolderCursor.close()
661
662         // Return the matrix cursor.
663         return cursorArrayList
664     }
665
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
670
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)
673
674         // Move to the first record.
675         folderCursor.moveToFirst()
676
677         // Ascertain if this database ID is a folder.
678         val isFolder = (folderCursor.getInt(folderCursor.getColumnIndexOrThrow(IS_FOLDER)) == 1)
679
680         // Close the cursor and the database handle.
681         folderCursor.close()
682         bookmarksDatabase.close()
683
684         // Return the folder status.
685         return isFolder
686     }
687
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
692
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)
695
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()
700
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`.
705             0
706         }
707
708         // Close the cursor.
709         newFolderCursor.close()
710
711         // Create a content values.
712         val bookmarkContentValues = ContentValues()
713
714         // Store the new values.
715         bookmarkContentValues.put(DISPLAY_ORDER, displayOrder)
716         bookmarkContentValues.put(PARENT_FOLDER_ID, newFolderId)
717
718         // Update the database.
719         bookmarksDatabase.update(BOOKMARKS_TABLE, bookmarkContentValues, "$ID = $databaseId", null)
720
721         // Close the database handle.
722         bookmarksDatabase.close()
723     }
724
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()
729
730         // Store the updated values.
731         bookmarkContentValues.put(BOOKMARK_NAME, bookmarkName)
732         bookmarkContentValues.put(BOOKMARK_URL, bookmarkUrl)
733
734         // Get a writable database handle.
735         val bookmarksDatabase = this.writableDatabase
736
737         // Update the bookmark.
738         bookmarksDatabase.update(BOOKMARKS_TABLE, bookmarkContentValues, "$ID = $databaseId", null)
739
740         // Close the database handle.
741         bookmarksDatabase.close()
742     }
743
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()
748
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)
754
755         // Get a writable database handle.
756         val bookmarksDatabase = this.writableDatabase
757
758         // Update the bookmark.
759         bookmarksDatabase.update(BOOKMARKS_TABLE, bookmarkContentValues, "$ID = $databaseId", null)
760
761         // Close the database handle.
762         bookmarksDatabase.close()
763     }
764
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()
769
770         // Store the updated values.
771         bookmarkContentValues.put(BOOKMARK_NAME, bookmarkName)
772         bookmarkContentValues.put(BOOKMARK_URL, bookmarkUrl)
773         bookmarkContentValues.put(FAVORITE_ICON, favoriteIcon)
774
775         // Get a writable database handle.
776         val bookmarksDatabase = this.writableDatabase
777
778         // Update the bookmark.
779         bookmarksDatabase.update(BOOKMARKS_TABLE, bookmarkContentValues, "$ID = $databaseId", null)
780
781         // Close the database handle.
782         bookmarksDatabase.close()
783     }
784
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()
789
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)
796
797         // Get a writable database handle.
798         val bookmarksDatabase = this.writableDatabase
799
800         // Update the bookmark.
801         bookmarksDatabase.update(BOOKMARKS_TABLE, bookmarkContentValues, "$ID = $databaseId", null)
802
803         // Close the database handle.
804         bookmarksDatabase.close()
805     }
806
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
811
812         // Create a content values.
813         val bookmarkContentValues = ContentValues()
814
815         // Store the new display order.
816         bookmarkContentValues.put(DISPLAY_ORDER, displayOrder)
817
818         // Update the database.
819         bookmarksDatabase.update(BOOKMARKS_TABLE, bookmarkContentValues, "$ID = $databaseId", null)
820
821         // Close the database handle.
822         bookmarksDatabase.close()
823     }
824
825     // Update the folder name.
826     fun updateFolder(databaseId: Int, newFolderName: String) {
827         // Get a writable database handle.
828         val bookmarksDatabase = this.writableDatabase
829
830         // Create a folder content values.
831         val folderContentValues = ContentValues()
832
833         // Store the new folder name.
834         folderContentValues.put(BOOKMARK_NAME, newFolderName)
835
836         // Run the update on the folder.
837         bookmarksDatabase.update(BOOKMARKS_TABLE, folderContentValues, "$ID = $databaseId", null)
838
839         // Close the database handle.
840         bookmarksDatabase.close()
841     }
842
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
847
848         // Create a folder content values.
849         val folderContentValues = ContentValues()
850
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)
855
856         // Run the update on the folder.
857         bookmarksDatabase.update(BOOKMARKS_TABLE, folderContentValues, "$ID = $databaseId", null)
858
859         // Close the database handle.
860         bookmarksDatabase.close()
861     }
862
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
867
868         // Create a folder content values.
869         val folderContentValues = ContentValues()
870
871         // Store the updated values.
872         folderContentValues.put(BOOKMARK_NAME, newFolderName)
873         folderContentValues.put(FAVORITE_ICON, folderIcon)
874
875         // Run the update on the folder.
876         bookmarksDatabase.update(BOOKMARKS_TABLE, folderContentValues, "$ID = $databaseId", null)
877
878         // Close the database handle.
879         bookmarksDatabase.close()
880     }
881
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
886
887         // Create a folder content values.
888         val folderContentValues = ContentValues()
889
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)
895
896         // Run the update on the folder.
897         bookmarksDatabase.update(BOOKMARKS_TABLE, folderContentValues, "$ID = $databaseId", null)
898
899         // Close the database handle.
900         bookmarksDatabase.close()
901     }
902
903     private fun generateFolderId(): Long {
904         // Get the current time in epoch format (in milliseconds).
905         val possibleFolderId = Date().time
906
907         // Get a readable database.
908         val bookmarksDatabase = this.readableDatabase
909
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)
912
913         // Check if the folder ID is unique.
914         val folderIdIsUnique = (existingFolderCursor.count == 0)
915
916         // Close the cursor.
917         existingFolderCursor.close()
918
919         // Either return the folder ID or test a new one.
920         return if (folderIdIsUnique)
921             possibleFolderId
922         else
923             generateFolderId()
924     }
925 }