]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/activities/BookmarksActivity.java
05bb2f5874146baa3f2c1ddcfc21ffdcf3af1070
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / activities / BookmarksActivity.java
1 /*
2  * Copyright 2016-2022 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.  When the minimum API >= 24 a `forEach()` command can be used instead.
629                 if (checkedBookmarksArrayList != null) {
630                     for (int i = 0; i < checkedBookmarksArrayList.size(); i++) {
631                         bookmarksListView.setItemChecked(checkedBookmarksArrayList.get(i), true);
632                     }
633                 }
634             });
635         }
636
637         // Load the home folder.
638         loadFolder();
639     }
640
641     @Override
642     public void onRestart() {
643         // Run the default commands.
644         super.onRestart();
645
646         // Update the list view if returning from the bookmarks database view activity.
647         if (restartFromBookmarksDatabaseViewActivity) {
648             // Load the current folder in the list view.
649             loadFolder();
650
651             // Reset `restartFromBookmarksDatabaseViewActivity`.
652             restartFromBookmarksDatabaseViewActivity = false;
653         }
654     }
655
656     @Override
657     public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
658         // Run the default commands.
659         super.onSaveInstanceState(savedInstanceState);
660
661         // Get the array of the checked items.
662         SparseBooleanArray checkedBookmarksSparseBooleanArray = bookmarksListView.getCheckedItemPositions();
663
664         // Create a checked items array list.
665         ArrayList<Integer> checkedBookmarksArrayList = new ArrayList<>();
666
667         // Add each checked bookmark position to the array list.
668         for (int i = 0; i < checkedBookmarksSparseBooleanArray.size(); i++) {
669             // 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.
670             if (checkedBookmarksSparseBooleanArray.valueAt(i)) {
671                 // Add the bookmark position to the checked bookmarks array list.
672                 checkedBookmarksArrayList.add(checkedBookmarksSparseBooleanArray.keyAt(i));
673             }
674         }
675
676         // Store the variables in the saved instance state.
677         savedInstanceState.putString(CURRENT_FOLDER, currentFolder);
678         savedInstanceState.putIntegerArrayList(CHECKED_BOOKMARKS_ARRAY_LIST, checkedBookmarksArrayList);
679     }
680
681     @Override
682     public boolean onCreateOptionsMenu(Menu menu) {
683         // Inflate the menu.
684         getMenuInflater().inflate(R.menu.bookmarks_options_menu, menu);
685
686         // Success.
687         return true;
688     }
689
690     @Override
691     public boolean onOptionsItemSelected(MenuItem menuItem) {
692         // Get a handle for the menu item ID.
693         int menuItemId = menuItem.getItemId();
694
695         // Run the command according to the selected option.
696         if (menuItemId == android.R.id.home) {  // Home.  The home arrow is identified as `android.R.id.home`, not just `R.id.home`.
697             if (currentFolder.isEmpty()) {  // Currently in the home folder.
698                 // Prepare to finish the activity.
699                 prepareFinish();
700             } else {  // Currently in a subfolder.
701                 // Place the former parent folder in `currentFolder`.
702                 currentFolder = bookmarksDatabaseHelper.getParentFolderName(currentFolder);
703
704                 // Load the new folder.
705                 loadFolder();
706             }
707         } else if (menuItemId == R.id.options_menu_select_all_bookmarks) {  // Select all.
708             // Get the total number of bookmarks.
709             int numberOfBookmarks = bookmarksListView.getCount();
710
711             // Select them all.
712             for (int i = 0; i < numberOfBookmarks; i++) {
713                 bookmarksListView.setItemChecked(i, true);
714             }
715         } else if (menuItemId == R.id.bookmarks_database_view) {
716             // Close the contextual action bar if it is displayed.  This can happen if the bottom app bar is enabled.
717             if (contextualActionMode != null) {
718                 contextualActionMode.finish();
719             }
720
721             // Create an intent to launch the bookmarks database view activity.
722             Intent bookmarksDatabaseViewIntent = new Intent(this, BookmarksDatabaseViewActivity.class);
723
724             // Include the favorite icon byte array to the intent.
725             bookmarksDatabaseViewIntent.putExtra("favorite_icon_byte_array", favoriteIconByteArray);
726
727             // Make it so.
728             startActivity(bookmarksDatabaseViewIntent);
729         }
730         return true;
731     }
732
733     @Override
734     public void onCreateBookmark(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) {
735         // Get the alert dialog from the fragment.
736         Dialog dialog = dialogFragment.getDialog();
737
738         // Remove the incorrect lint warning below that the dialog might be null.
739         assert dialog != null;
740
741         // Get the views from the dialog fragment.
742         EditText createBookmarkNameEditText = dialog.findViewById(R.id.create_bookmark_name_edittext);
743         EditText createBookmarkUrlEditText = dialog.findViewById(R.id.create_bookmark_url_edittext);
744
745         // Extract the strings from the edit texts.
746         String bookmarkNameString = createBookmarkNameEditText.getText().toString();
747         String bookmarkUrlString = createBookmarkUrlEditText.getText().toString();
748
749         // Create a favorite icon byte array output stream.
750         ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
751
752         // Convert the favorite icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
753         favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
754
755         // Convert the favorite icon byte array stream to a byte array.
756         byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
757
758         // Display the new bookmark below the current items in the (0 indexed) list.
759         int newBookmarkDisplayOrder = bookmarksListView.getCount();
760
761         // Create the bookmark.
762         bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentFolder, newBookmarkDisplayOrder, favoriteIconByteArray);
763
764         // Update the bookmarks cursor with the current contents of this folder.
765         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
766
767         // Update the `ListView`.
768         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
769
770         // Scroll to the new bookmark.
771         bookmarksListView.setSelection(newBookmarkDisplayOrder);
772     }
773
774     @Override
775     public void onCreateBookmarkFolder(DialogFragment dialogFragment, @NonNull Bitmap favoriteIconBitmap) {
776         // Get the dialog from the dialog fragment.
777         Dialog dialog = dialogFragment.getDialog();
778
779         // Remove the incorrect lint warning below that the dialog might be null.
780         assert dialog != null;
781
782         // Get handles for the views in the dialog fragment.
783         EditText folderNameEditText = dialog.findViewById(R.id.folder_name_edittext);
784         RadioButton defaultIconRadioButton = dialog.findViewById(R.id.default_icon_radiobutton);
785         ImageView defaultIconImageView = dialog.findViewById(R.id.default_icon_imageview);
786
787         // Get new folder name string.
788         String folderNameString = folderNameEditText.getText().toString();
789
790         // Create a folder icon bitmap.
791         Bitmap folderIconBitmap;
792
793         // Set the folder icon bitmap according to the dialog.
794         if (defaultIconRadioButton.isChecked()) {  // Use the default folder icon.
795             // Get the default folder icon drawable.
796             Drawable folderIconDrawable = defaultIconImageView.getDrawable();
797
798             // Convert the folder icon drawable to a bitmap drawable.
799             BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
800
801             // Convert the folder icon bitmap drawable to a bitmap.
802             folderIconBitmap = folderIconBitmapDrawable.getBitmap();
803         } else {  // Use the WebView favorite icon.
804             // Copy the favorite icon bitmap to the folder icon bitmap.
805             folderIconBitmap = favoriteIconBitmap;
806         }
807
808         // Create a folder icon byte array output stream.
809         ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream();
810
811         // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
812         folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream);
813
814         // Convert the folder icon byte array stream to a byte array.
815         byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray();
816
817         // Move all the bookmarks down one in the display order.
818         for (int i = 0; i < bookmarksListView.getCount(); i++) {
819             int databaseId = (int) bookmarksListView.getItemIdAtPosition(i);
820             bookmarksDatabaseHelper.updateDisplayOrder(databaseId, i + 1);
821         }
822
823         // Create the folder, which will be placed at the top of the `ListView`.
824         bookmarksDatabaseHelper.createFolder(folderNameString, currentFolder, folderIconByteArray);
825
826         // Update the bookmarks cursor with the current contents of this folder.
827         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
828
829         // Update the list view.
830         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
831
832         // Scroll to the new folder.
833         bookmarksListView.setSelection(0);
834     }
835
836     @Override
837     public void onSaveBookmark(DialogFragment dialogFragment, int selectedBookmarkDatabaseId, @NonNull Bitmap favoriteIconBitmap) {
838         // Get the dialog from the dialog fragment.
839         Dialog dialog = dialogFragment.getDialog();
840
841         // Remove the incorrect lint warning below that the dialog might be null.
842         assert dialog != null;
843
844         // Get handles for the views from the dialog fragment.
845         EditText bookmarkNameEditText = dialog.findViewById(R.id.bookmark_name_edittext);
846         EditText bookmarkUrlEditText = dialog.findViewById(R.id.bookmark_url_edittext);
847         RadioButton currentIconRadioButton = dialog.findViewById(R.id.current_icon_radiobutton);
848
849         // Store the bookmark strings.
850         String bookmarkNameString = bookmarkNameEditText.getText().toString();
851         String bookmarkUrlString = bookmarkUrlEditText.getText().toString();
852
853         // Update the bookmark.
854         if (currentIconRadioButton.isChecked()) {  // Update the bookmark without changing the favorite icon.
855             bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString);
856         } else {  // Update the bookmark using the WebView favorite icon.
857             // Create a favorite icon byte array output stream.
858             ByteArrayOutputStream newFavoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
859
860             // Convert the favorite icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
861             favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFavoriteIconByteArrayOutputStream);
862
863             // Convert the favorite icon byte array stream to a byte array.
864             byte[] newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray();
865
866             //  Update the bookmark and the favorite icon.
867             bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, newFavoriteIconByteArray);
868         }
869
870         // Check to see if the contextual action mode has been created.
871         if (contextualActionMode != null) {
872             // Close the contextual action mode if it is open.
873             contextualActionMode.finish();
874         }
875
876         // Update the bookmarks cursor with the contents of the current folder.
877         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
878
879         // Update the `ListView`.
880         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
881     }
882
883     @Override
884     public void onSaveBookmarkFolder(DialogFragment dialogFragment, int selectedFolderDatabaseId, @NonNull Bitmap favoriteIconBitmap) {
885         // Get the dialog from the dialog fragment.
886         Dialog dialog = dialogFragment.getDialog();
887
888         // Remove the incorrect lint warning below that the dialog might be null.
889         assert dialog != null;
890
891         // Get handles for the views from the dialog fragment.
892         RadioButton currentFolderIconRadioButton = dialog.findViewById(R.id.current_icon_radiobutton);
893         RadioButton defaultFolderIconRadioButton = dialog.findViewById(R.id.default_icon_radiobutton);
894         ImageView defaultFolderIconImageView = dialog.findViewById(R.id.default_icon_imageview);
895         EditText editFolderNameEditText = dialog.findViewById(R.id.folder_name_edittext);
896
897         // Get the new folder name.
898         String newFolderNameString = editFolderNameEditText.getText().toString();
899
900         // Check if the favorite icon has changed.
901         if (currentFolderIconRadioButton.isChecked()) {  // Only the name has changed.
902             // Update the name in the database.
903             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString);
904         } else if (!currentFolderIconRadioButton.isChecked() && newFolderNameString.equals(oldFolderNameString)) {  // Only the icon has changed.
905             // Create the new folder icon Bitmap.
906             Bitmap folderIconBitmap;
907
908             // Populate the new folder icon bitmap.
909             if (defaultFolderIconRadioButton.isChecked()) {
910                 // Get the default folder icon drawable.
911                 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
912
913                 // Convert the folder icon drawable to a bitmap drawable.
914                 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
915
916                 // Convert the folder icon bitmap drawable to a bitmap.
917                 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
918             } else {  // Use the WebView favorite icon.
919                 // Copy the favorite icon bitmap to the folder icon bitmap.
920                 folderIconBitmap = favoriteIconBitmap;
921             }
922
923             // Create a folder icon byte array output stream.
924             ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
925
926             // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
927             folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
928
929             // Convert the folder icon byte array stream to a byte array.
930             byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
931
932             // Update the folder icon in the database.
933             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderIconByteArray);
934         } else {  // The folder icon and the name have changed.
935             // Instantiate the new folder icon `Bitmap`.
936             Bitmap folderIconBitmap;
937
938             // Populate the new folder icon bitmap.
939             if (defaultFolderIconRadioButton.isChecked()) {
940                 // Get the default folder icon drawable.
941                 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
942
943                 // Convert the folder icon drawable to a bitmap drawable.
944                 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
945
946                 // Convert the folder icon bitmap drawable to a bitmap.
947                 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
948             } else {  // Use the WebView favorite icon.
949                 // Copy the favorite icon bitmap to the folder icon bitmap.
950                 folderIconBitmap = favoriteIconBitmap;
951             }
952
953             // Create a folder icon byte array output stream.
954             ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
955
956             // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
957             folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
958
959             // Convert the folder icon byte array stream to a byte array.
960             byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
961
962             // Update the folder name and icon in the database.
963             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, newFolderIconByteArray);
964         }
965
966         // Update the bookmarks cursor with the current contents of this folder.
967         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
968
969         // Update the `ListView`.
970         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
971
972         // Close the contextual action mode.
973         contextualActionMode.finish();
974     }
975
976     @Override
977     public void onMoveToFolder(DialogFragment dialogFragment) {
978         // Get the dialog from the dialog fragment.
979         Dialog dialog = dialogFragment.getDialog();
980
981         // Remove the incorrect lint warning below that the dialog might be null.
982         assert dialog != null;
983
984         // Get a handle for the list view from the dialog.
985         ListView folderListView = dialog.findViewById(R.id.move_to_folder_listview);
986
987         // Store a long array of the selected folders.
988         long[] newFolderLongArray = folderListView.getCheckedItemIds();
989
990         // Get the new folder database ID.  Only one folder will be selected.
991         int newFolderDatabaseId = (int) newFolderLongArray[0];
992
993         // Instantiate `newFolderName`.
994         String newFolderName;
995
996         // Set the new folder name.
997         if (newFolderDatabaseId == 0) {
998             // The new folder is the home folder, represented as `""` in the database.
999             newFolderName = "";
1000         } else {
1001             // Get the new folder name from the database.
1002             newFolderName = bookmarksDatabaseHelper.getFolderName(newFolderDatabaseId);
1003         }
1004
1005         // Get a long array with the the database ID of the selected bookmarks.
1006         long[] selectedBookmarksLongArray = bookmarksListView.getCheckedItemIds();
1007
1008         // Move each of the selected bookmarks to the new folder.
1009         for (long databaseIdLong : selectedBookmarksLongArray) {
1010             // Get `databaseIdInt` for each selected bookmark.
1011             int databaseIdInt = (int) databaseIdLong;
1012
1013             // Move the selected bookmark to the new folder.
1014             bookmarksDatabaseHelper.moveToFolder(databaseIdInt, newFolderName);
1015         }
1016
1017         // Update the bookmarks cursor with the current contents of this folder.
1018         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
1019
1020         // Update the `ListView`.
1021         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
1022
1023         // Close the contextual app bar.
1024         contextualActionMode.finish();
1025     }
1026
1027     private int countBookmarkFolderContents(int databaseId) {
1028         // Initialize the bookmark counter.
1029         int bookmarkCounter = 0;
1030
1031         // Get the name of the folder.
1032         String folderName = bookmarksDatabaseHelper.getFolderName(databaseId);
1033
1034         // Get the contents of the folder.
1035         Cursor folderCursor = bookmarksDatabaseHelper.getBookmarkIds(folderName);
1036
1037         // Count each of the bookmarks in the folder.
1038         for (int i = 0; i < folderCursor.getCount(); i++) {
1039             // Move the folder cursor to the current row.
1040             folderCursor.moveToPosition(i);
1041
1042             // Get the database ID of the item.
1043             int itemDatabaseId = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.ID));
1044
1045             // If this is a folder, recursively count the contents first.
1046             if (bookmarksDatabaseHelper.isFolder(itemDatabaseId)) {
1047                 // Add the bookmarks from the folder to the running total.
1048                 bookmarkCounter = bookmarkCounter + countBookmarkFolderContents(itemDatabaseId);
1049             }
1050
1051             // Add the bookmark to the running total.
1052             bookmarkCounter++;
1053         }
1054
1055         // Return the bookmark counter.
1056         return bookmarkCounter;
1057     }
1058
1059     private void deleteBookmarkFolderContents(int databaseId) {
1060         // Get the name of the folder.
1061         String folderName = bookmarksDatabaseHelper.getFolderName(databaseId);
1062
1063         // Get the contents of the folder.
1064         Cursor folderCursor = bookmarksDatabaseHelper.getBookmarkIds(folderName);
1065
1066         // Delete each of the bookmarks in the folder.
1067         for (int i = 0; i < folderCursor.getCount(); i++) {
1068             // Move the folder cursor to the current row.
1069             folderCursor.moveToPosition(i);
1070
1071             // Get the database ID of the item.
1072             int itemDatabaseId = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.ID));
1073
1074             // If this is a folder, recursively delete the contents first.
1075             if (bookmarksDatabaseHelper.isFolder(itemDatabaseId)) {
1076                 deleteBookmarkFolderContents(itemDatabaseId);
1077             }
1078
1079             // Delete the bookmark.
1080             bookmarksDatabaseHelper.deleteBookmark(itemDatabaseId);
1081         }
1082     }
1083
1084     private void prepareFinish() {
1085         // 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.
1086         if ((bookmarksDeletedSnackbar != null) && bookmarksDeletedSnackbar.isShown()) {  // Close the bookmarks deleted snackbar before going home.
1087             // Set the close flag.
1088             closeActivityAfterDismissingSnackbar = true;
1089
1090             // Dismiss the snackbar.
1091             bookmarksDeletedSnackbar.dismiss();
1092         } else {  // Go home immediately.
1093             // Update the bookmarks folder for the bookmarks drawer in the main WebView activity.
1094             MainWebViewActivity.currentBookmarksFolder = currentFolder;
1095
1096             // Close the bookmarks drawer and reload the bookmarks ListView when returning to the main WebView activity.
1097             MainWebViewActivity.restartFromBookmarksActivity = true;
1098
1099             // Exit the bookmarks activity.
1100             finish();
1101         }
1102     }
1103
1104     private void updateMoveIcons() {
1105         // Get a long array of the selected bookmarks.
1106         long[] selectedBookmarksLongArray = bookmarksListView.getCheckedItemIds();
1107
1108         // Get the database IDs for the first, last, and selected bookmarks.
1109         int selectedBookmarkDatabaseId = (int) selectedBookmarksLongArray[0];
1110         int firstBookmarkDatabaseId = (int) bookmarksListView.getItemIdAtPosition(0);
1111         // bookmarksListView is 0 indexed.
1112         int lastBookmarkDatabaseId = (int) bookmarksListView.getItemIdAtPosition(bookmarksListView.getCount() - 1);
1113
1114         // Update the move bookmark up menu item.
1115         if (selectedBookmarkDatabaseId == firstBookmarkDatabaseId) {  // The selected bookmark is in the first position.
1116             // Disable the move bookmark up menu item.
1117             moveBookmarkUpMenuItem.setEnabled(false);
1118
1119             //  Set the icon.
1120             moveBookmarkUpMenuItem.setIcon(R.drawable.move_up_disabled);
1121         } else {  // The selected bookmark is not in the first position.
1122             // Enable the move bookmark up menu item.
1123             moveBookmarkUpMenuItem.setEnabled(true);
1124
1125             // Set the icon according to the theme.
1126             moveBookmarkUpMenuItem.setIcon(R.drawable.move_up_enabled);
1127         }
1128
1129         // Update the move bookmark down menu item.
1130         if (selectedBookmarkDatabaseId == lastBookmarkDatabaseId) {  // The selected bookmark is in the last position.
1131             // Disable the move bookmark down menu item.
1132             moveBookmarkDownMenuItem.setEnabled(false);
1133
1134             // Set the icon.
1135             moveBookmarkDownMenuItem.setIcon(R.drawable.move_down_disabled);
1136         } else {  // The selected bookmark is not in the last position.
1137             // Enable the move bookmark down menu item.
1138             moveBookmarkDownMenuItem.setEnabled(true);
1139
1140             // Set the icon.
1141             moveBookmarkDownMenuItem.setIcon(R.drawable.move_down_enabled);
1142         }
1143     }
1144
1145     private void scrollBookmarks(int selectedBookmarkPosition) {
1146         // Get the first and last visible bookmark positions.
1147         int firstVisibleBookmarkPosition = bookmarksListView.getFirstVisiblePosition();
1148         int lastVisibleBookmarkPosition = bookmarksListView.getLastVisiblePosition();
1149
1150         // Calculate the number of bookmarks per screen.
1151         int numberOfBookmarksPerScreen = lastVisibleBookmarkPosition - firstVisibleBookmarkPosition;
1152
1153         // Scroll with the moved bookmark if necessary.
1154         if (selectedBookmarkPosition <= firstVisibleBookmarkPosition) {  // The selected bookmark position is at or above the top of the screen.
1155             // Scroll to the selected bookmark position.
1156             bookmarksListView.setSelection(selectedBookmarkPosition);
1157         } else if (selectedBookmarkPosition >= (lastVisibleBookmarkPosition - 1)) {  // The selected bookmark is at or below the bottom of the screen.
1158             // 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.
1159             // `+1` assures that the entire bookmark will be displayed in situations where only a partial bookmark fits at the bottom of the list view.
1160             bookmarksListView.setSelection(selectedBookmarkPosition - numberOfBookmarksPerScreen + 1);
1161         }
1162     }
1163
1164     private void loadFolder() {
1165         // Update bookmarks cursor with the contents of the bookmarks database for the current folder.
1166         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
1167
1168         // Setup a `CursorAdapter`.  `this` specifies the `Context`.  `false` disables `autoRequery`.
1169         bookmarksCursorAdapter = new CursorAdapter(this, bookmarksCursor, false) {
1170             @Override
1171             public View newView(Context context, Cursor cursor, ViewGroup parent) {
1172                 // Inflate the individual item layout.  `false` does not attach it to the root.
1173                 return getLayoutInflater().inflate(R.layout.bookmarks_activity_item_linearlayout, parent, false);
1174             }
1175
1176             @Override
1177             public void bindView(View view, Context context, Cursor cursor) {
1178                 // Get handles for the views.
1179                 ImageView bookmarkFavoriteIcon = view.findViewById(R.id.bookmark_favorite_icon);
1180                 TextView bookmarkNameTextView = view.findViewById(R.id.bookmark_name);
1181
1182                 // Get the favorite icon byte array from the `Cursor`.
1183                 byte[] favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.FAVORITE_ICON));
1184
1185                 // Convert the byte array to a `Bitmap` beginning at the first byte and ending at the last.
1186                 Bitmap favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.length);
1187
1188                 // Display the bitmap in `bookmarkFavoriteIcon`.
1189                 bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap);
1190
1191                 // Get the bookmark name from the cursor and display it in `bookmarkNameTextView`.
1192                 String bookmarkNameString = cursor.getString(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME));
1193                 bookmarkNameTextView.setText(bookmarkNameString);
1194
1195                 // Make the font bold for folders.
1196                 if (cursor.getInt(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {
1197                     bookmarkNameTextView.setTypeface(Typeface.DEFAULT_BOLD);
1198                 } else {  // Reset the font to default for normal bookmarks.
1199                     bookmarkNameTextView.setTypeface(Typeface.DEFAULT);
1200                 }
1201             }
1202         };
1203
1204         // Populate the list view with the adapter.
1205         bookmarksListView.setAdapter(bookmarksCursorAdapter);
1206
1207         // Set the `AppBar` title.
1208         if (currentFolder.isEmpty()) {
1209             appBar.setTitle(R.string.bookmarks);
1210         } else {
1211             appBar.setTitle(currentFolder);
1212         }
1213     }
1214
1215     @Override
1216     public void onDestroy() {
1217         // Close the bookmarks cursor and database.
1218         bookmarksCursor.close();
1219         bookmarksDatabaseHelper.close();
1220
1221         // Run the default commands.
1222         super.onDestroy();
1223     }
1224 }