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.database.Cursor
24 import android.database.MatrixCursor
25 import android.database.MergeCursor
26 import android.graphics.Bitmap
27 import android.graphics.BitmapFactory
28 import android.graphics.Typeface
29 import android.os.Bundle
30 import android.view.ActionMode
31 import android.view.Menu
32 import android.view.MenuItem
33 import android.view.View
34 import android.view.ViewGroup
35 import android.view.Window
36 import android.view.WindowManager
37 import android.widget.AbsListView.MultiChoiceModeListener
38 import android.widget.AdapterView
39 import android.widget.EditText
40 import android.widget.ImageView
41 import android.widget.ListView
42 import android.widget.RadioButton
43 import android.widget.Spinner
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.content.res.AppCompatResources
50 import androidx.appcompat.widget.Toolbar
51 import androidx.core.graphics.drawable.toBitmap
52 import androidx.cursoradapter.widget.CursorAdapter
53 import androidx.cursoradapter.widget.ResourceCursorAdapter
54 import androidx.fragment.app.DialogFragment
55 import androidx.preference.PreferenceManager
57 import com.google.android.material.snackbar.Snackbar
59 import com.stoutner.privacybrowser.R
60 import com.stoutner.privacybrowser.dialogs.EditBookmarkDatabaseViewDialog.Companion.bookmarkDatabaseId
61 import com.stoutner.privacybrowser.dialogs.EditBookmarkDatabaseViewDialog.EditBookmarkDatabaseViewListener
62 import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDatabaseViewDialog.Companion.folderDatabaseId
63 import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDatabaseViewDialog.EditBookmarkFolderDatabaseViewListener
64 import com.stoutner.privacybrowser.helpers.BOOKMARK_NAME
65 import com.stoutner.privacybrowser.helpers.BOOKMARK_URL
66 import com.stoutner.privacybrowser.helpers.DISPLAY_ORDER
67 import com.stoutner.privacybrowser.helpers.FAVORITE_ICON
68 import com.stoutner.privacybrowser.helpers.FOLDER_ID
69 import com.stoutner.privacybrowser.helpers.ID
70 import com.stoutner.privacybrowser.helpers.IS_FOLDER
71 import com.stoutner.privacybrowser.helpers.PARENT_FOLDER_ID
72 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper
74 import java.io.ByteArrayOutputStream
76 // Define the public class constants.
77 const val HOME_FOLDER_DATABASE_ID = -1
78 const val HOME_FOLDER_ID = 0L
80 // Define the private class constants.
81 private const val ALL_FOLDERS_DATABASE_ID = -2
82 private const val CURRENT_FOLDER_DATABASE_ID = "A"
83 private const val SORT_BY_DISPLAY_ORDER = "B"
85 class BookmarksDatabaseViewActivity : AppCompatActivity(), EditBookmarkDatabaseViewListener, EditBookmarkFolderDatabaseViewListener {
86 // Define the class variables.
87 private var closeActivityAfterDismissingSnackbar = false
88 private var currentFolderDatabaseId = 0
89 private var bookmarksCursorAdapter: CursorAdapter? = null
90 private var bookmarksDeletedSnackbar: Snackbar? = null
91 private var sortByDisplayOrder = false
93 // Declare the class variables.
94 private lateinit var bookmarksCursor: Cursor
95 private lateinit var bookmarksDatabaseHelper: BookmarksDatabaseHelper
96 private lateinit var bookmarksListView: ListView
98 public override fun onCreate(savedInstanceState: Bundle?) {
99 // Get a handle for the shared preferences.
100 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
102 // Get the preferences.
103 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
104 val bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false)
106 // Disable screenshots if not allowed.
107 if (!allowScreenshots) {
108 window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
111 // Run the default commands.
112 super.onCreate(savedInstanceState)
114 // Get the favorite icon byte array.
115 val currentFavoriteIconByteArray = intent.getByteArrayExtra(CURRENT_FAVORITE_ICON_BYTE_ARRAY)!!
117 // Convert the favorite icon byte array to a bitmap and store it in a class variable.
118 val currentFavoriteIconBitmap = BitmapFactory.decodeByteArray(currentFavoriteIconByteArray, 0, currentFavoriteIconByteArray.size)
120 // Set the view according to the theme.
122 // Set the content view.
123 setContentView(R.layout.bookmarks_databaseview_bottom_appbar)
125 // `Window.FEATURE_ACTION_MODE_OVERLAY` makes the contextual action mode cover the support action bar. It must be requested before the content is set.
126 supportRequestWindowFeature(Window.FEATURE_ACTION_MODE_OVERLAY)
128 // Set the content view.
129 setContentView(R.layout.bookmarks_databaseview_top_appbar)
132 // Get a handle for the toolbar.
133 val toolbar = findViewById<Toolbar>(R.id.bookmarks_databaseview_toolbar)
134 bookmarksListView = findViewById(R.id.bookmarks_databaseview_listview)
136 // Set the support action bar.
137 setSupportActionBar(toolbar)
139 // Get a handle for the app bar.
140 val appBar = supportActionBar!!
142 // Set the app bar custom view.
143 appBar.setCustomView(R.layout.spinner)
145 // Display the back arrow in the app bar.
146 appBar.displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM or ActionBar.DISPLAY_HOME_AS_UP
148 // Control what the system back command does.
149 val onBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) {
150 override fun handleOnBackPressed() {
151 // Prepare to finish the activity.
156 // Register the on back pressed callback.
157 onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
159 // Initialize the database handler.
160 bookmarksDatabaseHelper = BookmarksDatabaseHelper(this)
162 // Create a matrix cursor column name string array.
163 val matrixCursorColumnNames = arrayOf(ID, BOOKMARK_NAME, PARENT_FOLDER_ID)
165 // Setup the matrix cursor.
166 MatrixCursor(matrixCursorColumnNames).use { matrixCursor ->
167 // Add "All Folders" and "Home Folder" to the matrix cursor.
168 matrixCursor.addRow(arrayOf<Any>(ALL_FOLDERS_DATABASE_ID, getString(R.string.all_folders), HOME_FOLDER_ID))
169 matrixCursor.addRow(arrayOf<Any>(HOME_FOLDER_DATABASE_ID, getString(R.string.home_folder), HOME_FOLDER_ID))
171 // Get a cursor with the list of all the folders.
172 val foldersCursor = bookmarksDatabaseHelper.getFoldersExcept(listOf())
174 // Combine the matrix cursor and the folders cursor.
175 val foldersMergeCursor = MergeCursor(arrayOf(matrixCursor, foldersCursor))
177 // Create a resource cursor adapter for the spinner.
178 val foldersCursorAdapter: ResourceCursorAdapter = object : ResourceCursorAdapter(this, R.layout.bookmarks_databaseview_appbar_spinner_item, foldersMergeCursor, 0) {
179 override fun bindView(view: View, context: Context, cursor: Cursor) {
180 // Get handles for the spinner views.
181 val subfolderSpacerTextView = view.findViewById<TextView>(R.id.subfolder_spacer_textview)
182 val folderIconImageView = view.findViewById<ImageView>(R.id.folder_icon_imageview)
183 val folderNameTextView = view.findViewById<TextView>(R.id.folder_name_textview)
185 // Populate the subfolder spacer if it is not null (the spinner is open).
186 if (subfolderSpacerTextView != null) {
187 // Indent subfolders.
188 if (cursor.getLong(cursor.getColumnIndexOrThrow(PARENT_FOLDER_ID)) != HOME_FOLDER_ID) { // The folder is not in the home folder.
189 // Get the subfolder spacer.
190 subfolderSpacerTextView.text = bookmarksDatabaseHelper.getSubfolderSpacer(cursor.getLong(cursor.getColumnIndexOrThrow(FOLDER_ID)))
191 } else { // The folder is in the home folder.
192 // Reset the subfolder spacer.
193 subfolderSpacerTextView.text = ""
197 // Set the folder icon according to the type.
198 if (foldersMergeCursor.position > 1) { // Set a user folder icon.
199 // Get the folder icon byte array from the cursor.
200 val folderIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(FAVORITE_ICON))
202 // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
203 val folderIconBitmap = BitmapFactory.decodeByteArray(folderIconByteArray, 0, folderIconByteArray.size)
205 // Set the folder image stored in the cursor.
206 folderIconImageView.setImageBitmap(folderIconBitmap)
207 } else { // Set the `All Folders` or `Home Folder` icon.
208 // Set the gray folder image.
209 folderIconImageView.setImageDrawable(AppCompatResources.getDrawable(context, R.drawable.folder_gray))
212 // Set the folder name.
213 folderNameTextView.text = cursor.getString(cursor.getColumnIndexOrThrow(BOOKMARK_NAME))
217 // Set the resource cursor adapter drop drown view resource.
218 foldersCursorAdapter.setDropDownViewResource(R.layout.bookmarks_databaseview_appbar_spinner_dropdown_item)
220 // Get a handle for the folder spinner.
221 val folderSpinner = findViewById<Spinner>(R.id.spinner)
223 // Set the folder spinner adapter.
224 folderSpinner.adapter = foldersCursorAdapter
226 // Wait to set the on item selected listener until the spinner has been inflated. Otherwise the activity will crash on restart.
228 // Handle taps on the spinner dropdown.
229 folderSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
230 override fun onItemSelected(parent: AdapterView<*>?, view: View, position: Int, id: Long) {
231 // Store the current folder database ID.
232 currentFolderDatabaseId = id.toInt()
234 // Update the list view.
235 updateBookmarksListView()
238 override fun onNothingSelected(parent: AdapterView<*>?) {
244 // Check to see if the activity was restarted.
245 if (savedInstanceState == null) { // The activity was not restarted.
246 // Set the default current folder database ID.
247 currentFolderDatabaseId = ALL_FOLDERS_DATABASE_ID
248 } else { // The activity was restarted.
249 // Restore the class variables from the saved instance state.
250 currentFolderDatabaseId = savedInstanceState.getInt(CURRENT_FOLDER_DATABASE_ID)
251 sortByDisplayOrder = savedInstanceState.getBoolean(SORT_BY_DISPLAY_ORDER)
253 // Update the spinner if the home folder is selected. Android handles this by default for the main cursor but not the matrix cursor.
254 if (currentFolderDatabaseId == HOME_FOLDER_DATABASE_ID) {
255 folderSpinner.setSelection(1)
259 // Update the bookmarks listview.
260 updateBookmarksListView()
262 // Setup a cursor adapter.
263 bookmarksCursorAdapter = object : CursorAdapter(this, bookmarksCursor, false) {
264 override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
265 // Inflate the individual item layout. `false` does not attach it to the root.
266 return layoutInflater.inflate(R.layout.bookmarks_databaseview_item_linearlayout, parent, false)
269 override fun bindView(view: View, context: Context, cursor: Cursor) {
270 // Get handles for the views.
271 val databaseIdTextView = view.findViewById<TextView>(R.id.database_id_textview)
272 val bookmarkFavoriteIconImageView = view.findViewById<ImageView>(R.id.favorite_icon_imageview)
273 val nameTextView = view.findViewById<TextView>(R.id.name_textview)
274 val folderIdTextView = view.findViewById<TextView>(R.id.folder_id_textview)
275 val urlTextView = view.findViewById<TextView>(R.id.url_textview)
276 val displayOrderTextView = view.findViewById<TextView>(R.id.display_order_textview)
277 val parentFolderIconImageView = view.findViewById<ImageView>(R.id.parent_folder_icon_imageview)
278 val parentFolderTextView = view.findViewById<TextView>(R.id.parent_folder_textview)
280 // Get the information from the cursor.
281 val databaseId = cursor.getInt(cursor.getColumnIndexOrThrow(ID))
282 val bookmarkFavoriteIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(FAVORITE_ICON))
283 val nameString = cursor.getString(cursor.getColumnIndexOrThrow(BOOKMARK_NAME))
284 val folderId = cursor.getLong(cursor.getColumnIndexOrThrow(FOLDER_ID))
285 val urlString = cursor.getString(cursor.getColumnIndexOrThrow(BOOKMARK_URL))
286 val displayOrder = cursor.getInt(cursor.getColumnIndexOrThrow(DISPLAY_ORDER))
287 val parentFolderId = cursor.getLong(cursor.getColumnIndexOrThrow(PARENT_FOLDER_ID))
289 // Convert the byte array to a `Bitmap` beginning at the beginning at the first byte and ending at the last.
290 val bookmarkFavoriteIconBitmap = BitmapFactory.decodeByteArray(bookmarkFavoriteIconByteArray, 0, bookmarkFavoriteIconByteArray.size)
292 // Populate the views.
293 databaseIdTextView.text = databaseId.toString()
294 bookmarkFavoriteIconImageView.setImageBitmap(bookmarkFavoriteIconBitmap)
295 nameTextView.text = nameString
296 urlTextView.text = urlString
297 displayOrderTextView.text = displayOrder.toString()
299 // Check to see if the bookmark is a folder.
300 if (cursor.getInt(cursor.getColumnIndexOrThrow(IS_FOLDER)) == 1) { // The bookmark is a folder.
301 // Make the font bold. When the first argument is null the font is not changed.
302 nameTextView.setTypeface(null, Typeface.BOLD)
304 // Display the folder ID.
305 folderIdTextView.text = getString(R.string.folder_id_separator, folderId)
308 urlTextView.visibility = View.GONE
309 } else { // The bookmark is not a folder.
310 // Reset the font to default.
311 nameTextView.typeface = Typeface.DEFAULT
314 urlTextView.visibility = View.VISIBLE
317 // Make the folder name gray if it is the home folder.
318 if (parentFolderId == HOME_FOLDER_ID) { // The bookmark is in the home folder.
319 // Get the home folder icon.
320 parentFolderIconImageView.setImageDrawable(AppCompatResources.getDrawable(applicationContext, R.drawable.folder_gray))
322 // Set the parent folder text to be `Home Folder`.
323 parentFolderTextView.setText(R.string.home_folder)
325 // Set the home folder text to be gray.
326 parentFolderTextView.setTextColor(getColor(R.color.gray_500))
327 } else { // The bookmark is in a subfolder.
328 // Set the parent folder icon.
329 parentFolderIconImageView.setImageDrawable(AppCompatResources.getDrawable(applicationContext, R.drawable.folder_dark_blue))
331 // Set the parent folder name.
332 parentFolderTextView.text = bookmarksDatabaseHelper.getFolderName(parentFolderId)
334 // Set the parent folder text color.
335 parentFolderTextView.setTextColor(getColor(R.color.parent_folder_text))
340 // Update the ListView.
341 bookmarksListView.adapter = bookmarksCursorAdapter
343 // Set a listener to edit a bookmark when it is tapped.
344 bookmarksListView.onItemClickListener = AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, _: Int, id: Long ->
345 // Convert the database ID to an int.
346 val databaseId = id.toInt()
348 // Show the edit bookmark or edit bookmark folder dialog.
349 if (bookmarksDatabaseHelper.isFolder(databaseId)) {
350 // Instantiate the edit bookmark folder dialog.
351 val editBookmarkFolderDatabaseViewDialog = folderDatabaseId(databaseId, currentFavoriteIconBitmap)
354 editBookmarkFolderDatabaseViewDialog.show(supportFragmentManager, resources.getString(R.string.edit_folder))
356 // Instantiate the edit bookmark dialog.
357 val editBookmarkDatabaseViewDialog = bookmarkDatabaseId(databaseId, currentFavoriteIconBitmap)
360 editBookmarkDatabaseViewDialog.show(supportFragmentManager, resources.getString(R.string.edit_bookmark))
364 // Handle long presses on the list view.
365 bookmarksListView.setMultiChoiceModeListener(object : MultiChoiceModeListener {
366 // Instantiate the common variables.
367 private lateinit var selectAllMenuItem: MenuItem
368 private lateinit var deleteMenuItem: MenuItem
369 private var deletingBookmarks = false
371 override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
372 // Inflate the menu for the contextual app bar.
373 menuInflater.inflate(R.menu.bookmarks_databaseview_context_menu, menu)
375 // Get handles for the menu items.
376 selectAllMenuItem = menu.findItem(R.id.select_all)
377 deleteMenuItem = menu.findItem(R.id.delete)
379 // Disable the delete menu item if a delete is pending.
380 deleteMenuItem.isEnabled = !deletingBookmarks
382 // Get the number of currently selected bookmarks.
383 val numberOfSelectedBookmarks = bookmarksListView.checkedItemCount
386 mode.setTitle(R.string.bookmarks)
388 // Set the action mode subtitle according to the number of selected bookmarks. This must be set here or it will be missing if the activity is restarted.
389 mode.subtitle = getString(R.string.selected, numberOfSelectedBookmarks)
391 // Do not show the select all menu item if all the bookmarks are already checked.
392 if (numberOfSelectedBookmarks == bookmarksListView.count)
393 selectAllMenuItem.isVisible = false
399 override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
404 override fun onItemCheckedStateChanged(mode: ActionMode, position: Int, id: Long, checked: Boolean) {
405 // Calculate the number of selected bookmarks.
406 val numberOfSelectedBookmarks = bookmarksListView.checkedItemCount
408 // Only run the commands if at least one bookmark is selected. Otherwise, a context menu with 0 selected bookmarks is briefly displayed.
409 if (numberOfSelectedBookmarks > 0) {
410 // Update the action mode subtitle according to the number of selected bookmarks.
411 mode.subtitle = getString(R.string.selected, numberOfSelectedBookmarks)
413 // Only show the select all menu item if all of the bookmarks are not already selected.
414 selectAllMenuItem.isVisible = (bookmarksListView.checkedItemCount != bookmarksListView.count)
416 // Convert the database ID to an int.
417 val databaseId = id.toInt()
419 // If a folder was selected, also select all the contents.
420 if (checked && bookmarksDatabaseHelper.isFolder(databaseId))
421 selectAllBookmarksInFolder(databaseId)
423 // Do not allow a bookmark to be deselected if the folder is selected.
425 // Get the parent folder ID.
426 val parentFolderId = bookmarksDatabaseHelper.getParentFolderId(databaseId)
428 // If the bookmark is not in the root folder, check to see if the folder is selected.
429 if (parentFolderId != HOME_FOLDER_ID) {
430 // Get the folder database ID.
431 val folderDatabaseId = bookmarksDatabaseHelper.getFolderDatabaseId(parentFolderId)
433 // Move the bookmarks cursor to the first position.
434 bookmarksCursor.moveToFirst()
436 // Initialize the folder position variable.
437 var folderPosition = -1
439 // Get the position of the folder in the bookmarks cursor.
440 while ((folderPosition < 0) && (bookmarksCursor.position < bookmarksCursor.count)) {
441 // Check if the folder database ID matches the bookmark database ID.
442 if (folderDatabaseId == bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(ID))) {
443 // Get the folder position.
444 folderPosition = bookmarksCursor.position
446 // Check if the folder is selected.
447 if (bookmarksListView.isItemChecked(folderPosition)) {
448 // Reselect the bookmark.
449 bookmarksListView.setItemChecked(position, true)
451 // Display a snackbar explaining why the bookmark cannot be deselected.
452 Snackbar.make(bookmarksListView, R.string.cannot_deselect_bookmark, Snackbar.LENGTH_LONG).show()
456 // Increment the bookmarks cursor.
457 bookmarksCursor.moveToNext()
464 override fun onActionItemClicked(mode: ActionMode, menuItem: MenuItem): Boolean {
465 // Get a the menu item ID.
466 val menuItemId = menuItem.itemId
468 // Run the command that corresponds to the selected menu item.
469 if (menuItemId == R.id.select_all) { // Select all the bookmarks.
470 // Get the total number of bookmarks.
471 val numberOfBookmarks = bookmarksListView.count
474 for (i in 0 until numberOfBookmarks) {
475 bookmarksListView.setItemChecked(i, true)
477 } else if (menuItemId == R.id.delete) { // Delete the selected bookmarks.
478 // Set the deleting bookmarks flag, which prevents the delete menu item from being enabled until the current process finishes.
479 deletingBookmarks = true
481 // Get an array of the selected row IDs.
482 val selectedBookmarksIdsLongArray = bookmarksListView.checkedItemIds
484 // 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.
485 val selectedBookmarksPositionsSparseBooleanArray = bookmarksListView.checkedItemPositions.clone()
487 // Populate the bookmarks cursor.
488 bookmarksCursor = when (currentFolderDatabaseId) {
489 // Get all the bookmarks except the ones being deleted.
490 ALL_FOLDERS_DATABASE_ID -> {
491 if (sortByDisplayOrder)
492 bookmarksDatabaseHelper.getAllBookmarksByDisplayOrderExcept(selectedBookmarksIdsLongArray)
494 bookmarksDatabaseHelper.getAllBookmarksExcept(selectedBookmarksIdsLongArray)
497 // Get the home folder bookmarks except the ones being deleted.
498 HOME_FOLDER_DATABASE_ID -> {
499 if (sortByDisplayOrder)
500 bookmarksDatabaseHelper.getBookmarksByDisplayOrderExcept(selectedBookmarksIdsLongArray, HOME_FOLDER_ID)
502 bookmarksDatabaseHelper.getBookmarksExcept(selectedBookmarksIdsLongArray, HOME_FOLDER_ID)
505 // Get the current folder bookmarks except the ones being deleted.
507 // Get the current folder ID.
508 val currentFolderId = bookmarksDatabaseHelper.getFolderId(currentFolderDatabaseId)
511 if (sortByDisplayOrder)
512 bookmarksDatabaseHelper.getBookmarksByDisplayOrderExcept(selectedBookmarksIdsLongArray, currentFolderId)
514 bookmarksDatabaseHelper.getBookmarksExcept(selectedBookmarksIdsLongArray, currentFolderId)
518 // Update the list view.
519 bookmarksCursorAdapter!!.changeCursor(bookmarksCursor)
521 // Create a snackbar with the number of deleted bookmarks.
522 bookmarksDeletedSnackbar = Snackbar.make(findViewById(R.id.bookmarks_databaseview_coordinatorlayout), getString(R.string.bookmarks_deleted, selectedBookmarksIdsLongArray.size),
523 Snackbar.LENGTH_LONG)
524 .setAction(R.string.undo) {} // Undo will be handles by `onDismissed()` below.
525 .addCallback(object : Snackbar.Callback() {
526 override fun onDismissed(snackbar: Snackbar, event: Int) {
527 if (event == DISMISS_EVENT_ACTION) { // The user pushed the undo button.
528 // Update the bookmarks list view with the current contents of the bookmarks database, including the "deleted" bookmarks.
529 updateBookmarksListView()
531 // Re-select the previously selected bookmarks.
532 for (i in 0 until selectedBookmarksPositionsSparseBooleanArray.size())
533 bookmarksListView.setItemChecked(selectedBookmarksPositionsSparseBooleanArray.keyAt(i), true)
534 } else { // The snackbar was dismissed without the undo button being pushed.
535 // Delete each selected bookmark.
536 for (databaseIdLong in selectedBookmarksIdsLongArray) {
537 // Convert the database long ID to an int.
538 val databaseIdInt = databaseIdLong.toInt()
540 // Delete the selected bookmark.
541 bookmarksDatabaseHelper.deleteBookmark(databaseIdInt)
545 // Reset the deleting bookmarks flag.
546 deletingBookmarks = false
548 // Enable the delete menu item.
549 deleteMenuItem.isEnabled = true
551 // Close the activity if back has been pressed.
552 if (closeActivityAfterDismissingSnackbar) {
553 // Reload the bookmarks list view when returning to the bookmarks activity.
554 BookmarksActivity.restartFromBookmarksDatabaseViewActivity = true
556 // Finish the activity.
562 // Show the snackbar.
563 bookmarksDeletedSnackbar!!.show()
566 // Consume the click.
570 override fun onDestroyActionMode(mode: ActionMode) {
577 override fun onCreateOptionsMenu(menu: Menu): Boolean {
579 menuInflater.inflate(R.menu.bookmarks_databaseview_options_menu, menu)
581 // Get a handle for the sort menu item.
582 val sortMenuItem = menu.findItem(R.id.sort)
584 // Change the sort menu item icon if the listview is sorted by display order, which restores the state after a restart.
585 if (sortByDisplayOrder)
586 sortMenuItem.setIcon(R.drawable.sort_selected)
592 public override fun onDestroy() {
593 // Close the bookmarks cursor and database.
594 bookmarksCursor.close()
595 bookmarksDatabaseHelper.close()
597 // Run the default commands.
601 override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
602 // Get the menu item ID.
603 val menuItemId = menuItem.itemId
605 // Run the command that corresponds to the selected menu item.
606 if (menuItemId == android.R.id.home) { // Go Home. The home arrow is identified as `android.R.id.home`, not just `R.id.home`.
607 // Prepare to finish the activity.
609 } else if (menuItemId == R.id.sort) { // Toggle the sort mode.
610 // Update the sort by display order tracker.
611 sortByDisplayOrder = !sortByDisplayOrder
613 // Update the icon and display a snackbar.
614 if (sortByDisplayOrder) { // Sort by display order.
616 menuItem.setIcon(R.drawable.sort_selected)
618 // Display a snackbar indicating the current sort type.
619 Snackbar.make(bookmarksListView, R.string.sorted_by_display_order, Snackbar.LENGTH_SHORT).show()
620 } else { // Sort by database id.
622 menuItem.setIcon(R.drawable.sort)
624 // Display a snackbar indicating the current sort type.
625 Snackbar.make(bookmarksListView, R.string.sorted_by_database_id, Snackbar.LENGTH_SHORT).show()
628 // Update the list view.
629 updateBookmarksListView()
632 // Consume the event.
636 public override fun onSaveInstanceState(savedInstanceState: Bundle) {
637 // Run the default commands.
638 super.onSaveInstanceState(savedInstanceState)
640 // Store the class variables in the bundle.
641 savedInstanceState.putInt(CURRENT_FOLDER_DATABASE_ID, currentFolderDatabaseId)
642 savedInstanceState.putBoolean(SORT_BY_DISPLAY_ORDER, sortByDisplayOrder)
645 override fun saveBookmark(dialogFragment: DialogFragment, selectedBookmarkDatabaseId: Int) {
646 // Get the dialog from the dialog fragment.
647 val dialog = dialogFragment.dialog!!
649 // Get handles for the views from dialog fragment.
650 val currentIconRadioButton = dialog.findViewById<RadioButton>(R.id.current_icon_radiobutton)
651 val webpageFavoriteIconRadioButton = dialog.findViewById<RadioButton>(R.id.webpage_favorite_icon_radiobutton)
652 val webpageFavoriteIconImageView = dialog.findViewById<ImageView>(R.id.webpage_favorite_icon_imageview)
653 val customIconImageView = dialog.findViewById<ImageView>(R.id.custom_icon_imageview)
654 val bookmarkNameEditText = dialog.findViewById<EditText>(R.id.bookmark_name_edittext)
655 val bookmarkUrlEditText = dialog.findViewById<EditText>(R.id.bookmark_url_edittext)
656 val folderSpinner = dialog.findViewById<Spinner>(R.id.bookmark_folder_spinner)
657 val displayOrderEditText = dialog.findViewById<EditText>(R.id.bookmark_display_order_edittext)
659 // Extract the bookmark information.
660 val bookmarkNameString = bookmarkNameEditText.text.toString()
661 val bookmarkUrlString = bookmarkUrlEditText.text.toString()
662 val folderDatabaseId = folderSpinner.selectedItemId.toInt()
663 val displayOrderInt = displayOrderEditText.text.toString().toInt()
665 // Get the parent folder ID.
666 val parentFolderId: Long = if (folderDatabaseId == HOME_FOLDER_DATABASE_ID) // The home folder is selected.
668 else // Get the parent folder name from the database.
669 bookmarksDatabaseHelper.getFolderId(folderDatabaseId)
671 // Update the bookmark.
672 if (currentIconRadioButton.isChecked) { // Update the bookmark without changing the favorite icon.
673 bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, parentFolderId, displayOrderInt)
674 } else { // Update the bookmark using the WebView favorite icon.
675 // Get the selected favorite icon drawable.
676 val favoriteIconDrawable = if (webpageFavoriteIconRadioButton.isChecked) // The webpage favorite icon is checked.
677 webpageFavoriteIconImageView.drawable
678 else // The custom favorite icon is checked.
679 customIconImageView.drawable
681 // Convert the favorite icon drawable to a bitmap. Once the minimum API >= 33, this can use Bitmap.Config.RGBA_1010102.
682 val favoriteIconBitmap = favoriteIconDrawable.toBitmap(128, 128, Bitmap.Config.ARGB_8888)
684 // Create a favorite icon byte array output stream.
685 val newFavoriteIconByteArrayOutputStream = ByteArrayOutputStream()
687 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
688 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFavoriteIconByteArrayOutputStream)
690 // Convert the favorite icon byte array stream to a byte array.
691 val newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray()
693 // Update the bookmark and the favorite icon.
694 bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, parentFolderId, displayOrderInt, newFavoriteIconByteArray)
697 // Update the list view.
698 updateBookmarksListView()
701 override fun saveBookmarkFolder(dialogFragment: DialogFragment, selectedFolderDatabaseId: Int) {
702 // Get the dialog from the dialog fragment.
703 val dialog = dialogFragment.dialog!!
705 // Get handles for the views from dialog fragment.
706 val currentIconRadioButton = dialog.findViewById<RadioButton>(R.id.current_icon_radiobutton)
707 val defaultFolderIconRadioButton = dialog.findViewById<RadioButton>(R.id.default_folder_icon_radiobutton)
708 val defaultFolderIconImageView = dialog.findViewById<ImageView>(R.id.default_folder_icon_imageview)
709 val webpageFavoriteIconRadioButton = dialog.findViewById<RadioButton>(R.id.webpage_favorite_icon_radiobutton)
710 val webpageFavoriteIconImageView = dialog.findViewById<ImageView>(R.id.webpage_favorite_icon_imageview)
711 val customIconImageView = dialog.findViewById<ImageView>(R.id.custom_icon_imageview)
712 val folderNameEditText = dialog.findViewById<EditText>(R.id.folder_name_edittext)
713 val parentFolderSpinner = dialog.findViewById<Spinner>(R.id.parent_folder_spinner)
714 val displayOrderEditText = dialog.findViewById<EditText>(R.id.display_order_edittext)
716 // Get the folder information.
717 val newFolderNameString = folderNameEditText.text.toString()
718 val parentFolderDatabaseId = parentFolderSpinner.selectedItemId.toInt()
719 val displayOrderInt = displayOrderEditText.text.toString().toInt()
721 // Set the parent folder ID.
722 val parentFolderIdLong: Long = if (parentFolderDatabaseId == HOME_FOLDER_DATABASE_ID) // The home folder is selected.
724 else // Get the parent folder name from the database.
725 bookmarksDatabaseHelper.getFolderId(parentFolderDatabaseId)
727 // Update the folder.
728 if (currentIconRadioButton.isChecked) { // Update the folder without changing the favorite icon.
729 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderNameString, parentFolderIdLong, displayOrderInt)
730 } else { // Update the folder and the icon.
731 // Get the selected folder icon drawable.
732 val folderIconDrawable = if (defaultFolderIconRadioButton.isChecked) // The default folder icon is checked.
733 defaultFolderIconImageView.drawable
734 else if (webpageFavoriteIconRadioButton.isChecked) // The webpage favorite icon is checked.
735 webpageFavoriteIconImageView.drawable
736 else // The custom icon is checked.
737 customIconImageView.drawable
739 // Convert the folder icon drawable to a bitmap. Once the minimum API >= 33, this can use Bitmap.Config.RGBA_1010102.
740 val folderIconBitmap = folderIconDrawable.toBitmap(128, 128, Bitmap.Config.ARGB_8888)
742 // Create a folder icon byte array output stream.
743 val newFolderIconByteArrayOutputStream = ByteArrayOutputStream()
745 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
746 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream)
748 // Convert the folder icon byte array stream to a byte array.
749 val newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray()
751 // Update the folder and the icon.
752 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderNameString, parentFolderIdLong, displayOrderInt, newFolderIconByteArray)
755 // Update the list view.
756 updateBookmarksListView()
759 private fun updateBookmarksListView() {
760 // Populate the bookmarks list view based on the spinner selection.
761 bookmarksCursor = when (currentFolderDatabaseId) {
762 // Get all the bookmarks.
763 ALL_FOLDERS_DATABASE_ID -> {
764 if (sortByDisplayOrder)
765 bookmarksDatabaseHelper.allBookmarksByDisplayOrder
767 bookmarksDatabaseHelper.allBookmarks
770 // Get the bookmarks in the home folder.
771 HOME_FOLDER_DATABASE_ID -> {
772 if (sortByDisplayOrder)
773 bookmarksDatabaseHelper.getBookmarksByDisplayOrder(HOME_FOLDER_ID)
775 bookmarksDatabaseHelper.getBookmarks(HOME_FOLDER_ID)
778 // Get the bookmarks in the specified folder.
780 // Get the current folder ID.
781 val currentFolderId = bookmarksDatabaseHelper.getFolderId(currentFolderDatabaseId)
783 if (sortByDisplayOrder)
784 bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderId)
786 bookmarksDatabaseHelper.getBookmarks(currentFolderId)
790 // Update the cursor adapter if it isn't null, which happens when the activity is restarted.
791 if (bookmarksCursorAdapter != null) {
792 bookmarksCursorAdapter!!.changeCursor(bookmarksCursor)
796 private fun prepareFinish() {
797 // Check to see if a snackbar is currently displayed. If so, it must be closed before existing so that a pending delete is completed before reloading the list view in the bookmarks activity.
798 if ((bookmarksDeletedSnackbar != null) && bookmarksDeletedSnackbar!!.isShown) { // Close the bookmarks deleted snackbar before going home.
799 // Set the close flag.
800 closeActivityAfterDismissingSnackbar = true
802 // Dismiss the snackbar.
803 bookmarksDeletedSnackbar!!.dismiss()
804 } else { // Go home immediately.
805 // Update the current folder in the bookmarks activity.
806 if ((currentFolderDatabaseId == ALL_FOLDERS_DATABASE_ID) || (currentFolderDatabaseId == HOME_FOLDER_DATABASE_ID)) { // All folders or the the home folder are currently displayed.
807 // Load the home folder.
808 BookmarksActivity.currentFolderId = HOME_FOLDER_ID
809 } else { // A subfolder is currently displayed.
810 // Load the current folder.
811 BookmarksActivity.currentFolderId = bookmarksDatabaseHelper.getFolderId(currentFolderDatabaseId)
814 // Reload the bookmarks list view when returning to the bookmarks activity.
815 BookmarksActivity.restartFromBookmarksDatabaseViewActivity = true
817 // Exit the bookmarks database view activity.
822 private fun selectAllBookmarksInFolder(folderDatabaseId: Int) {
823 // Get the folder ID.
824 val folderId = bookmarksDatabaseHelper.getFolderId(folderDatabaseId)
826 // Get a cursor with the contents of the folder.
827 val folderCursor = bookmarksDatabaseHelper.getBookmarks(folderId)
829 // Get the column indexes.
830 val idColumnIndex = bookmarksCursor.getColumnIndexOrThrow(ID)
832 // Move to the beginning of the cursor.
833 folderCursor.moveToFirst()
835 while (folderCursor.position < folderCursor.count) {
836 // Get the bookmark database ID.
837 val bookmarkId = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(ID))
839 // Move the bookmarks cursor to the first position.
840 bookmarksCursor.moveToFirst()
842 // Initialize the bookmark position variable.
843 var bookmarkPosition = -1
845 // Get the position of this bookmark in the bookmarks cursor.
846 while ((bookmarkPosition < 0) && (bookmarksCursor.position < bookmarksCursor.count)) {
847 // Check if the bookmark IDs match.
848 if (bookmarkId == bookmarksCursor.getInt(idColumnIndex)) {
849 // Get the bookmark position.
850 bookmarkPosition = bookmarksCursor.position
852 // If this bookmark is a folder, select all the bookmarks inside it.
853 if (bookmarksDatabaseHelper.isFolder(bookmarkId))
854 selectAllBookmarksInFolder(bookmarkId)
856 // Select the bookmark.
857 bookmarksListView.setItemChecked(bookmarkPosition, true)
860 // Increment the bookmarks cursor position.
861 bookmarksCursor.moveToNext()
864 // Move to the next position.
865 folderCursor.moveToNext()