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)
110 // Run the default commands.
111 super.onCreate(savedInstanceState)
113 // Get the favorite icon byte array.
114 val currentFavoriteIconByteArray = intent.getByteArrayExtra(CURRENT_FAVORITE_ICON_BYTE_ARRAY)!!
116 // Convert the favorite icon byte array to a bitmap and store it in a class variable.
117 val currentFavoriteIconBitmap = BitmapFactory.decodeByteArray(currentFavoriteIconByteArray, 0, currentFavoriteIconByteArray.size)
119 // Set the view according to the theme.
121 // Set the content view.
122 setContentView(R.layout.bookmarks_databaseview_bottom_appbar)
124 // `Window.FEATURE_ACTION_MODE_OVERLAY` makes the contextual action mode cover the support action bar. It must be requested before the content is set.
125 supportRequestWindowFeature(Window.FEATURE_ACTION_MODE_OVERLAY)
127 // Set the content view.
128 setContentView(R.layout.bookmarks_databaseview_top_appbar)
131 // Get handles for the views.
132 val toolbar = findViewById<Toolbar>(R.id.bookmarks_databaseview_toolbar)
133 bookmarksListView = findViewById(R.id.bookmarks_databaseview_listview)
135 // Set the support action bar.
136 setSupportActionBar(toolbar)
138 // Get a handle for the app bar.
139 val appBar = supportActionBar!!
141 // Set the app bar custom view.
142 appBar.setCustomView(R.layout.spinner)
144 // Display the back arrow in the app bar.
145 appBar.displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM or ActionBar.DISPLAY_HOME_AS_UP
147 // Control what the system back command does.
148 val onBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) {
149 override fun handleOnBackPressed() {
150 // Prepare to finish the activity.
155 // Register the on back pressed callback.
156 onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
158 // Initialize the database handler.
159 bookmarksDatabaseHelper = BookmarksDatabaseHelper(this)
161 // Create a matrix cursor column name string array.
162 val matrixCursorColumnNames = arrayOf(ID, BOOKMARK_NAME, PARENT_FOLDER_ID)
164 // Setup the matrix cursor.
165 MatrixCursor(matrixCursorColumnNames).use { matrixCursor ->
166 // Add "All Folders" and "Home Folder" to the matrix cursor.
167 matrixCursor.addRow(arrayOf<Any>(ALL_FOLDERS_DATABASE_ID, getString(R.string.all_folders), HOME_FOLDER_ID))
168 matrixCursor.addRow(arrayOf<Any>(HOME_FOLDER_DATABASE_ID, getString(R.string.home_folder), HOME_FOLDER_ID))
170 // Get a cursor with the list of all the folders.
171 val foldersCursor = bookmarksDatabaseHelper.getFoldersExcept(listOf())
173 // Combine the matrix cursor and the folders cursor.
174 val foldersMergeCursor = MergeCursor(arrayOf(matrixCursor, foldersCursor))
176 // Create a resource cursor adapter for the spinner.
177 val foldersCursorAdapter: ResourceCursorAdapter = object : ResourceCursorAdapter(this, R.layout.bookmarks_databaseview_appbar_spinner_item, foldersMergeCursor, 0) {
178 override fun bindView(view: View, context: Context, cursor: Cursor) {
179 // Get handles for the spinner views.
180 val subfolderSpacerTextView = view.findViewById<TextView>(R.id.subfolder_spacer_textview)
181 val folderIconImageView = view.findViewById<ImageView>(R.id.folder_icon_imageview)
182 val folderNameTextView = view.findViewById<TextView>(R.id.folder_name_textview)
184 // Populate the subfolder spacer if it is not null (the spinner is open).
185 if (subfolderSpacerTextView != null) {
186 // Indent subfolders.
187 if (cursor.getLong(cursor.getColumnIndexOrThrow(PARENT_FOLDER_ID)) != HOME_FOLDER_ID) { // The folder is not in the home folder.
188 // Get the subfolder spacer.
189 subfolderSpacerTextView.text = bookmarksDatabaseHelper.getSubfolderSpacer(cursor.getLong(cursor.getColumnIndexOrThrow(FOLDER_ID)))
190 } else { // The folder is in the home folder.
191 // Reset the subfolder spacer.
192 subfolderSpacerTextView.text = ""
196 // Set the folder icon according to the type.
197 if (foldersMergeCursor.position > 1) { // Set a user folder icon.
198 // Get the folder icon byte array from the cursor.
199 val folderIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(FAVORITE_ICON))
201 // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
202 val folderIconBitmap = BitmapFactory.decodeByteArray(folderIconByteArray, 0, folderIconByteArray.size)
204 // Set the folder image stored in the cursor.
205 folderIconImageView.setImageBitmap(folderIconBitmap)
206 } else { // Set the `All Folders` or `Home Folder` icon.
207 // Set the gray folder image.
208 folderIconImageView.setImageDrawable(AppCompatResources.getDrawable(context, R.drawable.folder_gray))
211 // Set the folder name.
212 folderNameTextView.text = cursor.getString(cursor.getColumnIndexOrThrow(BOOKMARK_NAME))
216 // Set the resource cursor adapter drop drown view resource.
217 foldersCursorAdapter.setDropDownViewResource(R.layout.bookmarks_databaseview_appbar_spinner_dropdown_item)
219 // Get a handle for the folder spinner.
220 val folderSpinner = findViewById<Spinner>(R.id.spinner)
222 // Set the folder spinner adapter.
223 folderSpinner.adapter = foldersCursorAdapter
225 // Wait to set the on item selected listener until the spinner has been inflated. Otherwise the activity will crash on restart.
227 // Handle taps on the spinner dropdown.
228 folderSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
229 override fun onItemSelected(parent: AdapterView<*>?, view: View, position: Int, id: Long) {
230 // Store the current folder database ID.
231 currentFolderDatabaseId = id.toInt()
233 // Update the list view.
234 updateBookmarksListView()
237 override fun onNothingSelected(parent: AdapterView<*>?) {
243 // Check to see if the activity was restarted.
244 if (savedInstanceState == null) { // The activity was not restarted.
245 // Set the default current folder database ID.
246 currentFolderDatabaseId = ALL_FOLDERS_DATABASE_ID
247 } else { // The activity was restarted.
248 // Restore the class variables from the saved instance state.
249 currentFolderDatabaseId = savedInstanceState.getInt(CURRENT_FOLDER_DATABASE_ID)
250 sortByDisplayOrder = savedInstanceState.getBoolean(SORT_BY_DISPLAY_ORDER)
252 // Update the spinner if the home folder is selected. Android handles this by default for the main cursor but not the matrix cursor.
253 if (currentFolderDatabaseId == HOME_FOLDER_DATABASE_ID) {
254 folderSpinner.setSelection(1)
258 // Update the bookmarks listview.
259 updateBookmarksListView()
261 // Setup a cursor adapter.
262 bookmarksCursorAdapter = object : CursorAdapter(this, bookmarksCursor, false) {
263 override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
264 // Inflate the individual item layout. `false` does not attach it to the root.
265 return layoutInflater.inflate(R.layout.bookmarks_databaseview_item_linearlayout, parent, false)
268 override fun bindView(view: View, context: Context, cursor: Cursor) {
269 // Get handles for the views.
270 val databaseIdTextView = view.findViewById<TextView>(R.id.database_id_textview)
271 val bookmarkFavoriteIconImageView = view.findViewById<ImageView>(R.id.favorite_icon_imageview)
272 val nameTextView = view.findViewById<TextView>(R.id.name_textview)
273 val folderIdTextView = view.findViewById<TextView>(R.id.folder_id_textview)
274 val urlTextView = view.findViewById<TextView>(R.id.url_textview)
275 val displayOrderTextView = view.findViewById<TextView>(R.id.display_order_textview)
276 val parentFolderIconImageView = view.findViewById<ImageView>(R.id.parent_folder_icon_imageview)
277 val parentFolderTextView = view.findViewById<TextView>(R.id.parent_folder_textview)
279 // Get the information from the cursor.
280 val databaseId = cursor.getInt(cursor.getColumnIndexOrThrow(ID))
281 val bookmarkFavoriteIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(FAVORITE_ICON))
282 val nameString = cursor.getString(cursor.getColumnIndexOrThrow(BOOKMARK_NAME))
283 val folderId = cursor.getLong(cursor.getColumnIndexOrThrow(FOLDER_ID))
284 val urlString = cursor.getString(cursor.getColumnIndexOrThrow(BOOKMARK_URL))
285 val displayOrder = cursor.getInt(cursor.getColumnIndexOrThrow(DISPLAY_ORDER))
286 val parentFolderId = cursor.getLong(cursor.getColumnIndexOrThrow(PARENT_FOLDER_ID))
288 // Convert the byte array to a `Bitmap` beginning at the beginning at the first byte and ending at the last.
289 val bookmarkFavoriteIconBitmap = BitmapFactory.decodeByteArray(bookmarkFavoriteIconByteArray, 0, bookmarkFavoriteIconByteArray.size)
291 // Populate the views.
292 databaseIdTextView.text = databaseId.toString()
293 bookmarkFavoriteIconImageView.setImageBitmap(bookmarkFavoriteIconBitmap)
294 nameTextView.text = nameString
295 urlTextView.text = urlString
296 displayOrderTextView.text = displayOrder.toString()
298 // Check to see if the bookmark is a folder.
299 if (cursor.getInt(cursor.getColumnIndexOrThrow(IS_FOLDER)) == 1) { // The bookmark is a folder.
300 // Make the font bold. When the first argument is null the font is not changed.
301 nameTextView.setTypeface(null, Typeface.BOLD)
303 // Display the folder ID.
304 folderIdTextView.text = getString(R.string.folder_id_separator, folderId)
307 urlTextView.visibility = View.GONE
308 } else { // The bookmark is not a folder.
309 // Reset the font to default.
310 nameTextView.typeface = Typeface.DEFAULT
313 urlTextView.visibility = View.VISIBLE
316 // Make the folder name gray if it is the home folder.
317 if (parentFolderId == HOME_FOLDER_ID) { // The bookmark is in the home folder.
318 // Get the home folder icon.
319 parentFolderIconImageView.setImageDrawable(AppCompatResources.getDrawable(applicationContext, R.drawable.folder_gray))
321 // Set the parent folder text to be `Home Folder`.
322 parentFolderTextView.setText(R.string.home_folder)
324 // Set the home folder text to be gray.
325 parentFolderTextView.setTextColor(getColor(R.color.gray_500))
326 } else { // The bookmark is in a subfolder.
327 // Set the parent folder icon.
328 parentFolderIconImageView.setImageDrawable(AppCompatResources.getDrawable(applicationContext, R.drawable.folder_dark_blue))
330 // Set the parent folder name.
331 parentFolderTextView.text = bookmarksDatabaseHelper.getFolderName(parentFolderId)
333 // Set the parent folder text color.
334 parentFolderTextView.setTextColor(getColor(R.color.parent_folder_text))
339 // Update the ListView.
340 bookmarksListView.adapter = bookmarksCursorAdapter
342 // Set a listener to edit a bookmark when it is tapped.
343 bookmarksListView.onItemClickListener = AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, _: Int, id: Long ->
344 // Convert the database ID to an int.
345 val databaseId = id.toInt()
347 // Show the edit bookmark or edit bookmark folder dialog.
348 if (bookmarksDatabaseHelper.isFolder(databaseId)) {
349 // Instantiate the edit bookmark folder dialog.
350 val editBookmarkFolderDatabaseViewDialog = folderDatabaseId(databaseId, currentFavoriteIconBitmap)
353 editBookmarkFolderDatabaseViewDialog.show(supportFragmentManager, resources.getString(R.string.edit_folder))
355 // Instantiate the edit bookmark dialog.
356 val editBookmarkDatabaseViewDialog = bookmarkDatabaseId(databaseId, currentFavoriteIconBitmap)
359 editBookmarkDatabaseViewDialog.show(supportFragmentManager, resources.getString(R.string.edit_bookmark))
363 // Handle long presses on the list view.
364 bookmarksListView.setMultiChoiceModeListener(object : MultiChoiceModeListener {
365 // Instantiate the common variables.
366 private lateinit var selectAllMenuItem: MenuItem
367 private lateinit var deleteMenuItem: MenuItem
368 private var deletingBookmarks = false
370 override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
371 // Inflate the menu for the contextual app bar.
372 menuInflater.inflate(R.menu.bookmarks_databaseview_context_menu, menu)
374 // Get handles for the menu items.
375 selectAllMenuItem = menu.findItem(R.id.select_all)
376 deleteMenuItem = menu.findItem(R.id.delete)
378 // Disable the delete menu item if a delete is pending.
379 deleteMenuItem.isEnabled = !deletingBookmarks
381 // Get the number of currently selected bookmarks.
382 val numberOfSelectedBookmarks = bookmarksListView.checkedItemCount
385 mode.setTitle(R.string.bookmarks)
387 // 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.
388 mode.subtitle = getString(R.string.selected, numberOfSelectedBookmarks)
390 // Do not show the select all menu item if all the bookmarks are already checked.
391 if (numberOfSelectedBookmarks == bookmarksListView.count)
392 selectAllMenuItem.isVisible = false
398 override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
403 override fun onItemCheckedStateChanged(mode: ActionMode, position: Int, id: Long, checked: Boolean) {
404 // Calculate the number of selected bookmarks.
405 val numberOfSelectedBookmarks = bookmarksListView.checkedItemCount
407 // Only run the commands if at least one bookmark is selected. Otherwise, a context menu with 0 selected bookmarks is briefly displayed.
408 if (numberOfSelectedBookmarks > 0) {
409 // Update the action mode subtitle according to the number of selected bookmarks.
410 mode.subtitle = getString(R.string.selected, numberOfSelectedBookmarks)
412 // Only show the select all menu item if all of the bookmarks are not already selected.
413 selectAllMenuItem.isVisible = (bookmarksListView.checkedItemCount != bookmarksListView.count)
415 // Convert the database ID to an int.
416 val databaseId = id.toInt()
418 // If a folder was selected, also select all the contents.
419 if (checked && bookmarksDatabaseHelper.isFolder(databaseId))
420 selectAllBookmarksInFolder(databaseId)
422 // Do not allow a bookmark to be deselected if the folder is selected.
424 // Get the parent folder ID.
425 val parentFolderId = bookmarksDatabaseHelper.getParentFolderId(databaseId)
427 // If the bookmark is not in the root folder, check to see if the folder is selected.
428 if (parentFolderId != HOME_FOLDER_ID) {
429 // Get the folder database ID.
430 val folderDatabaseId = bookmarksDatabaseHelper.getFolderDatabaseId(parentFolderId)
432 // Move the bookmarks cursor to the first position.
433 bookmarksCursor.moveToFirst()
435 // Initialize the folder position variable.
436 var folderPosition = -1
438 // Get the position of the folder in the bookmarks cursor.
439 while ((folderPosition < 0) && (bookmarksCursor.position < bookmarksCursor.count)) {
440 // Check if the folder database ID matches the bookmark database ID.
441 if (folderDatabaseId == bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(ID))) {
442 // Get the folder position.
443 folderPosition = bookmarksCursor.position
445 // Check if the folder is selected.
446 if (bookmarksListView.isItemChecked(folderPosition)) {
447 // Reselect the bookmark.
448 bookmarksListView.setItemChecked(position, true)
450 // Display a snackbar explaining why the bookmark cannot be deselected.
451 Snackbar.make(bookmarksListView, R.string.cannot_deselect_bookmark, Snackbar.LENGTH_LONG).show()
455 // Increment the bookmarks cursor.
456 bookmarksCursor.moveToNext()
463 override fun onActionItemClicked(mode: ActionMode, menuItem: MenuItem): Boolean {
464 // Get a the menu item ID.
465 val menuItemId = menuItem.itemId
467 // Run the command that corresponds to the selected menu item.
468 if (menuItemId == R.id.select_all) { // Select all the bookmarks.
469 // Get the total number of bookmarks.
470 val numberOfBookmarks = bookmarksListView.count
473 for (i in 0 until numberOfBookmarks) {
474 bookmarksListView.setItemChecked(i, true)
476 } else if (menuItemId == R.id.delete) { // Delete the selected bookmarks.
477 // Set the deleting bookmarks flag, which prevents the delete menu item from being enabled until the current process finishes.
478 deletingBookmarks = true
480 // Get an array of the selected row IDs.
481 val selectedBookmarksIdsLongArray = bookmarksListView.checkedItemIds
483 // 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.
484 val selectedBookmarksPositionsSparseBooleanArray = bookmarksListView.checkedItemPositions.clone()
486 // Populate the bookmarks cursor.
487 bookmarksCursor = when (currentFolderDatabaseId) {
488 // Get all the bookmarks except the ones being deleted.
489 ALL_FOLDERS_DATABASE_ID -> {
490 if (sortByDisplayOrder)
491 bookmarksDatabaseHelper.getAllBookmarksByDisplayOrderExcept(selectedBookmarksIdsLongArray)
493 bookmarksDatabaseHelper.getAllBookmarksExcept(selectedBookmarksIdsLongArray)
496 // Get the home folder bookmarks except the ones being deleted.
497 HOME_FOLDER_DATABASE_ID -> {
498 if (sortByDisplayOrder)
499 bookmarksDatabaseHelper.getBookmarksByDisplayOrderExcept(selectedBookmarksIdsLongArray, HOME_FOLDER_ID)
501 bookmarksDatabaseHelper.getBookmarksExcept(selectedBookmarksIdsLongArray, HOME_FOLDER_ID)
504 // Get the current folder bookmarks except the ones being deleted.
506 // Get the current folder ID.
507 val currentFolderId = bookmarksDatabaseHelper.getFolderId(currentFolderDatabaseId)
510 if (sortByDisplayOrder)
511 bookmarksDatabaseHelper.getBookmarksByDisplayOrderExcept(selectedBookmarksIdsLongArray, currentFolderId)
513 bookmarksDatabaseHelper.getBookmarksExcept(selectedBookmarksIdsLongArray, currentFolderId)
517 // Update the list view.
518 bookmarksCursorAdapter!!.changeCursor(bookmarksCursor)
520 // Create a snackbar with the number of deleted bookmarks.
521 bookmarksDeletedSnackbar = Snackbar.make(findViewById(R.id.bookmarks_databaseview_coordinatorlayout), getString(R.string.bookmarks_deleted, selectedBookmarksIdsLongArray.size),
522 Snackbar.LENGTH_LONG)
523 .setAction(R.string.undo) {} // Undo will be handles by `onDismissed()` below.
524 .addCallback(object : Snackbar.Callback() {
525 override fun onDismissed(snackbar: Snackbar, event: Int) {
526 if (event == DISMISS_EVENT_ACTION) { // The user pushed the undo button.
527 // Update the bookmarks list view with the current contents of the bookmarks database, including the "deleted" bookmarks.
528 updateBookmarksListView()
530 // Re-select the previously selected bookmarks.
531 for (i in 0 until selectedBookmarksPositionsSparseBooleanArray.size())
532 bookmarksListView.setItemChecked(selectedBookmarksPositionsSparseBooleanArray.keyAt(i), true)
533 } else { // The snackbar was dismissed without the undo button being pushed.
534 // Delete each selected bookmark.
535 for (databaseIdLong in selectedBookmarksIdsLongArray) {
536 // Convert the database long ID to an int.
537 val databaseIdInt = databaseIdLong.toInt()
539 // Delete the selected bookmark.
540 bookmarksDatabaseHelper.deleteBookmark(databaseIdInt)
544 // Reset the deleting bookmarks flag.
545 deletingBookmarks = false
547 // Enable the delete menu item.
548 deleteMenuItem.isEnabled = true
550 // Close the activity if back has been pressed.
551 if (closeActivityAfterDismissingSnackbar) {
552 // Reload the bookmarks list view when returning to the bookmarks activity.
553 BookmarksActivity.restartFromBookmarksDatabaseViewActivity = true
555 // Finish the activity.
561 // Show the snackbar.
562 bookmarksDeletedSnackbar!!.show()
565 // Consume the click.
569 override fun onDestroyActionMode(mode: ActionMode) {
576 override fun onCreateOptionsMenu(menu: Menu): Boolean {
578 menuInflater.inflate(R.menu.bookmarks_databaseview_options_menu, menu)
580 // Get a handle for the sort menu item.
581 val sortMenuItem = menu.findItem(R.id.sort)
583 // Change the sort menu item icon if the listview is sorted by display order, which restores the state after a restart.
584 if (sortByDisplayOrder)
585 sortMenuItem.setIcon(R.drawable.sort_selected)
591 public override fun onDestroy() {
592 // Close the bookmarks cursor and database.
593 bookmarksCursor.close()
594 bookmarksDatabaseHelper.close()
596 // Run the default commands.
600 override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
601 // Get the menu item ID.
602 val menuItemId = menuItem.itemId
604 // Run the command that corresponds to the selected menu item.
605 if (menuItemId == android.R.id.home) { // Go Home. The home arrow is identified as `android.R.id.home`, not just `R.id.home`.
606 // Prepare to finish the activity.
608 } else if (menuItemId == R.id.sort) { // Toggle the sort mode.
609 // Update the sort by display order tracker.
610 sortByDisplayOrder = !sortByDisplayOrder
612 // Update the icon and display a snackbar.
613 if (sortByDisplayOrder) { // Sort by display order.
615 menuItem.setIcon(R.drawable.sort_selected)
617 // Display a snackbar indicating the current sort type.
618 Snackbar.make(bookmarksListView, R.string.sorted_by_display_order, Snackbar.LENGTH_SHORT).show()
619 } else { // Sort by database id.
621 menuItem.setIcon(R.drawable.sort)
623 // Display a snackbar indicating the current sort type.
624 Snackbar.make(bookmarksListView, R.string.sorted_by_database_id, Snackbar.LENGTH_SHORT).show()
627 // Update the list view.
628 updateBookmarksListView()
631 // Consume the event.
635 public override fun onSaveInstanceState(outState: Bundle) {
636 // Run the default commands.
637 super.onSaveInstanceState(outState)
639 // Store the class variables in the bundle.
640 outState.putInt(CURRENT_FOLDER_DATABASE_ID, currentFolderDatabaseId)
641 outState.putBoolean(SORT_BY_DISPLAY_ORDER, sortByDisplayOrder)
644 override fun saveBookmark(dialogFragment: DialogFragment, selectedBookmarkDatabaseId: Int) {
645 // Get the dialog from the dialog fragment.
646 val dialog = dialogFragment.dialog!!
648 // Get handles for the views from dialog fragment.
649 val currentIconRadioButton = dialog.findViewById<RadioButton>(R.id.current_icon_radiobutton)
650 val webpageFavoriteIconRadioButton = dialog.findViewById<RadioButton>(R.id.webpage_favorite_icon_radiobutton)
651 val webpageFavoriteIconImageView = dialog.findViewById<ImageView>(R.id.webpage_favorite_icon_imageview)
652 val customIconImageView = dialog.findViewById<ImageView>(R.id.custom_icon_imageview)
653 val bookmarkNameEditText = dialog.findViewById<EditText>(R.id.bookmark_name_edittext)
654 val bookmarkUrlEditText = dialog.findViewById<EditText>(R.id.bookmark_url_edittext)
655 val folderSpinner = dialog.findViewById<Spinner>(R.id.bookmark_folder_spinner)
656 val displayOrderEditText = dialog.findViewById<EditText>(R.id.bookmark_display_order_edittext)
658 // Extract the bookmark information.
659 val bookmarkNameString = bookmarkNameEditText.text.toString()
660 val bookmarkUrlString = bookmarkUrlEditText.text.toString()
661 val folderDatabaseId = folderSpinner.selectedItemId.toInt()
662 val displayOrderInt = displayOrderEditText.text.toString().toInt()
664 // Get the parent folder ID.
665 val parentFolderId: Long = if (folderDatabaseId == HOME_FOLDER_DATABASE_ID) // The home folder is selected.
667 else // Get the parent folder name from the database.
668 bookmarksDatabaseHelper.getFolderId(folderDatabaseId)
670 // Update the bookmark.
671 if (currentIconRadioButton.isChecked) { // Update the bookmark without changing the favorite icon.
672 bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, parentFolderId, displayOrderInt)
673 } else { // Update the bookmark using the WebView favorite icon.
674 // Get the selected favorite icon drawable.
675 val favoriteIconDrawable = if (webpageFavoriteIconRadioButton.isChecked) // The webpage favorite icon is checked.
676 webpageFavoriteIconImageView.drawable
677 else // The custom favorite icon is checked.
678 customIconImageView.drawable
680 // Convert the favorite icon drawable to a bitmap. Once the minimum API >= 33, this can use Bitmap.Config.RGBA_1010102.
681 val favoriteIconBitmap = favoriteIconDrawable.toBitmap(128, 128, Bitmap.Config.ARGB_8888)
683 // Create a favorite icon byte array output stream.
684 val newFavoriteIconByteArrayOutputStream = ByteArrayOutputStream()
686 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
687 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFavoriteIconByteArrayOutputStream)
689 // Convert the favorite icon byte array stream to a byte array.
690 val newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray()
692 // Update the bookmark and the favorite icon.
693 bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, parentFolderId, displayOrderInt, newFavoriteIconByteArray)
696 // Update the list view.
697 updateBookmarksListView()
700 override fun saveBookmarkFolder(dialogFragment: DialogFragment, selectedFolderDatabaseId: Int) {
701 // Get the dialog from the dialog fragment.
702 val dialog = dialogFragment.dialog!!
704 // Get handles for the views from dialog fragment.
705 val currentIconRadioButton = dialog.findViewById<RadioButton>(R.id.current_icon_radiobutton)
706 val defaultFolderIconRadioButton = dialog.findViewById<RadioButton>(R.id.default_folder_icon_radiobutton)
707 val defaultFolderIconImageView = dialog.findViewById<ImageView>(R.id.default_folder_icon_imageview)
708 val webpageFavoriteIconRadioButton = dialog.findViewById<RadioButton>(R.id.webpage_favorite_icon_radiobutton)
709 val webpageFavoriteIconImageView = dialog.findViewById<ImageView>(R.id.webpage_favorite_icon_imageview)
710 val customIconImageView = dialog.findViewById<ImageView>(R.id.custom_icon_imageview)
711 val folderNameEditText = dialog.findViewById<EditText>(R.id.folder_name_edittext)
712 val parentFolderSpinner = dialog.findViewById<Spinner>(R.id.parent_folder_spinner)
713 val displayOrderEditText = dialog.findViewById<EditText>(R.id.display_order_edittext)
715 // Get the folder information.
716 val newFolderNameString = folderNameEditText.text.toString()
717 val parentFolderDatabaseId = parentFolderSpinner.selectedItemId.toInt()
718 val displayOrderInt = displayOrderEditText.text.toString().toInt()
720 // Set the parent folder ID.
721 val parentFolderIdLong: Long = if (parentFolderDatabaseId == HOME_FOLDER_DATABASE_ID) // The home folder is selected.
723 else // Get the parent folder name from the database.
724 bookmarksDatabaseHelper.getFolderId(parentFolderDatabaseId)
726 // Update the folder.
727 if (currentIconRadioButton.isChecked) { // Update the folder without changing the favorite icon.
728 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderNameString, parentFolderIdLong, displayOrderInt)
729 } else { // Update the folder and the icon.
730 // Get the selected folder icon drawable.
731 val folderIconDrawable = if (defaultFolderIconRadioButton.isChecked) // The default folder icon is checked.
732 defaultFolderIconImageView.drawable
733 else if (webpageFavoriteIconRadioButton.isChecked) // The webpage favorite icon is checked.
734 webpageFavoriteIconImageView.drawable
735 else // The custom icon is checked.
736 customIconImageView.drawable
738 // Convert the folder icon drawable to a bitmap. Once the minimum API >= 33, this can use Bitmap.Config.RGBA_1010102.
739 val folderIconBitmap = folderIconDrawable.toBitmap(128, 128, Bitmap.Config.ARGB_8888)
741 // Create a folder icon byte array output stream.
742 val newFolderIconByteArrayOutputStream = ByteArrayOutputStream()
744 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
745 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream)
747 // Convert the folder icon byte array stream to a byte array.
748 val newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray()
750 // Update the folder and the icon.
751 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderNameString, parentFolderIdLong, displayOrderInt, newFolderIconByteArray)
754 // Update the list view.
755 updateBookmarksListView()
758 private fun updateBookmarksListView() {
759 // Populate the bookmarks list view based on the spinner selection.
760 bookmarksCursor = when (currentFolderDatabaseId) {
761 // Get all the bookmarks.
762 ALL_FOLDERS_DATABASE_ID -> {
763 if (sortByDisplayOrder)
764 bookmarksDatabaseHelper.allBookmarksByDisplayOrder
766 bookmarksDatabaseHelper.allBookmarks
769 // Get the bookmarks in the home folder.
770 HOME_FOLDER_DATABASE_ID -> {
771 if (sortByDisplayOrder)
772 bookmarksDatabaseHelper.getBookmarksByDisplayOrder(HOME_FOLDER_ID)
774 bookmarksDatabaseHelper.getBookmarks(HOME_FOLDER_ID)
777 // Get the bookmarks in the specified folder.
779 // Get the current folder ID.
780 val currentFolderId = bookmarksDatabaseHelper.getFolderId(currentFolderDatabaseId)
782 if (sortByDisplayOrder)
783 bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderId)
785 bookmarksDatabaseHelper.getBookmarks(currentFolderId)
789 // Update the cursor adapter if it isn't null, which happens when the activity is restarted.
790 if (bookmarksCursorAdapter != null) {
791 bookmarksCursorAdapter!!.changeCursor(bookmarksCursor)
795 private fun prepareFinish() {
796 // 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.
797 if ((bookmarksDeletedSnackbar != null) && bookmarksDeletedSnackbar!!.isShown) { // Close the bookmarks deleted snackbar before going home.
798 // Set the close flag.
799 closeActivityAfterDismissingSnackbar = true
801 // Dismiss the snackbar.
802 bookmarksDeletedSnackbar!!.dismiss()
803 } else { // Go home immediately.
804 // Update the current folder in the bookmarks activity.
805 if ((currentFolderDatabaseId == ALL_FOLDERS_DATABASE_ID) || (currentFolderDatabaseId == HOME_FOLDER_DATABASE_ID)) { // All folders or the the home folder are currently displayed.
806 // Load the home folder.
807 BookmarksActivity.currentFolderId = HOME_FOLDER_ID
808 } else { // A subfolder is currently displayed.
809 // Load the current folder.
810 BookmarksActivity.currentFolderId = bookmarksDatabaseHelper.getFolderId(currentFolderDatabaseId)
813 // Reload the bookmarks list view when returning to the bookmarks activity.
814 BookmarksActivity.restartFromBookmarksDatabaseViewActivity = true
816 // Exit the bookmarks database view activity.
821 private fun selectAllBookmarksInFolder(folderDatabaseId: Int) {
822 // Get the folder ID.
823 val folderId = bookmarksDatabaseHelper.getFolderId(folderDatabaseId)
825 // Get a cursor with the contents of the folder.
826 val folderCursor = bookmarksDatabaseHelper.getBookmarks(folderId)
828 // Get the column indexes.
829 val idColumnIndex = bookmarksCursor.getColumnIndexOrThrow(ID)
831 // Move to the beginning of the cursor.
832 folderCursor.moveToFirst()
834 while (folderCursor.position < folderCursor.count) {
835 // Get the bookmark database ID.
836 val bookmarkId = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(ID))
838 // Move the bookmarks cursor to the first position.
839 bookmarksCursor.moveToFirst()
841 // Initialize the bookmark position variable.
842 var bookmarkPosition = -1
844 // Get the position of this bookmark in the bookmarks cursor.
845 while ((bookmarkPosition < 0) && (bookmarksCursor.position < bookmarksCursor.count)) {
846 // Check if the bookmark IDs match.
847 if (bookmarkId == bookmarksCursor.getInt(idColumnIndex)) {
848 // Get the bookmark position.
849 bookmarkPosition = bookmarksCursor.position
851 // If this bookmark is a folder, select all the bookmarks inside it.
852 if (bookmarksDatabaseHelper.isFolder(bookmarkId))
853 selectAllBookmarksInFolder(bookmarkId)
855 // Select the bookmark.
856 bookmarksListView.setItemChecked(bookmarkPosition, true)
859 // Increment the bookmarks cursor position.
860 bookmarksCursor.moveToNext()
863 // Move to the next position.
864 folderCursor.moveToNext()