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