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