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