Add a back arrow to the header of the bookmarks drawer. 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                 // Adjust the ActionMode and the menu according to the number of selected bookmarks.
291                 if (numberOfSelectedBookmarks == 1) {  // One bookmark is selected.
292                     // List the number of selected bookmarks in the subtitle.
293                     mode.setSubtitle(getString(R.string.selected) + "  1");
294
295                     // Show the `Move Up`, `Move Down`, and  `Edit` options.
296                     moveBookmarkUpMenuItem.setVisible(true);
297                     moveBookmarkDownMenuItem.setVisible(true);
298                     editBookmarkMenuItem.setVisible(true);
299
300                     // Update the enabled status of the move icons.
301                     updateMoveIcons();
302                 } else {  // More than one bookmark is selected.
303                     // List the number of selected bookmarks in the subtitle.
304                     mode.setSubtitle(getString(R.string.selected) + "  " + numberOfSelectedBookmarks);
305
306                     // Hide non-applicable `MenuItems`.
307                     moveBookmarkUpMenuItem.setVisible(false);
308                     moveBookmarkDownMenuItem.setVisible(false);
309                     editBookmarkMenuItem.setVisible(false);
310                 }
311
312                 // Do not show the select all menu item if all the bookmarks are already checked.
313                 if (bookmarksListView.getCheckedItemCount() == bookmarksListView.getCount()) {
314                     selectAllBookmarksMenuItem.setVisible(false);
315                 } else {
316                     selectAllBookmarksMenuItem.setVisible(true);
317                 }
318             }
319
320             @Override
321             public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
322                 // Instantiate the common variables.
323                 int selectedBookmarkPosition;
324                 int selectedBookmarkNewPosition;
325                 final SparseBooleanArray selectedBookmarksPositionsSparseBooleanArray;
326
327                 switch (item.getItemId()) {
328                     case R.id.move_bookmark_up:
329                         // Get the array of checked bookmark positions.
330                         selectedBookmarksPositionsSparseBooleanArray = bookmarksListView.getCheckedItemPositions();
331
332                         // Store the position of the selected bookmark.  Only one bookmark is selected when `move_bookmark_up` is enabled.
333                         selectedBookmarkPosition = selectedBookmarksPositionsSparseBooleanArray.keyAt(0);
334
335                         // Calculate the new position of the selected bookmark.
336                         selectedBookmarkNewPosition = selectedBookmarkPosition - 1;
337
338                         // Iterate through the bookmarks.
339                         for (int i = 0; i < bookmarksListView.getCount(); i++) {
340                             // Get the database ID for the current bookmark.
341                             int currentBookmarkDatabaseId = (int) bookmarksListView.getItemIdAtPosition(i);
342
343                             // Update the display order for the current bookmark.
344                             if (i == selectedBookmarkPosition) {  // The current bookmark is the selected bookmark.
345                                 // Move the current bookmark up one.
346                                 bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i - 1);
347                             } else if ((i + 1) == selectedBookmarkPosition){  // The current bookmark is immediately above the selected bookmark.
348                                 // Move the current bookmark down one.
349                                 bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i + 1);
350                             } else {  // The current bookmark is not changing positions.
351                                 // Move `bookmarksCursor` to the current bookmark position.
352                                 bookmarksCursor.moveToPosition(i);
353
354                                 // Update the display order only if it is not correct in the database.
355                                 if (bookmarksCursor.getInt(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.DISPLAY_ORDER)) != i) {
356                                     bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i);
357                                 }
358                             }
359                         }
360
361                         // Update the bookmarks cursor with the current contents of the bookmarks database.
362                         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
363
364                         // Update the `ListView`.
365                         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
366
367                         // Scroll with the bookmark.
368                         scrollBookmarks(selectedBookmarkNewPosition);
369
370                         // Update the enabled status of the move icons.
371                         updateMoveIcons();
372                         break;
373
374                     case R.id.move_bookmark_down:
375                         // Get the array of checked bookmark positions.
376                         selectedBookmarksPositionsSparseBooleanArray = bookmarksListView.getCheckedItemPositions();
377
378                         // Store the position of the selected bookmark.  Only one bookmark is selected when `move_bookmark_down` is enabled.
379                         selectedBookmarkPosition = selectedBookmarksPositionsSparseBooleanArray.keyAt(0);
380
381                         // Calculate the new position of the selected bookmark.
382                         selectedBookmarkNewPosition = selectedBookmarkPosition + 1;
383
384                         // Iterate through the bookmarks.
385                         for (int i = 0; i <bookmarksListView.getCount(); i++) {
386                             // Get the database ID for the current bookmark.
387                             int currentBookmarkDatabaseId = (int) bookmarksListView.getItemIdAtPosition(i);
388
389                             // Update the display order for the current bookmark.
390                             if (i == selectedBookmarkPosition) {  // The current bookmark is the selected bookmark.
391                                 // Move the current bookmark down one.
392                                 bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i + 1);
393                             } else if ((i - 1) == selectedBookmarkPosition) {  // The current bookmark is immediately below the selected bookmark.
394                                 // Move the bookmark below the selected bookmark up one.
395                                 bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i - 1);
396                             } else {  // The current bookmark is not changing positions.
397                                 // Move `bookmarksCursor` to the current bookmark position.
398                                 bookmarksCursor.moveToPosition(i);
399
400                                 // Update the display order only if it is not correct in the database.
401                                 if (bookmarksCursor.getInt(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.DISPLAY_ORDER)) != i) {
402                                     bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i);
403                                 }
404                             }
405                         }
406
407                         // Update the bookmarks cursor with the current contents of the bookmarks database.
408                         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
409
410                         // Update the `ListView`.
411                         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
412
413                         // Scroll with the bookmark.
414                         scrollBookmarks(selectedBookmarkNewPosition);
415
416                         // Update the enabled status of the move icons.
417                         updateMoveIcons();
418                         break;
419
420                     case R.id.move_to_folder:
421                         // Store `checkedItemIds` for use by the `AlertDialog`.
422                         checkedItemIds = bookmarksListView.getCheckedItemIds();
423
424                         // Show the `MoveToFolderDialog` `AlertDialog` and name the instance `@string/move_to_folder
425                         DialogFragment moveToFolderDialog = new MoveToFolderDialog();
426                         moveToFolderDialog.show(getSupportFragmentManager(), getResources().getString(R.string.move_to_folder));
427                         break;
428
429                     case R.id.edit_bookmark:
430                         // Get the array of checked bookmark positions.
431                         selectedBookmarksPositionsSparseBooleanArray = bookmarksListView.getCheckedItemPositions();
432
433                         // Get the position of the selected bookmark.  Only one bookmark is selected when `edit_bookmark_down` is enabled.
434                         selectedBookmarkPosition = selectedBookmarksPositionsSparseBooleanArray.keyAt(0);
435
436                         // Move the `Cursor` to the selected position and find out if it is a folder.
437                         bookmarksCursor.moveToPosition(selectedBookmarkPosition);
438                         boolean isFolder = (bookmarksCursor.getInt(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1);
439
440                         // Get the selected bookmark database ID.
441                         int databaseId = bookmarksCursor.getInt(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper._ID));
442
443                         // Show the edit bookmark or edit bookmark folder dialog.
444                         if (isFolder) {
445                             // Save the current folder name, which is used in `onSaveBookmarkFolder()`.
446                             oldFolderNameString = bookmarksCursor.getString(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
447
448                             // Show the edit bookmark folder dialog.
449                             DialogFragment editFolderDialog = EditBookmarkFolderDialog.folderDatabaseId(databaseId, favoriteIconBitmap);
450                             editFolderDialog.show(getSupportFragmentManager(), getResources().getString(R.string.edit_folder));
451                         } else {
452                             // Show the edit bookmark dialog.
453                             DialogFragment editBookmarkDialog = EditBookmarkDialog.bookmarkDatabaseId(databaseId, favoriteIconBitmap);
454                             editBookmarkDialog.show(getSupportFragmentManager(), getResources().getString(R.string.edit_bookmark));
455                         }
456                         break;
457
458                     case R.id.delete_bookmark:
459                         // Set the deleting bookmarks flag, which prevents the delete menu item from being enabled until the current process finishes.
460                         deletingBookmarks = true;
461
462                         // Get an array of the selected row IDs.
463                         final long[] selectedBookmarksIdsLongArray = bookmarksListView.getCheckedItemIds();
464
465                         // 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.
466                         selectedBookmarksPositionsSparseBooleanArray = bookmarksListView.getCheckedItemPositions().clone();
467
468                         // Update the bookmarks cursor with the current contents of the bookmarks database except for the specified database IDs.
469                         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrderExcept(selectedBookmarksIdsLongArray, currentFolder);
470
471                         // Update the list view.
472                         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
473
474                         // Create a Snackbar with the number of deleted bookmarks.
475                         bookmarksDeletedSnackbar = Snackbar.make(findViewById(R.id.bookmarks_coordinatorlayout), getString(R.string.bookmarks_deleted) + "  " + selectedBookmarksIdsLongArray.length,
476                                 Snackbar.LENGTH_LONG)
477                                 .setAction(R.string.undo, view -> {
478                                     // Do nothing because everything will be handled by `onDismissed()` below.
479                                 })
480                                 .addCallback(new Snackbar.Callback() {
481                                     @SuppressLint("SwitchIntDef")  // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
482                                     @Override
483                                     public void onDismissed(Snackbar snackbar, int event) {
484                                         if (event == Snackbar.Callback.DISMISS_EVENT_ACTION) {  // The user pushed the undo button.
485                                             // Update the bookmarks cursor with the current contents of the bookmarks database, including the "deleted" bookmarks.
486                                             bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
487
488                                             // Update the list view.
489                                             bookmarksCursorAdapter.changeCursor(bookmarksCursor);
490
491                                             // Re-select the previously selected bookmarks.
492                                             for (int i = 0; i < selectedBookmarksPositionsSparseBooleanArray.size(); i++) {
493                                                 bookmarksListView.setItemChecked(selectedBookmarksPositionsSparseBooleanArray.keyAt(i), true);
494                                             }
495                                         } else {  // The snackbar was dismissed without the undo button being pushed.
496                                             // Delete each selected bookmark.
497                                             for (long databaseIdLong : selectedBookmarksIdsLongArray) {
498                                                 // Convert `databaseIdLong` to an int.
499                                                 int databaseIdInt = (int) databaseIdLong;
500
501                                                 // Delete the contents of the folder if the selected bookmark is a folder.
502                                                 if (bookmarksDatabaseHelper.isFolder(databaseIdInt)) {
503                                                     deleteBookmarkFolderContents(databaseIdInt);
504                                                 }
505
506                                                 // Delete the selected bookmark.
507                                                 bookmarksDatabaseHelper.deleteBookmark(databaseIdInt);
508                                             }
509
510                                             // Update the display order.
511                                             for (int i = 0; i < bookmarksListView.getCount(); i++) {
512                                                 // Get the database ID for the current bookmark.
513                                                 int currentBookmarkDatabaseId = (int) bookmarksListView.getItemIdAtPosition(i);
514
515                                                 // Move `bookmarksCursor` to the current bookmark position.
516                                                 bookmarksCursor.moveToPosition(i);
517
518                                                 // Update the display order only if it is not correct in the database.
519                                                 if (bookmarksCursor.getInt(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.DISPLAY_ORDER)) != i) {
520                                                     bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i);
521                                                 }
522                                             }
523                                         }
524
525                                         // Reset the deleting bookmarks flag.
526                                         deletingBookmarks = false;
527
528                                         // Enable the delete bookmarks menu item.
529                                         deleteBookmarksMenuItem.setEnabled(true);
530
531                                         // Close the activity if back has been pressed.
532                                         if (closeActivityAfterDismissingSnackbar) {
533                                             onBackPressed();
534                                         }
535                                     }
536                                 });
537
538                         //Show the Snackbar.
539                         bookmarksDeletedSnackbar.show();
540                         break;
541
542                     case R.id.context_menu_select_all_bookmarks:
543                         // Get the total number of bookmarks.
544                         int numberOfBookmarks = bookmarksListView.getCount();
545
546                         // Select them all.
547                         for (int i = 0; i < numberOfBookmarks; i++) {
548                             bookmarksListView.setItemChecked(i, true);
549                         }
550                         break;
551                 }
552
553                 // Consume the click.
554                 return true;
555             }
556
557             @Override
558             public void onDestroyActionMode(ActionMode mode) {
559                 // Do nothing.
560             }
561         });
562
563         // Get handles for the `FloatingActionButtons`.
564         FloatingActionButton createBookmarkFolderFab = findViewById(R.id.create_bookmark_folder_fab);
565         FloatingActionButton createBookmarkFab = findViewById(R.id.create_bookmark_fab);
566
567         // Set the create new bookmark folder FAB to display the `AlertDialog`.
568         createBookmarkFolderFab.setOnClickListener(v -> {
569             // Create a create bookmark folder dialog.
570             DialogFragment createBookmarkFolderDialog = CreateBookmarkFolderDialog.createBookmarkFolder(favoriteIconBitmap);
571
572             // Show the create bookmark folder dialog.
573             createBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.create_folder));
574         });
575
576         // Set the create new bookmark FAB to display the `AlertDialog`.
577         createBookmarkFab.setOnClickListener(view -> {
578             // Remove the incorrect lint warning below.
579             assert currentUrl != null;
580             assert currentTitle != null;
581
582             // Instantiate the create bookmark dialog.
583             DialogFragment createBookmarkDialog = CreateBookmarkDialog.createBookmark(currentUrl, currentTitle, favoriteIconBitmap);
584
585             // Display the create bookmark dialog.
586             createBookmarkDialog.show(getSupportFragmentManager(), getResources().getString(R.string.create_bookmark));
587         });
588
589         // Restore the state if the app has been restarted.
590         if (savedInstanceState != null) {
591             // Update the bookmarks list view after it has loaded.
592             bookmarksListView.post(() -> {
593                 // Get the checked bookmarks array list.
594                 ArrayList<Integer> checkedBookmarksArrayList = savedInstanceState.getIntegerArrayList(CHECKED_BOOKMARKS_ARRAY_LIST);
595
596                 // Check each previously checked bookmark in the list view.  When the minimum API >= 24 a `forEach()` command can be used instead.
597                 if (checkedBookmarksArrayList != null) {
598                     for (int i = 0; i < checkedBookmarksArrayList.size(); i++) {
599                         bookmarksListView.setItemChecked(checkedBookmarksArrayList.get(i), true);
600                     }
601                 }
602             });
603         }
604     }
605
606     @Override
607     public void onRestart() {
608         // Run the default commands.
609         super.onRestart();
610
611         // Update the list view if returning from the bookmarks database view activity.
612         if (restartFromBookmarksDatabaseViewActivity) {
613             // Load the current folder in the list view.
614             loadFolder();
615
616             // Reset `restartFromBookmarksDatabaseViewActivity`.
617             restartFromBookmarksDatabaseViewActivity = false;
618         }
619     }
620
621     @Override
622     public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
623         // Run the default commands.
624         super.onSaveInstanceState(savedInstanceState);
625
626         // Get the array of the checked items.
627         SparseBooleanArray checkedBookmarksSparseBooleanArray = bookmarksListView.getCheckedItemPositions();
628
629         // Create a checked items array list.
630         ArrayList<Integer> checkedBookmarksArrayList = new ArrayList<>();
631
632         // Add each checked bookmark position to the array list.
633         for (int i = 0; i < checkedBookmarksSparseBooleanArray.size(); i++) {
634             // 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.
635             if (checkedBookmarksSparseBooleanArray.valueAt(i)) {
636                 // Add the bookmark position to the checked bookmarks array list.
637                 checkedBookmarksArrayList.add(checkedBookmarksSparseBooleanArray.keyAt(i));
638             }
639         }
640
641         // Store the checked items array list in the saved instance state.
642         savedInstanceState.putIntegerArrayList(CHECKED_BOOKMARKS_ARRAY_LIST, checkedBookmarksArrayList);
643     }
644
645     @Override
646     public boolean onCreateOptionsMenu(Menu menu) {
647         // Inflate the menu.
648         getMenuInflater().inflate(R.menu.bookmarks_options_menu, menu);
649
650         // Success.
651         return true;
652     }
653
654     @Override
655     public boolean onOptionsItemSelected(MenuItem menuItem) {
656         switch (menuItem.getItemId()) {
657             case android.R.id.home:  // The home arrow is identified as `android.R.id.home`, not just `R.id.home`.
658                 if (currentFolder.isEmpty()) {  // Currently in the home folder.
659                     // Run the back commands.
660                     onBackPressed();
661                 } else {  // Currently in a subfolder.
662                     // Place the former parent folder in `currentFolder`.
663                     currentFolder = bookmarksDatabaseHelper.getParentFolderName(currentFolder);
664
665                     // Load the new folder.
666                     loadFolder();
667                 }
668                 break;
669
670             case R.id.options_menu_select_all_bookmarks:
671                 // Get the total number of bookmarks.
672                 int numberOfBookmarks = bookmarksListView.getCount();
673
674                 // Select them all.
675                 for (int i = 0; i < numberOfBookmarks; i++) {
676                     bookmarksListView.setItemChecked(i, true);
677                 }
678                 break;
679
680             case R.id.bookmarks_database_view:
681                 // Create an intent to launch the bookmarks database view activity.
682                 Intent bookmarksDatabaseViewIntent = new Intent(this, BookmarksDatabaseViewActivity.class);
683
684                 // Include the favorite icon byte array to the intent.
685                 bookmarksDatabaseViewIntent.putExtra("favorite_icon_byte_array", favoriteIconByteArray);
686
687                 // Make it so.
688                 startActivity(bookmarksDatabaseViewIntent);
689                 break;
690         }
691         return true;
692     }
693
694     @Override
695     public void onBackPressed() {
696         // 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.
697         if ((bookmarksDeletedSnackbar != null) && bookmarksDeletedSnackbar.isShown()) {  // Close the bookmarks deleted snackbar before going home.
698             // Set the close flag.
699             closeActivityAfterDismissingSnackbar = true;
700
701             // Dismiss the snackbar.
702             bookmarksDeletedSnackbar.dismiss();
703         } else {  // Go home immediately.
704             // Update the bookmarks folder for the bookmarks drawer in the main WebView activity.
705             MainWebViewActivity.currentBookmarksFolder = currentFolder;
706
707             // Close the bookmarks drawer and reload the bookmarks ListView when returning to the main WebView activity.
708             MainWebViewActivity.restartFromBookmarksActivity = true;
709
710             // Exit the bookmarks activity.
711             super.onBackPressed();
712         }
713     }
714
715     @Override
716     public void onCreateBookmark(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) {
717         // Get the alert dialog from the fragment.
718         Dialog dialog = dialogFragment.getDialog();
719
720         // Remove the incorrect lint warning below that the dialog might be null.
721         assert dialog != null;
722
723         // Get the views from the dialog fragment.
724         EditText createBookmarkNameEditText = dialog.findViewById(R.id.create_bookmark_name_edittext);
725         EditText createBookmarkUrlEditText = dialog.findViewById(R.id.create_bookmark_url_edittext);
726
727         // Extract the strings from the edit texts.
728         String bookmarkNameString = createBookmarkNameEditText.getText().toString();
729         String bookmarkUrlString = createBookmarkUrlEditText.getText().toString();
730
731         // Create a favorite icon byte array output stream.
732         ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
733
734         // Convert the favorite icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
735         favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
736
737         // Convert the favorite icon byte array stream to a byte array.
738         byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
739
740         // Display the new bookmark below the current items in the (0 indexed) list.
741         int newBookmarkDisplayOrder = bookmarksListView.getCount();
742
743         // Create the bookmark.
744         bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentFolder, newBookmarkDisplayOrder, favoriteIconByteArray);
745
746         // Update the bookmarks cursor with the current contents of this folder.
747         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
748
749         // Update the `ListView`.
750         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
751
752         // Scroll to the new bookmark.
753         bookmarksListView.setSelection(newBookmarkDisplayOrder);
754     }
755
756     @Override
757     public void onCreateBookmarkFolder(DialogFragment dialogFragment, @NonNull Bitmap favoriteIconBitmap) {
758         // Get the dialog from the dialog fragment.
759         Dialog dialog = dialogFragment.getDialog();
760
761         // Remove the incorrect lint warning below that the dialog might be null.
762         assert dialog != null;
763
764         // Get handles for the views in the dialog fragment.
765         EditText createFolderNameEditText = dialog.findViewById(R.id.create_folder_name_edittext);
766         RadioButton defaultFolderIconRadioButton = dialog.findViewById(R.id.create_folder_default_icon_radiobutton);
767         ImageView folderIconImageView = dialog.findViewById(R.id.create_folder_default_icon);
768
769         // Get new folder name string.
770         String folderNameString = createFolderNameEditText.getText().toString();
771
772         // Create a folder icon bitmap.
773         Bitmap folderIconBitmap;
774
775         // Set the folder icon bitmap according to the dialog.
776         if (defaultFolderIconRadioButton.isChecked()) {  // Use the default folder icon.
777             // Get the default folder icon drawable.
778             Drawable folderIconDrawable = folderIconImageView.getDrawable();
779
780             // Convert the folder icon drawable to a bitmap drawable.
781             BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
782
783             // Convert the folder icon bitmap drawable to a bitmap.
784             folderIconBitmap = folderIconBitmapDrawable.getBitmap();
785         } else {  // Use the WebView favorite icon.
786             // Copy the favorite icon bitmap to the folder icon bitmap.
787             folderIconBitmap = favoriteIconBitmap;
788         }
789
790         // Create a folder icon byte array output stream.
791         ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream();
792
793         // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
794         folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream);
795
796         // Convert the folder icon byte array stream to a byte array.
797         byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray();
798
799         // Move all the bookmarks down one in the display order.
800         for (int i = 0; i < bookmarksListView.getCount(); i++) {
801             int databaseId = (int) bookmarksListView.getItemIdAtPosition(i);
802             bookmarksDatabaseHelper.updateDisplayOrder(databaseId, i + 1);
803         }
804
805         // Create the folder, which will be placed at the top of the `ListView`.
806         bookmarksDatabaseHelper.createFolder(folderNameString, currentFolder, folderIconByteArray);
807
808         // Update the bookmarks cursor with the current contents of this folder.
809         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
810
811         // Update the `ListView`.
812         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
813
814         // Scroll to the new folder.
815         bookmarksListView.setSelection(0);
816     }
817
818     @Override
819     public void onSaveBookmark(DialogFragment dialogFragment, int selectedBookmarkDatabaseId, @NonNull Bitmap favoriteIconBitmap) {
820         // Get the dialog from the dialog fragment.
821         Dialog dialog = dialogFragment.getDialog();
822
823         // Remove the incorrect lint warning below that the dialog might be null.
824         assert dialog != null;
825
826         // Get handles for the views from `dialogFragment`.
827         EditText editBookmarkNameEditText = dialog.findViewById(R.id.edit_bookmark_name_edittext);
828         EditText editBookmarkUrlEditText = dialog.findViewById(R.id.edit_bookmark_url_edittext);
829         RadioButton currentBookmarkIconRadioButton = dialog.findViewById(R.id.edit_bookmark_current_icon_radiobutton);
830
831         // Store the bookmark strings.
832         String bookmarkNameString = editBookmarkNameEditText.getText().toString();
833         String bookmarkUrlString = editBookmarkUrlEditText.getText().toString();
834
835         // Update the bookmark.
836         if (currentBookmarkIconRadioButton.isChecked()) {  // Update the bookmark without changing the favorite icon.
837             bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString);
838         } else {  // Update the bookmark using the WebView favorite icon.
839             // Create a favorite icon byte array output stream.
840             ByteArrayOutputStream newFavoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
841
842             // Convert the favorite icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
843             favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFavoriteIconByteArrayOutputStream);
844
845             // Convert the favorite icon byte array stream to a byte array.
846             byte[] newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray();
847
848             //  Update the bookmark and the favorite icon.
849             bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, newFavoriteIconByteArray);
850         }
851
852         // Close the contextual action mode.
853         contextualActionMode.finish();
854
855         // Update the bookmarks cursor with the contents of the current folder.
856         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
857
858         // Update the `ListView`.
859         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
860     }
861
862     @Override
863     public void onSaveBookmarkFolder(DialogFragment dialogFragment, int selectedFolderDatabaseId, Bitmap favoriteIconBitmap) {
864         // Get the dialog from the dialog fragment.
865         Dialog dialog = dialogFragment.getDialog();
866
867         // Remove the incorrect lint warning below that the dialog might be null.
868         assert dialog != null;
869
870         // Get handles for the views from `dialogFragment`.
871         RadioButton currentFolderIconRadioButton = dialog.findViewById(R.id.edit_folder_current_icon_radiobutton);
872         RadioButton defaultFolderIconRadioButton = dialog.findViewById(R.id.edit_folder_default_icon_radiobutton);
873         ImageView defaultFolderIconImageView = dialog.findViewById(R.id.edit_folder_default_icon_imageview);
874         EditText editFolderNameEditText = dialog.findViewById(R.id.edit_folder_name_edittext);
875
876         // Get the new folder name.
877         String newFolderNameString = editFolderNameEditText.getText().toString();
878
879         // Check if the favorite icon has changed.
880         if (currentFolderIconRadioButton.isChecked()) {  // Only the name has changed.
881             // Update the name in the database.
882             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString);
883         } else if (!currentFolderIconRadioButton.isChecked() && newFolderNameString.equals(oldFolderNameString)) {  // Only the icon has changed.
884             // Create the new folder icon Bitmap.
885             Bitmap folderIconBitmap;
886
887             // Populate the new folder icon bitmap.
888             if (defaultFolderIconRadioButton.isChecked()) {
889                 // Get the default folder icon drawable.
890                 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
891
892                 // Convert the folder icon drawable to a bitmap drawable.
893                 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
894
895                 // Convert the folder icon bitmap drawable to a bitmap.
896                 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
897             } else {  // Use the WebView favorite icon.
898                 // Copy the favorite icon bitmap to the folder icon bitmap.
899                 folderIconBitmap = favoriteIconBitmap;
900             }
901
902             // Create a folder icon byte array output stream.
903             ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
904
905             // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
906             folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
907
908             // Convert the folder icon byte array stream to a byte array.
909             byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
910
911             // Update the folder icon in the database.
912             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderIconByteArray);
913         } else {  // The folder icon and the name have changed.
914             // Instantiate the new folder icon `Bitmap`.
915             Bitmap folderIconBitmap;
916
917             // Populate the new folder icon bitmap.
918             if (defaultFolderIconRadioButton.isChecked()) {
919                 // Get the default folder icon drawable.
920                 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
921
922                 // Convert the folder icon drawable to a bitmap drawable.
923                 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
924
925                 // Convert the folder icon bitmap drawable to a bitmap.
926                 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
927             } else {  // Use the WebView favorite icon.
928                 // Copy the favorite icon bitmap to the folder icon bitmap.
929                 folderIconBitmap = favoriteIconBitmap;
930             }
931
932             // Create a folder icon byte array output stream.
933             ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
934
935             // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
936             folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
937
938             // Convert the folder icon byte array stream to a byte array.
939             byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
940
941             // Update the folder name and icon in the database.
942             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, newFolderIconByteArray);
943         }
944
945         // Update the bookmarks cursor with the current contents of this folder.
946         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
947
948         // Update the `ListView`.
949         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
950
951         // Close the contextual action mode.
952         contextualActionMode.finish();
953     }
954
955     @Override
956     public void onMoveToFolder(DialogFragment dialogFragment) {
957         // Get the dialog from the dialog fragment.
958         Dialog dialog = dialogFragment.getDialog();
959
960         // Remove the incorrect lint warning below that the dialog might be null.
961         assert dialog != null;
962
963         // Get a handle for the list view from the dialog.
964         ListView folderListView = dialog.findViewById(R.id.move_to_folder_listview);
965
966         // Store a long array of the selected folders.
967         long[] newFolderLongArray = folderListView.getCheckedItemIds();
968
969         // Get the new folder database ID.  Only one folder will be selected.
970         int newFolderDatabaseId = (int) newFolderLongArray[0];
971
972         // Instantiate `newFolderName`.
973         String newFolderName;
974
975         // Set the new folder name.
976         if (newFolderDatabaseId == 0) {
977             // The new folder is the home folder, represented as `""` in the database.
978             newFolderName = "";
979         } else {
980             // Get the new folder name from the database.
981             newFolderName = bookmarksDatabaseHelper.getFolderName(newFolderDatabaseId);
982         }
983
984         // Get a long array with the the database ID of the selected bookmarks.
985         long[] selectedBookmarksLongArray = bookmarksListView.getCheckedItemIds();
986
987         // Move each of the selected bookmarks to the new folder.
988         for (long databaseIdLong : selectedBookmarksLongArray) {
989             // Get `databaseIdInt` for each selected bookmark.
990             int databaseIdInt = (int) databaseIdLong;
991
992             // Move the selected bookmark to the new folder.
993             bookmarksDatabaseHelper.moveToFolder(databaseIdInt, newFolderName);
994         }
995
996         // Update the bookmarks cursor with the current contents of this folder.
997         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
998
999         // Update the `ListView`.
1000         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
1001
1002         // Close the contextual app bar.
1003         contextualActionMode.finish();
1004     }
1005
1006     private void deleteBookmarkFolderContents(int databaseId) {
1007         // Get the name of the folder.
1008         String folderName = bookmarksDatabaseHelper.getFolderName(databaseId);
1009
1010         // Get the contents of the folder.
1011         Cursor folderCursor = bookmarksDatabaseHelper.getBookmarkIDs(folderName);
1012
1013         // Delete each of the bookmarks in the folder.
1014         for (int i = 0; i < folderCursor.getCount(); i++) {
1015             // Move `folderCursor` to the current row.
1016             folderCursor.moveToPosition(i);
1017
1018             // Get the database ID of the item.
1019             int itemDatabaseId = folderCursor.getInt(folderCursor.getColumnIndex(BookmarksDatabaseHelper._ID));
1020
1021             // If this is a folder, recursively delete the contents first.
1022             if (bookmarksDatabaseHelper.isFolder(itemDatabaseId)) {
1023                 deleteBookmarkFolderContents(itemDatabaseId);
1024             }
1025
1026             // Delete the bookmark.
1027             bookmarksDatabaseHelper.deleteBookmark(itemDatabaseId);
1028         }
1029     }
1030
1031     private void updateMoveIcons() {
1032         // Get a long array of the selected bookmarks.
1033         long[] selectedBookmarksLongArray = bookmarksListView.getCheckedItemIds();
1034
1035         // Get the database IDs for the first, last, and selected bookmarks.
1036         int selectedBookmarkDatabaseId = (int) selectedBookmarksLongArray[0];
1037         int firstBookmarkDatabaseId = (int) bookmarksListView.getItemIdAtPosition(0);
1038         // bookmarksListView is 0 indexed.
1039         int lastBookmarkDatabaseId = (int) bookmarksListView.getItemIdAtPosition(bookmarksListView.getCount() - 1);
1040
1041         // Get the current theme status.
1042         int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
1043
1044         // Update the move bookmark up `MenuItem`.
1045         if (selectedBookmarkDatabaseId == firstBookmarkDatabaseId) {  // The selected bookmark is in the first position.
1046             // Disable the move bookmark up `MenuItem`.
1047             moveBookmarkUpMenuItem.setEnabled(false);
1048
1049             //  Set the move bookmark up icon to be ghosted.
1050             moveBookmarkUpMenuItem.setIcon(R.drawable.move_up_disabled);
1051         } else {  // The selected bookmark is not in the first position.
1052             // Enable the move bookmark up menu item.
1053             moveBookmarkUpMenuItem.setEnabled(true);
1054
1055             // Set the icon according to the theme.
1056             if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) {
1057                 moveBookmarkUpMenuItem.setIcon(R.drawable.move_up_enabled_night);
1058             } else {
1059                 moveBookmarkUpMenuItem.setIcon(R.drawable.move_up_enabled_day);
1060             }
1061         }
1062
1063         // Update the move bookmark down `MenuItem`.
1064         if (selectedBookmarkDatabaseId == lastBookmarkDatabaseId) {  // The selected bookmark is in the last position.
1065             // Disable the move bookmark down `MenuItem`.
1066             moveBookmarkDownMenuItem.setEnabled(false);
1067
1068             // Set the move bookmark down icon to be ghosted.
1069             moveBookmarkDownMenuItem.setIcon(R.drawable.move_down_disabled);
1070         } else {  // The selected bookmark is not in the last position.
1071             // Enable the move bookmark down `MenuItem`.
1072             moveBookmarkDownMenuItem.setEnabled(true);
1073
1074             // Set the icon according to the theme.
1075             if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) {
1076                 moveBookmarkDownMenuItem.setIcon(R.drawable.move_down_enabled_night);
1077             } else {
1078                 moveBookmarkDownMenuItem.setIcon(R.drawable.move_down_enabled_day);
1079             }
1080         }
1081     }
1082
1083     private void scrollBookmarks(int selectedBookmarkPosition) {
1084         // Get the first and last visible bookmark positions.
1085         int firstVisibleBookmarkPosition = bookmarksListView.getFirstVisiblePosition();
1086         int lastVisibleBookmarkPosition = bookmarksListView.getLastVisiblePosition();
1087
1088         // Calculate the number of bookmarks per screen.
1089         int numberOfBookmarksPerScreen = lastVisibleBookmarkPosition - firstVisibleBookmarkPosition;
1090
1091         // Scroll with the moved bookmark if necessary.
1092         if (selectedBookmarkPosition <= firstVisibleBookmarkPosition) {  // The selected bookmark position is at or above the top of the screen.
1093             // Scroll to the selected bookmark position.
1094             bookmarksListView.setSelection(selectedBookmarkPosition);
1095         } else if (selectedBookmarkPosition >= (lastVisibleBookmarkPosition - 1)) {  // The selected bookmark is at or below the bottom of the screen.
1096             // 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.
1097             // `+1` assures that the entire bookmark will be displayed in situations where only a partial bookmark fits at the bottom of the list view.
1098             bookmarksListView.setSelection(selectedBookmarkPosition - numberOfBookmarksPerScreen + 1);
1099         }
1100     }
1101
1102     private void loadFolder() {
1103         // Update bookmarks cursor with the contents of the bookmarks database for the current folder.
1104         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
1105
1106         // Setup a `CursorAdapter`.  `this` specifies the `Context`.  `false` disables `autoRequery`.
1107         bookmarksCursorAdapter = new CursorAdapter(this, bookmarksCursor, false) {
1108             @Override
1109             public View newView(Context context, Cursor cursor, ViewGroup parent) {
1110                 // Inflate the individual item layout.  `false` does not attach it to the root.
1111                 return getLayoutInflater().inflate(R.layout.bookmarks_activity_item_linearlayout, parent, false);
1112             }
1113
1114             @Override
1115             public void bindView(View view, Context context, Cursor cursor) {
1116                 // Get handles for the views.
1117                 ImageView bookmarkFavoriteIcon = view.findViewById(R.id.bookmark_favorite_icon);
1118                 TextView bookmarkNameTextView = view.findViewById(R.id.bookmark_name);
1119
1120                 // Get the favorite icon byte array from the `Cursor`.
1121                 byte[] favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON));
1122
1123                 // Convert the byte array to a `Bitmap` beginning at the first byte and ending at the last.
1124                 Bitmap favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.length);
1125
1126                 // Display the bitmap in `bookmarkFavoriteIcon`.
1127                 bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap);
1128
1129                 // Get the bookmark name from the cursor and display it in `bookmarkNameTextView`.
1130                 String bookmarkNameString = cursor.getString(cursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
1131                 bookmarkNameTextView.setText(bookmarkNameString);
1132
1133                 // Make the font bold for folders.
1134                 if (cursor.getInt(cursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {
1135                     bookmarkNameTextView.setTypeface(Typeface.DEFAULT_BOLD);
1136                 } else {  // Reset the font to default for normal bookmarks.
1137                     bookmarkNameTextView.setTypeface(Typeface.DEFAULT);
1138                 }
1139             }
1140         };
1141
1142         // Populate the list view with the adapter.
1143         bookmarksListView.setAdapter(bookmarksCursorAdapter);
1144
1145         // Set the `AppBar` title.
1146         if (currentFolder.isEmpty()) {
1147             appBar.setTitle(R.string.bookmarks);
1148         } else {
1149             appBar.setTitle(currentFolder);
1150         }
1151     }
1152
1153     @Override
1154     public void onDestroy() {
1155         // Close the bookmarks cursor and database.
1156         bookmarksCursor.close();
1157         bookmarksDatabaseHelper.close();
1158
1159         // Run the default commands.
1160         super.onDestroy();
1161     }
1162 }