]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/activities/BookmarksDatabaseViewActivity.kt
Migrate the remaining classes to Kotlin. https://redmine.stoutner.com/issues/989
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / activities / BookmarksDatabaseViewActivity.kt
1 /*
2  * Copyright 2016-2023 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.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
46
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
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.BookmarksDatabaseHelper
65
66 import java.io.ByteArrayOutputStream
67
68 import java.util.Arrays
69
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"
75
76 class BookmarksDatabaseViewActivity : AppCompatActivity(), EditBookmarkDatabaseViewListener, EditBookmarkFolderDatabaseViewListener {
77     companion object {
78         // Define the public class constants.
79         const val HOME_FOLDER_DATABASE_ID = -1
80     }
81
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
88
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
95
96     public override fun onCreate(savedInstanceState: Bundle?) {
97         // Get a handle for the shared preferences.
98         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
99
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)
103
104         // Disable screenshots if not allowed.
105         if (!allowScreenshots) {
106             window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
107         }
108
109         // Run the default commands.
110         super.onCreate(savedInstanceState)
111
112         // Get the favorite icon byte array.
113         val favoriteIconByteArray = intent.getByteArrayExtra(CURRENT_FAVORITE_ICON_BYTE_ARRAY)!!
114
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)
117
118         // Set the view according to the theme.
119         if (bottomAppBar) {
120             // Set the content view.
121             setContentView(R.layout.bookmarks_databaseview_bottom_appbar)
122         } else {
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)
125
126             // Set the content view.
127             setContentView(R.layout.bookmarks_databaseview_top_appbar)
128         }
129
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)
133
134         // Set the support action bar.
135         setSupportActionBar(toolbar)
136
137         // Get a handle for the app bar.
138         val appBar = supportActionBar!!
139
140         // Set the app bar custom view.
141         appBar.setCustomView(R.layout.spinner)
142
143         // Display the back arrow in the app bar.
144         appBar.displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM or ActionBar.DISPLAY_HOME_AS_UP
145
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.
150                 prepareFinish()
151             }
152         }
153
154         // Register the on back pressed callback.
155         onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
156
157         // Initialize the database handler.
158         bookmarksDatabaseHelper = BookmarksDatabaseHelper(this)
159
160         // Create a matrix cursor column name string array.
161         val matrixCursorColumnNames = arrayOf(BookmarksDatabaseHelper.ID, BookmarksDatabaseHelper.BOOKMARK_NAME)
162
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)))
168
169             // Get a cursor with the list of all the folders.
170             val foldersCursor = bookmarksDatabaseHelper.allFolders
171
172             // Combine the matrix cursor and the folders cursor.
173             val foldersMergeCursor = MergeCursor(arrayOf(matrixCursor, foldersCursor))
174
175             // Get the default folder bitmap.
176             val defaultFolderDrawable = AppCompatResources.getDrawable(this, R.drawable.folder_blue_bitmap)
177
178             // Cast the default folder drawable to a bitmap drawable.
179             val defaultFolderBitmapDrawable = (defaultFolderDrawable as BitmapDrawable)
180
181             // Convert the default folder bitmap drawable to a bitmap.
182             val defaultFolderBitmap = defaultFolderBitmapDrawable.bitmap
183
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)
190
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()
195
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)
198
199                         // Convert the default folder icon output stream to a byte array.
200                         val defaultFolderIconByteArray = defaultFolderIconByteArrayOutputStream.toByteArray()
201
202                         // Get the folder icon byte array from the cursor.
203                         val folderIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.FAVORITE_ICON))
204
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)
207
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)
215                         }
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))
219                     }
220
221                     // Set the text view to display the folder name.
222                     spinnerItemTextView.text = cursor.getString(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME))
223                 }
224             }
225
226             // Set the resource cursor adapter drop drown view resource.
227             foldersCursorAdapter.setDropDownViewResource(R.layout.appbar_spinner_dropdown_item)
228
229             // Get a handle for the folder spinner.
230             val folderSpinner = findViewById<Spinner>(R.id.spinner)
231
232             // Set the folder spinner adapter.
233             folderSpinner.adapter = foldersCursorAdapter
234
235             // Wait to set the on item selected listener until the spinner has been inflated.  Otherwise the activity will crash on restart.
236             folderSpinner.post {
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()
242
243                         // Get a handle for the selected view.
244                         val selectedFolderTextView = findViewById<TextView>(R.id.spinner_item_textview)
245
246                         // Store the current folder name.
247                         currentFolderName = selectedFolderTextView.text.toString()
248
249                         // Update the list view.
250                         updateBookmarksListView()
251                     }
252
253                     override fun onNothingSelected(parent: AdapterView<*>?) {
254                         // Do nothing.
255                     }
256                 }
257             }
258
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)
268
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)
272                 }
273             }
274
275             // Update the bookmarks listview.
276             updateBookmarksListView()
277
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)
283                 }
284
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)
294
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))
302
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)
305
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()
312
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)
317
318                         // Hide the URL.
319                         bookmarkUrlTextView.visibility = View.GONE
320                     } else {  // The bookmark is not a folder.
321                         // Reset the font to default.
322                         bookmarkNameTextView.typeface = Typeface.DEFAULT
323
324                         // Show the URL.
325                         bookmarkUrlTextView.visibility = View.VISIBLE
326                     }
327
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))
332
333                         // Set the parent folder text to be `Home Folder`.
334                         bookmarkParentFolderTextView.setText(R.string.home_folder)
335
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))
341
342                         // Set the parent folder name.
343                         bookmarkParentFolderTextView.text = bookmarkParentFolder
344
345                         // Set the parent folder text color.
346                         bookmarkParentFolderTextView.setTextColor(getColor(R.color.parent_folder_text))
347                     }
348                 }
349             }
350
351             // Update the ListView.
352             bookmarksListView.adapter = bookmarksCursorAdapter
353
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()
358
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))
363
364                     // Instantiate the edit bookmark folder dialog.
365                     val editBookmarkFolderDatabaseViewDialog: DialogFragment = folderDatabaseId(databaseId, favoriteIconBitmap)
366
367                     // Make it so.
368                     editBookmarkFolderDatabaseViewDialog.show(supportFragmentManager, resources.getString(R.string.edit_folder))
369                 } else {
370                     // Instantiate the edit bookmark dialog.
371                     val editBookmarkDatabaseViewDialog: DialogFragment = bookmarkDatabaseId(databaseId, favoriteIconBitmap)
372
373                     // Make it so.
374                     editBookmarkDatabaseViewDialog.show(supportFragmentManager, resources.getString(R.string.edit_bookmark))
375                 }
376             }
377
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
384
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)
388
389                     // Get handles for the menu items.
390                     selectAllMenuItem = menu.findItem(R.id.select_all)
391                     deleteMenuItem = menu.findItem(R.id.delete)
392
393                     // Disable the delete menu item if a delete is pending.
394                     deleteMenuItem.isEnabled = !deletingBookmarks
395
396                     // Get the number of currently selected bookmarks.
397                     val numberOfSelectedBookmarks = bookmarksListView.checkedItemCount
398
399                     // Set the title.
400                     mode.setTitle(R.string.bookmarks)
401
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)
404
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
408
409                     // Make it so.
410                     return true
411                 }
412
413                 override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
414                     // Do nothing.
415                     return false
416                 }
417
418                 override fun onItemCheckedStateChanged(mode: ActionMode, position: Int, id: Long, checked: Boolean) {
419                     // Calculate the number of selected bookmarks.
420                     val numberOfSelectedBookmarks = bookmarksListView.checkedItemCount
421
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)
426
427                         // Only show the select all menu item if all of the bookmarks are not already selected.
428                         selectAllMenuItem.isVisible = (bookmarksListView.checkedItemCount != bookmarksListView.count)
429
430                         // Convert the database ID to an int.
431                         val databaseId = id.toInt()
432
433                         // If a folder was selected, also select all the contents.
434                         if (checked && bookmarksDatabaseHelper.isFolder(databaseId))
435                             selectAllBookmarksInFolder(databaseId)
436
437                         // Do not allow a bookmark to be deselected if the folder is selected.
438                         if (!checked) {
439                             // Get the folder name.
440                             val folderName = bookmarksDatabaseHelper.getParentFolderName(id.toInt())
441
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)
446
447                                 // Move the bookmarks cursor to the first position.
448                                 bookmarksCursor.moveToFirst()
449
450                                 // Initialize the folder position variable.
451                                 var folderPosition = -1
452
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
459
460                                         // Check if the folder is selected.
461                                         if (bookmarksListView.isItemChecked(folderPosition)) {
462                                             // Reselect the bookmark.
463                                             bookmarksListView.setItemChecked(position, true)
464
465                                             // Display a snackbar explaining why the bookmark cannot be deselected.
466                                             Snackbar.make(bookmarksListView, R.string.cannot_deselect_bookmark, Snackbar.LENGTH_LONG).show()
467                                         }
468                                     }
469
470                                     // Increment the bookmarks cursor.
471                                     bookmarksCursor.moveToNext()
472                                 }
473                             }
474                         }
475                     }
476                 }
477
478                 override fun onActionItemClicked(mode: ActionMode, menuItem: MenuItem): Boolean {
479                     // Get a the menu item ID.
480                     val menuItemId = menuItem.itemId
481
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
486
487                         // Select them all.
488                         for (i in 0 until numberOfBookmarks) {
489                             bookmarksListView.setItemChecked(i, true)
490                         }
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
494
495                         // Get an array of the selected row IDs.
496                         val selectedBookmarksIdsLongArray = bookmarksListView.checkedItemIds
497
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()
500
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)
507                                 else
508                                     bookmarksDatabaseHelper.getAllBookmarksExcept(selectedBookmarksIdsLongArray)
509
510                             // Get the home folder bookmarks except the ones being deleted.
511                             HOME_FOLDER_DATABASE_ID ->
512                                 if (sortByDisplayOrder)
513                                     bookmarksDatabaseHelper.getBookmarksByDisplayOrderExcept(selectedBookmarksIdsLongArray, "")
514                                 else
515                                     bookmarksDatabaseHelper.getBookmarksExcept(selectedBookmarksIdsLongArray, "")
516
517
518                             // Get the current folder bookmarks except the ones being deleted.
519                             else ->
520                                 if (sortByDisplayOrder)
521                                     bookmarksDatabaseHelper.getBookmarksByDisplayOrderExcept(selectedBookmarksIdsLongArray, currentFolderName)
522                                 else
523                                     bookmarksDatabaseHelper.getBookmarksExcept(selectedBookmarksIdsLongArray, currentFolderName)
524                         }
525
526                         // Update the list view.
527                         bookmarksCursorAdapter!!.changeCursor(bookmarksCursor)
528
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()
538
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()
547
548                                             // Delete the selected bookmark.
549                                             bookmarksDatabaseHelper.deleteBookmark(databaseIdInt)
550                                         }
551                                     }
552
553                                     // Reset the deleting bookmarks flag.
554                                     deletingBookmarks = false
555
556                                     // Enable the delete menu item.
557                                     deleteMenuItem.isEnabled = true
558
559                                     // Close the activity if back has been pressed.
560                                     if (closeActivityAfterDismissingSnackbar)
561                                         finish()
562                                 }
563                             })
564
565                         // Show the snackbar.
566                         bookmarksDeletedSnackbar!!.show()
567                     }
568
569                     // Consume the click.
570                     return false
571                 }
572
573                 override fun onDestroyActionMode(mode: ActionMode) {
574                     // Do nothing.
575                 }
576             })
577         }
578     }
579
580     override fun onCreateOptionsMenu(menu: Menu): Boolean {
581         // Inflate the menu.
582         menuInflater.inflate(R.menu.bookmarks_databaseview_options_menu, menu)
583
584         // Get a handle for the sort menu item.
585         val sortMenuItem = menu.findItem(R.id.sort)
586
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)
590
591         // Success.
592         return true
593     }
594
595     override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
596         // Get the menu item ID.
597         val menuItemId = menuItem.itemId
598
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.
602             prepareFinish()
603         } else if (menuItemId == R.id.sort) {  // Toggle the sort mode.
604             // Update the sort by display order tracker.
605             sortByDisplayOrder = !sortByDisplayOrder
606
607             // Update the icon and display a snackbar.
608             if (sortByDisplayOrder) {  // Sort by display order.
609                 // Update the icon.
610                 menuItem.setIcon(R.drawable.sort_selected)
611
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.
615                 // Update the icon.
616                 menuItem.setIcon(R.drawable.sort)
617
618                 // Display a snackbar indicating the current sort type.
619                 Snackbar.make(bookmarksListView, R.string.sorted_by_database_id, Snackbar.LENGTH_SHORT).show()
620             }
621
622             // Update the list view.
623             updateBookmarksListView()
624         }
625
626         // Consume the event.
627         return true
628     }
629
630     public override fun onSaveInstanceState(savedInstanceState: Bundle) {
631         // Run the default commands.
632         super.onSaveInstanceState(savedInstanceState)
633
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)
638     }
639
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
645
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
656             }
657
658             // Reload the bookmarks list view when returning to the bookmarks activity.
659             BookmarksActivity.restartFromBookmarksDatabaseViewActivity = true
660
661             // Exit the bookmarks database view activity.
662             finish()
663         }
664     }
665
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
673                 else
674                     bookmarksDatabaseHelper.allBookmarks
675
676             // Get the bookmarks in the current folder.
677             HOME_FOLDER_DATABASE_ID ->
678                 if (sortByDisplayOrder)
679                     bookmarksDatabaseHelper.getBookmarksByDisplayOrder("")
680                 else
681                     bookmarksDatabaseHelper.getBookmarks("")
682
683             // Get the bookmarks in the specified folder.
684             else ->
685                 if (sortByDisplayOrder)
686                     bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderName)
687                 else
688                     bookmarksDatabaseHelper.getBookmarks(currentFolderName)
689         }
690
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)
694         }
695     }
696
697     private fun selectAllBookmarksInFolder(folderId: Int) {
698         // Get the folder name.
699         val folderName = bookmarksDatabaseHelper.getFolderName(folderId)
700
701         // Get a cursor with the contents of the folder.
702         val folderCursor = bookmarksDatabaseHelper.getBookmarks(folderName)
703
704         // Move to the beginning of the cursor.
705         folderCursor.moveToFirst()
706
707         while (folderCursor.position < folderCursor.count) {
708             // Get the bookmark database ID.
709             val bookmarkId = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.ID))
710
711             // Move the bookmarks cursor to the first position.
712             bookmarksCursor.moveToFirst()
713
714             // Initialize the bookmark position variable.
715             var bookmarkPosition = -1
716
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
723
724                     // If this bookmark is a folder, select all the bookmarks inside it.
725                     if (bookmarksDatabaseHelper.isFolder(bookmarkId))
726                         selectAllBookmarksInFolder(bookmarkId)
727
728                     // Select the bookmark.
729                     bookmarksListView.setItemChecked(bookmarkPosition, true)
730                 }
731
732                 // Increment the bookmarks cursor position.
733                 bookmarksCursor.moveToNext()
734             }
735
736             // Move to the next position.
737             folderCursor.moveToNext()
738         }
739     }
740
741     override fun onSaveBookmark(dialogFragment: DialogFragment, selectedBookmarkDatabaseId: Int, favoriteIconBitmap: Bitmap) {
742         // Get the dialog from the dialog fragment.
743         val dialog = dialogFragment.dialog!!
744
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)
751
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()
757
758         // Get the parent folder name.
759         val parentFolderNameString: String = if (folderDatabaseId == HOME_FOLDER_DATABASE_ID)  // The home folder is selected.  Use `""`.
760             ""
761         else  // Get the parent folder name from the database.
762             bookmarksDatabaseHelper.getFolderName(folderDatabaseId)
763
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()
770
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)
773
774             // Convert the favorite icon byte array stream to a byte array.
775             val newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray()
776
777             //  Update the bookmark and the favorite icon.
778             bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, parentFolderNameString, displayOrderInt, newFavoriteIconByteArray)
779         }
780
781         // Update the list view.
782         updateBookmarksListView()
783     }
784
785     override fun onSaveBookmarkFolder(dialogFragment: DialogFragment, selectedFolderDatabaseId: Int, favoriteIconBitmap: Bitmap) {
786         // Get the dialog from the dialog fragment.
787         val dialog = dialogFragment.dialog!!
788
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)
796
797         // Extract the folder information.
798         val newFolderNameString = folderNameEditText.text.toString()
799         val parentFolderDatabaseId = parentFolderSpinner.selectedItemId.toInt()
800         val displayOrderInt = displayOrderEditText.text.toString().toInt()
801
802         // Set the parent folder name.
803         val parentFolderNameString: String = if (parentFolderDatabaseId == HOME_FOLDER_DATABASE_ID)  // The home folder is selected.  Use `""`.
804             ""
805         else  // Get the parent folder name from the database.
806             bookmarksDatabaseHelper.getFolderName(parentFolderDatabaseId)
807
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
816
817                 // Convert the folder icon drawable to a bitmap drawable.
818                 val folderIconBitmapDrawable = folderIconDrawable as BitmapDrawable
819
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.
824                 favoriteIconBitmap
825             }
826
827             // Create a folder icon byte array output stream.
828             val newFolderIconByteArrayOutputStream = ByteArrayOutputStream()
829
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)
832
833             // Convert the folder icon byte array stream to a byte array.
834             val newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray()
835
836             //  Update the folder and the icon.
837             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, parentFolderNameString, displayOrderInt, newFolderIconByteArray)
838         }
839
840         // Update the list view.
841         updateBookmarksListView()
842     }
843
844     public override fun onDestroy() {
845         // Close the bookmarks cursor and database.
846         bookmarksCursor.close()
847         bookmarksDatabaseHelper.close()
848
849         // Run the default commands.
850         super.onDestroy()
851     }
852 }