2 * Copyright 2016-2023 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.graphics.drawable.BitmapDrawable
30 import android.os.Bundle
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.Spinner
45 import android.widget.TextView
47 import androidx.activity.OnBackPressedCallback
48 import androidx.appcompat.app.ActionBar
49 import androidx.appcompat.app.AppCompatActivity
50 import androidx.appcompat.content.res.AppCompatResources
51 import androidx.appcompat.widget.Toolbar
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 import java.util.Arrays
78 // Define the public class constants.
79 const val HOME_FOLDER_DATABASE_ID = -1
80 const val HOME_FOLDER_ID = 0L
82 // Define the private class constants.
83 private const val ALL_FOLDERS_DATABASE_ID = -2
84 private const val CURRENT_FOLDER_DATABASE_ID = "A"
85 private const val SORT_BY_DISPLAY_ORDER = "B"
87 class BookmarksDatabaseViewActivity : AppCompatActivity(), EditBookmarkDatabaseViewListener, EditBookmarkFolderDatabaseViewListener {
88 // Define the class variables.
89 private var closeActivityAfterDismissingSnackbar = false
90 private var currentFolderDatabaseId = 0
91 private var bookmarksCursorAdapter: CursorAdapter? = null
92 private var bookmarksDeletedSnackbar: Snackbar? = null
93 private var sortByDisplayOrder = false
95 // Declare the class variables.
96 private lateinit var bookmarksCursor: Cursor
97 private lateinit var bookmarksDatabaseHelper: BookmarksDatabaseHelper
98 private lateinit var bookmarksListView: ListView
100 public override fun onCreate(savedInstanceState: Bundle?) {
101 // Get a handle for the shared preferences.
102 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
104 // Get the preferences.
105 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
106 val bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false)
108 // Disable screenshots if not allowed.
109 if (!allowScreenshots) {
110 window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
113 // Run the default commands.
114 super.onCreate(savedInstanceState)
116 // Get the favorite icon byte array.
117 val currentFavoriteIconByteArray = intent.getByteArrayExtra(CURRENT_FAVORITE_ICON_BYTE_ARRAY)!!
119 // Convert the favorite icon byte array to a bitmap and store it in a class variable.
120 val currentFavoriteIconBitmap = BitmapFactory.decodeByteArray(currentFavoriteIconByteArray, 0, currentFavoriteIconByteArray.size)
122 // Set the view according to the theme.
124 // Set the content view.
125 setContentView(R.layout.bookmarks_databaseview_bottom_appbar)
127 // `Window.FEATURE_ACTION_MODE_OVERLAY` makes the contextual action mode cover the support action bar. It must be requested before the content is set.
128 supportRequestWindowFeature(Window.FEATURE_ACTION_MODE_OVERLAY)
130 // Set the content view.
131 setContentView(R.layout.bookmarks_databaseview_top_appbar)
134 // Get a handle for the toolbar.
135 val toolbar = findViewById<Toolbar>(R.id.bookmarks_databaseview_toolbar)
136 bookmarksListView = findViewById(R.id.bookmarks_databaseview_listview)
138 // Set the support action bar.
139 setSupportActionBar(toolbar)
141 // Get a handle for the app bar.
142 val appBar = supportActionBar!!
144 // Set the app bar custom view.
145 appBar.setCustomView(R.layout.spinner)
147 // Display the back arrow in the app bar.
148 appBar.displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM or ActionBar.DISPLAY_HOME_AS_UP
150 // Control what the system back command does.
151 val onBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) {
152 override fun handleOnBackPressed() {
153 // Prepare to finish the activity.
158 // Register the on back pressed callback.
159 onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
161 // Initialize the database handler.
162 bookmarksDatabaseHelper = BookmarksDatabaseHelper(this)
164 // Create a matrix cursor column name string array.
165 val matrixCursorColumnNames = arrayOf(ID, BOOKMARK_NAME, PARENT_FOLDER_ID)
167 // Setup the matrix cursor.
168 MatrixCursor(matrixCursorColumnNames).use { matrixCursor ->
169 // Add "All Folders" and "Home Folder" to the matrix cursor.
170 matrixCursor.addRow(arrayOf<Any>(ALL_FOLDERS_DATABASE_ID, getString(R.string.all_folders), HOME_FOLDER_ID))
171 matrixCursor.addRow(arrayOf<Any>(HOME_FOLDER_DATABASE_ID, getString(R.string.home_folder), HOME_FOLDER_ID))
173 // Get a cursor with the list of all the folders.
174 val foldersCursor = bookmarksDatabaseHelper.getFoldersExcept(listOf())
176 // Combine the matrix cursor and the folders cursor.
177 val foldersMergeCursor = MergeCursor(arrayOf(matrixCursor, foldersCursor))
179 // Get the default folder bitmap.
180 val defaultFolderDrawable = AppCompatResources.getDrawable(this, R.drawable.folder_blue_bitmap)
182 // Cast the default folder drawable to a bitmap drawable.
183 val defaultFolderBitmapDrawable = (defaultFolderDrawable as BitmapDrawable)
185 // Convert the default folder bitmap drawable to a bitmap.
186 val defaultFolderBitmap = defaultFolderBitmapDrawable.bitmap
188 // Create a resource cursor adapter for the spinner.
189 val foldersCursorAdapter: ResourceCursorAdapter = object : ResourceCursorAdapter(this, R.layout.bookmarks_databaseview_appbar_spinner_item, foldersMergeCursor, 0) {
190 override fun bindView(view: View, context: Context, cursor: Cursor) {
191 // Get handles for the spinner views.
192 val subfolderSpacerTextView = view.findViewById<TextView>(R.id.subfolder_spacer_textview)
193 val folderIconImageView = view.findViewById<ImageView>(R.id.folder_icon_imageview)
194 val folderNameTextView = view.findViewById<TextView>(R.id.folder_name_textview)
196 // Populate the subfolder spacer if it is not null (the spinner is open).
197 if (subfolderSpacerTextView != null) {
198 // Indent subfolders.
199 if (cursor.getLong(cursor.getColumnIndexOrThrow(PARENT_FOLDER_ID)) != HOME_FOLDER_ID) { // The folder is not in the home folder.
200 // Get the subfolder spacer.
201 subfolderSpacerTextView.text = bookmarksDatabaseHelper.getSubfolderSpacer(cursor.getLong(cursor.getColumnIndexOrThrow(FOLDER_ID)))
202 } else { // The folder is in the home folder.
203 // Reset the subfolder spacer.
204 subfolderSpacerTextView.text = ""
208 // Set the folder icon according to the type.
209 if (foldersMergeCursor.position > 1) { // Set a user folder icon.
210 // Initialize a default folder icon byte array output stream.
211 val defaultFolderIconByteArrayOutputStream = ByteArrayOutputStream()
213 // Covert the default folder bitmap to a PNG and store it in the output stream. `0` is for lossless compression (the only option for a PNG).
214 defaultFolderBitmap.compress(Bitmap.CompressFormat.PNG, 0, defaultFolderIconByteArrayOutputStream)
216 // Convert the default folder icon output stream to a byte array.
217 val defaultFolderIconByteArray = defaultFolderIconByteArrayOutputStream.toByteArray()
219 // Get the folder icon byte array from the cursor.
220 val folderIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(FAVORITE_ICON))
222 // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
223 val folderIconBitmap = BitmapFactory.decodeByteArray(folderIconByteArray, 0, folderIconByteArray.size)
225 // Set the icon according to the type.
226 if (Arrays.equals(folderIconByteArray, defaultFolderIconByteArray)) { // The default folder icon is used.
227 // Set a smaller and darker folder icon, which works well with the spinner.
228 folderIconImageView.setImageDrawable(AppCompatResources.getDrawable(context, R.drawable.folder_dark_blue))
229 } else { // A custom folder icon is uses.
230 // Set the folder image stored in the cursor.
231 folderIconImageView.setImageBitmap(folderIconBitmap)
233 } else { // Set the `All Folders` or `Home Folder` icon.
234 // Set the gray folder image.
235 folderIconImageView.setImageDrawable(AppCompatResources.getDrawable(context, R.drawable.folder_gray))
238 // Set the folder name.
239 folderNameTextView.text = cursor.getString(cursor.getColumnIndexOrThrow(BOOKMARK_NAME))
243 // Set the resource cursor adapter drop drown view resource.
244 foldersCursorAdapter.setDropDownViewResource(R.layout.bookmarks_databaseview_appbar_spinner_dropdown_item)
246 // Get a handle for the folder spinner.
247 val folderSpinner = findViewById<Spinner>(R.id.spinner)
249 // Set the folder spinner adapter.
250 folderSpinner.adapter = foldersCursorAdapter
252 // Wait to set the on item selected listener until the spinner has been inflated. Otherwise the activity will crash on restart.
254 // Handle taps on the spinner dropdown.
255 folderSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
256 override fun onItemSelected(parent: AdapterView<*>?, view: View, position: Int, id: Long) {
257 // Store the current folder database ID.
258 currentFolderDatabaseId = id.toInt()
260 // Update the list view.
261 updateBookmarksListView()
264 override fun onNothingSelected(parent: AdapterView<*>?) {
270 // Check to see if the activity was restarted.
271 if (savedInstanceState == null) { // The activity was not restarted.
272 // Set the default current folder database ID.
273 currentFolderDatabaseId = ALL_FOLDERS_DATABASE_ID
274 } else { // The activity was restarted.
275 // Restore the class variables from the saved instance state.
276 currentFolderDatabaseId = savedInstanceState.getInt(CURRENT_FOLDER_DATABASE_ID)
277 sortByDisplayOrder = savedInstanceState.getBoolean(SORT_BY_DISPLAY_ORDER)
279 // Update the spinner if the home folder is selected. Android handles this by default for the main cursor but not the matrix cursor.
280 if (currentFolderDatabaseId == HOME_FOLDER_DATABASE_ID) {
281 folderSpinner.setSelection(1)
285 // Update the bookmarks listview.
286 updateBookmarksListView()
288 // Setup a cursor adapter.
289 bookmarksCursorAdapter = object : CursorAdapter(this, bookmarksCursor, false) {
290 override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
291 // Inflate the individual item layout. `false` does not attach it to the root.
292 return layoutInflater.inflate(R.layout.bookmarks_databaseview_item_linearlayout, parent, false)
295 override fun bindView(view: View, context: Context, cursor: Cursor) {
296 // Get handles for the views.
297 val databaseIdTextView = view.findViewById<TextView>(R.id.database_id_textview)
298 val bookmarkFavoriteIconImageView = view.findViewById<ImageView>(R.id.favorite_icon_imageview)
299 val nameTextView = view.findViewById<TextView>(R.id.name_textview)
300 val folderIdTextView = view.findViewById<TextView>(R.id.folder_id_textview)
301 val urlTextView = view.findViewById<TextView>(R.id.url_textview)
302 val displayOrderTextView = view.findViewById<TextView>(R.id.display_order_textview)
303 val parentFolderIconImageView = view.findViewById<ImageView>(R.id.parent_folder_icon_imageview)
304 val parentFolderTextView = view.findViewById<TextView>(R.id.parent_folder_textview)
306 // Get the information from the cursor.
307 val databaseId = cursor.getInt(cursor.getColumnIndexOrThrow(ID))
308 val bookmarkFavoriteIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(FAVORITE_ICON))
309 val nameString = cursor.getString(cursor.getColumnIndexOrThrow(BOOKMARK_NAME))
310 val folderId = cursor.getLong(cursor.getColumnIndexOrThrow(FOLDER_ID))
311 val urlString = cursor.getString(cursor.getColumnIndexOrThrow(BOOKMARK_URL))
312 val displayOrder = cursor.getInt(cursor.getColumnIndexOrThrow(DISPLAY_ORDER))
313 val parentFolderId = cursor.getLong(cursor.getColumnIndexOrThrow(PARENT_FOLDER_ID))
315 // Convert the byte array to a `Bitmap` beginning at the beginning at the first byte and ending at the last.
316 val bookmarkFavoriteIconBitmap = BitmapFactory.decodeByteArray(bookmarkFavoriteIconByteArray, 0, bookmarkFavoriteIconByteArray.size)
318 // Populate the views.
319 databaseIdTextView.text = databaseId.toString()
320 bookmarkFavoriteIconImageView.setImageBitmap(bookmarkFavoriteIconBitmap)
321 nameTextView.text = nameString
322 urlTextView.text = urlString
323 displayOrderTextView.text = displayOrder.toString()
325 // Check to see if the bookmark is a folder.
326 if (cursor.getInt(cursor.getColumnIndexOrThrow(IS_FOLDER)) == 1) { // The bookmark is a folder.
327 // Make the font bold. When the first argument is null the font is not changed.
328 nameTextView.setTypeface(null, Typeface.BOLD)
330 // Display the folder ID.
331 folderIdTextView.text = getString(R.string.folder_id_separator, folderId)
334 urlTextView.visibility = View.GONE
335 } else { // The bookmark is not a folder.
336 // Reset the font to default.
337 nameTextView.typeface = Typeface.DEFAULT
340 urlTextView.visibility = View.VISIBLE
343 // Make the folder name gray if it is the home folder.
344 if (parentFolderId == HOME_FOLDER_ID) { // The bookmark is in the home folder.
345 // Get the home folder icon.
346 parentFolderIconImageView.setImageDrawable(AppCompatResources.getDrawable(applicationContext, R.drawable.folder_gray))
348 // Set the parent folder text to be `Home Folder`.
349 parentFolderTextView.setText(R.string.home_folder)
351 // Set the home folder text to be gray.
352 parentFolderTextView.setTextColor(getColor(R.color.gray_500))
353 } else { // The bookmark is in a subfolder.
354 // Set the parent folder icon.
355 parentFolderIconImageView.setImageDrawable(AppCompatResources.getDrawable(applicationContext, R.drawable.folder_dark_blue))
357 // Set the parent folder name.
358 parentFolderTextView.text = bookmarksDatabaseHelper.getFolderName(parentFolderId)
360 // Set the parent folder text color.
361 parentFolderTextView.setTextColor(getColor(R.color.parent_folder_text))
366 // Update the ListView.
367 bookmarksListView.adapter = bookmarksCursorAdapter
369 // Set a listener to edit a bookmark when it is tapped.
370 bookmarksListView.onItemClickListener = AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, _: Int, id: Long ->
371 // Convert the database ID to an int.
372 val databaseId = id.toInt()
374 // Show the edit bookmark or edit bookmark folder dialog.
375 if (bookmarksDatabaseHelper.isFolder(databaseId)) {
376 // Instantiate the edit bookmark folder dialog.
377 val editBookmarkFolderDatabaseViewDialog = folderDatabaseId(databaseId, currentFavoriteIconBitmap)
380 editBookmarkFolderDatabaseViewDialog.show(supportFragmentManager, resources.getString(R.string.edit_folder))
382 // Instantiate the edit bookmark dialog.
383 val editBookmarkDatabaseViewDialog = bookmarkDatabaseId(databaseId, currentFavoriteIconBitmap)
386 editBookmarkDatabaseViewDialog.show(supportFragmentManager, resources.getString(R.string.edit_bookmark))
390 // Handle long presses on the list view.
391 bookmarksListView.setMultiChoiceModeListener(object : MultiChoiceModeListener {
392 // Instantiate the common variables.
393 private lateinit var selectAllMenuItem: MenuItem
394 private lateinit var deleteMenuItem: MenuItem
395 private var deletingBookmarks = false
397 override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
398 // Inflate the menu for the contextual app bar.
399 menuInflater.inflate(R.menu.bookmarks_databaseview_context_menu, menu)
401 // Get handles for the menu items.
402 selectAllMenuItem = menu.findItem(R.id.select_all)
403 deleteMenuItem = menu.findItem(R.id.delete)
405 // Disable the delete menu item if a delete is pending.
406 deleteMenuItem.isEnabled = !deletingBookmarks
408 // Get the number of currently selected bookmarks.
409 val numberOfSelectedBookmarks = bookmarksListView.checkedItemCount
412 mode.setTitle(R.string.bookmarks)
414 // 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.
415 mode.subtitle = getString(R.string.selected, numberOfSelectedBookmarks)
417 // Do not show the select all menu item if all the bookmarks are already checked.
418 if (numberOfSelectedBookmarks == bookmarksListView.count)
419 selectAllMenuItem.isVisible = false
425 override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
430 override fun onItemCheckedStateChanged(mode: ActionMode, position: Int, id: Long, checked: Boolean) {
431 // Calculate the number of selected bookmarks.
432 val numberOfSelectedBookmarks = bookmarksListView.checkedItemCount
434 // Only run the commands if at least one bookmark is selected. Otherwise, a context menu with 0 selected bookmarks is briefly displayed.
435 if (numberOfSelectedBookmarks > 0) {
436 // Update the action mode subtitle according to the number of selected bookmarks.
437 mode.subtitle = getString(R.string.selected, numberOfSelectedBookmarks)
439 // Only show the select all menu item if all of the bookmarks are not already selected.
440 selectAllMenuItem.isVisible = (bookmarksListView.checkedItemCount != bookmarksListView.count)
442 // Convert the database ID to an int.
443 val databaseId = id.toInt()
445 // If a folder was selected, also select all the contents.
446 if (checked && bookmarksDatabaseHelper.isFolder(databaseId))
447 selectAllBookmarksInFolder(databaseId)
449 // Do not allow a bookmark to be deselected if the folder is selected.
451 // Get the parent folder ID.
452 val parentFolderId = bookmarksDatabaseHelper.getParentFolderId(databaseId)
454 // If the bookmark is not in the root folder, check to see if the folder is selected.
455 if (parentFolderId != HOME_FOLDER_ID) {
456 // Get the folder database ID.
457 val folderDatabaseId = bookmarksDatabaseHelper.getFolderDatabaseId(parentFolderId)
459 // Move the bookmarks cursor to the first position.
460 bookmarksCursor.moveToFirst()
462 // Initialize the folder position variable.
463 var folderPosition = -1
465 // Get the position of the folder in the bookmarks cursor.
466 while ((folderPosition < 0) && (bookmarksCursor.position < bookmarksCursor.count)) {
467 // Check if the folder database ID matches the bookmark database ID.
468 if (folderDatabaseId == bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(ID))) {
469 // Get the folder position.
470 folderPosition = bookmarksCursor.position
472 // Check if the folder is selected.
473 if (bookmarksListView.isItemChecked(folderPosition)) {
474 // Reselect the bookmark.
475 bookmarksListView.setItemChecked(position, true)
477 // Display a snackbar explaining why the bookmark cannot be deselected.
478 Snackbar.make(bookmarksListView, R.string.cannot_deselect_bookmark, Snackbar.LENGTH_LONG).show()
482 // Increment the bookmarks cursor.
483 bookmarksCursor.moveToNext()
490 override fun onActionItemClicked(mode: ActionMode, menuItem: MenuItem): Boolean {
491 // Get a the menu item ID.
492 val menuItemId = menuItem.itemId
494 // Run the command that corresponds to the selected menu item.
495 if (menuItemId == R.id.select_all) { // Select all the bookmarks.
496 // Get the total number of bookmarks.
497 val numberOfBookmarks = bookmarksListView.count
500 for (i in 0 until numberOfBookmarks) {
501 bookmarksListView.setItemChecked(i, true)
503 } else if (menuItemId == R.id.delete) { // Delete the selected bookmarks.
504 // Set the deleting bookmarks flag, which prevents the delete menu item from being enabled until the current process finishes.
505 deletingBookmarks = true
507 // Get an array of the selected row IDs.
508 val selectedBookmarksIdsLongArray = bookmarksListView.checkedItemIds
510 // 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.
511 val selectedBookmarksPositionsSparseBooleanArray = bookmarksListView.checkedItemPositions.clone()
513 // Populate the bookmarks cursor.
514 bookmarksCursor = when (currentFolderDatabaseId) {
515 // Get all the bookmarks except the ones being deleted.
516 ALL_FOLDERS_DATABASE_ID -> {
517 if (sortByDisplayOrder)
518 bookmarksDatabaseHelper.getAllBookmarksByDisplayOrderExcept(selectedBookmarksIdsLongArray)
520 bookmarksDatabaseHelper.getAllBookmarksExcept(selectedBookmarksIdsLongArray)
523 // Get the home folder bookmarks except the ones being deleted.
524 HOME_FOLDER_DATABASE_ID -> {
525 if (sortByDisplayOrder)
526 bookmarksDatabaseHelper.getBookmarksByDisplayOrderExcept(selectedBookmarksIdsLongArray, HOME_FOLDER_ID)
528 bookmarksDatabaseHelper.getBookmarksExcept(selectedBookmarksIdsLongArray, HOME_FOLDER_ID)
531 // Get the current folder bookmarks except the ones being deleted.
533 // Get the current folder ID.
534 val currentFolderId = bookmarksDatabaseHelper.getFolderId(currentFolderDatabaseId)
537 if (sortByDisplayOrder)
538 bookmarksDatabaseHelper.getBookmarksByDisplayOrderExcept(selectedBookmarksIdsLongArray, currentFolderId)
540 bookmarksDatabaseHelper.getBookmarksExcept(selectedBookmarksIdsLongArray, currentFolderId)
544 // Update the list view.
545 bookmarksCursorAdapter!!.changeCursor(bookmarksCursor)
547 // Create a snackbar with the number of deleted bookmarks.
548 bookmarksDeletedSnackbar = Snackbar.make(findViewById(R.id.bookmarks_databaseview_coordinatorlayout), getString(R.string.bookmarks_deleted, selectedBookmarksIdsLongArray.size),
549 Snackbar.LENGTH_LONG)
550 .setAction(R.string.undo) {} // Undo will be handles by `onDismissed()` below.
551 .addCallback(object : Snackbar.Callback() {
552 override fun onDismissed(snackbar: Snackbar, event: Int) {
553 if (event == DISMISS_EVENT_ACTION) { // The user pushed the undo button.
554 // Update the bookmarks list view with the current contents of the bookmarks database, including the "deleted" bookmarks.
555 updateBookmarksListView()
557 // Re-select the previously selected bookmarks.
558 for (i in 0 until selectedBookmarksPositionsSparseBooleanArray.size())
559 bookmarksListView.setItemChecked(selectedBookmarksPositionsSparseBooleanArray.keyAt(i), true)
560 } else { // The snackbar was dismissed without the undo button being pushed.
561 // Delete each selected bookmark.
562 for (databaseIdLong in selectedBookmarksIdsLongArray) {
563 // Convert the database long ID to an int.
564 val databaseIdInt = databaseIdLong.toInt()
566 // Delete the selected bookmark.
567 bookmarksDatabaseHelper.deleteBookmark(databaseIdInt)
571 // Reset the deleting bookmarks flag.
572 deletingBookmarks = false
574 // Enable the delete menu item.
575 deleteMenuItem.isEnabled = true
577 // Close the activity if back has been pressed.
578 if (closeActivityAfterDismissingSnackbar) {
579 // Reload the bookmarks list view when returning to the bookmarks activity.
580 BookmarksActivity.restartFromBookmarksDatabaseViewActivity = true
582 // Finish the activity.
588 // Show the snackbar.
589 bookmarksDeletedSnackbar!!.show()
592 // Consume the click.
596 override fun onDestroyActionMode(mode: ActionMode) {
603 override fun onCreateOptionsMenu(menu: Menu): Boolean {
605 menuInflater.inflate(R.menu.bookmarks_databaseview_options_menu, menu)
607 // Get a handle for the sort menu item.
608 val sortMenuItem = menu.findItem(R.id.sort)
610 // Change the sort menu item icon if the listview is sorted by display order, which restores the state after a restart.
611 if (sortByDisplayOrder)
612 sortMenuItem.setIcon(R.drawable.sort_selected)
618 public override fun onDestroy() {
619 // Close the bookmarks cursor and database.
620 bookmarksCursor.close()
621 bookmarksDatabaseHelper.close()
623 // Run the default commands.
627 override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
628 // Get the menu item ID.
629 val menuItemId = menuItem.itemId
631 // Run the command that corresponds to the selected menu item.
632 if (menuItemId == android.R.id.home) { // Go Home. The home arrow is identified as `android.R.id.home`, not just `R.id.home`.
633 // Prepare to finish the activity.
635 } else if (menuItemId == R.id.sort) { // Toggle the sort mode.
636 // Update the sort by display order tracker.
637 sortByDisplayOrder = !sortByDisplayOrder
639 // Update the icon and display a snackbar.
640 if (sortByDisplayOrder) { // Sort by display order.
642 menuItem.setIcon(R.drawable.sort_selected)
644 // Display a snackbar indicating the current sort type.
645 Snackbar.make(bookmarksListView, R.string.sorted_by_display_order, Snackbar.LENGTH_SHORT).show()
646 } else { // Sort by database id.
648 menuItem.setIcon(R.drawable.sort)
650 // Display a snackbar indicating the current sort type.
651 Snackbar.make(bookmarksListView, R.string.sorted_by_database_id, Snackbar.LENGTH_SHORT).show()
654 // Update the list view.
655 updateBookmarksListView()
658 // Consume the event.
662 public override fun onSaveInstanceState(savedInstanceState: Bundle) {
663 // Run the default commands.
664 super.onSaveInstanceState(savedInstanceState)
666 // Store the class variables in the bundle.
667 savedInstanceState.putInt(CURRENT_FOLDER_DATABASE_ID, currentFolderDatabaseId)
668 savedInstanceState.putBoolean(SORT_BY_DISPLAY_ORDER, sortByDisplayOrder)
671 override fun saveBookmark(dialogFragment: DialogFragment, selectedBookmarkDatabaseId: Int, favoriteIconBitmap: Bitmap) {
672 // Get the dialog from the dialog fragment.
673 val dialog = dialogFragment.dialog!!
675 // Get handles for the views from dialog fragment.
676 val currentIconRadioButton = dialog.findViewById<RadioButton>(R.id.current_icon_radiobutton)
677 val bookmarkNameEditText = dialog.findViewById<EditText>(R.id.bookmark_name_edittext)
678 val bookmarkUrlEditText = dialog.findViewById<EditText>(R.id.bookmark_url_edittext)
679 val folderSpinner = dialog.findViewById<Spinner>(R.id.bookmark_folder_spinner)
680 val displayOrderEditText = dialog.findViewById<EditText>(R.id.bookmark_display_order_edittext)
682 // Extract the bookmark information.
683 val bookmarkNameString = bookmarkNameEditText.text.toString()
684 val bookmarkUrlString = bookmarkUrlEditText.text.toString()
685 val folderDatabaseId = folderSpinner.selectedItemId.toInt()
686 val displayOrderInt = displayOrderEditText.text.toString().toInt()
688 // Get the parent folder ID.
689 val parentFolderId: Long = if (folderDatabaseId == HOME_FOLDER_DATABASE_ID) // The home folder is selected.
691 else // Get the parent folder name from the database.
692 bookmarksDatabaseHelper.getFolderId(folderDatabaseId)
694 // Update the bookmark.
695 if (currentIconRadioButton.isChecked) { // Update the bookmark without changing the favorite icon.
696 bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, parentFolderId, displayOrderInt)
697 } else { // Update the bookmark using the `WebView` favorite icon.
698 // Create a favorite icon byte array output stream.
699 val newFavoriteIconByteArrayOutputStream = ByteArrayOutputStream()
701 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
702 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFavoriteIconByteArrayOutputStream)
704 // Convert the favorite icon byte array stream to a byte array.
705 val newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray()
707 // Update the bookmark and the favorite icon.
708 bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, parentFolderId, displayOrderInt, newFavoriteIconByteArray)
711 // Update the list view.
712 updateBookmarksListView()
715 override fun saveBookmarkFolder(dialogFragment: DialogFragment, selectedFolderDatabaseId: Int, favoriteIconBitmap: Bitmap) {
716 // Get the dialog from the dialog fragment.
717 val dialog = dialogFragment.dialog!!
719 // Get handles for the views from dialog fragment.
720 val currentIconRadioButton = dialog.findViewById<RadioButton>(R.id.current_icon_radiobutton)
721 val defaultIconRadioButton = dialog.findViewById<RadioButton>(R.id.default_icon_radiobutton)
722 val defaultIconImageView = dialog.findViewById<ImageView>(R.id.default_icon_imageview)
723 val folderNameEditText = dialog.findViewById<EditText>(R.id.folder_name_edittext)
724 val parentFolderSpinner = dialog.findViewById<Spinner>(R.id.parent_folder_spinner)
725 val displayOrderEditText = dialog.findViewById<EditText>(R.id.display_order_edittext)
727 // Extract the folder information.
728 val newFolderNameString = folderNameEditText.text.toString()
729 val parentFolderDatabaseId = parentFolderSpinner.selectedItemId.toInt()
730 val displayOrderInt = displayOrderEditText.text.toString().toInt()
732 // Set the parent folder ID.
733 val parentFolderId: Long = if (parentFolderDatabaseId == HOME_FOLDER_DATABASE_ID) // The home folder is selected.
735 else // Get the parent folder name from the database.
736 bookmarksDatabaseHelper.getFolderId(parentFolderDatabaseId)
738 // Update the folder.
739 if (currentIconRadioButton.isChecked) { // Update the folder without changing the favorite icon.
740 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderNameString, parentFolderId, displayOrderInt)
741 } else { // Update the folder and the icon.
742 // Get the new folder icon bitmap.
743 val folderIconBitmap = if (defaultIconRadioButton.isChecked) {
744 // Get the default folder icon drawable.
745 val folderIconDrawable = defaultIconImageView.drawable
747 // Convert the folder icon drawable to a bitmap drawable.
748 val folderIconBitmapDrawable = folderIconDrawable as BitmapDrawable
750 // Convert the folder icon bitmap drawable to a bitmap.
751 folderIconBitmapDrawable.bitmap
752 } else { // Use the `WebView` favorite icon.
753 // Get a copy of the favorite icon bitmap.
757 // Create a folder icon byte array output stream.
758 val newFolderIconByteArrayOutputStream = ByteArrayOutputStream()
760 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
761 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream)
763 // Convert the folder icon byte array stream to a byte array.
764 val newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray()
766 // Update the folder and the icon.
767 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderNameString, parentFolderId, displayOrderInt, newFolderIconByteArray)
770 // Update the list view.
771 updateBookmarksListView()
774 private fun updateBookmarksListView() {
775 // Populate the bookmarks list view based on the spinner selection.
776 bookmarksCursor = when (currentFolderDatabaseId) {
777 // Get all the bookmarks.
778 ALL_FOLDERS_DATABASE_ID -> {
779 if (sortByDisplayOrder)
780 bookmarksDatabaseHelper.allBookmarksByDisplayOrder
782 bookmarksDatabaseHelper.allBookmarks
785 // Get the bookmarks in the home folder.
786 HOME_FOLDER_DATABASE_ID -> {
787 if (sortByDisplayOrder)
788 bookmarksDatabaseHelper.getBookmarksByDisplayOrder(HOME_FOLDER_ID)
790 bookmarksDatabaseHelper.getBookmarks(HOME_FOLDER_ID)
793 // Get the bookmarks in the specified folder.
795 // Get the current folder ID.
796 val currentFolderId = bookmarksDatabaseHelper.getFolderId(currentFolderDatabaseId)
798 if (sortByDisplayOrder)
799 bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderId)
801 bookmarksDatabaseHelper.getBookmarks(currentFolderId)
805 // Update the cursor adapter if it isn't null, which happens when the activity is restarted.
806 if (bookmarksCursorAdapter != null) {
807 bookmarksCursorAdapter!!.changeCursor(bookmarksCursor)
811 private fun prepareFinish() {
812 // 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.
813 if ((bookmarksDeletedSnackbar != null) && bookmarksDeletedSnackbar!!.isShown) { // Close the bookmarks deleted snackbar before going home.
814 // Set the close flag.
815 closeActivityAfterDismissingSnackbar = true
817 // Dismiss the snackbar.
818 bookmarksDeletedSnackbar!!.dismiss()
819 } else { // Go home immediately.
820 // Update the current folder in the bookmarks activity.
821 if ((currentFolderDatabaseId == ALL_FOLDERS_DATABASE_ID) || (currentFolderDatabaseId == HOME_FOLDER_DATABASE_ID)) { // All folders or the the home folder are currently displayed.
822 // Load the home folder.
823 BookmarksActivity.currentFolderId = HOME_FOLDER_ID
824 } else { // A subfolder is currently displayed.
825 // Load the current folder.
826 BookmarksActivity.currentFolderId = bookmarksDatabaseHelper.getFolderId(currentFolderDatabaseId)
829 // Reload the bookmarks list view when returning to the bookmarks activity.
830 BookmarksActivity.restartFromBookmarksDatabaseViewActivity = true
832 // Exit the bookmarks database view activity.
837 private fun selectAllBookmarksInFolder(folderDatabaseId: Int) {
838 // Get the folder ID.
839 val folderId = bookmarksDatabaseHelper.getFolderId(folderDatabaseId)
841 // Get a cursor with the contents of the folder.
842 val folderCursor = bookmarksDatabaseHelper.getBookmarks(folderId)
844 // Get the column indexes.
845 val idColumnIndex = bookmarksCursor.getColumnIndexOrThrow(ID)
847 // Move to the beginning of the cursor.
848 folderCursor.moveToFirst()
850 while (folderCursor.position < folderCursor.count) {
851 // Get the bookmark database ID.
852 val bookmarkId = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(ID))
854 // Move the bookmarks cursor to the first position.
855 bookmarksCursor.moveToFirst()
857 // Initialize the bookmark position variable.
858 var bookmarkPosition = -1
860 // Get the position of this bookmark in the bookmarks cursor.
861 while ((bookmarkPosition < 0) && (bookmarksCursor.position < bookmarksCursor.count)) {
862 // Check if the bookmark IDs match.
863 if (bookmarkId == bookmarksCursor.getInt(idColumnIndex)) {
864 // Get the bookmark position.
865 bookmarkPosition = bookmarksCursor.position
867 // If this bookmark is a folder, select all the bookmarks inside it.
868 if (bookmarksDatabaseHelper.isFolder(bookmarkId))
869 selectAllBookmarksInFolder(bookmarkId)
871 // Select the bookmark.
872 bookmarksListView.setItemChecked(bookmarkPosition, true)
875 // Increment the bookmarks cursor position.
876 bookmarksCursor.moveToNext()
879 // Move to the next position.
880 folderCursor.moveToNext()