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.BookmarksDatabaseHelper
66 import java.io.ByteArrayOutputStream
68 import java.util.Arrays
70 // Define the private class constants.
71 private const val ALL_FOLDERS_DATABASE_ID = -2
72 private const val CURRENT_FOLDER_DATABASE_ID = "current_folder_database_id"
73 private const val CURRENT_FOLDER_NAME = "current_folder_name"
74 private const val SORT_BY_DISPLAY_ORDER = "sort_by_display_order"
76 class BookmarksDatabaseViewActivity : AppCompatActivity(), EditBookmarkDatabaseViewListener, EditBookmarkFolderDatabaseViewListener {
78 // Define the public class constants.
79 const val HOME_FOLDER_DATABASE_ID = -1
82 // Define the class variables.
83 private var closeActivityAfterDismissingSnackbar = false
84 private var currentFolderDatabaseId = 0
85 private var bookmarksCursorAdapter: CursorAdapter? = null
86 private var bookmarksDeletedSnackbar: Snackbar? = null
87 private var sortByDisplayOrder = false
89 // Declare the class variables.
90 private lateinit var bookmarksCursor: Cursor
91 private lateinit var bookmarksDatabaseHelper: BookmarksDatabaseHelper
92 private lateinit var bookmarksListView: ListView
93 private lateinit var currentFolderName: String
94 private lateinit var oldFolderNameString: String
96 public override fun onCreate(savedInstanceState: Bundle?) {
97 // Get a handle for the shared preferences.
98 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
100 // Get the preferences.
101 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
102 val bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false)
104 // Disable screenshots if not allowed.
105 if (!allowScreenshots) {
106 window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
109 // Run the default commands.
110 super.onCreate(savedInstanceState)
112 // Get the favorite icon byte array.
113 val favoriteIconByteArray = intent.getByteArrayExtra(CURRENT_FAVORITE_ICON_BYTE_ARRAY)!!
115 // Convert the favorite icon byte array to a bitmap and store it in a class variable.
116 val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
118 // Set the view according to the theme.
120 // Set the content view.
121 setContentView(R.layout.bookmarks_databaseview_bottom_appbar)
123 // `Window.FEATURE_ACTION_MODE_OVERLAY` makes the contextual action mode cover the support action bar. It must be requested before the content is set.
124 supportRequestWindowFeature(Window.FEATURE_ACTION_MODE_OVERLAY)
126 // Set the content view.
127 setContentView(R.layout.bookmarks_databaseview_top_appbar)
130 // Get a handle for the toolbar.
131 val toolbar = findViewById<Toolbar>(R.id.bookmarks_databaseview_toolbar)
132 val bookmarksListView = findViewById<ListView>(R.id.bookmarks_databaseview_listview)
134 // Set the support action bar.
135 setSupportActionBar(toolbar)
137 // Get a handle for the app bar.
138 val appBar = supportActionBar!!
140 // Set the app bar custom view.
141 appBar.setCustomView(R.layout.spinner)
143 // Display the back arrow in the app bar.
144 appBar.displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM or ActionBar.DISPLAY_HOME_AS_UP
146 // Control what the system back command does.
147 val onBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) {
148 override fun handleOnBackPressed() {
149 // Prepare to finish the activity.
154 // Register the on back pressed callback.
155 onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
157 // Initialize the database handler.
158 bookmarksDatabaseHelper = BookmarksDatabaseHelper(this)
160 // Create a matrix cursor column name string array.
161 val matrixCursorColumnNames = arrayOf(BookmarksDatabaseHelper.ID, BookmarksDatabaseHelper.BOOKMARK_NAME)
163 // Setup the matrix cursor.
164 MatrixCursor(matrixCursorColumnNames).use { matrixCursor ->
165 // Add "All Folders" and "Home Folder" to the matrix cursor.
166 matrixCursor.addRow(arrayOf<Any>(ALL_FOLDERS_DATABASE_ID, getString(R.string.all_folders)))
167 matrixCursor.addRow(arrayOf<Any>(HOME_FOLDER_DATABASE_ID, getString(R.string.home_folder)))
169 // Get a cursor with the list of all the folders.
170 val foldersCursor = bookmarksDatabaseHelper.allFolders
172 // Combine the matrix cursor and the folders cursor.
173 val foldersMergeCursor = MergeCursor(arrayOf(matrixCursor, foldersCursor))
175 // Get the default folder bitmap.
176 val defaultFolderDrawable = AppCompatResources.getDrawable(this, R.drawable.folder_blue_bitmap)
178 // Cast the default folder drawable to a bitmap drawable.
179 val defaultFolderBitmapDrawable = (defaultFolderDrawable as BitmapDrawable)
181 // Convert the default folder bitmap drawable to a bitmap.
182 val defaultFolderBitmap = defaultFolderBitmapDrawable.bitmap
184 // Create a resource cursor adapter for the spinner.
185 val foldersCursorAdapter: ResourceCursorAdapter = object : ResourceCursorAdapter(this, R.layout.appbar_spinner_item, foldersMergeCursor, 0) {
186 override fun bindView(view: View, context: Context, cursor: Cursor) {
187 // Get handles for the spinner views.
188 val spinnerItemImageView = view.findViewById<ImageView>(R.id.spinner_item_imageview)
189 val spinnerItemTextView = view.findViewById<TextView>(R.id.spinner_item_textview)
191 // Set the folder icon according to the type.
192 if (foldersMergeCursor.position > 1) { // Set a user folder icon.
193 // Initialize a default folder icon byte array output stream.
194 val defaultFolderIconByteArrayOutputStream = ByteArrayOutputStream()
196 // 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).
197 defaultFolderBitmap.compress(Bitmap.CompressFormat.PNG, 0, defaultFolderIconByteArrayOutputStream)
199 // Convert the default folder icon output stream to a byte array.
200 val defaultFolderIconByteArray = defaultFolderIconByteArrayOutputStream.toByteArray()
202 // Get the folder icon byte array from the cursor.
203 val folderIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.FAVORITE_ICON))
205 // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
206 val folderIconBitmap = BitmapFactory.decodeByteArray(folderIconByteArray, 0, folderIconByteArray.size)
208 // Set the icon according to the type.
209 if (Arrays.equals(folderIconByteArray, defaultFolderIconByteArray)) { // The default folder icon is used.
210 // Set a smaller and darker folder icon, which works well with the spinner.
211 spinnerItemImageView.setImageDrawable(AppCompatResources.getDrawable(context, R.drawable.folder_dark_blue))
212 } else { // A custom folder icon is uses.
213 // Set the folder image stored in the cursor.
214 spinnerItemImageView.setImageBitmap(folderIconBitmap)
216 } else { // Set the `All Folders` or `Home Folder` icon.
217 // Set the gray folder image.
218 spinnerItemImageView.setImageDrawable(AppCompatResources.getDrawable(context, R.drawable.folder_gray))
221 // Set the text view to display the folder name.
222 spinnerItemTextView.text = cursor.getString(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME))
226 // Set the resource cursor adapter drop drown view resource.
227 foldersCursorAdapter.setDropDownViewResource(R.layout.appbar_spinner_dropdown_item)
229 // Get a handle for the folder spinner.
230 val folderSpinner = findViewById<Spinner>(R.id.spinner)
232 // Set the folder spinner adapter.
233 folderSpinner.adapter = foldersCursorAdapter
235 // Wait to set the on item selected listener until the spinner has been inflated. Otherwise the activity will crash on restart.
237 // Handle taps on the spinner dropdown.
238 folderSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
239 override fun onItemSelected(parent: AdapterView<*>?, view: View, position: Int, id: Long) {
240 // Store the current folder database ID.
241 currentFolderDatabaseId = id.toInt()
243 // Get a handle for the selected view.
244 val selectedFolderTextView = findViewById<TextView>(R.id.spinner_item_textview)
246 // Store the current folder name.
247 currentFolderName = selectedFolderTextView.text.toString()
249 // Update the list view.
250 updateBookmarksListView()
253 override fun onNothingSelected(parent: AdapterView<*>?) {
259 // Check to see if the activity was restarted.
260 if (savedInstanceState == null) { // The activity was not restarted.
261 // Set the default current folder database ID.
262 currentFolderDatabaseId = ALL_FOLDERS_DATABASE_ID
263 } else { // The activity was restarted.
264 // Restore the class variables from the saved instance state.
265 currentFolderDatabaseId = savedInstanceState.getInt(CURRENT_FOLDER_DATABASE_ID)
266 currentFolderName = savedInstanceState.getString(CURRENT_FOLDER_NAME)!!
267 sortByDisplayOrder = savedInstanceState.getBoolean(SORT_BY_DISPLAY_ORDER)
269 // Update the spinner if the home folder is selected. Android handles this by default for the main cursor but not the matrix cursor.
270 if (currentFolderDatabaseId == HOME_FOLDER_DATABASE_ID) {
271 folderSpinner.setSelection(1)
275 // Update the bookmarks listview.
276 updateBookmarksListView()
278 // Setup a cursor adapter.
279 bookmarksCursorAdapter = object : CursorAdapter(this, bookmarksCursor, false) {
280 override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
281 // Inflate the individual item layout. `false` does not attach it to the root.
282 return layoutInflater.inflate(R.layout.bookmarks_databaseview_item_linearlayout, parent, false)
285 override fun bindView(view: View, context: Context, cursor: Cursor) {
286 // Get handles for the views.
287 val bookmarkDatabaseIdTextView = view.findViewById<TextView>(R.id.bookmarks_databaseview_database_id)
288 val bookmarkFavoriteIcon = view.findViewById<ImageView>(R.id.bookmarks_databaseview_favorite_icon)
289 val bookmarkNameTextView = view.findViewById<TextView>(R.id.bookmarks_databaseview_bookmark_name)
290 val bookmarkUrlTextView = view.findViewById<TextView>(R.id.bookmarks_databaseview_bookmark_url)
291 val bookmarkDisplayOrderTextView = view.findViewById<TextView>(R.id.bookmarks_databaseview_display_order)
292 val parentFolderImageView = view.findViewById<ImageView>(R.id.bookmarks_databaseview_parent_folder_icon)
293 val bookmarkParentFolderTextView = view.findViewById<TextView>(R.id.bookmarks_databaseview_parent_folder)
295 // Get the information from the cursor.
296 val bookmarkDatabaseId = cursor.getInt(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.ID))
297 val bookmarkFavoriteIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.FAVORITE_ICON))
298 val bookmarkNameString = cursor.getString(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME))
299 val bookmarkUrlString = cursor.getString(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_URL))
300 val bookmarkDisplayOrder = cursor.getInt(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.DISPLAY_ORDER))
301 val bookmarkParentFolder = cursor.getString(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.PARENT_FOLDER))
303 // Convert the byte array to a `Bitmap` beginning at the beginning at the first byte and ending at the last.
304 val bookmarkFavoriteIconBitmap = BitmapFactory.decodeByteArray(bookmarkFavoriteIconByteArray, 0, bookmarkFavoriteIconByteArray.size)
306 // Populate the views.
307 bookmarkDatabaseIdTextView.text = bookmarkDatabaseId.toString()
308 bookmarkFavoriteIcon.setImageBitmap(bookmarkFavoriteIconBitmap)
309 bookmarkNameTextView.text = bookmarkNameString
310 bookmarkUrlTextView.text = bookmarkUrlString
311 bookmarkDisplayOrderTextView.text = bookmarkDisplayOrder.toString()
313 // Check to see if the bookmark is a folder.
314 if (cursor.getInt(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.IS_FOLDER)) == 1) { // The bookmark is a folder.
315 // Make the font bold. When the first argument is null the font is not changed.
316 bookmarkNameTextView.setTypeface(null, Typeface.BOLD)
319 bookmarkUrlTextView.visibility = View.GONE
320 } else { // The bookmark is not a folder.
321 // Reset the font to default.
322 bookmarkNameTextView.typeface = Typeface.DEFAULT
325 bookmarkUrlTextView.visibility = View.VISIBLE
328 // Make the folder name gray if it is the home folder.
329 if (bookmarkParentFolder.isEmpty()) { // The bookmark is in the home folder.
330 // Get the home folder icon.
331 parentFolderImageView.setImageDrawable(AppCompatResources.getDrawable(applicationContext, R.drawable.folder_gray))
333 // Set the parent folder text to be `Home Folder`.
334 bookmarkParentFolderTextView.setText(R.string.home_folder)
336 // Set the home folder text to be gray.
337 bookmarkParentFolderTextView.setTextColor(getColor(R.color.gray_500))
338 } else { // The bookmark is in a subfolder.
339 // Get the parent folder icon.
340 parentFolderImageView.setImageDrawable(AppCompatResources.getDrawable(applicationContext, R.drawable.folder_dark_blue))
342 // Set the parent folder name.
343 bookmarkParentFolderTextView.text = bookmarkParentFolder
345 // Set the parent folder text color.
346 bookmarkParentFolderTextView.setTextColor(getColor(R.color.parent_folder_text))
351 // Update the ListView.
352 bookmarksListView.adapter = bookmarksCursorAdapter
354 // Set a listener to edit a bookmark when it is tapped.
355 bookmarksListView.onItemClickListener = AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, _: Int, id: Long ->
356 // Convert the database ID to an int.
357 val databaseId = id.toInt()
359 // Show the edit bookmark or edit bookmark folder dialog.
360 if (bookmarksDatabaseHelper.isFolder(databaseId)) {
361 // Save the current folder name, which is used in `onSaveBookmarkFolder()`.
362 oldFolderNameString = bookmarksCursor.getString(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME))
364 // Instantiate the edit bookmark folder dialog.
365 val editBookmarkFolderDatabaseViewDialog: DialogFragment = folderDatabaseId(databaseId, favoriteIconBitmap)
368 editBookmarkFolderDatabaseViewDialog.show(supportFragmentManager, resources.getString(R.string.edit_folder))
370 // Instantiate the edit bookmark dialog.
371 val editBookmarkDatabaseViewDialog: DialogFragment = bookmarkDatabaseId(databaseId, favoriteIconBitmap)
374 editBookmarkDatabaseViewDialog.show(supportFragmentManager, resources.getString(R.string.edit_bookmark))
378 // Handle long presses on the list view.
379 bookmarksListView.setMultiChoiceModeListener(object : MultiChoiceModeListener {
380 // Instantiate the common variables.
381 private lateinit var selectAllMenuItem: MenuItem
382 private lateinit var deleteMenuItem: MenuItem
383 private var deletingBookmarks = false
385 override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
386 // Inflate the menu for the contextual app bar.
387 menuInflater.inflate(R.menu.bookmarks_databaseview_context_menu, menu)
389 // Get handles for the menu items.
390 selectAllMenuItem = menu.findItem(R.id.select_all)
391 deleteMenuItem = menu.findItem(R.id.delete)
393 // Disable the delete menu item if a delete is pending.
394 deleteMenuItem.isEnabled = !deletingBookmarks
396 // Get the number of currently selected bookmarks.
397 val numberOfSelectedBookmarks = bookmarksListView.checkedItemCount
400 mode.setTitle(R.string.bookmarks)
402 // 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.
403 mode.subtitle = getString(R.string.selected, numberOfSelectedBookmarks)
405 // Do not show the select all menu item if all the bookmarks are already checked.
406 if (numberOfSelectedBookmarks == bookmarksListView.count)
407 selectAllMenuItem.isVisible = false
413 override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
418 override fun onItemCheckedStateChanged(mode: ActionMode, position: Int, id: Long, checked: Boolean) {
419 // Calculate the number of selected bookmarks.
420 val numberOfSelectedBookmarks = bookmarksListView.checkedItemCount
422 // Only run the commands if at least one bookmark is selected. Otherwise, a context menu with 0 selected bookmarks is briefly displayed.
423 if (numberOfSelectedBookmarks > 0) {
424 // Update the action mode subtitle according to the number of selected bookmarks.
425 mode.subtitle = getString(R.string.selected, numberOfSelectedBookmarks)
427 // Only show the select all menu item if all of the bookmarks are not already selected.
428 selectAllMenuItem.isVisible = (bookmarksListView.checkedItemCount != bookmarksListView.count)
430 // Convert the database ID to an int.
431 val databaseId = id.toInt()
433 // If a folder was selected, also select all the contents.
434 if (checked && bookmarksDatabaseHelper.isFolder(databaseId))
435 selectAllBookmarksInFolder(databaseId)
437 // Do not allow a bookmark to be deselected if the folder is selected.
439 // Get the folder name.
440 val folderName = bookmarksDatabaseHelper.getParentFolderName(id.toInt())
442 // If the bookmark is not in the root folder, check to see if the folder is selected.
443 if (folderName.isNotEmpty()) {
444 // Get the database ID of the folder.
445 val folderDatabaseId = bookmarksDatabaseHelper.getFolderDatabaseId(folderName)
447 // Move the bookmarks cursor to the first position.
448 bookmarksCursor.moveToFirst()
450 // Initialize the folder position variable.
451 var folderPosition = -1
453 // Get the position of the folder in the bookmarks cursor.
454 while ((folderPosition < 0) && (bookmarksCursor.position < bookmarksCursor.count)) {
455 // Check if the folder database ID matches the bookmark database ID.
456 if (folderDatabaseId == bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.ID))) {
457 // Get the folder position.
458 folderPosition = bookmarksCursor.position
460 // Check if the folder is selected.
461 if (bookmarksListView.isItemChecked(folderPosition)) {
462 // Reselect the bookmark.
463 bookmarksListView.setItemChecked(position, true)
465 // Display a snackbar explaining why the bookmark cannot be deselected.
466 Snackbar.make(bookmarksListView, R.string.cannot_deselect_bookmark, Snackbar.LENGTH_LONG).show()
470 // Increment the bookmarks cursor.
471 bookmarksCursor.moveToNext()
478 override fun onActionItemClicked(mode: ActionMode, menuItem: MenuItem): Boolean {
479 // Get a the menu item ID.
480 val menuItemId = menuItem.itemId
482 // Run the command that corresponds to the selected menu item.
483 if (menuItemId == R.id.select_all) { // Select all the bookmarks.
484 // Get the total number of bookmarks.
485 val numberOfBookmarks = bookmarksListView.count
488 for (i in 0 until numberOfBookmarks) {
489 bookmarksListView.setItemChecked(i, true)
491 } else if (menuItemId == R.id.delete) { // Delete the selected bookmarks.
492 // Set the deleting bookmarks flag, which prevents the delete menu item from being enabled until the current process finishes.
493 deletingBookmarks = true
495 // Get an array of the selected row IDs.
496 val selectedBookmarksIdsLongArray = bookmarksListView.checkedItemIds
498 // 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.
499 val selectedBookmarksPositionsSparseBooleanArray = bookmarksListView.checkedItemPositions.clone()
501 // Populate the bookmarks cursor.
502 bookmarksCursor = when (currentFolderDatabaseId) {
503 // Get all the bookmarks except the ones being deleted.
504 ALL_FOLDERS_DATABASE_ID ->
505 if (sortByDisplayOrder)
506 bookmarksDatabaseHelper.getAllBookmarksByDisplayOrderExcept(selectedBookmarksIdsLongArray)
508 bookmarksDatabaseHelper.getAllBookmarksExcept(selectedBookmarksIdsLongArray)
510 // Get the home folder bookmarks except the ones being deleted.
511 HOME_FOLDER_DATABASE_ID ->
512 if (sortByDisplayOrder)
513 bookmarksDatabaseHelper.getBookmarksByDisplayOrderExcept(selectedBookmarksIdsLongArray, "")
515 bookmarksDatabaseHelper.getBookmarksExcept(selectedBookmarksIdsLongArray, "")
518 // Get the current folder bookmarks except the ones being deleted.
520 if (sortByDisplayOrder)
521 bookmarksDatabaseHelper.getBookmarksByDisplayOrderExcept(selectedBookmarksIdsLongArray, currentFolderName)
523 bookmarksDatabaseHelper.getBookmarksExcept(selectedBookmarksIdsLongArray, currentFolderName)
526 // Update the list view.
527 bookmarksCursorAdapter!!.changeCursor(bookmarksCursor)
529 // Create a snackbar with the number of deleted bookmarks.
530 bookmarksDeletedSnackbar = Snackbar.make(findViewById(R.id.bookmarks_databaseview_coordinatorlayout), getString(R.string.bookmarks_deleted, selectedBookmarksIdsLongArray.size),
531 Snackbar.LENGTH_LONG)
532 .setAction(R.string.undo) {} // Undo will be handles by `onDismissed()` below.
533 .addCallback(object : Snackbar.Callback() {
534 override fun onDismissed(snackbar: Snackbar, event: Int) {
535 if (event == DISMISS_EVENT_ACTION) { // The user pushed the undo button.
536 // Update the bookmarks list view with the current contents of the bookmarks database, including the "deleted" bookmarks.
537 updateBookmarksListView()
539 // Re-select the previously selected bookmarks.
540 for (i in 0 until selectedBookmarksPositionsSparseBooleanArray.size())
541 bookmarksListView.setItemChecked(selectedBookmarksPositionsSparseBooleanArray.keyAt(i), true)
542 } else { // The snackbar was dismissed without the undo button being pushed.
543 // Delete each selected bookmark.
544 for (databaseIdLong in selectedBookmarksIdsLongArray) {
545 // Convert the database long ID to an int.
546 val databaseIdInt = databaseIdLong.toInt()
548 // Delete the selected bookmark.
549 bookmarksDatabaseHelper.deleteBookmark(databaseIdInt)
553 // Reset the deleting bookmarks flag.
554 deletingBookmarks = false
556 // Enable the delete menu item.
557 deleteMenuItem.isEnabled = true
559 // Close the activity if back has been pressed.
560 if (closeActivityAfterDismissingSnackbar)
565 // Show the snackbar.
566 bookmarksDeletedSnackbar!!.show()
569 // Consume the click.
573 override fun onDestroyActionMode(mode: ActionMode) {
580 override fun onCreateOptionsMenu(menu: Menu): Boolean {
582 menuInflater.inflate(R.menu.bookmarks_databaseview_options_menu, menu)
584 // Get a handle for the sort menu item.
585 val sortMenuItem = menu.findItem(R.id.sort)
587 // Change the sort menu item icon if the listview is sorted by display order, which restores the state after a restart.
588 if (sortByDisplayOrder)
589 sortMenuItem.setIcon(R.drawable.sort_selected)
595 override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
596 // Get the menu item ID.
597 val menuItemId = menuItem.itemId
599 // Run the command that corresponds to the selected menu item.
600 if (menuItemId == android.R.id.home) { // Go Home. The home arrow is identified as `android.R.id.home`, not just `R.id.home`.
601 // Prepare to finish the activity.
603 } else if (menuItemId == R.id.sort) { // Toggle the sort mode.
604 // Update the sort by display order tracker.
605 sortByDisplayOrder = !sortByDisplayOrder
607 // Update the icon and display a snackbar.
608 if (sortByDisplayOrder) { // Sort by display order.
610 menuItem.setIcon(R.drawable.sort_selected)
612 // Display a snackbar indicating the current sort type.
613 Snackbar.make(bookmarksListView, R.string.sorted_by_display_order, Snackbar.LENGTH_SHORT).show()
614 } else { // Sort by database id.
616 menuItem.setIcon(R.drawable.sort)
618 // Display a snackbar indicating the current sort type.
619 Snackbar.make(bookmarksListView, R.string.sorted_by_database_id, Snackbar.LENGTH_SHORT).show()
622 // Update the list view.
623 updateBookmarksListView()
626 // Consume the event.
630 public override fun onSaveInstanceState(savedInstanceState: Bundle) {
631 // Run the default commands.
632 super.onSaveInstanceState(savedInstanceState)
634 // Store the class variables in the bundle.
635 savedInstanceState.putInt(CURRENT_FOLDER_DATABASE_ID, currentFolderDatabaseId)
636 savedInstanceState.putString(CURRENT_FOLDER_NAME, currentFolderName)
637 savedInstanceState.putBoolean(SORT_BY_DISPLAY_ORDER, sortByDisplayOrder)
640 private fun prepareFinish() {
641 // 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.
642 if ((bookmarksDeletedSnackbar != null) && bookmarksDeletedSnackbar!!.isShown) { // Close the bookmarks deleted snackbar before going home.
643 // Set the close flag.
644 closeActivityAfterDismissingSnackbar = true
646 // Dismiss the snackbar.
647 bookmarksDeletedSnackbar!!.dismiss()
648 } else { // Go home immediately.
649 // Update the current folder in the bookmarks activity.
650 if (currentFolderDatabaseId == ALL_FOLDERS_DATABASE_ID || currentFolderDatabaseId == HOME_FOLDER_DATABASE_ID) { // All folders or the the home folder are currently displayed.
651 // Load the home folder.
652 BookmarksActivity.currentFolder = ""
653 } else { // A subfolder is currently displayed.
654 // Load the current folder.
655 BookmarksActivity.currentFolder = currentFolderName
658 // Reload the bookmarks list view when returning to the bookmarks activity.
659 BookmarksActivity.restartFromBookmarksDatabaseViewActivity = true
661 // Exit the bookmarks database view activity.
666 private fun updateBookmarksListView() {
667 // Populate the bookmarks list view based on the spinner selection.
668 bookmarksCursor = when (currentFolderDatabaseId) {
669 // Get all the bookmarks.
670 ALL_FOLDERS_DATABASE_ID ->
671 if (sortByDisplayOrder)
672 bookmarksDatabaseHelper.allBookmarksByDisplayOrder
674 bookmarksDatabaseHelper.allBookmarks
676 // Get the bookmarks in the current folder.
677 HOME_FOLDER_DATABASE_ID ->
678 if (sortByDisplayOrder)
679 bookmarksDatabaseHelper.getBookmarksByDisplayOrder("")
681 bookmarksDatabaseHelper.getBookmarks("")
683 // Get the bookmarks in the specified folder.
685 if (sortByDisplayOrder)
686 bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderName)
688 bookmarksDatabaseHelper.getBookmarks(currentFolderName)
691 // Update the cursor adapter if it isn't null, which happens when the activity is restarted.
692 if (bookmarksCursorAdapter != null) {
693 bookmarksCursorAdapter!!.changeCursor(bookmarksCursor)
697 private fun selectAllBookmarksInFolder(folderId: Int) {
698 // Get the folder name.
699 val folderName = bookmarksDatabaseHelper.getFolderName(folderId)
701 // Get a cursor with the contents of the folder.
702 val folderCursor = bookmarksDatabaseHelper.getBookmarks(folderName)
704 // Move to the beginning of the cursor.
705 folderCursor.moveToFirst()
707 while (folderCursor.position < folderCursor.count) {
708 // Get the bookmark database ID.
709 val bookmarkId = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.ID))
711 // Move the bookmarks cursor to the first position.
712 bookmarksCursor.moveToFirst()
714 // Initialize the bookmark position variable.
715 var bookmarkPosition = -1
717 // Get the position of this bookmark in the bookmarks cursor.
718 while ((bookmarkPosition < 0) && (bookmarksCursor.position < bookmarksCursor.count)) {
719 // Check if the bookmark IDs match.
720 if (bookmarkId == bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.ID))) {
721 // Get the bookmark position.
722 bookmarkPosition = bookmarksCursor.position
724 // If this bookmark is a folder, select all the bookmarks inside it.
725 if (bookmarksDatabaseHelper.isFolder(bookmarkId))
726 selectAllBookmarksInFolder(bookmarkId)
728 // Select the bookmark.
729 bookmarksListView.setItemChecked(bookmarkPosition, true)
732 // Increment the bookmarks cursor position.
733 bookmarksCursor.moveToNext()
736 // Move to the next position.
737 folderCursor.moveToNext()
741 override fun onSaveBookmark(dialogFragment: DialogFragment, selectedBookmarkDatabaseId: Int, favoriteIconBitmap: Bitmap) {
742 // Get the dialog from the dialog fragment.
743 val dialog = dialogFragment.dialog!!
745 // Get handles for the views from dialog fragment.
746 val currentIconRadioButton = dialog.findViewById<RadioButton>(R.id.current_icon_radiobutton)
747 val bookmarkNameEditText = dialog.findViewById<EditText>(R.id.bookmark_name_edittext)
748 val bookmarkUrlEditText = dialog.findViewById<EditText>(R.id.bookmark_url_edittext)
749 val folderSpinner = dialog.findViewById<Spinner>(R.id.bookmark_folder_spinner)
750 val displayOrderEditText = dialog.findViewById<EditText>(R.id.bookmark_display_order_edittext)
752 // Extract the bookmark information.
753 val bookmarkNameString = bookmarkNameEditText.text.toString()
754 val bookmarkUrlString = bookmarkUrlEditText.text.toString()
755 val folderDatabaseId = folderSpinner.selectedItemId.toInt()
756 val displayOrderInt = displayOrderEditText.text.toString().toInt()
758 // Get the parent folder name.
759 val parentFolderNameString: String = if (folderDatabaseId == HOME_FOLDER_DATABASE_ID) // The home folder is selected. Use `""`.
761 else // Get the parent folder name from the database.
762 bookmarksDatabaseHelper.getFolderName(folderDatabaseId)
764 // Update the bookmark.
765 if (currentIconRadioButton.isChecked) { // Update the bookmark without changing the favorite icon.
766 bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, parentFolderNameString, displayOrderInt)
767 } else { // Update the bookmark using the `WebView` favorite icon.
768 // Create a favorite icon byte array output stream.
769 val newFavoriteIconByteArrayOutputStream = ByteArrayOutputStream()
771 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
772 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFavoriteIconByteArrayOutputStream)
774 // Convert the favorite icon byte array stream to a byte array.
775 val newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray()
777 // Update the bookmark and the favorite icon.
778 bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, parentFolderNameString, displayOrderInt, newFavoriteIconByteArray)
781 // Update the list view.
782 updateBookmarksListView()
785 override fun onSaveBookmarkFolder(dialogFragment: DialogFragment, selectedFolderDatabaseId: Int, favoriteIconBitmap: Bitmap) {
786 // Get the dialog from the dialog fragment.
787 val dialog = dialogFragment.dialog!!
789 // Get handles for the views from dialog fragment.
790 val currentIconRadioButton = dialog.findViewById<RadioButton>(R.id.current_icon_radiobutton)
791 val defaultIconRadioButton = dialog.findViewById<RadioButton>(R.id.default_icon_radiobutton)
792 val defaultIconImageView = dialog.findViewById<ImageView>(R.id.default_icon_imageview)
793 val folderNameEditText = dialog.findViewById<EditText>(R.id.folder_name_edittext)
794 val parentFolderSpinner = dialog.findViewById<Spinner>(R.id.parent_folder_spinner)
795 val displayOrderEditText = dialog.findViewById<EditText>(R.id.display_order_edittext)
797 // Extract the folder information.
798 val newFolderNameString = folderNameEditText.text.toString()
799 val parentFolderDatabaseId = parentFolderSpinner.selectedItemId.toInt()
800 val displayOrderInt = displayOrderEditText.text.toString().toInt()
802 // Set the parent folder name.
803 val parentFolderNameString: String = if (parentFolderDatabaseId == HOME_FOLDER_DATABASE_ID) // The home folder is selected. Use `""`.
805 else // Get the parent folder name from the database.
806 bookmarksDatabaseHelper.getFolderName(parentFolderDatabaseId)
808 // Update the folder.
809 if (currentIconRadioButton.isChecked) { // Update the folder without changing the favorite icon.
810 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, parentFolderNameString, displayOrderInt)
811 } else { // Update the folder and the icon.
812 // Get the new folder icon bitmap.
813 val folderIconBitmap = if (defaultIconRadioButton.isChecked) {
814 // Get the default folder icon drawable.
815 val folderIconDrawable = defaultIconImageView.drawable
817 // Convert the folder icon drawable to a bitmap drawable.
818 val folderIconBitmapDrawable = folderIconDrawable as BitmapDrawable
820 // Convert the folder icon bitmap drawable to a bitmap.
821 folderIconBitmapDrawable.bitmap
822 } else { // Use the `WebView` favorite icon.
823 // Get a copy of the favorite icon bitmap.
827 // Create a folder icon byte array output stream.
828 val newFolderIconByteArrayOutputStream = ByteArrayOutputStream()
830 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
831 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream)
833 // Convert the folder icon byte array stream to a byte array.
834 val newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray()
836 // Update the folder and the icon.
837 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, parentFolderNameString, displayOrderInt, newFolderIconByteArray)
840 // Update the list view.
841 updateBookmarksListView()
844 public override fun onDestroy() {
845 // Close the bookmarks cursor and database.
846 bookmarksCursor.close()
847 bookmarksDatabaseHelper.close()
849 // Run the default commands.