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