]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/activities/BookmarksDatabaseViewActivity.kt
First wrong button text in View Headers in night theme. https://redmine.stoutner...
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / activities / BookmarksDatabaseViewActivity.kt
1 /*
2  * Copyright 2016-2024 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android/>.
5  *
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.
10  *
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.
15  *
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/>.
18  */
19
20 package com.stoutner.privacybrowser.activities
21
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
45
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
56
57 import com.google.android.material.snackbar.Snackbar
58
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
73
74 import java.io.ByteArrayOutputStream
75
76 // Define the public class constants.
77 const val HOME_FOLDER_DATABASE_ID = -1
78 const val HOME_FOLDER_ID = 0L
79
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"
84
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
92
93     // Declare the class variables.
94     private lateinit var bookmarksCursor: Cursor
95     private lateinit var bookmarksDatabaseHelper: BookmarksDatabaseHelper
96     private lateinit var bookmarksListView: ListView
97
98     public override fun onCreate(savedInstanceState: Bundle?) {
99         // Get a handle for the shared preferences.
100         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
101
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)
105
106         // Disable screenshots if not allowed.
107         if (!allowScreenshots)
108             window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
109
110         // Run the default commands.
111         super.onCreate(savedInstanceState)
112
113         // Get the favorite icon byte array.
114         val currentFavoriteIconByteArray = intent.getByteArrayExtra(CURRENT_FAVORITE_ICON_BYTE_ARRAY)!!
115
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)
118
119         // Set the view according to the theme.
120         if (bottomAppBar) {
121             // Set the content view.
122             setContentView(R.layout.bookmarks_databaseview_bottom_appbar)
123         } else {
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)
126
127             // Set the content view.
128             setContentView(R.layout.bookmarks_databaseview_top_appbar)
129         }
130
131         // Get handles for the views.
132         val toolbar = findViewById<Toolbar>(R.id.bookmarks_databaseview_toolbar)
133         bookmarksListView = findViewById(R.id.bookmarks_databaseview_listview)
134
135         // Set the support action bar.
136         setSupportActionBar(toolbar)
137
138         // Get a handle for the app bar.
139         val appBar = supportActionBar!!
140
141         // Set the app bar custom view.
142         appBar.setCustomView(R.layout.spinner)
143
144         // Display the back arrow in the app bar.
145         appBar.displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM or ActionBar.DISPLAY_HOME_AS_UP
146
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.
151                 prepareFinish()
152             }
153         }
154
155         // Register the on back pressed callback.
156         onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
157
158         // Initialize the database handler.
159         bookmarksDatabaseHelper = BookmarksDatabaseHelper(this)
160
161         // Create a matrix cursor column name string array.
162         val matrixCursorColumnNames = arrayOf(ID, BOOKMARK_NAME, PARENT_FOLDER_ID)
163
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))
169
170             // Get a cursor with the list of all the folders.
171             val foldersCursor = bookmarksDatabaseHelper.getFoldersExcept(listOf())
172
173             // Combine the matrix cursor and the folders cursor.
174             val foldersMergeCursor = MergeCursor(arrayOf(matrixCursor, foldersCursor))
175
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)
183
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 = ""
193                         }
194                     }
195
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))
200
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)
203
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))
209                     }
210
211                     // Set the folder name.
212                     folderNameTextView.text = cursor.getString(cursor.getColumnIndexOrThrow(BOOKMARK_NAME))
213                 }
214             }
215
216             // Set the resource cursor adapter drop drown view resource.
217             foldersCursorAdapter.setDropDownViewResource(R.layout.bookmarks_databaseview_appbar_spinner_dropdown_item)
218
219             // Get a handle for the folder spinner.
220             val folderSpinner = findViewById<Spinner>(R.id.spinner)
221
222             // Set the folder spinner adapter.
223             folderSpinner.adapter = foldersCursorAdapter
224
225             // Wait to set the on item selected listener until the spinner has been inflated.  Otherwise the activity will crash on restart.
226             folderSpinner.post {
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()
232
233                         // Update the list view.
234                         updateBookmarksListView()
235                     }
236
237                     override fun onNothingSelected(parent: AdapterView<*>?) {
238                         // Do nothing.
239                     }
240                 }
241             }
242
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)
251
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)
255                 }
256             }
257
258             // Update the bookmarks listview.
259             updateBookmarksListView()
260
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)
266                 }
267
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)
278
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))
287
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)
290
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()
297
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)
302
303                         // Display the folder ID.
304                         folderIdTextView.text = getString(R.string.folder_id_separator, folderId)
305
306                         // Hide the URL.
307                         urlTextView.visibility = View.GONE
308                     } else {  // The bookmark is not a folder.
309                         // Reset the font to default.
310                         nameTextView.typeface = Typeface.DEFAULT
311
312                         // Show the URL.
313                         urlTextView.visibility = View.VISIBLE
314                     }
315
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))
320
321                         // Set the parent folder text to be `Home Folder`.
322                         parentFolderTextView.setText(R.string.home_folder)
323
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))
329
330                         // Set the parent folder name.
331                         parentFolderTextView.text = bookmarksDatabaseHelper.getFolderName(parentFolderId)
332
333                         // Set the parent folder text color.
334                         parentFolderTextView.setTextColor(getColor(R.color.parent_folder_text))
335                     }
336                 }
337             }
338
339             // Update the ListView.
340             bookmarksListView.adapter = bookmarksCursorAdapter
341
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()
346
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)
351
352                     // Make it so.
353                     editBookmarkFolderDatabaseViewDialog.show(supportFragmentManager, resources.getString(R.string.edit_folder))
354                 } else {
355                     // Instantiate the edit bookmark dialog.
356                     val editBookmarkDatabaseViewDialog = bookmarkDatabaseId(databaseId, currentFavoriteIconBitmap)
357
358                     // Make it so.
359                     editBookmarkDatabaseViewDialog.show(supportFragmentManager, resources.getString(R.string.edit_bookmark))
360                 }
361             }
362
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
369
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)
373
374                     // Get handles for the menu items.
375                     selectAllMenuItem = menu.findItem(R.id.select_all)
376                     deleteMenuItem = menu.findItem(R.id.delete)
377
378                     // Disable the delete menu item if a delete is pending.
379                     deleteMenuItem.isEnabled = !deletingBookmarks
380
381                     // Get the number of currently selected bookmarks.
382                     val numberOfSelectedBookmarks = bookmarksListView.checkedItemCount
383
384                     // Set the title.
385                     mode.setTitle(R.string.bookmarks)
386
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)
389
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
393
394                     // Make it so.
395                     return true
396                 }
397
398                 override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
399                     // Do nothing.
400                     return false
401                 }
402
403                 override fun onItemCheckedStateChanged(mode: ActionMode, position: Int, id: Long, checked: Boolean) {
404                     // Calculate the number of selected bookmarks.
405                     val numberOfSelectedBookmarks = bookmarksListView.checkedItemCount
406
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)
411
412                         // Only show the select all menu item if all of the bookmarks are not already selected.
413                         selectAllMenuItem.isVisible = (bookmarksListView.checkedItemCount != bookmarksListView.count)
414
415                         // Convert the database ID to an int.
416                         val databaseId = id.toInt()
417
418                         // If a folder was selected, also select all the contents.
419                         if (checked && bookmarksDatabaseHelper.isFolder(databaseId))
420                             selectAllBookmarksInFolder(databaseId)
421
422                         // Do not allow a bookmark to be deselected if the folder is selected.
423                         if (!checked) {
424                             // Get the parent folder ID.
425                             val parentFolderId = bookmarksDatabaseHelper.getParentFolderId(databaseId)
426
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)
431
432                                 // Move the bookmarks cursor to the first position.
433                                 bookmarksCursor.moveToFirst()
434
435                                 // Initialize the folder position variable.
436                                 var folderPosition = -1
437
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
444
445                                         // Check if the folder is selected.
446                                         if (bookmarksListView.isItemChecked(folderPosition)) {
447                                             // Reselect the bookmark.
448                                             bookmarksListView.setItemChecked(position, true)
449
450                                             // Display a snackbar explaining why the bookmark cannot be deselected.
451                                             Snackbar.make(bookmarksListView, R.string.cannot_deselect_bookmark, Snackbar.LENGTH_LONG).show()
452                                         }
453                                     }
454
455                                     // Increment the bookmarks cursor.
456                                     bookmarksCursor.moveToNext()
457                                 }
458                             }
459                         }
460                     }
461                 }
462
463                 override fun onActionItemClicked(mode: ActionMode, menuItem: MenuItem): Boolean {
464                     // Get a the menu item ID.
465                     val menuItemId = menuItem.itemId
466
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
471
472                         // Select them all.
473                         for (i in 0 until numberOfBookmarks) {
474                             bookmarksListView.setItemChecked(i, true)
475                         }
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
479
480                         // Get an array of the selected row IDs.
481                         val selectedBookmarksIdsLongArray = bookmarksListView.checkedItemIds
482
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()
485
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)
492                                 else
493                                     bookmarksDatabaseHelper.getAllBookmarksExcept(selectedBookmarksIdsLongArray)
494                             }
495
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)
500                                 else
501                                     bookmarksDatabaseHelper.getBookmarksExcept(selectedBookmarksIdsLongArray, HOME_FOLDER_ID)
502                             }
503
504                             // Get the current folder bookmarks except the ones being deleted.
505                             else -> {
506                                 // Get the current folder ID.
507                                 val currentFolderId = bookmarksDatabaseHelper.getFolderId(currentFolderDatabaseId)
508
509                                 // Get the cursor.
510                                 if (sortByDisplayOrder)
511                                     bookmarksDatabaseHelper.getBookmarksByDisplayOrderExcept(selectedBookmarksIdsLongArray, currentFolderId)
512                                 else
513                                     bookmarksDatabaseHelper.getBookmarksExcept(selectedBookmarksIdsLongArray, currentFolderId)
514                             }
515                         }
516
517                         // Update the list view.
518                         bookmarksCursorAdapter!!.changeCursor(bookmarksCursor)
519
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()
529
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()
538
539                                             // Delete the selected bookmark.
540                                             bookmarksDatabaseHelper.deleteBookmark(databaseIdInt)
541                                         }
542                                     }
543
544                                     // Reset the deleting bookmarks flag.
545                                     deletingBookmarks = false
546
547                                     // Enable the delete menu item.
548                                     deleteMenuItem.isEnabled = true
549
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
554
555                                         // Finish the activity.
556                                         finish()
557                                     }
558                                 }
559                             })
560
561                         // Show the snackbar.
562                         bookmarksDeletedSnackbar!!.show()
563                     }
564
565                     // Consume the click.
566                     return false
567                 }
568
569                 override fun onDestroyActionMode(mode: ActionMode) {
570                     // Do nothing.
571                 }
572             })
573         }
574     }
575
576     override fun onCreateOptionsMenu(menu: Menu): Boolean {
577         // Inflate the menu.
578         menuInflater.inflate(R.menu.bookmarks_databaseview_options_menu, menu)
579
580         // Get a handle for the sort menu item.
581         val sortMenuItem = menu.findItem(R.id.sort)
582
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)
586
587         // Success.
588         return true
589     }
590
591     public override fun onDestroy() {
592         // Close the bookmarks cursor and database.
593         bookmarksCursor.close()
594         bookmarksDatabaseHelper.close()
595
596         // Run the default commands.
597         super.onDestroy()
598     }
599
600     override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
601         // Get the menu item ID.
602         val menuItemId = menuItem.itemId
603
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.
607             prepareFinish()
608         } else if (menuItemId == R.id.sort) {  // Toggle the sort mode.
609             // Update the sort by display order tracker.
610             sortByDisplayOrder = !sortByDisplayOrder
611
612             // Update the icon and display a snackbar.
613             if (sortByDisplayOrder) {  // Sort by display order.
614                 // Update the icon.
615                 menuItem.setIcon(R.drawable.sort_selected)
616
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.
620                 // Update the icon.
621                 menuItem.setIcon(R.drawable.sort)
622
623                 // Display a snackbar indicating the current sort type.
624                 Snackbar.make(bookmarksListView, R.string.sorted_by_database_id, Snackbar.LENGTH_SHORT).show()
625             }
626
627             // Update the list view.
628             updateBookmarksListView()
629         }
630
631         // Consume the event.
632         return true
633     }
634
635     public override fun onSaveInstanceState(savedInstanceState: Bundle) {
636         // Run the default commands.
637         super.onSaveInstanceState(savedInstanceState)
638
639         // Store the class variables in the bundle.
640         savedInstanceState.putInt(CURRENT_FOLDER_DATABASE_ID, currentFolderDatabaseId)
641         savedInstanceState.putBoolean(SORT_BY_DISPLAY_ORDER, sortByDisplayOrder)
642     }
643
644     override fun saveBookmark(dialogFragment: DialogFragment, selectedBookmarkDatabaseId: Int) {
645         // Get the dialog from the dialog fragment.
646         val dialog = dialogFragment.dialog!!
647
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)
657
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()
663
664         // Get the parent folder ID.
665         val parentFolderId: Long = if (folderDatabaseId == HOME_FOLDER_DATABASE_ID)  // The home folder is selected.
666             HOME_FOLDER_ID
667         else  // Get the parent folder name from the database.
668             bookmarksDatabaseHelper.getFolderId(folderDatabaseId)
669
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
679
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)
682
683             // Create a favorite icon byte array output stream.
684             val newFavoriteIconByteArrayOutputStream = ByteArrayOutputStream()
685
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)
688
689             // Convert the favorite icon byte array stream to a byte array.
690             val newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray()
691
692             //  Update the bookmark and the favorite icon.
693             bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, parentFolderId, displayOrderInt, newFavoriteIconByteArray)
694         }
695
696         // Update the list view.
697         updateBookmarksListView()
698     }
699
700     override fun saveBookmarkFolder(dialogFragment: DialogFragment, selectedFolderDatabaseId: Int) {
701         // Get the dialog from the dialog fragment.
702         val dialog = dialogFragment.dialog!!
703
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)
714
715         // Get the folder information.
716         val newFolderNameString = folderNameEditText.text.toString()
717         val parentFolderDatabaseId = parentFolderSpinner.selectedItemId.toInt()
718         val displayOrderInt = displayOrderEditText.text.toString().toInt()
719
720         // Set the parent folder ID.
721         val parentFolderIdLong: Long = if (parentFolderDatabaseId == HOME_FOLDER_DATABASE_ID)  // The home folder is selected.
722             HOME_FOLDER_ID
723         else  // Get the parent folder name from the database.
724             bookmarksDatabaseHelper.getFolderId(parentFolderDatabaseId)
725
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
737
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)
740
741             // Create a folder icon byte array output stream.
742             val newFolderIconByteArrayOutputStream = ByteArrayOutputStream()
743
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)
746
747             // Convert the folder icon byte array stream to a byte array.
748             val newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray()
749
750             //  Update the folder and the icon.
751             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderNameString, parentFolderIdLong, displayOrderInt, newFolderIconByteArray)
752         }
753
754         // Update the list view.
755         updateBookmarksListView()
756     }
757
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
765                 else
766                     bookmarksDatabaseHelper.allBookmarks
767             }
768
769             // Get the bookmarks in the home folder.
770             HOME_FOLDER_DATABASE_ID -> {
771                 if (sortByDisplayOrder)
772                     bookmarksDatabaseHelper.getBookmarksByDisplayOrder(HOME_FOLDER_ID)
773                 else
774                     bookmarksDatabaseHelper.getBookmarks(HOME_FOLDER_ID)
775             }
776
777             // Get the bookmarks in the specified folder.
778             else -> {
779                 // Get the current folder ID.
780                 val currentFolderId = bookmarksDatabaseHelper.getFolderId(currentFolderDatabaseId)
781
782                 if (sortByDisplayOrder)
783                     bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderId)
784                 else
785                     bookmarksDatabaseHelper.getBookmarks(currentFolderId)
786             }
787         }
788
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)
792         }
793     }
794
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
800
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)
811             }
812
813             // Reload the bookmarks list view when returning to the bookmarks activity.
814             BookmarksActivity.restartFromBookmarksDatabaseViewActivity = true
815
816             // Exit the bookmarks database view activity.
817             finish()
818         }
819     }
820
821     private fun selectAllBookmarksInFolder(folderDatabaseId: Int) {
822         // Get the folder ID.
823         val folderId = bookmarksDatabaseHelper.getFolderId(folderDatabaseId)
824
825         // Get a cursor with the contents of the folder.
826         val folderCursor = bookmarksDatabaseHelper.getBookmarks(folderId)
827
828         // Get the column indexes.
829         val idColumnIndex = bookmarksCursor.getColumnIndexOrThrow(ID)
830
831         // Move to the beginning of the cursor.
832         folderCursor.moveToFirst()
833
834         while (folderCursor.position < folderCursor.count) {
835             // Get the bookmark database ID.
836             val bookmarkId = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(ID))
837
838             // Move the bookmarks cursor to the first position.
839             bookmarksCursor.moveToFirst()
840
841             // Initialize the bookmark position variable.
842             var bookmarkPosition = -1
843
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
850
851                     // If this bookmark is a folder, select all the bookmarks inside it.
852                     if (bookmarksDatabaseHelper.isFolder(bookmarkId))
853                         selectAllBookmarksInFolder(bookmarkId)
854
855                     // Select the bookmark.
856                     bookmarksListView.setItemChecked(bookmarkPosition, true)
857                 }
858
859                 // Increment the bookmarks cursor position.
860                 bookmarksCursor.moveToNext()
861             }
862
863             // Move to the next position.
864             folderCursor.moveToNext()
865         }
866     }
867 }