]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/activities/BookmarksActivity.kt
First wrong button text in View Headers in night theme. https://redmine.stoutner...
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / activities / BookmarksActivity.kt
1 /*
2  * Copyright 2016-2024 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.activities
21
22 import android.content.Context
23 import android.content.Intent
24 import android.database.Cursor
25 import android.graphics.Bitmap
26 import android.graphics.BitmapFactory
27 import android.graphics.Typeface
28 import android.graphics.drawable.BitmapDrawable
29 import android.os.Bundle
30 import android.util.SparseBooleanArray
31 import android.view.ActionMode
32 import android.view.Menu
33 import android.view.MenuItem
34 import android.view.View
35 import android.view.ViewGroup
36 import android.view.Window
37 import android.view.WindowManager
38 import android.widget.AbsListView.MultiChoiceModeListener
39 import android.widget.AdapterView
40 import android.widget.EditText
41 import android.widget.ImageView
42 import android.widget.ListView
43 import android.widget.RadioButton
44 import android.widget.TextView
45
46 import androidx.activity.OnBackPressedCallback
47 import androidx.appcompat.app.ActionBar
48 import androidx.appcompat.app.AppCompatActivity
49 import androidx.appcompat.widget.Toolbar
50 import androidx.core.graphics.drawable.toBitmap
51 import androidx.cursoradapter.widget.CursorAdapter
52 import androidx.fragment.app.DialogFragment
53 import androidx.preference.PreferenceManager
54
55 import com.google.android.material.floatingactionbutton.FloatingActionButton
56 import com.google.android.material.snackbar.Snackbar
57
58 import com.stoutner.privacybrowser.R
59 import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog
60 import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog
61 import com.stoutner.privacybrowser.dialogs.EditBookmarkDialog
62 import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDialog
63 import com.stoutner.privacybrowser.dialogs.MoveToFolderDialog
64 import com.stoutner.privacybrowser.helpers.BOOKMARK_NAME
65 import com.stoutner.privacybrowser.helpers.DISPLAY_ORDER
66 import com.stoutner.privacybrowser.helpers.FAVORITE_ICON
67 import com.stoutner.privacybrowser.helpers.FOLDER_ID
68 import com.stoutner.privacybrowser.helpers.ID
69 import com.stoutner.privacybrowser.helpers.IS_FOLDER
70 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper
71
72 import java.io.ByteArrayOutputStream
73 import java.util.function.Consumer
74
75 // Define the public constants.
76 const val CURRENT_FAVORITE_ICON_BYTE_ARRAY = "A"
77 const val CURRENT_FOLDER_ID = "B"
78 const val CURRENT_TITLE = "C"
79
80 // Define the private constants.
81 private const val CHECKED_BOOKMARKS_ARRAY_LIST = "D"
82
83 class BookmarksActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener, EditBookmarkDialog.EditBookmarkListener,
84     EditBookmarkFolderDialog.EditBookmarkFolderListener, MoveToFolderDialog.MoveToFolderListener {
85
86     companion object {
87         // Define the public static variables, which are accessed from the bookmarks database view activity.
88         var currentFolderId: Long = 0
89         var restartFromBookmarksDatabaseViewActivity = false
90     }
91
92     // Define the class variables.
93     private var bookmarksDeletedSnackbar: Snackbar? = null
94     private var checkingManyBookmarks = false
95     private var closeActivityAfterDismissingSnackbar = false
96     private var contextualActionMode: ActionMode? = null
97
98     // Declare the class variables.
99     private lateinit var appBar: ActionBar
100     private lateinit var bookmarksCursor: Cursor
101     private lateinit var bookmarksCursorAdapter: CursorAdapter
102     private lateinit var bookmarksDatabaseHelper: BookmarksDatabaseHelper
103     private lateinit var bookmarksListView: ListView
104     private lateinit var currentFavoriteIconByteArray: ByteArray
105     private lateinit var moveBookmarkDownMenuItem: MenuItem
106     private lateinit var moveBookmarkUpMenuItem: MenuItem
107     private lateinit var moveToFolderMenuItem: MenuItem
108
109     override fun onCreate(savedInstanceState: Bundle?) {
110         // Get a handle for the shared preferences.
111         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
112
113         // Get the preferences.
114         val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
115         val bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false)
116
117         // Disable screenshots if not allowed.
118         if (!allowScreenshots) {
119             window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
120         }
121
122         // Run the default commands.
123         super.onCreate(savedInstanceState)
124
125         // Get the intent that launched the activity.
126         val launchingIntent = intent
127
128         // Populate the variables from the launching intent.
129         currentFolderId = launchingIntent.getLongExtra(CURRENT_FOLDER_ID, HOME_FOLDER_ID)
130         val currentTitle = launchingIntent.getStringExtra(CURRENT_TITLE)!!
131         val currentUrl = launchingIntent.getStringExtra(CURRENT_URL)!!
132         currentFavoriteIconByteArray = launchingIntent.getByteArrayExtra(CURRENT_FAVORITE_ICON_BYTE_ARRAY)!!
133
134         // Convert the favorite icon byte array to a bitmap.
135         val currentFavoriteIconBitmap = BitmapFactory.decodeByteArray(currentFavoriteIconByteArray, 0, currentFavoriteIconByteArray.size)
136
137         // Set the content according to the app bar position.
138         if (bottomAppBar) {
139             // Set the content view.
140             setContentView(R.layout.bookmarks_bottom_appbar)
141         } else {
142             // `Window.FEATURE_ACTION_MODE_OVERLAY` makes the contextual action mode cover the support action bar.  It must be requested before the content is set.
143             supportRequestWindowFeature(Window.FEATURE_ACTION_MODE_OVERLAY)
144
145             // Set the content view.
146             setContentView(R.layout.bookmarks_top_appbar)
147         }
148
149         // Get handles for the views.
150         val toolbar = findViewById<Toolbar>(R.id.bookmarks_toolbar)
151         bookmarksListView = findViewById(R.id.bookmarks_listview)
152         val createBookmarkFolderFab = findViewById<FloatingActionButton>(R.id.create_bookmark_folder_fab)
153         val createBookmarkFab = findViewById<FloatingActionButton>(R.id.create_bookmark_fab)
154
155         // Set the support action bar.
156         setSupportActionBar(toolbar)
157
158         // Get a handle for the app bar.
159         appBar = supportActionBar!!
160
161         // Display the home arrow on the app bar.
162         appBar.setDisplayHomeAsUpEnabled(true)
163
164         // Control what the system back command does.
165         val onBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) {
166             override fun handleOnBackPressed() {
167                 // Prepare to finish the activity.
168                 prepareFinish()
169             }
170         }
171
172         // Register the on back pressed callback.
173         onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
174
175         // Initialize the database helper.
176         bookmarksDatabaseHelper = BookmarksDatabaseHelper(this)
177
178         // Set a listener so that tapping a list item edits the bookmark or opens a folder.
179         bookmarksListView.onItemClickListener = AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, _: Int, id: Long ->
180             // Convert the id from long to int to match the format of the bookmarks database.
181             val databaseId = id.toInt()
182
183             // Get the bookmark cursor for this ID.
184             val bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseId)
185
186             // Move the cursor to the first entry.
187             bookmarkCursor.moveToFirst()
188
189             // Act upon the bookmark according to the type.
190             if (bookmarkCursor.getInt(bookmarkCursor.getColumnIndexOrThrow(IS_FOLDER)) == 1) {  // The selected bookmark is a folder.
191                 // Update the current folder ID.
192                 currentFolderId = bookmarkCursor.getLong(bookmarkCursor.getColumnIndexOrThrow(FOLDER_ID))
193
194                 // Load the new folder.
195                 loadFolder()
196             } else {  // The selected bookmark is not a folder.
197                 // Instantiate the edit bookmark dialog.
198                 val editBookmarkDialog = EditBookmarkDialog.editBookmark(databaseId, currentFavoriteIconBitmap)
199
200                 // Make it so.
201                 editBookmarkDialog.show(supportFragmentManager, resources.getString(R.string.edit_bookmark))
202             }
203
204             // Close the cursor.
205             bookmarkCursor.close()
206         }
207
208         // Handle long-presses on the list view.
209         bookmarksListView.setMultiChoiceModeListener(object : MultiChoiceModeListener {
210             // Define the object variables.
211             private var deletingBookmarks = false
212
213             // Declare the object variables.
214             private lateinit var editBookmarkMenuItem: MenuItem
215             private lateinit var deleteBookmarksMenuItem: MenuItem
216             private lateinit var selectAllBookmarksMenuItem: MenuItem
217
218             override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
219                 // Inflate the menu for the contextual app bar.
220                 menuInflater.inflate(R.menu.bookmarks_context_menu, menu)
221
222                 // Set the title.
223                 if (currentFolderId == HOME_FOLDER_ID) {  // The current folder is the home folder.
224                     mode.setTitle(R.string.bookmarks)
225                 } else {  // Use the current folder name as the title.
226                     mode.title = bookmarksDatabaseHelper.getFolderName(currentFolderId)
227                 }
228
229                 // Get handles for menu items that need to be selectively disabled.
230                 moveBookmarkUpMenuItem = menu.findItem(R.id.move_bookmark_up)
231                 moveBookmarkDownMenuItem = menu.findItem(R.id.move_bookmark_down)
232                 moveToFolderMenuItem = menu.findItem(R.id.move_to_folder)
233                 editBookmarkMenuItem = menu.findItem(R.id.edit_bookmark)
234                 deleteBookmarksMenuItem = menu.findItem(R.id.delete_bookmark)
235                 selectAllBookmarksMenuItem = menu.findItem(R.id.context_menu_select_all_bookmarks)
236
237                 // Disable the delete bookmarks menu item if a delete is pending.
238                 deleteBookmarksMenuItem.isEnabled = !deletingBookmarks
239
240                 // Store a handle for the contextual action bar so it can be closed programatically.
241                 contextualActionMode = mode
242
243                 // Make it so.
244                 return true
245             }
246
247             override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
248                 // Display the move to folder menu item if at least one other folder exists.
249                 moveToFolderMenuItem.isVisible = bookmarksDatabaseHelper.hasFoldersExceptDatabaseId(bookmarksListView.checkedItemIds)
250
251                 // Make it so.
252                 return true
253             }
254
255             override fun onItemCheckedStateChanged(actionMode: ActionMode, position: Int, id: Long, checked: Boolean) {
256                 // Only update the UI if not checking many bookmarks.  In that case, the flag will be reset on the last bookmark so the UI is only updated once.
257                 if (!checkingManyBookmarks) {
258                     // Get the number of selected bookmarks.
259                     val numberOfSelectedBookmarks = bookmarksListView.checkedItemCount
260
261                     // Only process commands if at least one bookmark is selected.  Otherwise, a context menu with 0 selected bookmarks is briefly displayed.
262                     if (numberOfSelectedBookmarks > 0) {
263                         // Adjust the action mode and the menu according to the number of selected bookmarks.
264                         if (numberOfSelectedBookmarks == 1) {  // One bookmark is selected.
265                             // Show the applicable menu items.
266                             moveBookmarkUpMenuItem.isVisible = true
267                             moveBookmarkDownMenuItem.isVisible = true
268                             editBookmarkMenuItem.isVisible = true
269
270                             // Update the enabled status of the move icons.
271                             updateMoveIcons()
272                         } else {  // More than one bookmark is selected.
273                             // Hide non-applicable `MenuItems`.
274                             moveBookmarkUpMenuItem.isVisible = false
275                             moveBookmarkDownMenuItem.isVisible = false
276                             editBookmarkMenuItem.isVisible = false
277                         }
278
279                         // Display the move to folder menu item if at least one other folder exists.
280                         moveToFolderMenuItem.isVisible = bookmarksDatabaseHelper.hasFoldersExceptDatabaseId(bookmarksListView.checkedItemIds)
281
282                         // List the number of selected bookmarks in the subtitle.
283                         actionMode.subtitle = getString(R.string.selected, numberOfSelectedBookmarks)
284
285                         // Show the select all menu item if all the bookmarks are not selected.
286                         selectAllBookmarksMenuItem.isVisible = (numberOfSelectedBookmarks != bookmarksListView.count)
287                     }
288                 }
289             }
290
291             override fun onActionItemClicked(actionMode: ActionMode, menuItem: MenuItem): Boolean {
292                 // Declare the variables.
293                 val checkedBookmarkNewPosition: Int
294                 val checkedBookmarksPositionsSparseBooleanArray: SparseBooleanArray
295
296                 // Initialize the checked bookmark position.
297                 var checkedBookmarkPosition = 0
298
299                 // Get the menu item ID.
300                 val menuItemId = menuItem.itemId
301
302                 // Run the commands according to the selected action item.
303                 if (menuItemId == R.id.move_bookmark_up) {  // Move the bookmark up.
304                     // Get the array of checked bookmark positions.
305                     checkedBookmarksPositionsSparseBooleanArray = bookmarksListView.checkedItemPositions
306
307                     // Get the position of the bookmark that is selected.  If other bookmarks have previously been selected they will be included in the sparse boolean array with a value of `false`.
308                     for (i in 0 until checkedBookmarksPositionsSparseBooleanArray.size()) {
309                         // Check to see if the value for the bookmark is true, meaning it is currently selected.
310                         if (checkedBookmarksPositionsSparseBooleanArray.valueAt(i)) {
311                             // Only one bookmark should have a value of `true` when move bookmark up is enabled.
312                             checkedBookmarkPosition = checkedBookmarksPositionsSparseBooleanArray.keyAt(i)
313                         }
314                     }
315
316                     // Calculate the new position of the checked bookmark.
317                     checkedBookmarkNewPosition = checkedBookmarkPosition - 1
318
319                     // Get the bookmarks count.
320                     val bookmarksCount = bookmarksListView.count
321
322                     // Iterate through the bookmarks.
323                     for (i in 0 until bookmarksCount) {
324                         // Get the database ID for the current bookmark.
325                         val currentBookmarkDatabaseId = bookmarksListView.getItemIdAtPosition(i).toInt()
326
327                         // Update the display order for the current bookmark.
328                         if (i == checkedBookmarkPosition) {  // The current bookmark is the selected bookmark.
329                             // Move the current bookmark up one.
330                             bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i - 1)
331                         } else if ((i + 1) == checkedBookmarkPosition) {  // The current bookmark is immediately above the selected bookmark.
332                             // Move the current bookmark down one.
333                             bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i + 1)
334                         } else {  // The current bookmark is not changing positions.
335                             // Move the bookmarks cursor to the current bookmark position.
336                             bookmarksCursor.moveToPosition(i)
337
338                             // Update the display order only if it is not correct in the database.  This fixes problems where the display order somehow got out of sync.
339                             if (bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(DISPLAY_ORDER)) != i)
340                                 bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i)
341                         }
342                     }
343
344                     // Update the bookmarks cursor with the current contents of the bookmarks database.
345                     bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderId)
346
347                     // Update the list view.
348                     bookmarksCursorAdapter.changeCursor(bookmarksCursor)
349
350                     // Scroll to the new bookmark position.
351                     scrollBookmarks(checkedBookmarkNewPosition)
352
353                     // Update the enabled status of the move icons.
354                     updateMoveIcons()
355                 } else if (menuItemId == R.id.move_bookmark_down) {  // Move the bookmark down.
356                     // Get the array of checked bookmark positions.
357                     checkedBookmarksPositionsSparseBooleanArray = bookmarksListView.checkedItemPositions
358
359                     // Get the position of the bookmark that is selected.  If other bookmarks have previously been checked they will be included in the sparse boolean array with a value of `false`.
360                     for (i in 0 until checkedBookmarksPositionsSparseBooleanArray.size()) {
361                         // Check to see if the value for the bookmark is true, meaning it is currently selected.
362                         if (checkedBookmarksPositionsSparseBooleanArray.valueAt(i)) {
363                             // Only one bookmark should have a value of `true` when move bookmark down is enabled.
364                             checkedBookmarkPosition = checkedBookmarksPositionsSparseBooleanArray.keyAt(i)
365                         }
366                     }
367
368                     // Calculate the new position of the checked bookmark.
369                     checkedBookmarkNewPosition = checkedBookmarkPosition + 1
370
371                     // Iterate through the bookmarks.
372                     for (i in 0 until bookmarksListView.count) {
373                         // Get the database ID for the current bookmark.
374                         val currentBookmarkDatabaseId = bookmarksListView.getItemIdAtPosition(i).toInt()
375
376                         // Update the display order for the current bookmark.
377                         if (i == checkedBookmarkPosition) {  // The current bookmark is the checked bookmark.
378                             // Move the current bookmark down one.
379                             bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i + 1)
380                         } else if ((i - 1) == checkedBookmarkPosition) {  // The current bookmark is immediately below the checked bookmark.
381                             // Move the bookmark below the selected bookmark up one.
382                             bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i - 1)
383                         } else {  // The current bookmark is not changing positions.
384                             // Move the bookmarks cursor to the current bookmark position.
385                             bookmarksCursor.moveToPosition(i)
386
387                             // Update the display order only if it is not correct in the database.  This fixes problems where the display order somehow got out of sync.
388                             if (bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(DISPLAY_ORDER)) != i) {
389                                 bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i)
390                             }
391                         }
392                     }
393
394                     // Update the bookmarks cursor with the current contents of the bookmarks database.
395                     bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderId)
396
397                     // Update the list view.
398                     bookmarksCursorAdapter.changeCursor(bookmarksCursor)
399
400                     // Scroll to the new bookmark position.
401                     scrollBookmarks(checkedBookmarkNewPosition)
402
403                     // Update the enabled status of the move icons.
404                     updateMoveIcons()
405                 } else if (menuItemId == R.id.move_to_folder) {  // Move to folder.
406                     // Instantiate the move to folder alert dialog.
407                     val moveToFolderDialog = MoveToFolderDialog.moveBookmarks(currentFolderId, bookmarksListView.checkedItemIds)
408
409                     // Show the move to folder alert dialog.
410                     moveToFolderDialog.show(supportFragmentManager, resources.getString(R.string.move_to_folder))
411                 } else if (menuItemId == R.id.edit_bookmark) {
412                     // Get the array of checked bookmark positions.
413                     checkedBookmarksPositionsSparseBooleanArray = bookmarksListView.checkedItemPositions
414
415                     // Get the position of the bookmark that is selected.  If other bookmarks have previously been selected they will be included in the sparse boolean array with a value of `false`.
416                     for (i in 0 until checkedBookmarksPositionsSparseBooleanArray.size()) {
417                         // Check to see if the value for the bookmark is true, meaning it is currently selected.
418                         if (checkedBookmarksPositionsSparseBooleanArray.valueAt(i)) {
419                             // Only one bookmark should have a value of `true` when move edit bookmark is enabled.
420                             checkedBookmarkPosition = checkedBookmarksPositionsSparseBooleanArray.keyAt(i)
421                         }
422                     }
423
424                     // Move the cursor to the selected position.
425                     bookmarksCursor.moveToPosition(checkedBookmarkPosition)
426
427                     // Get the selected bookmark database ID.
428                     val databaseId = bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(ID))
429
430                     // Show the edit bookmark or edit bookmark folder dialog.
431                     if (bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(IS_FOLDER)) == 1) {  // A folder is selected.
432                         // Instantiate the edit bookmark folder dialog.
433                         val editFolderDialog = EditBookmarkFolderDialog.editFolder(databaseId, currentFavoriteIconBitmap)
434
435                         // Make it so.
436                         editFolderDialog.show(supportFragmentManager, resources.getString(R.string.edit_folder))
437                     } else {  // A bookmark is selected.
438                         // Instantiate the edit bookmark dialog.
439                         val editBookmarkDialog = EditBookmarkDialog.editBookmark(databaseId, currentFavoriteIconBitmap)
440
441                         // Make it so.
442                         editBookmarkDialog.show(supportFragmentManager, resources.getString(R.string.edit_bookmark))
443                     }
444                 } else if (menuItemId == R.id.delete_bookmark) {  // Delete bookmark.
445                     // Set the deleting bookmarks flag, which prevents the delete menu item from being enabled until the current process finishes.
446                     deletingBookmarks = true
447
448                     // Get an array of the checked row IDs.
449                     val checkedBookmarksIdsLongArray = bookmarksListView.checkedItemIds
450
451                     // Initialize a variable to count the number of bookmarks to delete.
452                     var numberOfBookmarksToDelete = 0
453
454                     // Count the number of bookmarks to delete.
455                     for (databaseIdLong in checkedBookmarksIdsLongArray) {
456                         // Convert the database ID long to an int.
457                         val databaseIdInt = databaseIdLong.toInt()
458
459                         // Count the contents of the folder if the selected bookmark is a folder.
460                         if (bookmarksDatabaseHelper.isFolder(databaseIdInt)) {
461                             // Add the bookmarks from the folder to the running total.
462                             numberOfBookmarksToDelete += countBookmarkFolderContents(databaseIdInt)
463                         }
464
465                         // Increment the count of the number of bookmarks to delete.
466                         numberOfBookmarksToDelete++
467                     }
468
469                     // Get an array of checked bookmarks.  `.clone()` makes a copy that won't change if the list view is reloaded, which is needed for re-selecting the bookmarks on undelete.
470                     checkedBookmarksPositionsSparseBooleanArray = bookmarksListView.checkedItemPositions.clone()
471
472                     // Update the bookmarks cursor with the current contents of the bookmarks database except for the specified database IDs.
473                     bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrderExcept(checkedBookmarksIdsLongArray, currentFolderId)
474
475                     // Update the list view.
476                     bookmarksCursorAdapter.changeCursor(bookmarksCursor)
477
478                     // Create a Snackbar with the number of deleted bookmarks.
479                     bookmarksDeletedSnackbar = Snackbar.make(findViewById(R.id.bookmarks_coordinatorlayout), getString(R.string.bookmarks_deleted, numberOfBookmarksToDelete), Snackbar.LENGTH_LONG)
480                         .setAction(R.string.undo) { }  // Do nothing because everything will be handled by `onDismissed()` below.
481                         .addCallback(object : Snackbar.Callback() {
482                             override fun onDismissed(snackbar: Snackbar, event: Int) {
483                                 if (event == DISMISS_EVENT_ACTION) {  // The user pushed the undo button.
484                                     // Update the bookmarks cursor with the current contents of the bookmarks database, including the "deleted" bookmarks.
485                                     bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderId)
486
487                                     // Update the list view.
488                                     bookmarksCursorAdapter.changeCursor(bookmarksCursor)
489
490                                     // Get the number of checked bookmarks.
491                                     val numberOfCheckedBookmarks = checkedBookmarksPositionsSparseBooleanArray.size()
492
493                                     // Set the checking many bookmarks flag.
494                                     checkingManyBookmarks = true
495
496                                     // Re-check the previously checked bookmarks.
497                                     for (i in 0 until numberOfCheckedBookmarks) {
498                                         // Reset the checking many bookmarks flag on the last bookmark so the UI is updated.
499                                         if (i == (numberOfCheckedBookmarks - 1))
500                                             checkingManyBookmarks = false
501
502                                         // Check the bookmark.
503                                         bookmarksListView.setItemChecked(checkedBookmarksPositionsSparseBooleanArray.keyAt(i), true)
504                                     }
505                                 } else {  // The snackbar was dismissed without the undo button being pushed.
506                                     // Delete each selected bookmark.
507                                     for (databaseIdLong in checkedBookmarksIdsLongArray) {
508                                         // Convert the database long ID to an int.
509                                         val databaseIdInt = databaseIdLong.toInt()
510
511                                         // Delete the contents of the folder if the selected bookmark is a folder.
512                                         if (bookmarksDatabaseHelper.isFolder(databaseIdInt))
513                                             deleteBookmarkFolderContents(databaseIdInt)
514
515                                         // Delete the selected bookmark.
516                                         bookmarksDatabaseHelper.deleteBookmark(databaseIdInt)
517                                     }
518
519                                     // Update the display order.
520                                     for (i in 0 until bookmarksListView.count) {
521                                         // Get the database ID for the current bookmark.
522                                         val currentBookmarkDatabaseId = bookmarksListView.getItemIdAtPosition(i).toInt()
523
524                                         // Move bookmarks cursor to the current bookmark position.
525                                         bookmarksCursor.moveToPosition(i)
526
527                                         // Update the display order only if it is not correct in the database.
528                                         if (bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(DISPLAY_ORDER)) != i)
529                                             bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i)
530                                     }
531                                 }
532
533                                 // Reset the deleting bookmarks flag.
534                                 deletingBookmarks = false
535
536                                 // Enable the delete bookmarks menu item.
537                                 deleteBookmarksMenuItem.isEnabled = true
538
539                                 // Close the activity if back has been pressed.
540                                 if (closeActivityAfterDismissingSnackbar) {
541                                     // Close the bookmarks drawer and reload the bookmarks list view when returning to the main WebView activity.
542                                     MainWebViewActivity.restartFromBookmarksActivity = true
543
544                                     // Finish the activity.
545                                     finish()
546                                 }
547                             }
548                         })
549
550                     // Show the Snackbar.
551                     bookmarksDeletedSnackbar!!.show()
552                 } else if (menuItemId == R.id.context_menu_select_all_bookmarks) {  // Select all.
553                     // Get the total number of bookmarks.
554                     val numberOfBookmarks = bookmarksListView.count
555
556                     // Set the checking many bookmarks flag.
557                     checkingManyBookmarks = true
558
559                     // Select them all.
560                     for (i in 0 until numberOfBookmarks) {
561                         // Reset the checking many bookmarks flag on the last bookmark so the UI is updated.
562                         if (i == (numberOfBookmarks - 1))
563                             checkingManyBookmarks = false
564
565                         // Check the bookmark.
566                         bookmarksListView.setItemChecked(i, true)
567                     }
568                 }
569
570                 // Consume the click.
571                 return true
572             }
573
574             override fun onDestroyActionMode(mode: ActionMode) {
575                 // Do nothing.
576             }
577         })
578
579         // Set the create new bookmark folder FAB to display the alert dialog.
580         createBookmarkFolderFab.setOnClickListener {
581             // Create a create bookmark folder dialog.
582             val createBookmarkFolderDialog = CreateBookmarkFolderDialog.createBookmarkFolder(currentFavoriteIconBitmap)
583
584             // Show the create bookmark folder dialog.
585             createBookmarkFolderDialog.show(supportFragmentManager, getString(R.string.create_folder))
586         }
587
588         // Set the create new bookmark FAB to display the alert dialog.
589         createBookmarkFab.setOnClickListener {
590             // Instantiate the create bookmark dialog.
591             val createBookmarkDialog = CreateBookmarkDialog.createBookmark(currentUrl, currentTitle, currentFavoriteIconBitmap)
592
593             // Display the create bookmark dialog.
594             createBookmarkDialog.show(supportFragmentManager, resources.getString(R.string.create_bookmark))
595         }
596
597         // Restore the state if the app has been restarted.
598         if (savedInstanceState != null) {
599             // Restore the current folder.
600             currentFolderId = savedInstanceState.getLong(CURRENT_FOLDER_ID, HOME_FOLDER_ID)
601
602             // Update the bookmarks list view after it has loaded.
603             bookmarksListView.post {
604                 // Get the checked bookmarks array list.
605                 val checkedBookmarksArrayList = savedInstanceState.getIntegerArrayList(CHECKED_BOOKMARKS_ARRAY_LIST)!!
606
607                 // Check each previously checked bookmark in the list view.
608                 checkedBookmarksArrayList.forEach(Consumer { position: Int -> bookmarksListView.setItemChecked(position, true) })
609             }
610         }
611
612         // Load the current folder.
613         loadFolder()
614     }
615
616     public override fun onRestart() {
617         // Run the default commands.
618         super.onRestart()
619
620         // Update the list view if returning from the bookmarks database view activity.
621         if (restartFromBookmarksDatabaseViewActivity) {
622             // Load the current folder in the list view.
623             loadFolder()
624
625             // Reset the restart from bookmarks database view activity flag.
626             restartFromBookmarksDatabaseViewActivity = false
627         }
628     }
629
630     public override fun onSaveInstanceState(savedInstanceState: Bundle) {
631         // Run the default commands.
632         super.onSaveInstanceState(savedInstanceState)
633
634         // Get the sparse boolean array of the checked items.
635         val checkedBookmarksSparseBooleanArray = bookmarksListView.checkedItemPositions
636
637         // Create a checked items array list.
638         val checkedBookmarksArrayList = ArrayList<Int>()
639
640         // Add each checked bookmark position to the array list.
641         for (i in 0 until checkedBookmarksSparseBooleanArray.size()) {
642             // Check to see if the bookmark is currently checked.  Bookmarks that have previously been checked but currently aren't will be populated in the sparse boolean array, but will return false.
643             if (checkedBookmarksSparseBooleanArray.valueAt(i)) {
644                 // Add the bookmark position to the checked bookmarks array list.
645                 checkedBookmarksArrayList.add(checkedBookmarksSparseBooleanArray.keyAt(i))
646             }
647         }
648
649         // Store the variables in the saved instance state.
650         savedInstanceState.putLong(CURRENT_FOLDER_ID, currentFolderId)
651         savedInstanceState.putIntegerArrayList(CHECKED_BOOKMARKS_ARRAY_LIST, checkedBookmarksArrayList)
652     }
653
654     override fun onCreateOptionsMenu(menu: Menu): Boolean {
655         // Inflate the menu.
656         menuInflater.inflate(R.menu.bookmarks_options_menu, menu)
657
658         // Success.
659         return true
660     }
661
662     override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
663         // Get a handle for the menu item ID.
664         val menuItemId = menuItem.itemId
665
666         // Run the command according to the selected option.
667         if (menuItemId == android.R.id.home) {  // Home.  The home arrow is identified as `android.R.id.home`, not just `R.id.home`.
668             if (currentFolderId == HOME_FOLDER_ID) {  // The current folder is the home folder.
669                 // Prepare to finish the activity.
670                 prepareFinish()
671             } else {  // Currently in a subfolder.
672                 // Set the former parent folder as the current folder.
673                 currentFolderId = bookmarksDatabaseHelper.getParentFolderId(currentFolderId)
674
675                 // Load the new current folder.
676                 loadFolder()
677             }
678         } else if (menuItemId == R.id.options_menu_select_all_bookmarks) {  // Select all.
679             // Get the total number of bookmarks.
680             val numberOfBookmarks = bookmarksListView.count
681
682             // Set the checking many bookmarks flag.
683             checkingManyBookmarks = true
684
685             // Select them all.
686             for (i in 0 until numberOfBookmarks) {
687                 // Reset the checking many bookmarks flag on the last bookmark so the UI is updated.
688                 if (i == (numberOfBookmarks - 1))
689                     checkingManyBookmarks = false
690
691                 // Check the bookmark.
692                 bookmarksListView.setItemChecked(i, true)
693             }
694         } else if (menuItemId == R.id.bookmarks_database_view) {
695             // Close the contextual action bar if it is displayed.  This can happen if the bottom app bar is enabled.
696             contextualActionMode?.finish()
697
698             // Create an intent to launch the bookmarks database view activity.
699             val bookmarksDatabaseViewIntent = Intent(this, BookmarksDatabaseViewActivity::class.java)
700
701             // Include the favorite icon byte array to the intent.
702             bookmarksDatabaseViewIntent.putExtra(CURRENT_FAVORITE_ICON_BYTE_ARRAY, currentFavoriteIconByteArray)
703
704             // Make it so.
705             startActivity(bookmarksDatabaseViewIntent)
706         }
707         return true
708     }
709
710     override fun createBookmark(dialogFragment: DialogFragment) {
711         // Get the alert dialog from the fragment.
712         val dialog = dialogFragment.dialog!!
713
714         // Get handles for the views from the dialog fragment.
715         val webpageFavoriteIconRadioButton = dialog.findViewById<RadioButton>(R.id.webpage_favorite_icon_radiobutton)
716         val webpageFavoriteIconImageView = dialog.findViewById<ImageView>(R.id.webpage_favorite_icon_imageview)
717         val customIconImageView = dialog.findViewById<ImageView>(R.id.custom_icon_imageview)
718         val bookmarkNameEditText = dialog.findViewById<EditText>(R.id.bookmark_name_edittext)
719         val bookmarkUrlEditText = dialog.findViewById<EditText>(R.id.bookmark_url_edittext)
720
721         // Get the strings from the edit texts.
722         val bookmarkNameString = bookmarkNameEditText.text.toString()
723         val bookmarkUrlString = bookmarkUrlEditText.text.toString()
724
725         // Get the selected favorite icon drawable.
726         val favoriteIconDrawable = if (webpageFavoriteIconRadioButton.isChecked)  // The webpage favorite icon is checked.
727             webpageFavoriteIconImageView.drawable
728         else  // The custom favorite icon is checked.
729             customIconImageView.drawable
730
731         // Convert the favorite icon drawable to a bitmap.  Once the minimum API >= 33, this can use Bitmap.Config.RGBA_1010102.
732         val favoriteIconBitmap = favoriteIconDrawable.toBitmap(128, 128, Bitmap.Config.ARGB_8888)
733
734         // Create a favorite icon byte array output stream.
735         val favoriteIconByteArrayOutputStream = ByteArrayOutputStream()
736
737         // Convert the favorite icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
738         favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream)
739
740         // Convert the favorite icon byte array stream to a byte array.
741         val favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray()
742
743         // Display the new bookmark below the current items in the (0 indexed) list.
744         val newBookmarkDisplayOrder = bookmarksListView.count
745
746         // Create the bookmark.
747         bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentFolderId, newBookmarkDisplayOrder, favoriteIconByteArray)
748
749         // Update the bookmarks cursor with the current contents of this folder.
750         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderId)
751
752         // Update the list view.
753         bookmarksCursorAdapter.changeCursor(bookmarksCursor)
754
755         // Scroll to the new bookmark.
756         bookmarksListView.setSelection(newBookmarkDisplayOrder)
757     }
758
759     override fun createBookmarkFolder(dialogFragment: DialogFragment) {
760         // Get the dialog from the dialog fragment.
761         val dialog = dialogFragment.dialog!!
762
763         // Get handles for the views in the dialog fragment.
764         val defaultFolderIconRadioButton = dialog.findViewById<RadioButton>(R.id.default_folder_icon_radiobutton)
765         val defaultFolderIconImageView = dialog.findViewById<ImageView>(R.id.default_folder_icon_imageview)
766         val webpageFavoriteIconRadioButton = dialog.findViewById<RadioButton>(R.id.webpage_favorite_icon_radiobutton)
767         val webpageFavoriteIconImageView = dialog.findViewById<ImageView>(R.id.webpage_favorite_icon_imageview)
768         val customIconImageView = dialog.findViewById<ImageView>(R.id.custom_icon_imageview)
769         val folderNameEditText = dialog.findViewById<EditText>(R.id.folder_name_edittext)
770
771         // Get the folder name string.
772         val folderNameString = folderNameEditText.text.toString()
773
774         // Get the selected folder icon drawable.
775         val folderIconDrawable = if (defaultFolderIconRadioButton.isChecked)  // Use the default folder icon.
776             defaultFolderIconImageView.drawable
777         else if (webpageFavoriteIconRadioButton.isChecked)  // Use the webpage favorite icon.
778             webpageFavoriteIconImageView.drawable
779         else  // Use the custom icon.
780             customIconImageView.drawable
781
782         // Cast the folder icon bitmap to a bitmap drawable.
783         val folderIconBitmapDrawable = folderIconDrawable as BitmapDrawable
784
785         // Convert the folder icon bitmap drawable to a bitmap.
786         val folderIconBitmap = folderIconBitmapDrawable.bitmap
787
788         // Create a folder icon byte array output stream.
789         val folderIconByteArrayOutputStream = ByteArrayOutputStream()
790
791         // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
792         folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream)
793
794         // Convert the folder icon byte array stream to a byte array.
795         val folderIconByteArray = folderIconByteArrayOutputStream.toByteArray()
796
797         // Move all the bookmarks down one in the display order.
798         for (i in 0 until bookmarksListView.count) {
799             val databaseId = bookmarksListView.getItemIdAtPosition(i).toInt()
800             bookmarksDatabaseHelper.updateDisplayOrder(databaseId, displayOrder = i + 1)
801         }
802
803         // Create the folder, which will be placed at the top of the list view.
804         bookmarksDatabaseHelper.createFolder(folderNameString, currentFolderId, displayOrder = 0, folderIconByteArray)
805
806         // Update the bookmarks cursor with the contents of the current folder.
807         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderId)
808
809         // Update the list view.
810         bookmarksCursorAdapter.changeCursor(bookmarksCursor)
811
812         // Scroll to the new folder.
813         bookmarksListView.setSelection(0)
814     }
815
816     override fun saveBookmark(dialogFragment: DialogFragment, selectedBookmarkDatabaseId: Int) {
817         // Get the dialog from the dialog fragment.
818         val dialog = dialogFragment.dialog!!
819
820         // Get handles for the views from the dialog fragment.
821         val currentIconRadioButton = dialog.findViewById<RadioButton>(R.id.current_icon_radiobutton)
822         val webpageFavoriteIconRadioButton = dialog.findViewById<RadioButton>(R.id.webpage_favorite_icon_radiobutton)
823         val webpageFavoriteIconImageView = dialog.findViewById<ImageView>(R.id.webpage_favorite_icon_imageview)
824         val customIconImageView = dialog.findViewById<ImageView>(R.id.custom_icon_imageview)
825         val bookmarkNameEditText = dialog.findViewById<EditText>(R.id.bookmark_name_edittext)
826         val bookmarkUrlEditText = dialog.findViewById<EditText>(R.id.bookmark_url_edittext)
827
828         // Get the strings from the edit texts.
829         val bookmarkNameString = bookmarkNameEditText.text.toString()
830         val bookmarkUrlString = bookmarkUrlEditText.text.toString()
831
832         // Update the bookmark.
833         if (currentIconRadioButton.isChecked) {  // Update the bookmark without changing the favorite icon.
834             bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString)
835         } else {  // Update the bookmark using the WebView favorite icon.
836             // Get the selected favorite icon drawable.
837             val favoriteIconDrawable = if (webpageFavoriteIconRadioButton.isChecked)  // The webpage favorite icon is checked.
838                 webpageFavoriteIconImageView.drawable
839             else  // The custom icon is checked.
840                 customIconImageView.drawable
841
842             // Convert the favorite icon drawable to a bitmap.  Once the minimum API >= 33, this can use Bitmap.Config.RGBA_1010102.
843             val favoriteIconBitmap = favoriteIconDrawable.toBitmap(128, 128, Bitmap.Config.ARGB_8888)
844
845             // Create a favorite icon byte array output stream.
846             val newFavoriteIconByteArrayOutputStream = ByteArrayOutputStream()
847
848             // Convert the favorite icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
849             favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFavoriteIconByteArrayOutputStream)
850
851             // Convert the favorite icon byte array stream to a byte array.
852             val newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray()
853
854             //  Update the bookmark and the favorite icon.
855             bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, newFavoriteIconByteArray)
856         }
857
858         // Close the contextual action bar if it is displayed.
859         contextualActionMode?.finish()
860
861         // Update the bookmarks cursor with the contents of the current folder.
862         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderId)
863
864         // Update the list view.
865         bookmarksCursorAdapter.changeCursor(bookmarksCursor)
866     }
867
868     override fun saveBookmarkFolder(dialogFragment: DialogFragment, selectedFolderDatabaseId: Int) {
869         // Get the dialog from the dialog fragment.
870         val dialog = dialogFragment.dialog!!
871
872         // Get handles for the views from the dialog fragment.
873         val currentFolderIconRadioButton = dialog.findViewById<RadioButton>(R.id.current_icon_radiobutton)
874         val defaultFolderIconRadioButton = dialog.findViewById<RadioButton>(R.id.default_folder_icon_radiobutton)
875         val defaultFolderIconImageView = dialog.findViewById<ImageView>(R.id.default_folder_icon_imageview)
876         val webpageFavoriteIconRadioButton = dialog.findViewById<RadioButton>(R.id.webpage_favorite_icon_radiobutton)
877         val webpageFavoriteIconImageView = dialog.findViewById<ImageView>(R.id.webpage_favorite_icon_imageview)
878         val customIconImageView = dialog.findViewById<ImageView>(R.id.custom_icon_imageview)
879         val folderNameEditText = dialog.findViewById<EditText>(R.id.folder_name_edittext)
880
881         // Get the new folder name.
882         val newFolderName = folderNameEditText.text.toString()
883
884         // Check if the folder icon has changed.
885         if (currentFolderIconRadioButton.isChecked) {  // Only the name has changed.
886             // Update the name in the database.
887             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderName)
888         } else {  // The icon has changed.
889             // Get the selected folder icon drawable.
890             val folderIconDrawable = if (defaultFolderIconRadioButton.isChecked)  // The default folder icon is checked.
891                 defaultFolderIconImageView.drawable
892             else if (webpageFavoriteIconRadioButton.isChecked)  // The webpage favorite icon is checked.
893                 webpageFavoriteIconImageView.drawable
894             else  // The custom icon is checked.
895                 customIconImageView.drawable
896
897             // Convert the folder icon drawable to a bitmap.  Once the minimum API >= 33, this can use Bitmap.Config.RGBA_1010102.
898             val folderIconBitmap = folderIconDrawable.toBitmap(128, 128, Bitmap.Config.ARGB_8888)
899
900             // Create a new folder icon byte array output stream.
901             val newFolderIconByteArrayOutputStream = ByteArrayOutputStream()
902
903             // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
904             folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream)
905
906             // Convert the folder icon byte array stream to a byte array.
907             val newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray()
908
909             // Update the database.
910             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderName, newFolderIconByteArray)
911         }
912
913         // Update the bookmarks cursor with the current contents of this folder.
914         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderId)
915
916         // Update the list view.
917         bookmarksCursorAdapter.changeCursor(bookmarksCursor)
918
919         // Close the contextual action mode.
920         contextualActionMode!!.finish()
921     }
922
923     override fun onMoveToFolder(dialogFragment: DialogFragment) {
924         // Get the dialog from the dialog fragment.
925         val dialog = dialogFragment.dialog!!
926
927         // Get a handle for the folder list view from the dialog.
928         val folderListView = dialog.findViewById<ListView>(R.id.move_to_folder_listview)
929
930         // Store a long array of the selected folders.
931         val newFolderLongArray = folderListView.checkedItemIds
932
933         // Get the new folder database ID.  Only one folder will be selected so it will be the first one.
934         val newFolderDatabaseId = newFolderLongArray[0].toInt()
935
936         // Set the new folder name.
937         val newFolderId = if (newFolderDatabaseId == HOME_FOLDER_DATABASE_ID)
938             // The new folder is the home folder.
939             HOME_FOLDER_ID
940         else
941             // Get the new folder name from the database.
942             bookmarksDatabaseHelper.getFolderId(newFolderDatabaseId)
943
944         // Get a long array with the the database ID of the selected bookmarks.
945         val selectedBookmarksLongArray = bookmarksListView.checkedItemIds
946
947         // Move each of the selected bookmarks to the new folder.
948         for (databaseIdLong in selectedBookmarksLongArray) {
949             // Convert the database long ID to an int for each selected bookmark.
950             val databaseIdInt = databaseIdLong.toInt()
951
952             // Move the selected bookmark to the new folder.
953             bookmarksDatabaseHelper.moveToFolder(databaseIdInt, newFolderId)
954         }
955
956         // Update the bookmarks cursor with the current contents of this folder.
957         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderId)
958
959         // Update the list view.
960         bookmarksCursorAdapter.changeCursor(bookmarksCursor)
961
962         // Close the contextual app bar.
963         contextualActionMode!!.finish()
964     }
965
966     private fun countBookmarkFolderContents(folderDatabaseId: Int): Int {
967         // Get the folder ID.
968         val folderId = bookmarksDatabaseHelper.getFolderId(folderDatabaseId)
969
970         // Get the contents of the folder.
971         val folderCursor = bookmarksDatabaseHelper.getBookmarkAndFolderIds(folderId)
972
973         // Initialize the bookmark counter.
974         var bookmarkCounter = 0
975
976         // Count each of the bookmarks in the folder.
977         for (i in 0 until folderCursor.count) {
978             // Move the folder cursor to the current row.
979             folderCursor.moveToPosition(i)
980
981             // Get the database ID of the item.
982             val itemDatabaseId = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(ID))
983
984             // If this is a folder, recursively count the contents first.
985             if (bookmarksDatabaseHelper.isFolder(itemDatabaseId))
986                 bookmarkCounter += countBookmarkFolderContents(itemDatabaseId)
987
988             // Add the bookmark to the running total.
989             bookmarkCounter++
990         }
991
992         // Return the bookmark counter.
993         return bookmarkCounter
994     }
995
996     private fun deleteBookmarkFolderContents(folderDatabaseId: Int) {
997         // Get the folder ID.
998         val folderId = bookmarksDatabaseHelper.getFolderId(folderDatabaseId)
999
1000         // Get the contents of the folder.
1001         val folderCursor = bookmarksDatabaseHelper.getBookmarkAndFolderIds(folderId)
1002
1003         // Delete each of the bookmarks in the folder.
1004         for (i in 0 until folderCursor.count) {
1005             // Move the folder cursor to the current row.
1006             folderCursor.moveToPosition(i)
1007
1008             // Get the database ID of the item.
1009             val itemDatabaseId = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(ID))
1010
1011             // If this is a folder, recursively delete the contents first.
1012             if (bookmarksDatabaseHelper.isFolder(itemDatabaseId))
1013                 deleteBookmarkFolderContents(itemDatabaseId)
1014
1015             // Delete the bookmark.
1016             bookmarksDatabaseHelper.deleteBookmark(itemDatabaseId)
1017         }
1018     }
1019
1020     private fun prepareFinish() {
1021         // Check to see if a snackbar is currently displayed.  If so, it must be closed before exiting so that a pending delete is completed before reloading the list view in the bookmarks drawer.
1022         if (bookmarksDeletedSnackbar != null && bookmarksDeletedSnackbar!!.isShown) {  // Close the bookmarks deleted snackbar before going home.
1023             // Set the close flag.
1024             closeActivityAfterDismissingSnackbar = true
1025
1026             // Dismiss the snackbar.
1027             bookmarksDeletedSnackbar!!.dismiss()
1028         } else {  // Go home immediately.
1029             // Update the bookmarks folder for the bookmarks drawer in the main WebView activity.
1030             MainWebViewActivity.currentBookmarksFolderId = currentFolderId
1031
1032             // Close the bookmarks drawer and reload the bookmarks list view when returning to the main WebView activity.
1033             MainWebViewActivity.restartFromBookmarksActivity = true
1034
1035             // Exit the bookmarks activity.
1036             finish()
1037         }
1038     }
1039
1040     private fun updateMoveIcons() {
1041         // Get a long array of the selected bookmarks.
1042         val selectedBookmarksLongArray = bookmarksListView.checkedItemIds
1043
1044         // Get the database IDs for the first, last, and selected bookmarks.
1045         val firstBookmarkDatabaseId = bookmarksListView.getItemIdAtPosition(0).toInt()
1046         val lastBookmarkDatabaseId = bookmarksListView.getItemIdAtPosition(bookmarksListView.count - 1).toInt()  // The bookmarks list view is 0 indexed.
1047         val selectedBookmarkDatabaseId = selectedBookmarksLongArray[0].toInt()
1048
1049         // Update the move bookmark up menu item.
1050         if (selectedBookmarkDatabaseId == firstBookmarkDatabaseId) {  // The selected bookmark is in the first position.
1051             // Disable the move bookmark up menu item.
1052             moveBookmarkUpMenuItem.isEnabled = false
1053
1054             //  Set the icon.
1055             moveBookmarkUpMenuItem.setIcon(R.drawable.move_up_disabled)
1056         } else {  // The selected bookmark is not in the first position.
1057             // Enable the move bookmark up menu item.
1058             moveBookmarkUpMenuItem.isEnabled = true
1059
1060             // Set the icon according to the theme.
1061             moveBookmarkUpMenuItem.setIcon(R.drawable.move_up_enabled)
1062         }
1063
1064         // Update the move bookmark down menu item.
1065         if (selectedBookmarkDatabaseId == lastBookmarkDatabaseId) {  // The selected bookmark is in the last position.
1066             // Disable the move bookmark down menu item.
1067             moveBookmarkDownMenuItem.isEnabled = false
1068
1069             // Set the icon.
1070             moveBookmarkDownMenuItem.setIcon(R.drawable.move_down_disabled)
1071         } else {  // The selected bookmark is not in the last position.
1072             // Enable the move bookmark down menu item.
1073             moveBookmarkDownMenuItem.isEnabled = true
1074
1075             // Set the icon.
1076             moveBookmarkDownMenuItem.setIcon(R.drawable.move_down_enabled)
1077         }
1078     }
1079
1080     private fun scrollBookmarks(selectedBookmarkPosition: Int) {
1081         // Get the first and last visible bookmark positions.
1082         val firstVisibleBookmarkPosition = bookmarksListView.firstVisiblePosition
1083         val lastVisibleBookmarkPosition = bookmarksListView.lastVisiblePosition
1084
1085         // Calculate the number of bookmarks per screen.
1086         val numberOfBookmarksPerScreen = lastVisibleBookmarkPosition - firstVisibleBookmarkPosition
1087
1088         // Scroll with the moved bookmark if necessary.
1089         if (selectedBookmarkPosition <= firstVisibleBookmarkPosition) {  // The selected bookmark position is at or above the top of the screen.
1090             // Scroll to the selected bookmark position.
1091             bookmarksListView.setSelection(selectedBookmarkPosition)
1092         } else if (selectedBookmarkPosition >= lastVisibleBookmarkPosition - 1) {  // The selected bookmark is at or below the bottom of the screen.
1093             // The `-1` handles partial bookmarks displayed at the bottom of the list view.  This command scrolls to display the selected bookmark at the bottom of the screen.
1094             // `+1` assures that the entire bookmark will be displayed in situations where only a partial bookmark fits at the bottom of the list view.
1095             bookmarksListView.setSelection(selectedBookmarkPosition - numberOfBookmarksPerScreen + 1)
1096         }
1097     }
1098
1099     private fun loadFolder() {
1100         // Update the bookmarks cursor with the contents of the bookmarks database for the current folder.
1101         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderId)
1102
1103         // Setup a cursor adapter.
1104         bookmarksCursorAdapter = object : CursorAdapter(this, bookmarksCursor, false) {
1105             override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
1106                 // Inflate the individual item layout.
1107                 return layoutInflater.inflate(R.layout.bookmarks_activity_item_linearlayout, parent, false)
1108             }
1109
1110             override fun bindView(view: View, context: Context, cursor: Cursor) {
1111                 // Get handles for the views.
1112                 val bookmarkFavoriteIconImageView = view.findViewById<ImageView>(R.id.bookmark_favorite_icon)
1113                 val bookmarkNameTextView = view.findViewById<TextView>(R.id.bookmark_name)
1114
1115                 // Get the favorite icon byte array from the cursor.
1116                 val favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(FAVORITE_ICON))
1117
1118                 // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
1119                 val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
1120
1121                 // Display the bitmap in the bookmark favorite icon image view.
1122                 bookmarkFavoriteIconImageView.setImageBitmap(favoriteIconBitmap)
1123
1124                 // Get the bookmark name from the cursor.
1125                 val bookmarkNameString = cursor.getString(cursor.getColumnIndexOrThrow(BOOKMARK_NAME))
1126
1127                 // Display the bookmark name.
1128                 bookmarkNameTextView.text = bookmarkNameString
1129
1130                 // Make the font bold for folders.
1131                 if (cursor.getInt(cursor.getColumnIndexOrThrow(IS_FOLDER)) == 1)
1132                     bookmarkNameTextView.typeface = Typeface.DEFAULT_BOLD
1133                 else  // Reset the font to default for normal bookmarks.
1134                     bookmarkNameTextView.typeface = Typeface.DEFAULT
1135             }
1136         }
1137
1138         // Populate the list view with the adapter.
1139         bookmarksListView.adapter = bookmarksCursorAdapter
1140
1141         // Set the app bar title.
1142         if (currentFolderId == HOME_FOLDER_ID)  // The home folder is the current folder.
1143             appBar.setTitle(R.string.bookmarks)
1144         else
1145             appBar.title = bookmarksDatabaseHelper.getFolderName(currentFolderId)
1146     }
1147
1148     public override fun onDestroy() {
1149         // Close the bookmarks cursor and database.
1150         bookmarksCursor.close()
1151         bookmarksDatabaseHelper.close()
1152
1153         // Run the default commands.
1154         super.onDestroy()
1155     }
1156 }