2b46600ee6dc319ee819c003f09b5402e9de1265
[PrivacyBrowser.git] / app / src / main / java / com / stoutner / privacybrowser / activities / BookmarksActivity.java
1 /*
2  * Copyright © 2016-2021 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
5  *
6  * Privacy Browser 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 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.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 package com.stoutner.privacybrowser.activities;
21
22 import android.annotation.SuppressLint;
23 import android.app.Dialog;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.SharedPreferences;
27 import android.content.res.Configuration;
28 import android.database.Cursor;
29 import android.graphics.Bitmap;
30 import android.graphics.BitmapFactory;
31 import android.graphics.Typeface;
32 import android.graphics.drawable.BitmapDrawable;
33 import android.graphics.drawable.Drawable;
34 import android.os.Bundle;
35 import android.preference.PreferenceManager;
36 import android.util.SparseBooleanArray;
37 import android.view.ActionMode;
38 import android.view.Menu;
39 import android.view.MenuItem;
40 import android.view.View;
41 import android.view.ViewGroup;
42 import android.view.WindowManager;
43 import android.widget.AbsListView;
44 import android.widget.CursorAdapter;
45 import android.widget.EditText;
46 import android.widget.ImageView;
47 import android.widget.ListView;
48 import android.widget.RadioButton;
49 import android.widget.TextView;
50
51 import androidx.annotation.NonNull;
52 import androidx.appcompat.app.ActionBar;
53 import androidx.appcompat.app.AppCompatActivity;
54 import androidx.appcompat.widget.Toolbar;
55 import androidx.fragment.app.DialogFragment;
56
57 import com.google.android.material.floatingactionbutton.FloatingActionButton;
58 import com.google.android.material.snackbar.Snackbar;
59
60 import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog;
61 import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog;
62 import com.stoutner.privacybrowser.dialogs.EditBookmarkDialog;
63 import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDialog;
64 import com.stoutner.privacybrowser.dialogs.MoveToFolderDialog;
65 import com.stoutner.privacybrowser.R;
66 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper;
67
68 import java.io.ByteArrayOutputStream;
69 import java.util.ArrayList;
70
71 public class BookmarksActivity extends AppCompatActivity implements CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener, EditBookmarkDialog.EditBookmarkListener,
72         EditBookmarkFolderDialog.EditBookmarkFolderListener, MoveToFolderDialog.MoveToFolderListener {
73
74     // `currentFolder` is public static so it can be accessed from `BookmarksDatabaseViewActivity`.
75     public static String currentFolder;
76
77     // `restartFromBookmarksDatabaseViewActivity` is public static so it can be accessed from `BookmarksDatabaseViewActivity`.  It is also used in `onRestart()`.
78     public static boolean restartFromBookmarksDatabaseViewActivity;
79
80
81     // Define the saved instance state constants.
82     private final String CHECKED_BOOKMARKS_ARRAY_LIST = "checked_bookmarks_array_list";
83
84     // Define the class menu items.
85     private MenuItem moveBookmarkUpMenuItem;
86     private MenuItem moveBookmarkDownMenuItem;
87
88     // `bookmarksDatabaseHelper` is used in `onCreate()`, `onOptionsItemSelected()`, `onBackPressed()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveBookmark()`, `onSaveBookmarkFolder()`,
89     // `onMoveToFolder()`, `deleteBookmarkFolderContents()`, `loadFolder()`, and `onDestroy()`.
90     private BookmarksDatabaseHelper bookmarksDatabaseHelper;
91
92     // `bookmarksListView` is used in `onCreate()`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveBookmark()`, `onSaveBookmarkFolder()`, `onMoveToFolder()`,
93     // `updateMoveIcons()`, `scrollBookmarks()`, and `loadFolder()`.
94     private ListView bookmarksListView;
95
96     // `bookmarksCursor` is used in `onCreate()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveBookmark()`, `onSaveBookmarkFolder()`, `onMoveToFolder()`, `deleteBookmarkFolderContents()`,
97     // `loadFolder()`, and `onDestroy()`.
98     private Cursor bookmarksCursor;
99
100     // `bookmarksCursorAdapter` is used in `onCreate(), `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveBookmark()`, `onSaveBookmarkFolder()`, `onMoveToFolder()`, and `onLoadFolder()`.
101     private CursorAdapter bookmarksCursorAdapter;
102
103     // `contextualActionMode` is used in `onCreate()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()` and `onMoveToFolder()`.
104     private ActionMode contextualActionMode;
105
106     // `appBar` is used in `onCreate()` and `loadFolder()`.
107     private ActionBar appBar;
108
109     // `oldFolderName` is used in `onCreate()` and `onSaveBookmarkFolder()`.
110     private String oldFolderNameString;
111
112     // `bookmarksDeletedSnackbar` is used in `onCreate()`, `onOptionsItemSelected()`, and `onBackPressed()`.
113     private Snackbar bookmarksDeletedSnackbar;
114
115     // `closeActivityAfterDismissingSnackbar` is used in `onCreate()`, `onOptionsItemSelected()`, and `onBackPressed()`.
116     private boolean closeActivityAfterDismissingSnackbar;
117
118     // The favorite icon byte array is populated in `onCreate()` and used in `onOptionsItemSelected()`.
119     private byte[] favoriteIconByteArray;
120
121     @Override
122     protected void onCreate(Bundle savedInstanceState) {
123         // Get a handle for the shared preferences.
124         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
125
126         // Get the screenshot preference.
127         boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
128
129         // Disable screenshots if not allowed.
130         if (!allowScreenshots) {
131             getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
132         }
133
134         // Set the theme.
135         setTheme(R.style.PrivacyBrowser);
136
137         // Run the default commands.
138         super.onCreate(savedInstanceState);
139
140         // Get the intent that launched the activity.
141         Intent launchingIntent = getIntent();
142
143         // Store the current URL and title.
144         String currentUrl = launchingIntent.getStringExtra("current_url");
145         String currentTitle = launchingIntent.getStringExtra("current_title");
146
147         // Set the current folder variable.
148         if (launchingIntent.getStringExtra("current_folder") != null) {  // Set the current folder from the intent.
149             currentFolder = launchingIntent.getStringExtra("current_folder");
150         } else {  // Set the current folder to be `""`, which is the home folder.
151             currentFolder = "";
152         }
153
154         // Get the favorite icon byte array.
155         favoriteIconByteArray = launchingIntent.getByteArrayExtra("favorite_icon_byte_array");
156
157         // Remove the incorrect lint warning that the favorite icon byte array might be null.
158         assert favoriteIconByteArray != null;
159
160         // Convert the favorite icon byte array to a bitmap.
161         Bitmap favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.length);
162
163         // Set the content view.
164         setContentView(R.layout.bookmarks_coordinatorlayout);
165
166         // The AndroidX toolbar must be used until the minimum API is >= 21.
167         final Toolbar toolbar = findViewById(R.id.bookmarks_toolbar);
168         setSupportActionBar(toolbar);
169
170         // Get handles for the views.
171         appBar = getSupportActionBar();
172         bookmarksListView = findViewById(R.id.bookmarks_listview);
173
174         // Remove the incorrect lint warning that `appBar` might be null.
175         assert appBar != null;
176
177         // Display the home arrow on the app bar.
178         appBar.setDisplayHomeAsUpEnabled(true);
179
180         // Initialize the database helper.  `this` specifies the context.  The two `nulls` do not specify the database name or a `CursorFactory`.
181         // The `0` specifies a database version, but that is ignored and set instead using a constant in `BookmarksDatabaseHelper`.
182         bookmarksDatabaseHelper = new BookmarksDatabaseHelper(this, null, null, 0);
183
184         // Load the home folder.
185         loadFolder();
186
187         // Set a listener so that tapping a list item loads the URL or folder.
188         bookmarksListView.setOnItemClickListener((parent, view, position, id) -> {
189             // Convert the id from long to int to match the format of the bookmarks database.
190             int databaseId = (int) id;
191
192             // Get the bookmark cursor for this ID.
193             Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseId);
194
195             // Move the cursor to the first entry.
196             bookmarkCursor.moveToFirst();
197
198             // Act upon the bookmark according to the type.
199             if (bookmarkCursor.getInt(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {  // The selected bookmark is a folder.
200                 // Update the current folder.
201                 currentFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
202
203                 // Load the new folder.
204                 loadFolder();
205             } else {  // The selected bookmark is not a folder.
206                 // Instantiate the edit bookmark dialog.
207                 DialogFragment editBookmarkDialog = EditBookmarkDialog.bookmarkDatabaseId(databaseId, favoriteIconBitmap);
208
209                 // Make it so.
210                 editBookmarkDialog.show(getSupportFragmentManager(), getResources().getString(R.string.edit_bookmark));
211             }
212
213             // Close the cursor.
214             bookmarkCursor.close();
215         });
216
217         // Handle long presses on the list view.
218         bookmarksListView.setMultiChoiceModeListener(new AbsListView.MultiChoiceModeListener() {
219             // Instantiate the common variables.
220             MenuItem editBookmarkMenuItem;
221             MenuItem deleteBookmarksMenuItem;
222             MenuItem selectAllBookmarksMenuItem;
223             boolean deletingBookmarks;
224
225             @Override
226             public boolean onCreateActionMode(ActionMode mode, Menu menu) {
227                 // Inflate the menu for the contextual app bar.
228                 getMenuInflater().inflate(R.menu.bookmarks_context_menu, menu);
229
230                 // Set the title.
231                 if (currentFolder.isEmpty()) {  // Use `R.string.bookmarks` if in the home folder.
232                     mode.setTitle(R.string.bookmarks);
233                 } else {  // Use the current folder name as the title.
234                     mode.setTitle(currentFolder);
235                 }
236
237                 // Get handles for menu items that need to be selectively disabled.
238                 moveBookmarkUpMenuItem = menu.findItem(R.id.move_bookmark_up);
239                 moveBookmarkDownMenuItem = menu.findItem(R.id.move_bookmark_down);
240                 editBookmarkMenuItem = menu.findItem(R.id.edit_bookmark);
241                 deleteBookmarksMenuItem = menu.findItem(R.id.delete_bookmark);
242                 selectAllBookmarksMenuItem = menu.findItem(R.id.context_menu_select_all_bookmarks);
243
244                 // Disable the delete bookmarks menu item if a delete is pending.
245                 deleteBookmarksMenuItem.setEnabled(!deletingBookmarks);
246
247                 // Store a handle for the contextual action mode so it can be closed programatically.
248                 contextualActionMode = mode;
249
250                 // Make it so.
251                 return true;
252             }
253
254             @Override
255             public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
256                 // Get a handle for the move to folder menu item.
257                 MenuItem moveToFolderMenuItem = menu.findItem(R.id.move_to_folder);
258
259                 // Get a Cursor with all of the folders.
260                 Cursor folderCursor = bookmarksDatabaseHelper.getAllFolders();
261
262                 // Enable the move to folder menu item if at least one folder exists.
263                 moveToFolderMenuItem.setVisible(folderCursor.getCount() > 0);
264
265                 // Make it so.
266                 return true;
267             }
268
269             @Override
270             public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
271                 // Get the number of selected bookmarks.
272                 int numberOfSelectedBookmarks = bookmarksListView.getCheckedItemCount();
273
274                 // Only process commands if at least one bookmark is selected.  Otherwise, a context menu with 0 selected bookmarks is briefly displayed.
275                 if (numberOfSelectedBookmarks > 0) {
276                     // Adjust the ActionMode and the menu according to the number of selected bookmarks.
277                     if (numberOfSelectedBookmarks == 1) {  // One bookmark is selected.
278                         // List the number of selected bookmarks in the subtitle.
279                         mode.setSubtitle(getString(R.string.selected) + "  1");
280
281                         // Show the `Move Up`, `Move Down`, and  `Edit` options.
282                         moveBookmarkUpMenuItem.setVisible(true);
283                         moveBookmarkDownMenuItem.setVisible(true);
284                         editBookmarkMenuItem.setVisible(true);
285
286                         // Update the enabled status of the move icons.
287                         updateMoveIcons();
288                     } else {  // More than one bookmark is selected.
289                         // List the number of selected bookmarks in the subtitle.
290                         mode.setSubtitle(getString(R.string.selected) + "  " + numberOfSelectedBookmarks);
291
292                         // Hide non-applicable `MenuItems`.
293                         moveBookmarkUpMenuItem.setVisible(false);
294                         moveBookmarkDownMenuItem.setVisible(false);
295                         editBookmarkMenuItem.setVisible(false);
296                     }
297
298                     // Show the select all menu item if all the bookmarks are not selected.
299                     selectAllBookmarksMenuItem.setVisible(bookmarksListView.getCheckedItemCount() != bookmarksListView.getCount());
300                 }
301             }
302
303             @Override
304             public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
305                 // Declare the common variables.
306                 int selectedBookmarkNewPosition;
307                 final SparseBooleanArray selectedBookmarksPositionsSparseBooleanArray;
308
309                 // Initialize the selected bookmark position.
310                 int selectedBookmarkPosition = 0;
311
312                 // Get the menu item ID.
313                 int menuItemId = menuItem.getItemId();
314
315                 // Run the commands according to the selected action item.
316                 if (menuItemId == R.id.move_bookmark_up) {  // Move the bookmark up.
317                     // Get the array of checked bookmark positions.
318                     selectedBookmarksPositionsSparseBooleanArray = bookmarksListView.getCheckedItemPositions();
319
320                     // Get the position of the bookmark that is selected.  If other bookmarks have previously been selected they will be included in the sparse boolean array with a value of `false`.
321                     for (int i = 0; i < selectedBookmarksPositionsSparseBooleanArray.size(); i++) {
322                         // Check to see if the value for the bookmark is true, meaning it is currently selected.
323                         if (selectedBookmarksPositionsSparseBooleanArray.valueAt(i)) {
324                             // Only one bookmark should have a value of `true` when move bookmark up is enabled.
325                             selectedBookmarkPosition = selectedBookmarksPositionsSparseBooleanArray.keyAt(i);
326                         }
327                     }
328
329                     // Calculate the new position of the selected bookmark.
330                     selectedBookmarkNewPosition = selectedBookmarkPosition - 1;
331
332                     // Iterate through the bookmarks.
333                     for (int i = 0; i < bookmarksListView.getCount(); i++) {
334                         // Get the database ID for the current bookmark.
335                         int currentBookmarkDatabaseId = (int) bookmarksListView.getItemIdAtPosition(i);
336
337                         // Update the display order for the current bookmark.
338                         if (i == selectedBookmarkPosition) {  // The current bookmark is the selected bookmark.
339                             // Move the current bookmark up one.
340                             bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i - 1);
341                         } else if ((i + 1) == selectedBookmarkPosition) {  // The current bookmark is immediately above the selected bookmark.
342                             // Move the current bookmark down one.
343                             bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i + 1);
344                         } else {  // The current bookmark is not changing positions.
345                             // Move `bookmarksCursor` to the current bookmark position.
346                             bookmarksCursor.moveToPosition(i);
347
348                             // Update the display order only if it is not correct in the database.
349                             if (bookmarksCursor.getInt(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.DISPLAY_ORDER)) != i) {
350                                 bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i);
351                             }
352                         }
353                     }
354
355                     // Update the bookmarks cursor with the current contents of the bookmarks database.
356                     bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
357
358                     // Update the list view.
359                     bookmarksCursorAdapter.changeCursor(bookmarksCursor);
360
361                     // Scroll with the bookmark.
362                     scrollBookmarks(selectedBookmarkNewPosition);
363
364                     // Update the enabled status of the move icons.
365                     updateMoveIcons();
366                 } else if (menuItemId == R.id.move_bookmark_down) {  // Move the bookmark down.
367                     // Get the array of checked bookmark positions.
368                     selectedBookmarksPositionsSparseBooleanArray = bookmarksListView.getCheckedItemPositions();
369
370                     // Get the position of the bookmark that is selected.  If other bookmarks have previously been selected they will be included in the sparse boolean array with a value of `false`.
371                     for (int i = 0; i < selectedBookmarksPositionsSparseBooleanArray.size(); i++) {
372                         // Check to see if the value for the bookmark is true, meaning it is currently selected.
373                         if (selectedBookmarksPositionsSparseBooleanArray.valueAt(i)) {
374                             // Only one bookmark should have a value of `true` when move bookmark down is enabled.
375                             selectedBookmarkPosition = selectedBookmarksPositionsSparseBooleanArray.keyAt(i);
376                         }
377                     }
378
379                     // Calculate the new position of the selected bookmark.
380                     selectedBookmarkNewPosition = selectedBookmarkPosition + 1;
381
382                     // Iterate through the bookmarks.
383                     for (int i = 0; i < bookmarksListView.getCount(); i++) {
384                         // Get the database ID for the current bookmark.
385                         int currentBookmarkDatabaseId = (int) bookmarksListView.getItemIdAtPosition(i);
386
387                         // Update the display order for the current bookmark.
388                         if (i == selectedBookmarkPosition) {  // The current bookmark is the selected bookmark.
389                             // Move the current bookmark down one.
390                             bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i + 1);
391                         } else if ((i - 1) == selectedBookmarkPosition) {  // The current bookmark is immediately below the selected bookmark.
392                             // Move the bookmark below the selected bookmark up one.
393                             bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i - 1);
394                         } else {  // The current bookmark is not changing positions.
395                             // Move `bookmarksCursor` to the current bookmark position.
396                             bookmarksCursor.moveToPosition(i);
397
398                             // Update the display order only if it is not correct in the database.
399                             if (bookmarksCursor.getInt(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.DISPLAY_ORDER)) != i) {
400                                 bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i);
401                             }
402                         }
403                     }
404
405                     // Update the bookmarks cursor with the current contents of the bookmarks database.
406                     bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
407
408                     // Update the list view.
409                     bookmarksCursorAdapter.changeCursor(bookmarksCursor);
410
411                     // Scroll with the bookmark.
412                     scrollBookmarks(selectedBookmarkNewPosition);
413
414                     // Update the enabled status of the move icons.
415                     updateMoveIcons();
416                 } else if (menuItemId == R.id.move_to_folder) {  // Move to folder.
417                     // Instantiate the move to folder alert dialog.
418                     DialogFragment moveToFolderDialog = MoveToFolderDialog.moveBookmarks(currentFolder, bookmarksListView.getCheckedItemIds());
419
420                     // Show the move to folder alert dialog.
421                     moveToFolderDialog.show(getSupportFragmentManager(), getResources().getString(R.string.move_to_folder));
422                 } else if (menuItemId == R.id.edit_bookmark) {
423                     // Get the array of checked bookmark positions.
424                     selectedBookmarksPositionsSparseBooleanArray = bookmarksListView.getCheckedItemPositions();
425
426                     // Get the position of the bookmark that is selected.  If other bookmarks have previously been selected they will be included in the sparse boolean array with a value of `false`.
427                     for (int i = 0; i < selectedBookmarksPositionsSparseBooleanArray.size(); i++) {
428                         // Check to see if the value for the bookmark is true, meaning it is currently selected.
429                         if (selectedBookmarksPositionsSparseBooleanArray.valueAt(i)) {
430                             // Only one bookmark should have a value of `true` when move edit bookmark is enabled.
431                             selectedBookmarkPosition = selectedBookmarksPositionsSparseBooleanArray.keyAt(i);
432                         }
433                     }
434
435                     // Move the cursor to the selected position.
436                     bookmarksCursor.moveToPosition(selectedBookmarkPosition);
437
438                     // Find out if this bookmark is a folder.
439                     boolean isFolder = (bookmarksCursor.getInt(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1);
440
441                     // Get the selected bookmark database ID.
442                     int databaseId = bookmarksCursor.getInt(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper._ID));
443
444                     // Show the edit bookmark or edit bookmark folder dialog.
445                     if (isFolder) {
446                         // Save the current folder name, which is used in `onSaveBookmarkFolder()`.
447                         oldFolderNameString = bookmarksCursor.getString(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
448
449                         // Instantiate the edit bookmark folder dialog.
450                         DialogFragment editFolderDialog = EditBookmarkFolderDialog.folderDatabaseId(databaseId, favoriteIconBitmap);
451
452                         // Make it so.
453                         editFolderDialog.show(getSupportFragmentManager(), getResources().getString(R.string.edit_folder));
454                     } else {
455                         // Instantiate the edit bookmark dialog.
456                         DialogFragment editBookmarkDialog = EditBookmarkDialog.bookmarkDatabaseId(databaseId, favoriteIconBitmap);
457
458                         // Make it so.
459                         editBookmarkDialog.show(getSupportFragmentManager(), getResources().getString(R.string.edit_bookmark));
460                     }
461                 } else if (menuItemId == R.id.delete_bookmark) {  // Delete bookmark.
462                     // Set the deleting bookmarks flag, which prevents the delete menu item from being enabled until the current process finishes.
463                     deletingBookmarks = true;
464
465                     // Get an array of the selected row IDs.
466                     final long[] selectedBookmarksIdsLongArray = bookmarksListView.getCheckedItemIds();
467
468                     // Initialize a variable to count the number of bookmarks to delete.
469                     int numberOfBookmarksToDelete = 0;
470
471                     // Count the number of bookmarks.
472                     for (long databaseIdLong : selectedBookmarksIdsLongArray) {
473                         // Convert the database ID long to an int.
474                         int databaseIdInt = (int) databaseIdLong;
475
476                         // Count the contents of the folder if the selected bookmark is a folder.
477                         if (bookmarksDatabaseHelper.isFolder(databaseIdInt)) {
478                             // Add the bookmarks from the folder to the running total.
479                             numberOfBookmarksToDelete = numberOfBookmarksToDelete + countBookmarkFolderContents(databaseIdInt);
480                         }
481
482                         // Increment the count of the number of bookmarks to delete.
483                         numberOfBookmarksToDelete++;
484                     }
485
486                     // 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.
487                     selectedBookmarksPositionsSparseBooleanArray = bookmarksListView.getCheckedItemPositions().clone();
488
489                     // Update the bookmarks cursor with the current contents of the bookmarks database except for the specified database IDs.
490                     bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrderExcept(selectedBookmarksIdsLongArray, currentFolder);
491
492                     // Update the list view.
493                     bookmarksCursorAdapter.changeCursor(bookmarksCursor);
494
495                     // Create a Snackbar with the number of deleted bookmarks.
496                     bookmarksDeletedSnackbar = Snackbar.make(findViewById(R.id.bookmarks_coordinatorlayout), getString(R.string.bookmarks_deleted) + "  " + numberOfBookmarksToDelete,
497                             Snackbar.LENGTH_LONG)
498                             .setAction(R.string.undo, view -> {
499                                 // Do nothing because everything will be handled by `onDismissed()` below.
500                             })
501                             .addCallback(new Snackbar.Callback() {
502                                 @SuppressLint("SwitchIntDef")  // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
503                                 @Override
504                                 public void onDismissed(Snackbar snackbar, int event) {
505                                     if (event == Snackbar.Callback.DISMISS_EVENT_ACTION) {  // The user pushed the undo button.
506                                         // Update the bookmarks cursor with the current contents of the bookmarks database, including the "deleted" bookmarks.
507                                         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
508
509                                         // Update the list view.
510                                         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
511
512                                         // Re-select the previously selected bookmarks.
513                                         for (int i = 0; i < selectedBookmarksPositionsSparseBooleanArray.size(); i++) {
514                                             bookmarksListView.setItemChecked(selectedBookmarksPositionsSparseBooleanArray.keyAt(i), true);
515                                         }
516                                     } else {  // The snackbar was dismissed without the undo button being pushed.
517                                         // Delete each selected bookmark.
518                                         for (long databaseIdLong : selectedBookmarksIdsLongArray) {
519                                             // Convert `databaseIdLong` to an int.
520                                             int databaseIdInt = (int) databaseIdLong;
521
522                                             // Delete the contents of the folder if the selected bookmark is a folder.
523                                             if (bookmarksDatabaseHelper.isFolder(databaseIdInt)) {
524                                                 deleteBookmarkFolderContents(databaseIdInt);
525                                             }
526
527                                             // Delete the selected bookmark.
528                                             bookmarksDatabaseHelper.deleteBookmark(databaseIdInt);
529                                         }
530
531                                         // Update the display order.
532                                         for (int i = 0; i < bookmarksListView.getCount(); i++) {
533                                             // Get the database ID for the current bookmark.
534                                             int currentBookmarkDatabaseId = (int) bookmarksListView.getItemIdAtPosition(i);
535
536                                             // Move `bookmarksCursor` to the current bookmark position.
537                                             bookmarksCursor.moveToPosition(i);
538
539                                             // Update the display order only if it is not correct in the database.
540                                             if (bookmarksCursor.getInt(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.DISPLAY_ORDER)) != i) {
541                                                 bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i);
542                                             }
543                                         }
544                                     }
545
546                                     // Reset the deleting bookmarks flag.
547                                     deletingBookmarks = false;
548
549                                     // Enable the delete bookmarks menu item.
550                                     deleteBookmarksMenuItem.setEnabled(true);
551
552                                     // Close the activity if back has been pressed.
553                                     if (closeActivityAfterDismissingSnackbar) {
554                                         onBackPressed();
555                                     }
556                                 }
557                             });
558
559                     //Show the Snackbar.
560                     bookmarksDeletedSnackbar.show();
561                 } else if (menuItemId == R.id.context_menu_select_all_bookmarks) {  // Select all.
562                     // Get the total number of bookmarks.
563                     int numberOfBookmarks = bookmarksListView.getCount();
564
565                     // Select them all.
566                     for (int i = 0; i < numberOfBookmarks; i++) {
567                         bookmarksListView.setItemChecked(i, true);
568                     }
569                 }
570
571                 // Consume the click.
572                 return true;
573             }
574
575             @Override
576             public void onDestroyActionMode(ActionMode mode) {
577                 // Do nothing.
578             }
579         });
580
581         // Get handles for the floating action buttons.
582         FloatingActionButton createBookmarkFolderFab = findViewById(R.id.create_bookmark_folder_fab);
583         FloatingActionButton createBookmarkFab = findViewById(R.id.create_bookmark_fab);
584
585         // Set the create new bookmark folder FAB to display the `AlertDialog`.
586         createBookmarkFolderFab.setOnClickListener(v -> {
587             // Create a create bookmark folder dialog.
588             DialogFragment createBookmarkFolderDialog = CreateBookmarkFolderDialog.createBookmarkFolder(favoriteIconBitmap);
589
590             // Show the create bookmark folder dialog.
591             createBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.create_folder));
592         });
593
594         // Set the create new bookmark FAB to display the alert dialog.
595         createBookmarkFab.setOnClickListener(view -> {
596             // Remove the incorrect lint warning below.
597             assert currentUrl != null;
598             assert currentTitle != null;
599
600             // Instantiate the create bookmark dialog.
601             DialogFragment createBookmarkDialog = CreateBookmarkDialog.createBookmark(currentUrl, currentTitle, favoriteIconBitmap);
602
603             // Display the create bookmark dialog.
604             createBookmarkDialog.show(getSupportFragmentManager(), getResources().getString(R.string.create_bookmark));
605         });
606
607         // Restore the state if the app has been restarted.
608         if (savedInstanceState != null) {
609             // Update the bookmarks list view after it has loaded.
610             bookmarksListView.post(() -> {
611                 // Get the checked bookmarks array list.
612                 ArrayList<Integer> checkedBookmarksArrayList = savedInstanceState.getIntegerArrayList(CHECKED_BOOKMARKS_ARRAY_LIST);
613
614                 // Check each previously checked bookmark in the list view.  When the minimum API >= 24 a `forEach()` command can be used instead.
615                 if (checkedBookmarksArrayList != null) {
616                     for (int i = 0; i < checkedBookmarksArrayList.size(); i++) {
617                         bookmarksListView.setItemChecked(checkedBookmarksArrayList.get(i), true);
618                     }
619                 }
620             });
621         }
622     }
623
624     @Override
625     public void onRestart() {
626         // Run the default commands.
627         super.onRestart();
628
629         // Update the list view if returning from the bookmarks database view activity.
630         if (restartFromBookmarksDatabaseViewActivity) {
631             // Load the current folder in the list view.
632             loadFolder();
633
634             // Reset `restartFromBookmarksDatabaseViewActivity`.
635             restartFromBookmarksDatabaseViewActivity = false;
636         }
637     }
638
639     @Override
640     public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
641         // Run the default commands.
642         super.onSaveInstanceState(savedInstanceState);
643
644         // Get the array of the checked items.
645         SparseBooleanArray checkedBookmarksSparseBooleanArray = bookmarksListView.getCheckedItemPositions();
646
647         // Create a checked items array list.
648         ArrayList<Integer> checkedBookmarksArrayList = new ArrayList<>();
649
650         // Add each checked bookmark position to the array list.
651         for (int i = 0; i < checkedBookmarksSparseBooleanArray.size(); i++) {
652             // Check to see if the bookmark is currently checked.  Bookmarks that have previously been checked but currently aren't will be populated in the sparse boolean array, but will return false.
653             if (checkedBookmarksSparseBooleanArray.valueAt(i)) {
654                 // Add the bookmark position to the checked bookmarks array list.
655                 checkedBookmarksArrayList.add(checkedBookmarksSparseBooleanArray.keyAt(i));
656             }
657         }
658
659         // Store the checked items array list in the saved instance state.
660         savedInstanceState.putIntegerArrayList(CHECKED_BOOKMARKS_ARRAY_LIST, checkedBookmarksArrayList);
661     }
662
663     @Override
664     public boolean onCreateOptionsMenu(Menu menu) {
665         // Inflate the menu.
666         getMenuInflater().inflate(R.menu.bookmarks_options_menu, menu);
667
668         // Success.
669         return true;
670     }
671
672     @Override
673     public boolean onOptionsItemSelected(MenuItem menuItem) {
674         // Get a handle for the menu item ID.
675         int menuItemId = menuItem.getItemId();
676
677         // Run the command according to the selected option.
678         if (menuItemId == android.R.id.home) {  // Home.  The home arrow is identified as `android.R.id.home`, not just `R.id.home`.
679             if (currentFolder.isEmpty()) {  // Currently in the home folder.
680                 // Run the back commands.
681                 onBackPressed();
682             } else {  // Currently in a subfolder.
683                 // Place the former parent folder in `currentFolder`.
684                 currentFolder = bookmarksDatabaseHelper.getParentFolderName(currentFolder);
685
686                 // Load the new folder.
687                 loadFolder();
688             }
689         } else if (menuItemId == R.id.options_menu_select_all_bookmarks) {  // Select all.
690             // Get the total number of bookmarks.
691             int numberOfBookmarks = bookmarksListView.getCount();
692
693             // Select them all.
694             for (int i = 0; i < numberOfBookmarks; i++) {
695                 bookmarksListView.setItemChecked(i, true);
696             }
697         } else if (menuItemId == R.id.bookmarks_database_view) {
698             // Create an intent to launch the bookmarks database view activity.
699             Intent bookmarksDatabaseViewIntent = new Intent(this, BookmarksDatabaseViewActivity.class);
700
701             // Include the favorite icon byte array to the intent.
702             bookmarksDatabaseViewIntent.putExtra("favorite_icon_byte_array", favoriteIconByteArray);
703
704             // Make it so.
705             startActivity(bookmarksDatabaseViewIntent);
706         }
707         return true;
708     }
709
710     @Override
711     public void onBackPressed() {
712         // Check to see if a snackbar is currently displayed.  If so, it must be closed before exiting so that a pending delete is completed before reloading the list view in the bookmarks drawer.
713         if ((bookmarksDeletedSnackbar != null) && bookmarksDeletedSnackbar.isShown()) {  // Close the bookmarks deleted snackbar before going home.
714             // Set the close flag.
715             closeActivityAfterDismissingSnackbar = true;
716
717             // Dismiss the snackbar.
718             bookmarksDeletedSnackbar.dismiss();
719         } else {  // Go home immediately.
720             // Update the bookmarks folder for the bookmarks drawer in the main WebView activity.
721             MainWebViewActivity.currentBookmarksFolder = currentFolder;
722
723             // Close the bookmarks drawer and reload the bookmarks ListView when returning to the main WebView activity.
724             MainWebViewActivity.restartFromBookmarksActivity = true;
725
726             // Exit the bookmarks activity.
727             super.onBackPressed();
728         }
729     }
730
731     @Override
732     public void onCreateBookmark(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) {
733         // Get the alert dialog from the fragment.
734         Dialog dialog = dialogFragment.getDialog();
735
736         // Remove the incorrect lint warning below that the dialog might be null.
737         assert dialog != null;
738
739         // Get the views from the dialog fragment.
740         EditText createBookmarkNameEditText = dialog.findViewById(R.id.create_bookmark_name_edittext);
741         EditText createBookmarkUrlEditText = dialog.findViewById(R.id.create_bookmark_url_edittext);
742
743         // Extract the strings from the edit texts.
744         String bookmarkNameString = createBookmarkNameEditText.getText().toString();
745         String bookmarkUrlString = createBookmarkUrlEditText.getText().toString();
746
747         // Create a favorite icon byte array output stream.
748         ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
749
750         // Convert the favorite icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
751         favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
752
753         // Convert the favorite icon byte array stream to a byte array.
754         byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
755
756         // Display the new bookmark below the current items in the (0 indexed) list.
757         int newBookmarkDisplayOrder = bookmarksListView.getCount();
758
759         // Create the bookmark.
760         bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentFolder, newBookmarkDisplayOrder, favoriteIconByteArray);
761
762         // Update the bookmarks cursor with the current contents of this folder.
763         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
764
765         // Update the `ListView`.
766         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
767
768         // Scroll to the new bookmark.
769         bookmarksListView.setSelection(newBookmarkDisplayOrder);
770     }
771
772     @Override
773     public void onCreateBookmarkFolder(DialogFragment dialogFragment, @NonNull Bitmap favoriteIconBitmap) {
774         // Get the dialog from the dialog fragment.
775         Dialog dialog = dialogFragment.getDialog();
776
777         // Remove the incorrect lint warning below that the dialog might be null.
778         assert dialog != null;
779
780         // Get handles for the views in the dialog fragment.
781         EditText folderNameEditText = dialog.findViewById(R.id.folder_name_edittext);
782         RadioButton defaultIconRadioButton = dialog.findViewById(R.id.default_icon_radiobutton);
783         ImageView defaultIconImageView = dialog.findViewById(R.id.default_icon_imageview);
784
785         // Get new folder name string.
786         String folderNameString = folderNameEditText.getText().toString();
787
788         // Create a folder icon bitmap.
789         Bitmap folderIconBitmap;
790
791         // Set the folder icon bitmap according to the dialog.
792         if (defaultIconRadioButton.isChecked()) {  // Use the default folder icon.
793             // Get the default folder icon drawable.
794             Drawable folderIconDrawable = defaultIconImageView.getDrawable();
795
796             // Convert the folder icon drawable to a bitmap drawable.
797             BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
798
799             // Convert the folder icon bitmap drawable to a bitmap.
800             folderIconBitmap = folderIconBitmapDrawable.getBitmap();
801         } else {  // Use the WebView favorite icon.
802             // Copy the favorite icon bitmap to the folder icon bitmap.
803             folderIconBitmap = favoriteIconBitmap;
804         }
805
806         // Create a folder icon byte array output stream.
807         ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream();
808
809         // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
810         folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream);
811
812         // Convert the folder icon byte array stream to a byte array.
813         byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray();
814
815         // Move all the bookmarks down one in the display order.
816         for (int i = 0; i < bookmarksListView.getCount(); i++) {
817             int databaseId = (int) bookmarksListView.getItemIdAtPosition(i);
818             bookmarksDatabaseHelper.updateDisplayOrder(databaseId, i + 1);
819         }
820
821         // Create the folder, which will be placed at the top of the `ListView`.
822         bookmarksDatabaseHelper.createFolder(folderNameString, currentFolder, folderIconByteArray);
823
824         // Update the bookmarks cursor with the current contents of this folder.
825         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
826
827         // Update the list view.
828         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
829
830         // Scroll to the new folder.
831         bookmarksListView.setSelection(0);
832     }
833
834     @Override
835     public void onSaveBookmark(DialogFragment dialogFragment, int selectedBookmarkDatabaseId, @NonNull Bitmap favoriteIconBitmap) {
836         // Get the dialog from the dialog fragment.
837         Dialog dialog = dialogFragment.getDialog();
838
839         // Remove the incorrect lint warning below that the dialog might be null.
840         assert dialog != null;
841
842         // Get handles for the views from the dialog fragment.
843         EditText bookmarkNameEditText = dialog.findViewById(R.id.bookmark_name_edittext);
844         EditText bookmarkUrlEditText = dialog.findViewById(R.id.bookmark_url_edittext);
845         RadioButton currentIconRadioButton = dialog.findViewById(R.id.current_icon_radiobutton);
846
847         // Store the bookmark strings.
848         String bookmarkNameString = bookmarkNameEditText.getText().toString();
849         String bookmarkUrlString = bookmarkUrlEditText.getText().toString();
850
851         // Update the bookmark.
852         if (currentIconRadioButton.isChecked()) {  // Update the bookmark without changing the favorite icon.
853             bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString);
854         } else {  // Update the bookmark using the WebView favorite icon.
855             // Create a favorite icon byte array output stream.
856             ByteArrayOutputStream newFavoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
857
858             // Convert the favorite icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
859             favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFavoriteIconByteArrayOutputStream);
860
861             // Convert the favorite icon byte array stream to a byte array.
862             byte[] newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray();
863
864             //  Update the bookmark and the favorite icon.
865             bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, newFavoriteIconByteArray);
866         }
867
868         // Check to see if the contextual action mode has been created.
869         if (contextualActionMode != null) {
870             // Close the contextual action mode if it is open.
871             contextualActionMode.finish();
872         }
873
874         // Update the bookmarks cursor with the contents of the current folder.
875         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
876
877         // Update the `ListView`.
878         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
879     }
880
881     @Override
882     public void onSaveBookmarkFolder(DialogFragment dialogFragment, int selectedFolderDatabaseId, @NonNull Bitmap favoriteIconBitmap) {
883         // Get the dialog from the dialog fragment.
884         Dialog dialog = dialogFragment.getDialog();
885
886         // Remove the incorrect lint warning below that the dialog might be null.
887         assert dialog != null;
888
889         // Get handles for the views from the dialog fragment.
890         RadioButton currentFolderIconRadioButton = dialog.findViewById(R.id.current_icon_radiobutton);
891         RadioButton defaultFolderIconRadioButton = dialog.findViewById(R.id.default_icon_radiobutton);
892         ImageView defaultFolderIconImageView = dialog.findViewById(R.id.default_icon_imageview);
893         EditText editFolderNameEditText = dialog.findViewById(R.id.folder_name_edittext);
894
895         // Get the new folder name.
896         String newFolderNameString = editFolderNameEditText.getText().toString();
897
898         // Check if the favorite icon has changed.
899         if (currentFolderIconRadioButton.isChecked()) {  // Only the name has changed.
900             // Update the name in the database.
901             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString);
902         } else if (!currentFolderIconRadioButton.isChecked() && newFolderNameString.equals(oldFolderNameString)) {  // Only the icon has changed.
903             // Create the new folder icon Bitmap.
904             Bitmap folderIconBitmap;
905
906             // Populate the new folder icon bitmap.
907             if (defaultFolderIconRadioButton.isChecked()) {
908                 // Get the default folder icon drawable.
909                 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
910
911                 // Convert the folder icon drawable to a bitmap drawable.
912                 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
913
914                 // Convert the folder icon bitmap drawable to a bitmap.
915                 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
916             } else {  // Use the WebView favorite icon.
917                 // Copy the favorite icon bitmap to the folder icon bitmap.
918                 folderIconBitmap = favoriteIconBitmap;
919             }
920
921             // Create a folder icon byte array output stream.
922             ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
923
924             // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
925             folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
926
927             // Convert the folder icon byte array stream to a byte array.
928             byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
929
930             // Update the folder icon in the database.
931             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderIconByteArray);
932         } else {  // The folder icon and the name have changed.
933             // Instantiate the new folder icon `Bitmap`.
934             Bitmap folderIconBitmap;
935
936             // Populate the new folder icon bitmap.
937             if (defaultFolderIconRadioButton.isChecked()) {
938                 // Get the default folder icon drawable.
939                 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
940
941                 // Convert the folder icon drawable to a bitmap drawable.
942                 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
943
944                 // Convert the folder icon bitmap drawable to a bitmap.
945                 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
946             } else {  // Use the WebView favorite icon.
947                 // Copy the favorite icon bitmap to the folder icon bitmap.
948                 folderIconBitmap = favoriteIconBitmap;
949             }
950
951             // Create a folder icon byte array output stream.
952             ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
953
954             // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
955             folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
956
957             // Convert the folder icon byte array stream to a byte array.
958             byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
959
960             // Update the folder name and icon in the database.
961             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, newFolderIconByteArray);
962         }
963
964         // Update the bookmarks cursor with the current contents of this folder.
965         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
966
967         // Update the `ListView`.
968         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
969
970         // Close the contextual action mode.
971         contextualActionMode.finish();
972     }
973
974     @Override
975     public void onMoveToFolder(DialogFragment dialogFragment) {
976         // Get the dialog from the dialog fragment.
977         Dialog dialog = dialogFragment.getDialog();
978
979         // Remove the incorrect lint warning below that the dialog might be null.
980         assert dialog != null;
981
982         // Get a handle for the list view from the dialog.
983         ListView folderListView = dialog.findViewById(R.id.move_to_folder_listview);
984
985         // Store a long array of the selected folders.
986         long[] newFolderLongArray = folderListView.getCheckedItemIds();
987
988         // Get the new folder database ID.  Only one folder will be selected.
989         int newFolderDatabaseId = (int) newFolderLongArray[0];
990
991         // Instantiate `newFolderName`.
992         String newFolderName;
993
994         // Set the new folder name.
995         if (newFolderDatabaseId == 0) {
996             // The new folder is the home folder, represented as `""` in the database.
997             newFolderName = "";
998         } else {
999             // Get the new folder name from the database.
1000             newFolderName = bookmarksDatabaseHelper.getFolderName(newFolderDatabaseId);
1001         }
1002
1003         // Get a long array with the the database ID of the selected bookmarks.
1004         long[] selectedBookmarksLongArray = bookmarksListView.getCheckedItemIds();
1005
1006         // Move each of the selected bookmarks to the new folder.
1007         for (long databaseIdLong : selectedBookmarksLongArray) {
1008             // Get `databaseIdInt` for each selected bookmark.
1009             int databaseIdInt = (int) databaseIdLong;
1010
1011             // Move the selected bookmark to the new folder.
1012             bookmarksDatabaseHelper.moveToFolder(databaseIdInt, newFolderName);
1013         }
1014
1015         // Update the bookmarks cursor with the current contents of this folder.
1016         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
1017
1018         // Update the `ListView`.
1019         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
1020
1021         // Close the contextual app bar.
1022         contextualActionMode.finish();
1023     }
1024
1025     private int countBookmarkFolderContents(int databaseId) {
1026         // Initialize the bookmark counter.
1027         int bookmarkCounter = 0;
1028
1029         // Get the name of the folder.
1030         String folderName = bookmarksDatabaseHelper.getFolderName(databaseId);
1031
1032         // Get the contents of the folder.
1033         Cursor folderCursor = bookmarksDatabaseHelper.getBookmarkIds(folderName);
1034
1035         // Count each of the bookmarks in the folder.
1036         for (int i = 0; i < folderCursor.getCount(); i++) {
1037             // Move the folder cursor to the current row.
1038             folderCursor.moveToPosition(i);
1039
1040             // Get the database ID of the item.
1041             int itemDatabaseId = folderCursor.getInt(folderCursor.getColumnIndex(BookmarksDatabaseHelper._ID));
1042
1043             // If this is a folder, recursively count the contents first.
1044             if (bookmarksDatabaseHelper.isFolder(itemDatabaseId)) {
1045                 // Add the bookmarks from the folder to the running total.
1046                 bookmarkCounter = bookmarkCounter + countBookmarkFolderContents(itemDatabaseId);
1047             }
1048
1049             // Add the bookmark to the running total.
1050             bookmarkCounter++;
1051         }
1052
1053         // Return the bookmark counter.
1054         return bookmarkCounter;
1055     }
1056
1057     private void deleteBookmarkFolderContents(int databaseId) {
1058         // Get the name of the folder.
1059         String folderName = bookmarksDatabaseHelper.getFolderName(databaseId);
1060
1061         // Get the contents of the folder.
1062         Cursor folderCursor = bookmarksDatabaseHelper.getBookmarkIds(folderName);
1063
1064         // Delete each of the bookmarks in the folder.
1065         for (int i = 0; i < folderCursor.getCount(); i++) {
1066             // Move the folder cursor to the current row.
1067             folderCursor.moveToPosition(i);
1068
1069             // Get the database ID of the item.
1070             int itemDatabaseId = folderCursor.getInt(folderCursor.getColumnIndex(BookmarksDatabaseHelper._ID));
1071
1072             // If this is a folder, recursively delete the contents first.
1073             if (bookmarksDatabaseHelper.isFolder(itemDatabaseId)) {
1074                 deleteBookmarkFolderContents(itemDatabaseId);
1075             }
1076
1077             // Delete the bookmark.
1078             bookmarksDatabaseHelper.deleteBookmark(itemDatabaseId);
1079         }
1080     }
1081
1082     private void updateMoveIcons() {
1083         // Get a long array of the selected bookmarks.
1084         long[] selectedBookmarksLongArray = bookmarksListView.getCheckedItemIds();
1085
1086         // Get the database IDs for the first, last, and selected bookmarks.
1087         int selectedBookmarkDatabaseId = (int) selectedBookmarksLongArray[0];
1088         int firstBookmarkDatabaseId = (int) bookmarksListView.getItemIdAtPosition(0);
1089         // bookmarksListView is 0 indexed.
1090         int lastBookmarkDatabaseId = (int) bookmarksListView.getItemIdAtPosition(bookmarksListView.getCount() - 1);
1091
1092         // Get the current theme status.
1093         int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
1094
1095         // Update the move bookmark up `MenuItem`.
1096         if (selectedBookmarkDatabaseId == firstBookmarkDatabaseId) {  // The selected bookmark is in the first position.
1097             // Disable the move bookmark up `MenuItem`.
1098             moveBookmarkUpMenuItem.setEnabled(false);
1099
1100             //  Set the move bookmark up icon to be ghosted.
1101             moveBookmarkUpMenuItem.setIcon(R.drawable.move_up_disabled);
1102         } else {  // The selected bookmark is not in the first position.
1103             // Enable the move bookmark up menu item.
1104             moveBookmarkUpMenuItem.setEnabled(true);
1105
1106             // Set the icon according to the theme.
1107             if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) {
1108                 moveBookmarkUpMenuItem.setIcon(R.drawable.move_up_enabled_night);
1109             } else {
1110                 moveBookmarkUpMenuItem.setIcon(R.drawable.move_up_enabled_day);
1111             }
1112         }
1113
1114         // Update the move bookmark down `MenuItem`.
1115         if (selectedBookmarkDatabaseId == lastBookmarkDatabaseId) {  // The selected bookmark is in the last position.
1116             // Disable the move bookmark down `MenuItem`.
1117             moveBookmarkDownMenuItem.setEnabled(false);
1118
1119             // Set the move bookmark down icon to be ghosted.
1120             moveBookmarkDownMenuItem.setIcon(R.drawable.move_down_disabled);
1121         } else {  // The selected bookmark is not in the last position.
1122             // Enable the move bookmark down `MenuItem`.
1123             moveBookmarkDownMenuItem.setEnabled(true);
1124
1125             // Set the icon according to the theme.
1126             if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) {
1127                 moveBookmarkDownMenuItem.setIcon(R.drawable.move_down_enabled_night);
1128             } else {
1129                 moveBookmarkDownMenuItem.setIcon(R.drawable.move_down_enabled_day);
1130             }
1131         }
1132     }
1133
1134     private void scrollBookmarks(int selectedBookmarkPosition) {
1135         // Get the first and last visible bookmark positions.
1136         int firstVisibleBookmarkPosition = bookmarksListView.getFirstVisiblePosition();
1137         int lastVisibleBookmarkPosition = bookmarksListView.getLastVisiblePosition();
1138
1139         // Calculate the number of bookmarks per screen.
1140         int numberOfBookmarksPerScreen = lastVisibleBookmarkPosition - firstVisibleBookmarkPosition;
1141
1142         // Scroll with the moved bookmark if necessary.
1143         if (selectedBookmarkPosition <= firstVisibleBookmarkPosition) {  // The selected bookmark position is at or above the top of the screen.
1144             // Scroll to the selected bookmark position.
1145             bookmarksListView.setSelection(selectedBookmarkPosition);
1146         } else if (selectedBookmarkPosition >= (lastVisibleBookmarkPosition - 1)) {  // The selected bookmark is at or below the bottom of the screen.
1147             // The `-1` handles partial bookmarks displayed at the bottom of the list view.  This command scrolls to display the selected bookmark at the bottom of the screen.
1148             // `+1` assures that the entire bookmark will be displayed in situations where only a partial bookmark fits at the bottom of the list view.
1149             bookmarksListView.setSelection(selectedBookmarkPosition - numberOfBookmarksPerScreen + 1);
1150         }
1151     }
1152
1153     private void loadFolder() {
1154         // Update bookmarks cursor with the contents of the bookmarks database for the current folder.
1155         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
1156
1157         // Setup a `CursorAdapter`.  `this` specifies the `Context`.  `false` disables `autoRequery`.
1158         bookmarksCursorAdapter = new CursorAdapter(this, bookmarksCursor, false) {
1159             @Override
1160             public View newView(Context context, Cursor cursor, ViewGroup parent) {
1161                 // Inflate the individual item layout.  `false` does not attach it to the root.
1162                 return getLayoutInflater().inflate(R.layout.bookmarks_activity_item_linearlayout, parent, false);
1163             }
1164
1165             @Override
1166             public void bindView(View view, Context context, Cursor cursor) {
1167                 // Get handles for the views.
1168                 ImageView bookmarkFavoriteIcon = view.findViewById(R.id.bookmark_favorite_icon);
1169                 TextView bookmarkNameTextView = view.findViewById(R.id.bookmark_name);
1170
1171                 // Get the favorite icon byte array from the `Cursor`.
1172                 byte[] favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON));
1173
1174                 // Convert the byte array to a `Bitmap` beginning at the first byte and ending at the last.
1175                 Bitmap favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.length);
1176
1177                 // Display the bitmap in `bookmarkFavoriteIcon`.
1178                 bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap);
1179
1180                 // Get the bookmark name from the cursor and display it in `bookmarkNameTextView`.
1181                 String bookmarkNameString = cursor.getString(cursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
1182                 bookmarkNameTextView.setText(bookmarkNameString);
1183
1184                 // Make the font bold for folders.
1185                 if (cursor.getInt(cursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {
1186                     bookmarkNameTextView.setTypeface(Typeface.DEFAULT_BOLD);
1187                 } else {  // Reset the font to default for normal bookmarks.
1188                     bookmarkNameTextView.setTypeface(Typeface.DEFAULT);
1189                 }
1190             }
1191         };
1192
1193         // Populate the list view with the adapter.
1194         bookmarksListView.setAdapter(bookmarksCursorAdapter);
1195
1196         // Set the `AppBar` title.
1197         if (currentFolder.isEmpty()) {
1198             appBar.setTitle(R.string.bookmarks);
1199         } else {
1200             appBar.setTitle(currentFolder);
1201         }
1202     }
1203
1204     @Override
1205     public void onDestroy() {
1206         // Close the bookmarks cursor and database.
1207         bookmarksCursor.close();
1208         bookmarksDatabaseHelper.close();
1209
1210         // Run the default commands.
1211         super.onDestroy();
1212     }
1213 }