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