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