2 * Copyright 2016-2024 Soren Stoutner <soren@stoutner.com>.
4 * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android/>.
6 * Privacy Browser Android is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
11 * Privacy Browser Android is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with Privacy Browser Android. If not, see <http://www.gnu.org/licenses/>.
20 package com.stoutner.privacybrowser.activities
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
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
55 import com.google.android.material.floatingactionbutton.FloatingActionButton
56 import com.google.android.material.snackbar.Snackbar
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
72 import java.io.ByteArrayOutputStream
73 import java.util.function.Consumer
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"
80 // Define the private constants.
81 private const val CHECKED_BOOKMARKS_ARRAY_LIST = "D"
83 class BookmarksActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener, EditBookmarkDialog.EditBookmarkListener,
84 EditBookmarkFolderDialog.EditBookmarkFolderListener, MoveToFolderDialog.MoveToFolderListener {
87 // Define the public static variables, which are accessed from the bookmarks database view activity.
88 var currentFolderId: Long = 0
89 var restartFromBookmarksDatabaseViewActivity = false
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
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
109 override fun onCreate(savedInstanceState: Bundle?) {
110 // Get a handle for the shared preferences.
111 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
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)
117 // Disable screenshots if not allowed.
118 if (!allowScreenshots) {
119 window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
122 // Run the default commands.
123 super.onCreate(savedInstanceState)
125 // Get the intent that launched the activity.
126 val launchingIntent = intent
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)!!
134 // Convert the favorite icon byte array to a bitmap.
135 val currentFavoriteIconBitmap = BitmapFactory.decodeByteArray(currentFavoriteIconByteArray, 0, currentFavoriteIconByteArray.size)
137 // Set the content according to the app bar position.
139 // Set the content view.
140 setContentView(R.layout.bookmarks_bottom_appbar)
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)
145 // Set the content view.
146 setContentView(R.layout.bookmarks_top_appbar)
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)
155 // Set the support action bar.
156 setSupportActionBar(toolbar)
158 // Get a handle for the app bar.
159 appBar = supportActionBar!!
161 // Display the home arrow on the app bar.
162 appBar.setDisplayHomeAsUpEnabled(true)
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.
172 // Register the on back pressed callback.
173 onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
175 // Initialize the database helper.
176 bookmarksDatabaseHelper = BookmarksDatabaseHelper(this)
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()
183 // Get the bookmark cursor for this ID.
184 val bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseId)
186 // Move the cursor to the first entry.
187 bookmarkCursor.moveToFirst()
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))
194 // Load the new folder.
196 } else { // The selected bookmark is not a folder.
197 // Instantiate the edit bookmark dialog.
198 val editBookmarkDialog = EditBookmarkDialog.editBookmark(databaseId, currentFavoriteIconBitmap)
201 editBookmarkDialog.show(supportFragmentManager, resources.getString(R.string.edit_bookmark))
205 bookmarkCursor.close()
208 // Handle long-presses on the list view.
209 bookmarksListView.setMultiChoiceModeListener(object : MultiChoiceModeListener {
210 // Define the object variables.
211 private var deletingBookmarks = false
213 // Declare the object variables.
214 private lateinit var editBookmarkMenuItem: MenuItem
215 private lateinit var deleteBookmarksMenuItem: MenuItem
216 private lateinit var selectAllBookmarksMenuItem: MenuItem
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)
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)
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)
237 // Disable the delete bookmarks menu item if a delete is pending.
238 deleteBookmarksMenuItem.isEnabled = !deletingBookmarks
240 // Store a handle for the contextual action bar so it can be closed programatically.
241 contextualActionMode = mode
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)
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
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
270 // Update the enabled status of the move icons.
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
279 // Display the move to folder menu item if at least one other folder exists.
280 moveToFolderMenuItem.isVisible = bookmarksDatabaseHelper.hasFoldersExceptDatabaseId(bookmarksListView.checkedItemIds)
282 // List the number of selected bookmarks in the subtitle.
283 actionMode.subtitle = getString(R.string.selected, numberOfSelectedBookmarks)
285 // Show the select all menu item if all the bookmarks are not selected.
286 selectAllBookmarksMenuItem.isVisible = (numberOfSelectedBookmarks != bookmarksListView.count)
291 override fun onActionItemClicked(actionMode: ActionMode, menuItem: MenuItem): Boolean {
292 // Declare the variables.
293 val checkedBookmarkNewPosition: Int
294 val checkedBookmarksPositionsSparseBooleanArray: SparseBooleanArray
296 // Initialize the checked bookmark position.
297 var checkedBookmarkPosition = 0
299 // Get the menu item ID.
300 val menuItemId = menuItem.itemId
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
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)
316 // Calculate the new position of the checked bookmark.
317 checkedBookmarkNewPosition = checkedBookmarkPosition - 1
319 // Get the bookmarks count.
320 val bookmarksCount = bookmarksListView.count
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()
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)
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)
344 // Update the bookmarks cursor with the current contents of the bookmarks database.
345 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderId)
347 // Update the list view.
348 bookmarksCursorAdapter.changeCursor(bookmarksCursor)
350 // Scroll to the new bookmark position.
351 scrollBookmarks(checkedBookmarkNewPosition)
353 // Update the enabled status of the move icons.
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
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)
368 // Calculate the new position of the checked bookmark.
369 checkedBookmarkNewPosition = checkedBookmarkPosition + 1
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()
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)
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)
394 // Update the bookmarks cursor with the current contents of the bookmarks database.
395 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderId)
397 // Update the list view.
398 bookmarksCursorAdapter.changeCursor(bookmarksCursor)
400 // Scroll to the new bookmark position.
401 scrollBookmarks(checkedBookmarkNewPosition)
403 // Update the enabled status of the move icons.
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)
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
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)
424 // Move the cursor to the selected position.
425 bookmarksCursor.moveToPosition(checkedBookmarkPosition)
427 // Get the selected bookmark database ID.
428 val databaseId = bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(ID))
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)
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)
442 editBookmarkDialog.show(supportFragmentManager, resources.getString(R.string.edit_bookmark))
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
448 // Get an array of the checked row IDs.
449 val checkedBookmarksIdsLongArray = bookmarksListView.checkedItemIds
451 // Initialize a variable to count the number of bookmarks to delete.
452 var numberOfBookmarksToDelete = 0
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()
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)
465 // Increment the count of the number of bookmarks to delete.
466 numberOfBookmarksToDelete++
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()
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)
475 // Update the list view.
476 bookmarksCursorAdapter.changeCursor(bookmarksCursor)
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)
487 // Update the list view.
488 bookmarksCursorAdapter.changeCursor(bookmarksCursor)
490 // Get the number of checked bookmarks.
491 val numberOfCheckedBookmarks = checkedBookmarksPositionsSparseBooleanArray.size()
493 // Set the checking many bookmarks flag.
494 checkingManyBookmarks = true
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
502 // Check the bookmark.
503 bookmarksListView.setItemChecked(checkedBookmarksPositionsSparseBooleanArray.keyAt(i), true)
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()
511 // Delete the contents of the folder if the selected bookmark is a folder.
512 if (bookmarksDatabaseHelper.isFolder(databaseIdInt))
513 deleteBookmarkFolderContents(databaseIdInt)
515 // Delete the selected bookmark.
516 bookmarksDatabaseHelper.deleteBookmark(databaseIdInt)
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()
524 // Move bookmarks cursor to the current bookmark position.
525 bookmarksCursor.moveToPosition(i)
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)
533 // Reset the deleting bookmarks flag.
534 deletingBookmarks = false
536 // Enable the delete bookmarks menu item.
537 deleteBookmarksMenuItem.isEnabled = true
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
544 // Finish the activity.
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
556 // Set the checking many bookmarks flag.
557 checkingManyBookmarks = true
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
565 // Check the bookmark.
566 bookmarksListView.setItemChecked(i, true)
570 // Consume the click.
574 override fun onDestroyActionMode(mode: ActionMode) {
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)
584 // Show the create bookmark folder dialog.
585 createBookmarkFolderDialog.show(supportFragmentManager, getString(R.string.create_folder))
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)
593 // Display the create bookmark dialog.
594 createBookmarkDialog.show(supportFragmentManager, resources.getString(R.string.create_bookmark))
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)
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)!!
607 // Check each previously checked bookmark in the list view.
608 checkedBookmarksArrayList.forEach(Consumer { position: Int -> bookmarksListView.setItemChecked(position, true) })
612 // Load the current folder.
616 public override fun onRestart() {
617 // Run the default commands.
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.
625 // Reset the restart from bookmarks database view activity flag.
626 restartFromBookmarksDatabaseViewActivity = false
630 public override fun onSaveInstanceState(outState: Bundle) {
631 // Run the default commands.
632 super.onSaveInstanceState(outState)
634 // Get the sparse boolean array of the checked items.
635 val checkedBookmarksSparseBooleanArray = bookmarksListView.checkedItemPositions
637 // Create a checked items array list.
638 val checkedBookmarksArrayList = ArrayList<Int>()
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))
649 // Store the variables in the out state.
650 outState.putLong(CURRENT_FOLDER_ID, currentFolderId)
651 outState.putIntegerArrayList(CHECKED_BOOKMARKS_ARRAY_LIST, checkedBookmarksArrayList)
654 override fun onCreateOptionsMenu(menu: Menu): Boolean {
656 menuInflater.inflate(R.menu.bookmarks_options_menu, menu)
662 override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
663 // Get a handle for the menu item ID.
664 val menuItemId = menuItem.itemId
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.
671 } else { // Currently in a subfolder.
672 // Set the former parent folder as the current folder.
673 currentFolderId = bookmarksDatabaseHelper.getParentFolderId(currentFolderId)
675 // Load the new current folder.
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
682 // Set the checking many bookmarks flag.
683 checkingManyBookmarks = true
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
691 // Check the bookmark.
692 bookmarksListView.setItemChecked(i, true)
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()
698 // Create an intent to launch the bookmarks database view activity.
699 val bookmarksDatabaseViewIntent = Intent(this, BookmarksDatabaseViewActivity::class.java)
701 // Include the favorite icon byte array to the intent.
702 bookmarksDatabaseViewIntent.putExtra(CURRENT_FAVORITE_ICON_BYTE_ARRAY, currentFavoriteIconByteArray)
705 startActivity(bookmarksDatabaseViewIntent)
710 override fun createBookmark(dialogFragment: DialogFragment) {
711 // Get the alert dialog from the fragment.
712 val dialog = dialogFragment.dialog!!
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)
721 // Get the strings from the edit texts.
722 val bookmarkNameString = bookmarkNameEditText.text.toString()
723 val bookmarkUrlString = bookmarkUrlEditText.text.toString()
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
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)
734 // Create a favorite icon byte array output stream.
735 val favoriteIconByteArrayOutputStream = ByteArrayOutputStream()
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)
740 // Convert the favorite icon byte array stream to a byte array.
741 val favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray()
743 // Display the new bookmark below the current items in the (0 indexed) list.
744 val newBookmarkDisplayOrder = bookmarksListView.count
746 // Create the bookmark.
747 bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentFolderId, newBookmarkDisplayOrder, favoriteIconByteArray)
749 // Update the bookmarks cursor with the current contents of this folder.
750 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderId)
752 // Update the list view.
753 bookmarksCursorAdapter.changeCursor(bookmarksCursor)
755 // Scroll to the new bookmark.
756 bookmarksListView.setSelection(newBookmarkDisplayOrder)
759 override fun createBookmarkFolder(dialogFragment: DialogFragment) {
760 // Get the dialog from the dialog fragment.
761 val dialog = dialogFragment.dialog!!
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)
771 // Get the folder name string.
772 val folderNameString = folderNameEditText.text.toString()
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
782 // Cast the folder icon bitmap to a bitmap drawable.
783 val folderIconBitmapDrawable = folderIconDrawable as BitmapDrawable
785 // Convert the folder icon bitmap drawable to a bitmap.
786 val folderIconBitmap = folderIconBitmapDrawable.bitmap
788 // Create a folder icon byte array output stream.
789 val folderIconByteArrayOutputStream = ByteArrayOutputStream()
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)
794 // Convert the folder icon byte array stream to a byte array.
795 val folderIconByteArray = folderIconByteArrayOutputStream.toByteArray()
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)
803 // Create the folder, which will be placed at the top of the list view.
804 bookmarksDatabaseHelper.createFolder(folderNameString, currentFolderId, displayOrder = 0, folderIconByteArray)
806 // Update the bookmarks cursor with the contents of the current folder.
807 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderId)
809 // Update the list view.
810 bookmarksCursorAdapter.changeCursor(bookmarksCursor)
812 // Scroll to the new folder.
813 bookmarksListView.setSelection(0)
816 override fun saveBookmark(dialogFragment: DialogFragment, selectedBookmarkDatabaseId: Int) {
817 // Get the dialog from the dialog fragment.
818 val dialog = dialogFragment.dialog!!
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)
828 // Get the strings from the edit texts.
829 val bookmarkNameString = bookmarkNameEditText.text.toString()
830 val bookmarkUrlString = bookmarkUrlEditText.text.toString()
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
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)
845 // Create a favorite icon byte array output stream.
846 val newFavoriteIconByteArrayOutputStream = ByteArrayOutputStream()
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)
851 // Convert the favorite icon byte array stream to a byte array.
852 val newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray()
854 // Update the bookmark and the favorite icon.
855 bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, newFavoriteIconByteArray)
858 // Close the contextual action bar if it is displayed.
859 contextualActionMode?.finish()
861 // Update the bookmarks cursor with the contents of the current folder.
862 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderId)
864 // Update the list view.
865 bookmarksCursorAdapter.changeCursor(bookmarksCursor)
868 override fun saveBookmarkFolder(dialogFragment: DialogFragment, selectedFolderDatabaseId: Int) {
869 // Get the dialog from the dialog fragment.
870 val dialog = dialogFragment.dialog!!
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)
881 // Get the new folder name.
882 val newFolderName = folderNameEditText.text.toString()
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
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)
900 // Create a new folder icon byte array output stream.
901 val newFolderIconByteArrayOutputStream = ByteArrayOutputStream()
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)
906 // Convert the folder icon byte array stream to a byte array.
907 val newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray()
909 // Update the database.
910 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderName, newFolderIconByteArray)
913 // Update the bookmarks cursor with the current contents of this folder.
914 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderId)
916 // Update the list view.
917 bookmarksCursorAdapter.changeCursor(bookmarksCursor)
919 // Close the contextual action mode.
920 contextualActionMode!!.finish()
923 override fun onMoveToFolder(dialogFragment: DialogFragment) {
924 // Get the dialog from the dialog fragment.
925 val dialog = dialogFragment.dialog!!
927 // Get a handle for the folder list view from the dialog.
928 val folderListView = dialog.findViewById<ListView>(R.id.move_to_folder_listview)
930 // Store a long array of the selected folders.
931 val newFolderLongArray = folderListView.checkedItemIds
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()
936 // Set the new folder name.
937 val newFolderId = if (newFolderDatabaseId == HOME_FOLDER_DATABASE_ID)
938 // The new folder is the home folder.
941 // Get the new folder name from the database.
942 bookmarksDatabaseHelper.getFolderId(newFolderDatabaseId)
944 // Get a long array with the the database ID of the selected bookmarks.
945 val selectedBookmarksLongArray = bookmarksListView.checkedItemIds
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()
952 // Move the selected bookmark to the new folder.
953 bookmarksDatabaseHelper.moveToFolder(databaseIdInt, newFolderId)
956 // Update the bookmarks cursor with the current contents of this folder.
957 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderId)
959 // Update the list view.
960 bookmarksCursorAdapter.changeCursor(bookmarksCursor)
962 // Close the contextual app bar.
963 contextualActionMode!!.finish()
966 private fun countBookmarkFolderContents(folderDatabaseId: Int): Int {
967 // Get the folder ID.
968 val folderId = bookmarksDatabaseHelper.getFolderId(folderDatabaseId)
970 // Get the contents of the folder.
971 val folderCursor = bookmarksDatabaseHelper.getBookmarkAndFolderIds(folderId)
973 // Initialize the bookmark counter.
974 var bookmarkCounter = 0
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)
981 // Get the database ID of the item.
982 val itemDatabaseId = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(ID))
984 // If this is a folder, recursively count the contents first.
985 if (bookmarksDatabaseHelper.isFolder(itemDatabaseId))
986 bookmarkCounter += countBookmarkFolderContents(itemDatabaseId)
988 // Add the bookmark to the running total.
992 // Return the bookmark counter.
993 return bookmarkCounter
996 private fun deleteBookmarkFolderContents(folderDatabaseId: Int) {
997 // Get the folder ID.
998 val folderId = bookmarksDatabaseHelper.getFolderId(folderDatabaseId)
1000 // Get the contents of the folder.
1001 val folderCursor = bookmarksDatabaseHelper.getBookmarkAndFolderIds(folderId)
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)
1008 // Get the database ID of the item.
1009 val itemDatabaseId = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(ID))
1011 // If this is a folder, recursively delete the contents first.
1012 if (bookmarksDatabaseHelper.isFolder(itemDatabaseId))
1013 deleteBookmarkFolderContents(itemDatabaseId)
1015 // Delete the bookmark.
1016 bookmarksDatabaseHelper.deleteBookmark(itemDatabaseId)
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
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
1032 // Close the bookmarks drawer and reload the bookmarks list view when returning to the main WebView activity.
1033 MainWebViewActivity.restartFromBookmarksActivity = true
1035 // Exit the bookmarks activity.
1040 private fun updateMoveIcons() {
1041 // Get a long array of the selected bookmarks.
1042 val selectedBookmarksLongArray = bookmarksListView.checkedItemIds
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()
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
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
1060 // Set the icon according to the theme.
1061 moveBookmarkUpMenuItem.setIcon(R.drawable.move_up_enabled)
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
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
1076 moveBookmarkDownMenuItem.setIcon(R.drawable.move_down_enabled)
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
1085 // Calculate the number of bookmarks per screen.
1086 val numberOfBookmarksPerScreen = lastVisibleBookmarkPosition - firstVisibleBookmarkPosition
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)
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)
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)
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)
1115 // Get the favorite icon byte array from the cursor.
1116 val favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(FAVORITE_ICON))
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)
1121 // Display the bitmap in the bookmark favorite icon image view.
1122 bookmarkFavoriteIconImageView.setImageBitmap(favoriteIconBitmap)
1124 // Get the bookmark name from the cursor.
1125 val bookmarkNameString = cursor.getString(cursor.getColumnIndexOrThrow(BOOKMARK_NAME))
1127 // Display the bookmark name.
1128 bookmarkNameTextView.text = bookmarkNameString
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
1138 // Populate the list view with the adapter.
1139 bookmarksListView.adapter = bookmarksCursorAdapter
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)
1145 appBar.title = bookmarksDatabaseHelper.getFolderName(currentFolderId)
1148 public override fun onDestroy() {
1149 // Close the bookmarks cursor and database.
1150 bookmarksCursor.close()
1151 bookmarksDatabaseHelper.close()
1153 // Run the default commands.