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