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