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