]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/helpers/BookmarksDatabaseHelper.kt
c4c8ebb0a1e8f0de4ec973e27b5c9efe597f4e71
[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 for all bookmarks and folders ordered by display order.
176     val allBookmarksByDisplayOrder: Cursor
177         get() {
178             // Get a readable database handle.
179             val bookmarksDatabase = this.readableDatabase
180
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)
183         }
184
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()
189
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)
197
198         // Get a writable database handle.
199         val bookmarksDatabase = this.writableDatabase
200
201         // Insert a new row.
202         bookmarksDatabase.insert(BOOKMARKS_TABLE, null, bookmarkContentValues)
203
204         // Close the database handle.
205         bookmarksDatabase.close()
206     }
207
208     // Create a bookmark from content values.
209     fun createBookmark(contentValues: ContentValues) {
210         // Get a writable database.
211         val bookmarksDatabase = this.writableDatabase
212
213         // Insert a new row.
214         bookmarksDatabase.insert(BOOKMARKS_TABLE, null, contentValues)
215
216         // Close the database handle.
217         bookmarksDatabase.close()
218     }
219
220     // Create a folder.
221     fun createFolder(folderName: String, parentFolderId: Long, favoriteIcon: ByteArray) {
222         // Create a bookmark folder content values.
223         val bookmarkFolderContentValues = ContentValues()
224
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)
232
233         // Get a writable database handle.
234         val bookmarksDatabase = this.writableDatabase
235
236         // Insert the new folder.
237         bookmarksDatabase.insert(BOOKMARKS_TABLE, null, bookmarkFolderContentValues)
238
239         // Close the database handle.
240         bookmarksDatabase.close()
241     }
242
243     // Delete one bookmark.
244     fun deleteBookmark(databaseId: Int) {
245         // Get a writable database handle.
246         val bookmarksDatabase = this.writableDatabase
247
248         // Deletes the row with the given database ID.
249         bookmarksDatabase.delete(BOOKMARKS_TABLE, "$ID = $databaseId", null)
250
251         // Close the database handle.
252         bookmarksDatabase.close()
253     }
254
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
259
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)
262     }
263
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
268
269         // Prepare a string builder to contain the comma-separated list of IDs not to get.
270         val idsNotToGetStringBuilder = StringBuilder()
271
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(",")
278             }
279
280             // Add the new number to the builder.
281             idsNotToGetStringBuilder.append(databaseIdLong)
282         }
283
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)
286     }
287
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
292
293         // Prepare a string builder to contain the comma-separated list of IDs not to get.
294         val idsNotToGetStringBuilder = StringBuilder()
295
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(",")
302             }
303
304             // Add the new number to the builder.
305             idsNotToGetStringBuilder.append(databaseIdLong)
306         }
307
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)
310     }
311
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
316
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)
319     }
320
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
325
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)
328     }
329
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
334
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)
337     }
338
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
343
344         // Prepare a string builder to contain the comma-separated list of IDs not to get.
345         val idsNotToGetStringBuilder = StringBuilder()
346
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(",")
353             }
354
355             // Add the new number to the builder.
356             idsNotToGetStringBuilder.append(databaseIdLong)
357         }
358
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)
362     }
363
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
368
369         // Prepare a string builder to contain the comma-separated list of IDs not to get.
370         val idsNotToGetStringBuilder = StringBuilder()
371
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(",")
378             }
379
380             // Add the new number to the builder.
381             idsNotToGetStringBuilder.append(databaseIdLong)
382         }
383
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)
386     }
387
388     fun getFolderBookmarks(parentFolderId: Long): Cursor {
389         // Get a readable database handle.
390         val bookmarksDatabase = this.readableDatabase
391
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)
394     }
395
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
400
401         // Initialize the database ID.
402         var databaseId = 0
403
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)
406
407         // Get the database ID if it exists.
408         if (folderCursor.count > 0) {
409             // Move to the first record.
410             folderCursor.moveToFirst()
411
412             // Get the database ID.
413             databaseId = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(ID))
414         }
415
416         // Close the cursor and the database handle.
417         folderCursor.close()
418         bookmarksDatabase.close()
419
420         // Return the database ID.
421         return databaseId
422     }
423
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
428
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)
431
432         // Move to the first record.
433         folderCursor.moveToFirst()
434
435         // Get the folder ID.
436         val folderId = folderCursor.getLong(folderCursor.getColumnIndexOrThrow(FOLDER_ID))
437
438         // Close the cursor and the database handle.
439         folderCursor.close()
440         bookmarksDatabase.close()
441
442         // Return the folder ID.
443         return folderId
444     }
445
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
450
451         // Initialize the folder name.
452         var folderName = ""
453
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)
456
457         // Get the folder name if it exists.
458         if (folderCursor.count > 0) {
459             // Move to the first record.
460             folderCursor.moveToFirst()
461
462             // Get the folder name.
463             folderName = folderCursor.getString(folderCursor.getColumnIndexOrThrow(BOOKMARK_NAME))
464         }
465
466         // Close the cursor and the database handle.
467         folderCursor.close()
468         bookmarksDatabase.close()
469
470         // Return the folder name.
471         return folderName
472     }
473
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()
478
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(",")
485             }
486
487             // Add the new number to the builder.
488             folderIdsNotToGetStringBuilder.append(folderId)
489         }
490
491         // Get an array list with all of the requested subfolders.
492         val subfoldersCursorArrayList = getSubfoldersExcept(HOME_FOLDER_ID, folderIdsNotToGetStringBuilder.toString())
493
494         // Return a cursor.
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())
501         }
502     }
503
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>()
508
509         // Populate the list.
510         for (databaseId in exceptDatabaseIdLongArray) {
511             // Convert the database ID to an Int.
512             val databaseIdInt = databaseId.toInt()
513
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))
518             }
519         }
520
521         // Get a lit of all the folders except those specified and their subfolders.
522         val foldersCursor = getFoldersExcept(folderIdLongList)
523
524         // Determine if any other folders exists.
525         val hasFolder = (foldersCursor.count > 0)
526
527         // Close the cursor.
528         foldersCursor.close()
529
530         // Return the folder status.
531         return hasFolder
532     }
533
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
538
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)
541
542         // Move to the first record.
543         bookmarkCursor.moveToFirst()
544
545         // Store the parent folder ID.
546         val parentFolderId = bookmarkCursor.getLong(bookmarkCursor.getColumnIndexOrThrow(PARENT_FOLDER_ID))
547
548         // Close the cursor and the database.
549         bookmarkCursor.close()
550         bookmarksDatabase.close()
551
552         // Return the parent folder string ID.
553         return parentFolderId
554     }
555
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
560
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)
563
564         // Move to the first record.
565         bookmarkCursor.moveToFirst()
566
567         // Store the name of the parent folder.
568         val parentFolderId = bookmarkCursor.getLong(bookmarkCursor.getColumnIndexOrThrow(PARENT_FOLDER_ID))
569
570         // Close the cursor and the database.
571         bookmarkCursor.close()
572         bookmarksDatabase.close()
573
574         // Return the parent folder string.
575         return parentFolderId
576     }
577
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
582
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)
585     }
586
587     fun getSubfolderSpacer(folderId: Long): String {
588         // Create a spacer string
589         var spacerString = ""
590
591         // Get the parent folder ID.
592         val parentFolderId = getParentFolderId(folderId)
593
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.
597             spacerString += "  "
598
599             // Check the parent folder recursively.
600             spacerString += getSubfolderSpacer(parentFolderId)
601         }
602
603         // Return the spacer string.
604         return spacerString
605     }
606
607     private fun getSubfoldersExcept(folderId: Long, exceptFolderIdString: String): ArrayList<Cursor> {
608         // Get a readable database handle.
609         val bookmarksDatabase = this.readableDatabase
610
611         // Create a cursor array list.
612         val cursorArrayList = ArrayList<Cursor>()
613
614         // Create a matrix cursor column names.
615         val matrixCursorColumnNames = arrayOf(ID, BOOKMARK_NAME, FAVORITE_ICON, PARENT_FOLDER_ID, FOLDER_ID)
616
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)
620
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)
627
628         while (subfolderCursor.moveToNext()) {
629             // Create an array list.
630             val matrixCursor = MatrixCursor(matrixCursorColumnNames)
631
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)))
635
636             // Add the matrix cursor to the array list.
637             cursorArrayList.add(matrixCursor)
638
639             // Get all the sub-subfolders recursively
640             cursorArrayList.addAll(getSubfoldersExcept(subfolderCursor.getLong(folderIdColumnIndex), exceptFolderIdString))
641         }
642
643         // Close the subfolder cursor.
644         subfolderCursor.close()
645
646         // Return the matrix cursor.
647         return cursorArrayList
648     }
649
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
654
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)
657
658         // Move to the first record.
659         folderCursor.moveToFirst()
660
661         // Ascertain if this database ID is a folder.
662         val isFolder = (folderCursor.getInt(folderCursor.getColumnIndexOrThrow(IS_FOLDER)) == 1)
663
664         // Close the cursor and the database handle.
665         folderCursor.close()
666         bookmarksDatabase.close()
667
668         // Return the folder status.
669         return isFolder
670     }
671
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
676
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)
679
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()
684
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`.
689             0
690         }
691
692         // Close the cursor.
693         newFolderCursor.close()
694
695         // Create a content values.
696         val bookmarkContentValues = ContentValues()
697
698         // Store the new values.
699         bookmarkContentValues.put(DISPLAY_ORDER, displayOrder)
700         bookmarkContentValues.put(PARENT_FOLDER_ID, newFolderId)
701
702         // Update the database.
703         bookmarksDatabase.update(BOOKMARKS_TABLE, bookmarkContentValues, "$ID = $databaseId", null)
704
705         // Close the database handle.
706         bookmarksDatabase.close()
707     }
708
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()
713
714         // Store the updated values.
715         bookmarkContentValues.put(BOOKMARK_NAME, bookmarkName)
716         bookmarkContentValues.put(BOOKMARK_URL, bookmarkUrl)
717
718         // Get a writable database handle.
719         val bookmarksDatabase = this.writableDatabase
720
721         // Update the bookmark.
722         bookmarksDatabase.update(BOOKMARKS_TABLE, bookmarkContentValues, "$ID = $databaseId", null)
723
724         // Close the database handle.
725         bookmarksDatabase.close()
726     }
727
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()
732
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)
738
739         // Get a writable database handle.
740         val bookmarksDatabase = this.writableDatabase
741
742         // Update the bookmark.
743         bookmarksDatabase.update(BOOKMARKS_TABLE, bookmarkContentValues, "$ID = $databaseId", null)
744
745         // Close the database handle.
746         bookmarksDatabase.close()
747     }
748
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()
753
754         // Store the updated values.
755         bookmarkContentValues.put(BOOKMARK_NAME, bookmarkName)
756         bookmarkContentValues.put(BOOKMARK_URL, bookmarkUrl)
757         bookmarkContentValues.put(FAVORITE_ICON, favoriteIcon)
758
759         // Get a writable database handle.
760         val bookmarksDatabase = this.writableDatabase
761
762         // Update the bookmark.
763         bookmarksDatabase.update(BOOKMARKS_TABLE, bookmarkContentValues, "$ID = $databaseId", null)
764
765         // Close the database handle.
766         bookmarksDatabase.close()
767     }
768
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()
773
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)
780
781         // Get a writable database handle.
782         val bookmarksDatabase = this.writableDatabase
783
784         // Update the bookmark.
785         bookmarksDatabase.update(BOOKMARKS_TABLE, bookmarkContentValues, "$ID = $databaseId", null)
786
787         // Close the database handle.
788         bookmarksDatabase.close()
789     }
790
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
795
796         // Create a content values.
797         val bookmarkContentValues = ContentValues()
798
799         // Store the new display order.
800         bookmarkContentValues.put(DISPLAY_ORDER, displayOrder)
801
802         // Update the database.
803         bookmarksDatabase.update(BOOKMARKS_TABLE, bookmarkContentValues, "$ID = $databaseId", null)
804
805         // Close the database handle.
806         bookmarksDatabase.close()
807     }
808
809     // Update the folder name.
810     fun updateFolder(databaseId: Int, newFolderName: String) {
811         // Get a writable database handle.
812         val bookmarksDatabase = this.writableDatabase
813
814         // Create a folder content values.
815         val folderContentValues = ContentValues()
816
817         // Store the new folder name.
818         folderContentValues.put(BOOKMARK_NAME, newFolderName)
819
820         // Run the update on the folder.
821         bookmarksDatabase.update(BOOKMARKS_TABLE, folderContentValues, "$ID = $databaseId", null)
822
823         // Close the database handle.
824         bookmarksDatabase.close()
825     }
826
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
831
832         // Create a folder content values.
833         val folderContentValues = ContentValues()
834
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)
839
840         // Run the update on the folder.
841         bookmarksDatabase.update(BOOKMARKS_TABLE, folderContentValues, "$ID = $databaseId", null)
842
843         // Close the database handle.
844         bookmarksDatabase.close()
845     }
846
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
851
852         // Create a folder content values.
853         val folderContentValues = ContentValues()
854
855         // Store the updated values.
856         folderContentValues.put(BOOKMARK_NAME, newFolderName)
857         folderContentValues.put(FAVORITE_ICON, folderIcon)
858
859         // Run the update on the folder.
860         bookmarksDatabase.update(BOOKMARKS_TABLE, folderContentValues, "$ID = $databaseId", null)
861
862         // Close the database handle.
863         bookmarksDatabase.close()
864     }
865
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
870
871         // Create a folder content values.
872         val folderContentValues = ContentValues()
873
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)
879
880         // Run the update on the folder.
881         bookmarksDatabase.update(BOOKMARKS_TABLE, folderContentValues, "$ID = $databaseId", null)
882
883         // Close the database handle.
884         bookmarksDatabase.close()
885     }
886
887     private fun generateFolderId(): Long {
888         // Get the current time in epoch format.
889         val possibleFolderId = Date().time
890
891         // Get a readable database.
892         val bookmarksDatabase = this.readableDatabase
893
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)
896
897         // Check if the folder ID is unique.
898         val folderIdIsUnique = (existingFolderCursor.count == 0)
899
900         // Close the cursor.
901         existingFolderCursor.close()
902
903         // Either return the folder ID or test a new one.
904         return if (folderIdIsUnique)
905             possibleFolderId
906         else
907             generateFolderId()
908     }
909 }