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