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