]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/activities/BookmarksDatabaseViewActivity.kt
Allow duplicate bookmark folders. https://redmine.stoutner.com/issues/199
[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.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 import java.util.Arrays
77
78 // Define the public class constants.
79 const val HOME_FOLDER_DATABASE_ID = -1
80 const val HOME_FOLDER_ID = 0L
81
82 // Define the private class constants.
83 private const val ALL_FOLDERS_DATABASE_ID = -2
84 private const val CURRENT_FOLDER_DATABASE_ID = "A"
85 private const val SORT_BY_DISPLAY_ORDER = "B"
86
87 class BookmarksDatabaseViewActivity : AppCompatActivity(), EditBookmarkDatabaseViewListener, EditBookmarkFolderDatabaseViewListener {
88     // Define the class variables.
89     private var closeActivityAfterDismissingSnackbar = false
90     private var currentFolderDatabaseId = 0
91     private var bookmarksCursorAdapter: CursorAdapter? = null
92     private var bookmarksDeletedSnackbar: Snackbar? = null
93     private var sortByDisplayOrder = false
94
95     // Declare the class variables.
96     private lateinit var bookmarksCursor: Cursor
97     private lateinit var bookmarksDatabaseHelper: BookmarksDatabaseHelper
98     private lateinit var bookmarksListView: ListView
99
100     public override fun onCreate(savedInstanceState: Bundle?) {
101         // Get a handle for the shared preferences.
102         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
103
104         // Get the preferences.
105         val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
106         val bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false)
107
108         // Disable screenshots if not allowed.
109         if (!allowScreenshots) {
110             window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
111         }
112
113         // Run the default commands.
114         super.onCreate(savedInstanceState)
115
116         // Get the favorite icon byte array.
117         val currentFavoriteIconByteArray = intent.getByteArrayExtra(CURRENT_FAVORITE_ICON_BYTE_ARRAY)!!
118
119         // Convert the favorite icon byte array to a bitmap and store it in a class variable.
120         val currentFavoriteIconBitmap = BitmapFactory.decodeByteArray(currentFavoriteIconByteArray, 0, currentFavoriteIconByteArray.size)
121
122         // Set the view according to the theme.
123         if (bottomAppBar) {
124             // Set the content view.
125             setContentView(R.layout.bookmarks_databaseview_bottom_appbar)
126         } else {
127             // `Window.FEATURE_ACTION_MODE_OVERLAY` makes the contextual action mode cover the support action bar.  It must be requested before the content is set.
128             supportRequestWindowFeature(Window.FEATURE_ACTION_MODE_OVERLAY)
129
130             // Set the content view.
131             setContentView(R.layout.bookmarks_databaseview_top_appbar)
132         }
133
134         // Get a handle for the toolbar.
135         val toolbar = findViewById<Toolbar>(R.id.bookmarks_databaseview_toolbar)
136         bookmarksListView = findViewById(R.id.bookmarks_databaseview_listview)
137
138         // Set the support action bar.
139         setSupportActionBar(toolbar)
140
141         // Get a handle for the app bar.
142         val appBar = supportActionBar!!
143
144         // Set the app bar custom view.
145         appBar.setCustomView(R.layout.spinner)
146
147         // Display the back arrow in the app bar.
148         appBar.displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM or ActionBar.DISPLAY_HOME_AS_UP
149
150         // Control what the system back command does.
151         val onBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) {
152             override fun handleOnBackPressed() {
153                 // Prepare to finish the activity.
154                 prepareFinish()
155             }
156         }
157
158         // Register the on back pressed callback.
159         onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
160
161         // Initialize the database handler.
162         bookmarksDatabaseHelper = BookmarksDatabaseHelper(this)
163
164         // Create a matrix cursor column name string array.
165         val matrixCursorColumnNames = arrayOf(ID, BOOKMARK_NAME, PARENT_FOLDER_ID)
166
167         // Setup the matrix cursor.
168         MatrixCursor(matrixCursorColumnNames).use { matrixCursor ->
169             // Add "All Folders" and "Home Folder" to the matrix cursor.
170             matrixCursor.addRow(arrayOf<Any>(ALL_FOLDERS_DATABASE_ID, getString(R.string.all_folders), HOME_FOLDER_ID))
171             matrixCursor.addRow(arrayOf<Any>(HOME_FOLDER_DATABASE_ID, getString(R.string.home_folder), HOME_FOLDER_ID))
172
173             // Get a cursor with the list of all the folders.
174             val foldersCursor = bookmarksDatabaseHelper.getFoldersExcept(listOf())
175
176             // Combine the matrix cursor and the folders cursor.
177             val foldersMergeCursor = MergeCursor(arrayOf(matrixCursor, foldersCursor))
178
179             // Get the default folder bitmap.
180             val defaultFolderDrawable = AppCompatResources.getDrawable(this, R.drawable.folder_blue_bitmap)
181
182             // Cast the default folder drawable to a bitmap drawable.
183             val defaultFolderBitmapDrawable = (defaultFolderDrawable as BitmapDrawable)
184
185             // Convert the default folder bitmap drawable to a bitmap.
186             val defaultFolderBitmap = defaultFolderBitmapDrawable.bitmap
187
188             // Create a resource cursor adapter for the spinner.
189             val foldersCursorAdapter: ResourceCursorAdapter = object : ResourceCursorAdapter(this, R.layout.bookmarks_databaseview_appbar_spinner_item, foldersMergeCursor, 0) {
190                 override fun bindView(view: View, context: Context, cursor: Cursor) {
191                     // Get handles for the spinner views.
192                     val subfolderSpacerTextView = view.findViewById<TextView>(R.id.subfolder_spacer_textview)
193                     val folderIconImageView = view.findViewById<ImageView>(R.id.folder_icon_imageview)
194                     val folderNameTextView = view.findViewById<TextView>(R.id.folder_name_textview)
195
196                     // Populate the subfolder spacer if it is not null (the spinner is open).
197                     if (subfolderSpacerTextView != null) {
198                         // Indent subfolders.
199                         if (cursor.getLong(cursor.getColumnIndexOrThrow(PARENT_FOLDER_ID)) != HOME_FOLDER_ID) {  // The folder is not in the home folder.
200                             // Get the subfolder spacer.
201                             subfolderSpacerTextView.text = bookmarksDatabaseHelper.getSubfolderSpacer(cursor.getLong(cursor.getColumnIndexOrThrow(FOLDER_ID)))
202                         } else {  // The folder is in the home folder.
203                             // Reset the subfolder spacer.
204                             subfolderSpacerTextView.text = ""
205                         }
206                     }
207
208                     // Set the folder icon according to the type.
209                     if (foldersMergeCursor.position > 1) {  // Set a user folder icon.
210                         // Initialize a default folder icon byte array output stream.
211                         val defaultFolderIconByteArrayOutputStream = ByteArrayOutputStream()
212
213                         // Covert the default folder bitmap to a PNG and store it in the output stream.  `0` is for lossless compression (the only option for a PNG).
214                         defaultFolderBitmap.compress(Bitmap.CompressFormat.PNG, 0, defaultFolderIconByteArrayOutputStream)
215
216                         // Convert the default folder icon output stream to a byte array.
217                         val defaultFolderIconByteArray = defaultFolderIconByteArrayOutputStream.toByteArray()
218
219                         // Get the folder icon byte array from the cursor.
220                         val folderIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(FAVORITE_ICON))
221
222                         // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
223                         val folderIconBitmap = BitmapFactory.decodeByteArray(folderIconByteArray, 0, folderIconByteArray.size)
224
225                         // Set the icon according to the type.
226                         if (Arrays.equals(folderIconByteArray, defaultFolderIconByteArray)) {  // The default folder icon is used.
227                             // Set a smaller and darker folder icon, which works well with the spinner.
228                             folderIconImageView.setImageDrawable(AppCompatResources.getDrawable(context, R.drawable.folder_dark_blue))
229                         } else {  // A custom folder icon is uses.
230                             // Set the folder image stored in the cursor.
231                             folderIconImageView.setImageBitmap(folderIconBitmap)
232                         }
233                     } else {  // Set the `All Folders` or `Home Folder` icon.
234                         // Set the gray folder image.
235                         folderIconImageView.setImageDrawable(AppCompatResources.getDrawable(context, R.drawable.folder_gray))
236                     }
237
238                     // Set the folder name.
239                     folderNameTextView.text = cursor.getString(cursor.getColumnIndexOrThrow(BOOKMARK_NAME))
240                 }
241             }
242
243             // Set the resource cursor adapter drop drown view resource.
244             foldersCursorAdapter.setDropDownViewResource(R.layout.bookmarks_databaseview_appbar_spinner_dropdown_item)
245
246             // Get a handle for the folder spinner.
247             val folderSpinner = findViewById<Spinner>(R.id.spinner)
248
249             // Set the folder spinner adapter.
250             folderSpinner.adapter = foldersCursorAdapter
251
252             // Wait to set the on item selected listener until the spinner has been inflated.  Otherwise the activity will crash on restart.
253             folderSpinner.post {
254                 // Handle taps on the spinner dropdown.
255                 folderSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
256                     override fun onItemSelected(parent: AdapterView<*>?, view: View, position: Int, id: Long) {
257                         // Store the current folder database ID.
258                         currentFolderDatabaseId = id.toInt()
259
260                         // Update the list view.
261                         updateBookmarksListView()
262                     }
263
264                     override fun onNothingSelected(parent: AdapterView<*>?) {
265                         // Do nothing.
266                     }
267                 }
268             }
269
270             // Check to see if the activity was restarted.
271             if (savedInstanceState == null) {  // The activity was not restarted.
272                 // Set the default current folder database ID.
273                 currentFolderDatabaseId = ALL_FOLDERS_DATABASE_ID
274             } else {  // The activity was restarted.
275                 // Restore the class variables from the saved instance state.
276                 currentFolderDatabaseId = savedInstanceState.getInt(CURRENT_FOLDER_DATABASE_ID)
277                 sortByDisplayOrder = savedInstanceState.getBoolean(SORT_BY_DISPLAY_ORDER)
278
279                 // Update the spinner if the home folder is selected.  Android handles this by default for the main cursor but not the matrix cursor.
280                 if (currentFolderDatabaseId == HOME_FOLDER_DATABASE_ID) {
281                     folderSpinner.setSelection(1)
282                 }
283             }
284
285             // Update the bookmarks listview.
286             updateBookmarksListView()
287
288             // Setup a cursor adapter.
289             bookmarksCursorAdapter = object : CursorAdapter(this, bookmarksCursor, false) {
290                 override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
291                     // Inflate the individual item layout.  `false` does not attach it to the root.
292                     return layoutInflater.inflate(R.layout.bookmarks_databaseview_item_linearlayout, parent, false)
293                 }
294
295                 override fun bindView(view: View, context: Context, cursor: Cursor) {
296                     // Get handles for the views.
297                     val databaseIdTextView = view.findViewById<TextView>(R.id.database_id_textview)
298                     val bookmarkFavoriteIconImageView = view.findViewById<ImageView>(R.id.favorite_icon_imageview)
299                     val nameTextView = view.findViewById<TextView>(R.id.name_textview)
300                     val folderIdTextView = view.findViewById<TextView>(R.id.folder_id_textview)
301                     val urlTextView = view.findViewById<TextView>(R.id.url_textview)
302                     val displayOrderTextView = view.findViewById<TextView>(R.id.display_order_textview)
303                     val parentFolderIconImageView = view.findViewById<ImageView>(R.id.parent_folder_icon_imageview)
304                     val parentFolderTextView = view.findViewById<TextView>(R.id.parent_folder_textview)
305
306                     // Get the information from the cursor.
307                     val databaseId = cursor.getInt(cursor.getColumnIndexOrThrow(ID))
308                     val bookmarkFavoriteIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(FAVORITE_ICON))
309                     val nameString = cursor.getString(cursor.getColumnIndexOrThrow(BOOKMARK_NAME))
310                     val folderId = cursor.getLong(cursor.getColumnIndexOrThrow(FOLDER_ID))
311                     val urlString = cursor.getString(cursor.getColumnIndexOrThrow(BOOKMARK_URL))
312                     val displayOrder = cursor.getInt(cursor.getColumnIndexOrThrow(DISPLAY_ORDER))
313                     val parentFolderId = cursor.getLong(cursor.getColumnIndexOrThrow(PARENT_FOLDER_ID))
314
315                     // Convert the byte array to a `Bitmap` beginning at the beginning at the first byte and ending at the last.
316                     val bookmarkFavoriteIconBitmap = BitmapFactory.decodeByteArray(bookmarkFavoriteIconByteArray, 0, bookmarkFavoriteIconByteArray.size)
317
318                     // Populate the views.
319                     databaseIdTextView.text = databaseId.toString()
320                     bookmarkFavoriteIconImageView.setImageBitmap(bookmarkFavoriteIconBitmap)
321                     nameTextView.text = nameString
322                     urlTextView.text = urlString
323                     displayOrderTextView.text = displayOrder.toString()
324
325                     // Check to see if the bookmark is a folder.
326                     if (cursor.getInt(cursor.getColumnIndexOrThrow(IS_FOLDER)) == 1) {  // The bookmark is a folder.
327                         // Make the font bold.  When the first argument is null the font is not changed.
328                         nameTextView.setTypeface(null, Typeface.BOLD)
329
330                         // Display the folder ID.
331                         folderIdTextView.text = getString(R.string.folder_id_separator, folderId)
332
333                         // Hide the URL.
334                         urlTextView.visibility = View.GONE
335                     } else {  // The bookmark is not a folder.
336                         // Reset the font to default.
337                         nameTextView.typeface = Typeface.DEFAULT
338
339                         // Show the URL.
340                         urlTextView.visibility = View.VISIBLE
341                     }
342
343                     // Make the folder name gray if it is the home folder.
344                     if (parentFolderId == HOME_FOLDER_ID) {  // The bookmark is in the home folder.
345                         // Get the home folder icon.
346                         parentFolderIconImageView.setImageDrawable(AppCompatResources.getDrawable(applicationContext, R.drawable.folder_gray))
347
348                         // Set the parent folder text to be `Home Folder`.
349                         parentFolderTextView.setText(R.string.home_folder)
350
351                         // Set the home folder text to be gray.
352                         parentFolderTextView.setTextColor(getColor(R.color.gray_500))
353                     } else {  // The bookmark is in a subfolder.
354                         // Set the parent folder icon.
355                         parentFolderIconImageView.setImageDrawable(AppCompatResources.getDrawable(applicationContext, R.drawable.folder_dark_blue))
356
357                         // Set the parent folder name.
358                         parentFolderTextView.text = bookmarksDatabaseHelper.getFolderName(parentFolderId)
359
360                         // Set the parent folder text color.
361                         parentFolderTextView.setTextColor(getColor(R.color.parent_folder_text))
362                     }
363                 }
364             }
365
366             // Update the ListView.
367             bookmarksListView.adapter = bookmarksCursorAdapter
368
369             // Set a listener to edit a bookmark when it is tapped.
370             bookmarksListView.onItemClickListener = AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, _: Int, id: Long ->
371                 // Convert the database ID to an int.
372                 val databaseId = id.toInt()
373
374                 // Show the edit bookmark or edit bookmark folder dialog.
375                 if (bookmarksDatabaseHelper.isFolder(databaseId)) {
376                     // Instantiate the edit bookmark folder dialog.
377                     val editBookmarkFolderDatabaseViewDialog = folderDatabaseId(databaseId, currentFavoriteIconBitmap)
378
379                     // Make it so.
380                     editBookmarkFolderDatabaseViewDialog.show(supportFragmentManager, resources.getString(R.string.edit_folder))
381                 } else {
382                     // Instantiate the edit bookmark dialog.
383                     val editBookmarkDatabaseViewDialog = bookmarkDatabaseId(databaseId, currentFavoriteIconBitmap)
384
385                     // Make it so.
386                     editBookmarkDatabaseViewDialog.show(supportFragmentManager, resources.getString(R.string.edit_bookmark))
387                 }
388             }
389
390             // Handle long presses on the list view.
391             bookmarksListView.setMultiChoiceModeListener(object : MultiChoiceModeListener {
392                 // Instantiate the common variables.
393                 private lateinit var selectAllMenuItem: MenuItem
394                 private lateinit var deleteMenuItem: MenuItem
395                 private var deletingBookmarks = false
396
397                 override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
398                     // Inflate the menu for the contextual app bar.
399                     menuInflater.inflate(R.menu.bookmarks_databaseview_context_menu, menu)
400
401                     // Get handles for the menu items.
402                     selectAllMenuItem = menu.findItem(R.id.select_all)
403                     deleteMenuItem = menu.findItem(R.id.delete)
404
405                     // Disable the delete menu item if a delete is pending.
406                     deleteMenuItem.isEnabled = !deletingBookmarks
407
408                     // Get the number of currently selected bookmarks.
409                     val numberOfSelectedBookmarks = bookmarksListView.checkedItemCount
410
411                     // Set the title.
412                     mode.setTitle(R.string.bookmarks)
413
414                     // Set the action mode subtitle according to the number of selected bookmarks.  This must be set here or it will be missing if the activity is restarted.
415                     mode.subtitle = getString(R.string.selected, numberOfSelectedBookmarks)
416
417                     // Do not show the select all menu item if all the bookmarks are already checked.
418                     if (numberOfSelectedBookmarks == bookmarksListView.count)
419                         selectAllMenuItem.isVisible = false
420
421                     // Make it so.
422                     return true
423                 }
424
425                 override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
426                     // Do nothing.
427                     return false
428                 }
429
430                 override fun onItemCheckedStateChanged(mode: ActionMode, position: Int, id: Long, checked: Boolean) {
431                     // Calculate the number of selected bookmarks.
432                     val numberOfSelectedBookmarks = bookmarksListView.checkedItemCount
433
434                     // Only run the commands if at least one bookmark is selected.  Otherwise, a context menu with 0 selected bookmarks is briefly displayed.
435                     if (numberOfSelectedBookmarks > 0) {
436                         // Update the action mode subtitle according to the number of selected bookmarks.
437                         mode.subtitle = getString(R.string.selected, numberOfSelectedBookmarks)
438
439                         // Only show the select all menu item if all of the bookmarks are not already selected.
440                         selectAllMenuItem.isVisible = (bookmarksListView.checkedItemCount != bookmarksListView.count)
441
442                         // Convert the database ID to an int.
443                         val databaseId = id.toInt()
444
445                         // If a folder was selected, also select all the contents.
446                         if (checked && bookmarksDatabaseHelper.isFolder(databaseId))
447                             selectAllBookmarksInFolder(databaseId)
448
449                         // Do not allow a bookmark to be deselected if the folder is selected.
450                         if (!checked) {
451                             // Get the parent folder ID.
452                             val parentFolderId = bookmarksDatabaseHelper.getParentFolderId(databaseId)
453
454                             // If the bookmark is not in the root folder, check to see if the folder is selected.
455                             if (parentFolderId != HOME_FOLDER_ID) {
456                                 // Get the folder database ID.
457                                 val folderDatabaseId = bookmarksDatabaseHelper.getFolderDatabaseId(parentFolderId)
458
459                                 // Move the bookmarks cursor to the first position.
460                                 bookmarksCursor.moveToFirst()
461
462                                 // Initialize the folder position variable.
463                                 var folderPosition = -1
464
465                                 // Get the position of the folder in the bookmarks cursor.
466                                 while ((folderPosition < 0) && (bookmarksCursor.position < bookmarksCursor.count)) {
467                                     // Check if the folder database ID matches the bookmark database ID.
468                                     if (folderDatabaseId == bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(ID))) {
469                                         // Get the folder position.
470                                         folderPosition = bookmarksCursor.position
471
472                                         // Check if the folder is selected.
473                                         if (bookmarksListView.isItemChecked(folderPosition)) {
474                                             // Reselect the bookmark.
475                                             bookmarksListView.setItemChecked(position, true)
476
477                                             // Display a snackbar explaining why the bookmark cannot be deselected.
478                                             Snackbar.make(bookmarksListView, R.string.cannot_deselect_bookmark, Snackbar.LENGTH_LONG).show()
479                                         }
480                                     }
481
482                                     // Increment the bookmarks cursor.
483                                     bookmarksCursor.moveToNext()
484                                 }
485                             }
486                         }
487                     }
488                 }
489
490                 override fun onActionItemClicked(mode: ActionMode, menuItem: MenuItem): Boolean {
491                     // Get a the menu item ID.
492                     val menuItemId = menuItem.itemId
493
494                     // Run the command that corresponds to the selected menu item.
495                     if (menuItemId == R.id.select_all) {  // Select all the bookmarks.
496                         // Get the total number of bookmarks.
497                         val numberOfBookmarks = bookmarksListView.count
498
499                         // Select them all.
500                         for (i in 0 until numberOfBookmarks) {
501                             bookmarksListView.setItemChecked(i, true)
502                         }
503                     } else if (menuItemId == R.id.delete) {  // Delete the selected bookmarks.
504                         // Set the deleting bookmarks flag, which prevents the delete menu item from being enabled until the current process finishes.
505                         deletingBookmarks = true
506
507                         // Get an array of the selected row IDs.
508                         val selectedBookmarksIdsLongArray = bookmarksListView.checkedItemIds
509
510                         // Get an array of checked bookmarks.  `.clone()` makes a copy that won't change if the list view is reloaded, which is needed for re-selecting the bookmarks on undelete.
511                         val selectedBookmarksPositionsSparseBooleanArray = bookmarksListView.checkedItemPositions.clone()
512
513                         // Populate the bookmarks cursor.
514                         bookmarksCursor = when (currentFolderDatabaseId) {
515                             // Get all the bookmarks except the ones being deleted.
516                             ALL_FOLDERS_DATABASE_ID -> {
517                                 if (sortByDisplayOrder)
518                                     bookmarksDatabaseHelper.getAllBookmarksByDisplayOrderExcept(selectedBookmarksIdsLongArray)
519                                 else
520                                     bookmarksDatabaseHelper.getAllBookmarksExcept(selectedBookmarksIdsLongArray)
521                             }
522
523                             // Get the home folder bookmarks except the ones being deleted.
524                             HOME_FOLDER_DATABASE_ID -> {
525                                 if (sortByDisplayOrder)
526                                     bookmarksDatabaseHelper.getBookmarksByDisplayOrderExcept(selectedBookmarksIdsLongArray, HOME_FOLDER_ID)
527                                 else
528                                     bookmarksDatabaseHelper.getBookmarksExcept(selectedBookmarksIdsLongArray, HOME_FOLDER_ID)
529                             }
530
531                             // Get the current folder bookmarks except the ones being deleted.
532                             else -> {
533                                 // Get the current folder ID.
534                                 val currentFolderId = bookmarksDatabaseHelper.getFolderId(currentFolderDatabaseId)
535
536                                 // Get the cursor.
537                                 if (sortByDisplayOrder)
538                                     bookmarksDatabaseHelper.getBookmarksByDisplayOrderExcept(selectedBookmarksIdsLongArray, currentFolderId)
539                                 else
540                                     bookmarksDatabaseHelper.getBookmarksExcept(selectedBookmarksIdsLongArray, currentFolderId)
541                             }
542                         }
543
544                         // Update the list view.
545                         bookmarksCursorAdapter!!.changeCursor(bookmarksCursor)
546
547                         // Create a snackbar with the number of deleted bookmarks.
548                         bookmarksDeletedSnackbar = Snackbar.make(findViewById(R.id.bookmarks_databaseview_coordinatorlayout), getString(R.string.bookmarks_deleted, selectedBookmarksIdsLongArray.size),
549                             Snackbar.LENGTH_LONG)
550                             .setAction(R.string.undo) {}  // Undo will be handles by `onDismissed()` below.
551                             .addCallback(object : Snackbar.Callback() {
552                                 override fun onDismissed(snackbar: Snackbar, event: Int) {
553                                     if (event == DISMISS_EVENT_ACTION) {  // The user pushed the undo button.
554                                         // Update the bookmarks list view with the current contents of the bookmarks database, including the "deleted" bookmarks.
555                                         updateBookmarksListView()
556
557                                         // Re-select the previously selected bookmarks.
558                                         for (i in 0 until selectedBookmarksPositionsSparseBooleanArray.size())
559                                             bookmarksListView.setItemChecked(selectedBookmarksPositionsSparseBooleanArray.keyAt(i), true)
560                                     } else {  // The snackbar was dismissed without the undo button being pushed.
561                                         // Delete each selected bookmark.
562                                         for (databaseIdLong in selectedBookmarksIdsLongArray) {
563                                             // Convert the database long ID to an int.
564                                             val databaseIdInt = databaseIdLong.toInt()
565
566                                             // Delete the selected bookmark.
567                                             bookmarksDatabaseHelper.deleteBookmark(databaseIdInt)
568                                         }
569                                     }
570
571                                     // Reset the deleting bookmarks flag.
572                                     deletingBookmarks = false
573
574                                     // Enable the delete menu item.
575                                     deleteMenuItem.isEnabled = true
576
577                                     // Close the activity if back has been pressed.
578                                     if (closeActivityAfterDismissingSnackbar) {
579                                         // Reload the bookmarks list view when returning to the bookmarks activity.
580                                         BookmarksActivity.restartFromBookmarksDatabaseViewActivity = true
581
582                                         // Finish the activity.
583                                         finish()
584                                     }
585                                 }
586                             })
587
588                         // Show the snackbar.
589                         bookmarksDeletedSnackbar!!.show()
590                     }
591
592                     // Consume the click.
593                     return false
594                 }
595
596                 override fun onDestroyActionMode(mode: ActionMode) {
597                     // Do nothing.
598                 }
599             })
600         }
601     }
602
603     override fun onCreateOptionsMenu(menu: Menu): Boolean {
604         // Inflate the menu.
605         menuInflater.inflate(R.menu.bookmarks_databaseview_options_menu, menu)
606
607         // Get a handle for the sort menu item.
608         val sortMenuItem = menu.findItem(R.id.sort)
609
610         // Change the sort menu item icon if the listview is sorted by display order, which restores the state after a restart.
611         if (sortByDisplayOrder)
612             sortMenuItem.setIcon(R.drawable.sort_selected)
613
614         // Success.
615         return true
616     }
617
618     public override fun onDestroy() {
619         // Close the bookmarks cursor and database.
620         bookmarksCursor.close()
621         bookmarksDatabaseHelper.close()
622
623         // Run the default commands.
624         super.onDestroy()
625     }
626
627     override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
628         // Get the menu item ID.
629         val menuItemId = menuItem.itemId
630
631         // Run the command that corresponds to the selected menu item.
632         if (menuItemId == android.R.id.home) {  // Go Home.  The home arrow is identified as `android.R.id.home`, not just `R.id.home`.
633             // Prepare to finish the activity.
634             prepareFinish()
635         } else if (menuItemId == R.id.sort) {  // Toggle the sort mode.
636             // Update the sort by display order tracker.
637             sortByDisplayOrder = !sortByDisplayOrder
638
639             // Update the icon and display a snackbar.
640             if (sortByDisplayOrder) {  // Sort by display order.
641                 // Update the icon.
642                 menuItem.setIcon(R.drawable.sort_selected)
643
644                 // Display a snackbar indicating the current sort type.
645                 Snackbar.make(bookmarksListView, R.string.sorted_by_display_order, Snackbar.LENGTH_SHORT).show()
646             } else {  // Sort by database id.
647                 // Update the icon.
648                 menuItem.setIcon(R.drawable.sort)
649
650                 // Display a snackbar indicating the current sort type.
651                 Snackbar.make(bookmarksListView, R.string.sorted_by_database_id, Snackbar.LENGTH_SHORT).show()
652             }
653
654             // Update the list view.
655             updateBookmarksListView()
656         }
657
658         // Consume the event.
659         return true
660     }
661
662     public override fun onSaveInstanceState(savedInstanceState: Bundle) {
663         // Run the default commands.
664         super.onSaveInstanceState(savedInstanceState)
665
666         // Store the class variables in the bundle.
667         savedInstanceState.putInt(CURRENT_FOLDER_DATABASE_ID, currentFolderDatabaseId)
668         savedInstanceState.putBoolean(SORT_BY_DISPLAY_ORDER, sortByDisplayOrder)
669     }
670
671     override fun saveBookmark(dialogFragment: DialogFragment, selectedBookmarkDatabaseId: Int, favoriteIconBitmap: Bitmap) {
672         // Get the dialog from the dialog fragment.
673         val dialog = dialogFragment.dialog!!
674
675         // Get handles for the views from dialog fragment.
676         val currentIconRadioButton = dialog.findViewById<RadioButton>(R.id.current_icon_radiobutton)
677         val bookmarkNameEditText = dialog.findViewById<EditText>(R.id.bookmark_name_edittext)
678         val bookmarkUrlEditText = dialog.findViewById<EditText>(R.id.bookmark_url_edittext)
679         val folderSpinner = dialog.findViewById<Spinner>(R.id.bookmark_folder_spinner)
680         val displayOrderEditText = dialog.findViewById<EditText>(R.id.bookmark_display_order_edittext)
681
682         // Extract the bookmark information.
683         val bookmarkNameString = bookmarkNameEditText.text.toString()
684         val bookmarkUrlString = bookmarkUrlEditText.text.toString()
685         val folderDatabaseId = folderSpinner.selectedItemId.toInt()
686         val displayOrderInt = displayOrderEditText.text.toString().toInt()
687
688         // Get the parent folder ID.
689         val parentFolderId: Long = if (folderDatabaseId == HOME_FOLDER_DATABASE_ID)  // The home folder is selected.
690             HOME_FOLDER_ID
691         else  // Get the parent folder name from the database.
692             bookmarksDatabaseHelper.getFolderId(folderDatabaseId)
693
694         // Update the bookmark.
695         if (currentIconRadioButton.isChecked) {  // Update the bookmark without changing the favorite icon.
696             bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, parentFolderId, displayOrderInt)
697         } else {  // Update the bookmark using the `WebView` favorite icon.
698             // Create a favorite icon byte array output stream.
699             val newFavoriteIconByteArrayOutputStream = ByteArrayOutputStream()
700
701             // Convert the favorite icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
702             favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFavoriteIconByteArrayOutputStream)
703
704             // Convert the favorite icon byte array stream to a byte array.
705             val newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray()
706
707             //  Update the bookmark and the favorite icon.
708             bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, parentFolderId, displayOrderInt, newFavoriteIconByteArray)
709         }
710
711         // Update the list view.
712         updateBookmarksListView()
713     }
714
715     override fun saveBookmarkFolder(dialogFragment: DialogFragment, selectedFolderDatabaseId: Int, favoriteIconBitmap: Bitmap) {
716         // Get the dialog from the dialog fragment.
717         val dialog = dialogFragment.dialog!!
718
719         // Get handles for the views from dialog fragment.
720         val currentIconRadioButton = dialog.findViewById<RadioButton>(R.id.current_icon_radiobutton)
721         val defaultIconRadioButton = dialog.findViewById<RadioButton>(R.id.default_icon_radiobutton)
722         val defaultIconImageView = dialog.findViewById<ImageView>(R.id.default_icon_imageview)
723         val folderNameEditText = dialog.findViewById<EditText>(R.id.folder_name_edittext)
724         val parentFolderSpinner = dialog.findViewById<Spinner>(R.id.parent_folder_spinner)
725         val displayOrderEditText = dialog.findViewById<EditText>(R.id.display_order_edittext)
726
727         // Extract the folder information.
728         val newFolderNameString = folderNameEditText.text.toString()
729         val parentFolderDatabaseId = parentFolderSpinner.selectedItemId.toInt()
730         val displayOrderInt = displayOrderEditText.text.toString().toInt()
731
732         // Set the parent folder ID.
733         val parentFolderId: Long = if (parentFolderDatabaseId == HOME_FOLDER_DATABASE_ID)  // The home folder is selected.
734             HOME_FOLDER_ID
735         else  // Get the parent folder name from the database.
736             bookmarksDatabaseHelper.getFolderId(parentFolderDatabaseId)
737
738         // Update the folder.
739         if (currentIconRadioButton.isChecked) {  // Update the folder without changing the favorite icon.
740             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderNameString, parentFolderId, displayOrderInt)
741         } else {  // Update the folder and the icon.
742             // Get the new folder icon bitmap.
743             val folderIconBitmap = if (defaultIconRadioButton.isChecked) {
744                 // Get the default folder icon drawable.
745                 val folderIconDrawable = defaultIconImageView.drawable
746
747                 // Convert the folder icon drawable to a bitmap drawable.
748                 val folderIconBitmapDrawable = folderIconDrawable as BitmapDrawable
749
750                 // Convert the folder icon bitmap drawable to a bitmap.
751                 folderIconBitmapDrawable.bitmap
752             } else {  // Use the `WebView` favorite icon.
753                 // Get a copy of the favorite icon bitmap.
754                 favoriteIconBitmap
755             }
756
757             // Create a folder icon byte array output stream.
758             val newFolderIconByteArrayOutputStream = ByteArrayOutputStream()
759
760             // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
761             folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream)
762
763             // Convert the folder icon byte array stream to a byte array.
764             val newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray()
765
766             //  Update the folder and the icon.
767             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderNameString, parentFolderId, displayOrderInt, newFolderIconByteArray)
768         }
769
770         // Update the list view.
771         updateBookmarksListView()
772     }
773
774     private fun updateBookmarksListView() {
775         // Populate the bookmarks list view based on the spinner selection.
776         bookmarksCursor = when (currentFolderDatabaseId) {
777             // Get all the bookmarks.
778             ALL_FOLDERS_DATABASE_ID -> {
779                 if (sortByDisplayOrder)
780                     bookmarksDatabaseHelper.allBookmarksByDisplayOrder
781                 else
782                     bookmarksDatabaseHelper.allBookmarks
783             }
784
785             // Get the bookmarks in the home folder.
786             HOME_FOLDER_DATABASE_ID -> {
787                 if (sortByDisplayOrder)
788                     bookmarksDatabaseHelper.getBookmarksByDisplayOrder(HOME_FOLDER_ID)
789                 else
790                     bookmarksDatabaseHelper.getBookmarks(HOME_FOLDER_ID)
791             }
792
793             // Get the bookmarks in the specified folder.
794             else -> {
795                 // Get the current folder ID.
796                 val currentFolderId = bookmarksDatabaseHelper.getFolderId(currentFolderDatabaseId)
797
798                 if (sortByDisplayOrder)
799                     bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderId)
800                 else
801                     bookmarksDatabaseHelper.getBookmarks(currentFolderId)
802             }
803         }
804
805         // Update the cursor adapter if it isn't null, which happens when the activity is restarted.
806         if (bookmarksCursorAdapter != null) {
807             bookmarksCursorAdapter!!.changeCursor(bookmarksCursor)
808         }
809     }
810
811     private fun prepareFinish() {
812         // Check to see if a snackbar is currently displayed.  If so, it must be closed before existing so that a pending delete is completed before reloading the list view in the bookmarks activity.
813         if ((bookmarksDeletedSnackbar != null) && bookmarksDeletedSnackbar!!.isShown) { // Close the bookmarks deleted snackbar before going home.
814             // Set the close flag.
815             closeActivityAfterDismissingSnackbar = true
816
817             // Dismiss the snackbar.
818             bookmarksDeletedSnackbar!!.dismiss()
819         } else {  // Go home immediately.
820             // Update the current folder in the bookmarks activity.
821             if ((currentFolderDatabaseId == ALL_FOLDERS_DATABASE_ID) || (currentFolderDatabaseId == HOME_FOLDER_DATABASE_ID)) {  // All folders or the the home folder are currently displayed.
822                 // Load the home folder.
823                 BookmarksActivity.currentFolderId = HOME_FOLDER_ID
824             } else {  // A subfolder is currently displayed.
825                 // Load the current folder.
826                 BookmarksActivity.currentFolderId = bookmarksDatabaseHelper.getFolderId(currentFolderDatabaseId)
827             }
828
829             // Reload the bookmarks list view when returning to the bookmarks activity.
830             BookmarksActivity.restartFromBookmarksDatabaseViewActivity = true
831
832             // Exit the bookmarks database view activity.
833             finish()
834         }
835     }
836
837     private fun selectAllBookmarksInFolder(folderDatabaseId: Int) {
838         // Get the folder ID.
839         val folderId = bookmarksDatabaseHelper.getFolderId(folderDatabaseId)
840
841         // Get a cursor with the contents of the folder.
842         val folderCursor = bookmarksDatabaseHelper.getBookmarks(folderId)
843
844         // Get the column indexes.
845         val idColumnIndex = bookmarksCursor.getColumnIndexOrThrow(ID)
846
847         // Move to the beginning of the cursor.
848         folderCursor.moveToFirst()
849
850         while (folderCursor.position < folderCursor.count) {
851             // Get the bookmark database ID.
852             val bookmarkId = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(ID))
853
854             // Move the bookmarks cursor to the first position.
855             bookmarksCursor.moveToFirst()
856
857             // Initialize the bookmark position variable.
858             var bookmarkPosition = -1
859
860             // Get the position of this bookmark in the bookmarks cursor.
861             while ((bookmarkPosition < 0) && (bookmarksCursor.position < bookmarksCursor.count)) {
862                 // Check if the bookmark IDs match.
863                 if (bookmarkId == bookmarksCursor.getInt(idColumnIndex)) {
864                     // Get the bookmark position.
865                     bookmarkPosition = bookmarksCursor.position
866
867                     // If this bookmark is a folder, select all the bookmarks inside it.
868                     if (bookmarksDatabaseHelper.isFolder(bookmarkId))
869                         selectAllBookmarksInFolder(bookmarkId)
870
871                     // Select the bookmark.
872                     bookmarksListView.setItemChecked(bookmarkPosition, true)
873                 }
874
875                 // Increment the bookmarks cursor position.
876                 bookmarksCursor.moveToNext()
877             }
878
879             // Move to the next position.
880             folderCursor.moveToNext()
881         }
882     }
883 }