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