Display the folder icons in the spinners in the bookmarks database view activity...
[PrivacyBrowser.git] / app / src / main / java / com / stoutner / privacybrowser / activities / BookmarksDatabaseViewActivity.java
1 /*
2  * Copyright © 2016-2019 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.content.Context;
23 import android.database.Cursor;
24 import android.database.MatrixCursor;
25 import android.database.MergeCursor;
26 import android.graphics.Bitmap;
27 import android.graphics.BitmapFactory;
28 import android.graphics.Typeface;
29 import android.graphics.drawable.BitmapDrawable;
30 import android.graphics.drawable.Drawable;
31 import android.os.Bundle;
32 import android.support.design.widget.Snackbar;
33 import android.support.v4.content.ContextCompat;
34 import android.support.v4.widget.CursorAdapter;
35 import android.support.v4.widget.ResourceCursorAdapter;
36 import android.support.v7.app.ActionBar;
37 import android.support.v7.app.AppCompatActivity;
38 // `AppCompatDialogFragment` is required instead of `DialogFragment` or an error is produced on API <=22.
39 import android.support.v7.app.AppCompatDialogFragment;
40 import android.support.v7.widget.Toolbar;
41 import android.util.SparseBooleanArray;
42 import android.view.ActionMode;
43 import android.view.Menu;
44 import android.view.MenuItem;
45 import android.view.View;
46 import android.view.ViewGroup;
47 import android.view.WindowManager;
48 import android.widget.AbsListView;
49 import android.widget.AdapterView;
50 import android.widget.EditText;
51 import android.widget.ImageView;
52 import android.widget.ListView;
53 import android.widget.RadioButton;
54 import android.widget.Spinner;
55 import android.widget.TextView;
56
57 import com.stoutner.privacybrowser.R;
58 import com.stoutner.privacybrowser.dialogs.EditBookmarkDatabaseViewDialog;
59 import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDatabaseViewDialog;
60 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper;
61
62 import java.io.ByteArrayOutputStream;
63 import java.util.Arrays;
64
65 public class BookmarksDatabaseViewActivity extends AppCompatActivity implements EditBookmarkDatabaseViewDialog.EditBookmarkDatabaseViewListener,
66         EditBookmarkFolderDatabaseViewDialog.EditBookmarkFolderDatabaseViewListener {
67     // Instantiate the constants.
68     private static final int ALL_FOLDERS_DATABASE_ID = -2;
69     private static final int HOME_FOLDER_DATABASE_ID = -1;
70
71     // `bookmarksDatabaseHelper` is used in `onCreate()`, `updateBookmarksListView()`, `selectAllBookmarksInFolder()`, and `onDestroy()`.
72     private BookmarksDatabaseHelper bookmarksDatabaseHelper;
73
74     // `bookmarksCursor` is used in `onCreate()`, `updateBookmarksListView()`, `onSaveBookmark()`, `onSaveBookmarkFolder()`, and `onDestroy()`.
75     private Cursor bookmarksCursor;
76
77     // `bookmarksCursorAdapter` is used in `onCreate()`, `selectAllBookmarksInFolder()`, and `updateBookmarksListView()`.
78     private CursorAdapter bookmarksCursorAdapter;
79
80     // `oldFolderNameString` is used in `onCreate()` and `onSaveBookmarkFolder()`.
81     private String oldFolderNameString;
82
83     // `currentFolderDatabaseId` is used in `onCreate()`, `updateBookmarksListView()`, `onSaveBookmark()`, and `onSaveBookmarkFolder()`.
84     private int currentFolderDatabaseId;
85
86     // `currentFolder` is used in `onCreate()`, `onSaveBookmark()`, and `onSaveBookmarkFolder()`.
87     private String currentFolderName;
88
89     // `sortByDisplayOrder` is used in `onCreate()`, `onOptionsItemSelected()`, and `updateBookmarksListView()`.
90     private boolean sortByDisplayOrder;
91
92     // `bookmarksDeletedSnackbar` is used in `onCreate()`, `onOptionsItemSelected()`, and `onBackPressed()`.
93     private Snackbar bookmarksDeletedSnackbar;
94
95     // `closeActivityAfterDismissingSnackbar` is used in `onCreate()`, `onOptionsItemSelected()`, and `onBackPressed()`.
96     private boolean closeActivityAfterDismissingSnackbar;
97
98     @Override
99     public void onCreate(Bundle savedInstanceState) {
100         // Disable screenshots if not allowed.
101         if (!MainWebViewActivity.allowScreenshots) {
102             getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
103         }
104
105         // Set the activity theme.
106         if (MainWebViewActivity.darkTheme) {
107             setTheme(R.style.PrivacyBrowserDark_SecondaryActivity);
108         } else {
109             setTheme(R.style.PrivacyBrowserLight_SecondaryActivity);
110         }
111
112         // Run the default commands.
113         super.onCreate(savedInstanceState);
114
115         // Set the content view.
116         setContentView(R.layout.bookmarks_databaseview_coordinatorlayout);
117
118         // The `SupportActionBar` from `android.support.v7.app.ActionBar` must be used until the minimum API is >= 21.
119         Toolbar bookmarksDatabaseViewAppBar = findViewById(R.id.bookmarks_databaseview_toolbar);
120         setSupportActionBar(bookmarksDatabaseViewAppBar);
121
122         // Get a handle for the `AppBar`.
123         ActionBar appBar = getSupportActionBar();
124
125         // Remove the incorrect warning in Android Studio that `appBar` might be null.
126         assert appBar != null;
127
128         // Display the spinner and the back arrow in the app bar.
129         appBar.setCustomView(R.layout.spinner);
130         appBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_HOME_AS_UP);
131
132         // Initialize the database handler.  The `0` is to specify a database version, but that is set instead using a constant in `BookmarksDatabaseHelper`.
133         bookmarksDatabaseHelper = new BookmarksDatabaseHelper(this, null, null, 0);
134
135         // Setup a matrix cursor for "All Folders" and "Home Folder".
136         String[] matrixCursorColumnNames = {BookmarksDatabaseHelper._ID, BookmarksDatabaseHelper.BOOKMARK_NAME};
137         MatrixCursor matrixCursor = new MatrixCursor(matrixCursorColumnNames);
138         matrixCursor.addRow(new Object[]{ALL_FOLDERS_DATABASE_ID, getString(R.string.all_folders)});
139         matrixCursor.addRow(new Object[]{HOME_FOLDER_DATABASE_ID, getString(R.string.home_folder)});
140
141         // Get a cursor with the list of all the folders.
142         Cursor foldersCursor = bookmarksDatabaseHelper.getAllFolders();
143
144         // Combine `matrixCursor` and `foldersCursor`.
145         MergeCursor foldersMergeCursor = new MergeCursor(new Cursor[]{matrixCursor, foldersCursor});
146
147
148         // Get the default folder bitmap.  `ContextCompat` must be used until the minimum API >= 21.
149         Drawable defaultFolderDrawable = ContextCompat.getDrawable(getApplicationContext(), R.drawable.folder_blue_bitmap);
150
151         // Cast the default folder drawable to a `BitmapDrawable`.
152         BitmapDrawable defaultFolderBitmapDrawable = (BitmapDrawable) defaultFolderDrawable;
153
154         // Remove the incorrect lint warning that `.getBitmap()` might be null.
155         assert defaultFolderBitmapDrawable != null;
156
157         // Convert the default folder `BitmapDrawable` to a bitmap.
158         Bitmap defaultFolderBitmap = defaultFolderBitmapDrawable.getBitmap();
159
160
161         // Create a resource cursor adapter for the spinner.
162         ResourceCursorAdapter foldersCursorAdapter = new ResourceCursorAdapter(this, R.layout.appbar_spinner_item, foldersMergeCursor, 0) {
163             @Override
164             public void bindView(View view, Context context, Cursor cursor) {
165                 // Get handles for the spinner views.
166                 ImageView spinnerItemImageView = view.findViewById(R.id.spinner_item_imageview);
167                 TextView spinnerItemTextView = view.findViewById(R.id.spinner_item_textview);
168
169                 // Set the folder icon according to the type.
170                 if (foldersMergeCursor.getPosition() > 1) {  // Set a user folder icon.
171                     // Initialize a default folder icon byte array output stream.
172                     ByteArrayOutputStream defaultFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
173
174                     // Covert the default folder bitmap to a PNG and store it in the output stream.  `0` is for lossless compression (the only option for a PNG).
175                     defaultFolderBitmap.compress(Bitmap.CompressFormat.PNG, 0, defaultFolderIconByteArrayOutputStream);
176
177                     // Convert the default folder icon output stream to a byte array.
178                     byte[] defaultFolderIconByteArray = defaultFolderIconByteArrayOutputStream.toByteArray();
179
180
181                     // Get the folder icon byte array from the cursor.
182                     byte[] folderIconByteArray = cursor.getBlob(cursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON));
183
184                     // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
185                     Bitmap folderIconBitmap = BitmapFactory.decodeByteArray(folderIconByteArray, 0, folderIconByteArray.length);
186
187
188                     // Set the icon according to the type.
189                     if (Arrays.equals(folderIconByteArray, defaultFolderIconByteArray)) {  // The default folder icon is used.
190                         // Set a smaller and darker folder icon, which works well with the spinner.
191                         spinnerItemImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.folder_dark_blue));
192                     } else {  // A custom folder icon is uses.
193                         // Set the folder image stored in the cursor.
194                         spinnerItemImageView.setImageBitmap(folderIconBitmap);
195                     }
196                 } else {  // Set the `All Folders` or `Home Folder` icon.
197                     // Set the gray folder image.  `ContextCompat` must be used until the minimum API >= 21.
198                     spinnerItemImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.folder_gray));
199                 }
200
201                 // Set the text view to display the folder name.
202                 spinnerItemTextView.setText(cursor.getString(cursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME)));
203             }
204         };
205
206         // Set the resource cursor adapter drop drown view resource.
207         foldersCursorAdapter.setDropDownViewResource(R.layout.appbar_spinner_dropdown_item);
208
209         // Get a handle for the folder spinner and set the adapter.
210         Spinner folderSpinner = findViewById(R.id.spinner);
211         folderSpinner.setAdapter(foldersCursorAdapter);
212
213         // Handle taps on the spinner dropdown.
214         folderSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
215             @Override
216             public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
217                 // Store the current folder database ID.
218                 currentFolderDatabaseId = (int) id;
219
220                 // Get a handle for the selected view.
221                 TextView selectedFolderTextView = findViewById(R.id.spinner_item_textview);
222
223                 // Store the current folder name.
224                 currentFolderName = selectedFolderTextView.getText().toString();
225
226                 // Update the list view.
227                 updateBookmarksListView();
228             }
229
230             @Override
231             public void onNothingSelected(AdapterView<?> parent) {
232                 // Do nothing.
233             }
234         });
235
236         // Get a handle for the bookmarks `ListView`.
237         ListView bookmarksListView = findViewById(R.id.bookmarks_databaseview_listview);
238
239         // Get a `Cursor` with the current contents of the bookmarks database.
240         bookmarksCursor = bookmarksDatabaseHelper.getAllBookmarks();
241
242         // Setup a `CursorAdapter` with `this` context.  `false` disables autoRequery.
243         bookmarksCursorAdapter = new CursorAdapter(this, bookmarksCursor, false) {
244             @Override
245             public View newView(Context context, Cursor cursor, ViewGroup parent) {
246                 // Inflate the individual item layout.  `false` does not attach it to the root.
247                 return getLayoutInflater().inflate(R.layout.bookmarks_databaseview_item_linearlayout, parent, false);
248             }
249
250             @Override
251             public void bindView(View view, Context context, Cursor cursor) {
252                 boolean isFolder = (cursor.getInt(cursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1);
253
254                 // Get the database ID from the `Cursor` and display it in `bookmarkDatabaseIdTextView`.
255                 int bookmarkDatabaseId = cursor.getInt(cursor.getColumnIndex(BookmarksDatabaseHelper._ID));
256                 TextView bookmarkDatabaseIdTextView = view.findViewById(R.id.bookmarks_databaseview_database_id);
257                 bookmarkDatabaseIdTextView.setText(String.valueOf(bookmarkDatabaseId));
258
259                 // Get the favorite icon byte array from the `Cursor`.
260                 byte[] favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON));
261                 // Convert the byte array to a `Bitmap` beginning at the beginning at the first byte and ending at the last.
262                 Bitmap favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.length);
263                 // Display the bitmap in `bookmarkFavoriteIcon`.
264                 ImageView bookmarkFavoriteIcon = view.findViewById(R.id.bookmarks_databaseview_favorite_icon);
265                 bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap);
266
267                 // Get the bookmark name from the `Cursor` and display it in `bookmarkNameTextView`.
268                 String bookmarkNameString = cursor.getString(cursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
269                 TextView bookmarkNameTextView = view.findViewById(R.id.bookmarks_databaseview_bookmark_name);
270                 bookmarkNameTextView.setText(bookmarkNameString);
271
272                 // Make the font bold for folders.
273                 if (isFolder) {
274                     // The first argument is `null` prevent changing of the font.
275                     bookmarkNameTextView.setTypeface(null, Typeface.BOLD);
276                 } else {  // Reset the font to default.
277                     bookmarkNameTextView.setTypeface(Typeface.DEFAULT);
278                 }
279
280                 // Get the bookmark URL form the `Cursor` and display it in `bookmarkUrlTextView`.
281                 String bookmarkUrlString = cursor.getString(cursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_URL));
282                 TextView bookmarkUrlTextView = view.findViewById(R.id.bookmarks_databaseview_bookmark_url);
283                 bookmarkUrlTextView.setText(bookmarkUrlString);
284
285                 // Hide the URL if the bookmark is a folder.
286                 if (isFolder) {
287                     bookmarkUrlTextView.setVisibility(View.GONE);
288                 } else {
289                     bookmarkUrlTextView.setVisibility(View.VISIBLE);
290                 }
291
292                 // Get the display order from the `Cursor` and display it in `bookmarkDisplayOrderTextView`.
293                 int bookmarkDisplayOrder = cursor.getInt(cursor.getColumnIndex(BookmarksDatabaseHelper.DISPLAY_ORDER));
294                 TextView bookmarkDisplayOrderTextView = view.findViewById(R.id.bookmarks_databaseview_display_order);
295                 bookmarkDisplayOrderTextView.setText(String.valueOf(bookmarkDisplayOrder));
296
297                 // Get the parent folder from the `Cursor` and display it in `bookmarkParentFolder`.
298                 String bookmarkParentFolder = cursor.getString(cursor.getColumnIndex(BookmarksDatabaseHelper.PARENT_FOLDER));
299                 ImageView parentFolderImageView = view.findViewById(R.id.bookmarks_databaseview_parent_folder_icon);
300                 TextView bookmarkParentFolderTextView = view.findViewById(R.id.bookmarks_databaseview_parent_folder);
301
302                 // Make the folder name gray if it is the home folder.
303                 if (bookmarkParentFolder.isEmpty()) {
304                     parentFolderImageView.setImageDrawable(ContextCompat.getDrawable(getApplicationContext(), R.drawable.folder_gray));
305                     bookmarkParentFolderTextView.setText(R.string.home_folder);
306                     bookmarkParentFolderTextView.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.gray_500));
307                 } else {
308                     parentFolderImageView.setImageDrawable(ContextCompat.getDrawable(getApplicationContext(), R.drawable.folder_dark_blue));
309                     bookmarkParentFolderTextView.setText(bookmarkParentFolder);
310
311                     // Set the text color according to the theme.
312                     if (MainWebViewActivity.darkTheme) {
313                         bookmarkParentFolderTextView.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.gray_300));
314                     } else {
315                         bookmarkParentFolderTextView.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.black));
316                     }
317                 }
318             }
319         };
320
321         // Update the ListView.
322         bookmarksListView.setAdapter(bookmarksCursorAdapter);
323
324         // Set the current folder database ID.
325         currentFolderDatabaseId = ALL_FOLDERS_DATABASE_ID;
326
327         // Set a listener to edit a bookmark when it is tapped.
328         bookmarksListView.setOnItemClickListener((AdapterView<?> parent, View view, int position, long id) -> {
329             // Convert the database ID to an int.
330             int databaseId = (int) id;
331
332             // Show the edit bookmark or edit bookmark folder dialog.
333             if (bookmarksDatabaseHelper.isFolder(databaseId)) {
334                 // Save the current folder name, which is used in `onSaveBookmarkFolder()`.
335                 oldFolderNameString = bookmarksCursor.getString(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
336
337                 // Show the edit bookmark folder dialog.
338                 AppCompatDialogFragment editBookmarkFolderDatabaseViewDialog = EditBookmarkFolderDatabaseViewDialog.folderDatabaseId(databaseId);
339                 editBookmarkFolderDatabaseViewDialog.show(getSupportFragmentManager(), getResources().getString(R.string.edit_folder));
340             } else {
341                 // Show the edit bookmark dialog.
342                 AppCompatDialogFragment editBookmarkDatabaseViewDialog = EditBookmarkDatabaseViewDialog.bookmarkDatabaseId(databaseId);
343                 editBookmarkDatabaseViewDialog.show(getSupportFragmentManager(), getResources().getString(R.string.edit_bookmark));
344             }
345         });
346
347         // Handle long presses on the list view.
348         bookmarksListView.setMultiChoiceModeListener(new AbsListView.MultiChoiceModeListener() {
349             // Instantiate the common variables.
350             MenuItem selectAllMenuItem;
351             MenuItem deleteMenuItem;
352             boolean deletingBookmarks;
353
354             @Override
355             public boolean onCreateActionMode(ActionMode mode, Menu menu) {
356                 // Inflate the menu for the contextual app bar.
357                 getMenuInflater().inflate(R.menu.bookmarks_databaseview_context_menu, menu);
358
359                 // Set the title.
360                 mode.setTitle(R.string.bookmarks);
361
362                 // Get handles for the menu items.
363                 selectAllMenuItem = menu.findItem(R.id.select_all);
364                 deleteMenuItem = menu.findItem(R.id.delete);
365
366                 // Disable the delete menu item if a delete is pending.
367                 deleteMenuItem.setEnabled(!deletingBookmarks);
368
369                 // Make it so.
370                 return true;
371             }
372
373             @Override
374             public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
375                 // Do nothing.
376                 return false;
377             }
378
379             @Override
380             public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
381                 // Calculate the number of selected bookmarks.
382                 int numberOfSelectedBookmarks = bookmarksListView.getCheckedItemCount();
383
384                 // Adjust the ActionMode according to the number of selected bookmarks.
385                 mode.setSubtitle(getString(R.string.selected) + "  " + numberOfSelectedBookmarks);
386
387                 // Do not show the select all menu item if all the bookmarks are already checked.
388                 if (bookmarksListView.getCheckedItemCount() == bookmarksListView.getCount()) {
389                     selectAllMenuItem.setVisible(false);
390                 } else {
391                     selectAllMenuItem.setVisible(true);
392                 }
393
394                 // Convert the database ID to an int.
395                 int databaseId = (int) id;
396
397                 // If a folder was selected, also select all the contents.
398                 if (checked && bookmarksDatabaseHelper.isFolder(databaseId)) {
399                     selectAllBookmarksInFolder(databaseId);
400                 }
401
402                 // Do not allow a bookmark to be deselected if the folder is selected.
403                 if (!checked) {
404                     // Get the folder name.
405                     String folderName = bookmarksDatabaseHelper.getParentFolderName((int) id);
406
407                     // If the bookmark is not in the root folder, check to see if the folder is selected.
408                     if (!folderName.isEmpty()) {
409                         // Get the database ID of the folder.
410                         int folderDatabaseId = bookmarksDatabaseHelper.getFolderDatabaseId(folderName);
411
412                         // Move the bookmarks cursor to the first position.
413                         bookmarksCursor.moveToFirst();
414
415                         // Initialize the folder position variable.
416                         int folderPosition = -1;
417
418                         // Get the position of the folder in the bookmarks cursor.
419                         while ((folderPosition < 0) && (bookmarksCursor.getPosition() < bookmarksCursor.getCount())) {
420                             // Check if the folder database ID matches the bookmark database ID.
421                             if (folderDatabaseId == bookmarksCursor.getInt((bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper._ID)))) {
422                                 // Get the folder position.
423                                 folderPosition = bookmarksCursor.getPosition();
424
425                                 // Check if the folder is selected.
426                                 if (bookmarksListView.isItemChecked(folderPosition)) {
427                                     // Reselect the bookmark.
428                                     bookmarksListView.setItemChecked(position, true);
429
430                                     // Display a snackbar explaining why the bookmark cannot be deselected.
431                                     Snackbar.make(bookmarksListView, R.string.cannot_deselect_bookmark, Snackbar.LENGTH_LONG).show();
432                                 }
433                             }
434
435                             // Increment the bookmarks cursor.
436                             bookmarksCursor.moveToNext();
437                         }
438                     }
439                 }
440             }
441
442             @Override
443             public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
444                 switch (item.getItemId()) {
445                     case R.id.select_all:
446                         // Get the total number of bookmarks.
447                         int numberOfBookmarks = bookmarksListView.getCount();
448
449                         // Select them all.
450                         for (int i = 0; i < numberOfBookmarks; i++) {
451                             bookmarksListView.setItemChecked(i, true);
452                         }
453                         break;
454
455                     case R.id.delete:
456                         // Set the deleting bookmarks flag, which prevents the delete menu item from being enabled until the current process finishes.
457                         deletingBookmarks = true;
458
459                         // Get an array of the selected row IDs.
460                         long[] selectedBookmarksIdsLongArray = bookmarksListView.getCheckedItemIds();
461
462                         // 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.
463                         SparseBooleanArray selectedBookmarksPositionsSparseBooleanArray = bookmarksListView.getCheckedItemPositions().clone();
464
465                         // Update the bookmarks cursor with the current contents of the bookmarks database except for the specified database IDs.
466                         switch (currentFolderDatabaseId) {
467                             // Get a cursor with all the folders.
468                             case ALL_FOLDERS_DATABASE_ID:
469                                 if (sortByDisplayOrder) {
470                                     bookmarksCursor = bookmarksDatabaseHelper.getAllBookmarksByDisplayOrderExcept(selectedBookmarksIdsLongArray);
471                                 } else {
472                                     bookmarksCursor = bookmarksDatabaseHelper.getAllBookmarksExcept(selectedBookmarksIdsLongArray);
473                                 }
474                                 break;
475
476                             // Get a cursor for the home folder.
477                             case HOME_FOLDER_DATABASE_ID:
478                                 if (sortByDisplayOrder) {
479                                     bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrderExcept(selectedBookmarksIdsLongArray, "");
480                                 } else {
481                                     bookmarksCursor = bookmarksDatabaseHelper.getBookmarksExcept(selectedBookmarksIdsLongArray, "");
482                                 }
483                                 break;
484
485                             // Display the selected folder.
486                             default:
487                                 // Get a cursor for the selected folder.
488                                 if (sortByDisplayOrder) {
489                                     bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrderExcept(selectedBookmarksIdsLongArray, currentFolderName);
490                                 } else {
491                                     bookmarksCursor = bookmarksDatabaseHelper.getBookmarksExcept(selectedBookmarksIdsLongArray, currentFolderName);
492                                 }
493                         }
494
495                         // Update the list view.
496                         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
497
498                         // Show a Snackbar with the number of deleted bookmarks.
499                         bookmarksDeletedSnackbar = Snackbar.make(findViewById(R.id.bookmarks_databaseview_coordinatorlayout),
500                                 getString(R.string.bookmarks_deleted) + "  " + selectedBookmarksIdsLongArray.length, Snackbar.LENGTH_LONG)
501                                 .setAction(R.string.undo, view -> {
502                                     // Do nothing because everything will be handled by `onDismissed()` below.
503                                 })
504                                 .addCallback(new Snackbar.Callback() {
505                                     @Override
506                                     public void onDismissed(Snackbar snackbar, int event) {
507                                         switch (event) {
508                                             // The user pushed the `Undo` button.
509                                             case Snackbar.Callback.DISMISS_EVENT_ACTION:
510                                                 // Update the bookmarks list view with the current contents of the bookmarks database, including the "deleted bookmarks.
511                                                 updateBookmarksListView();
512
513                                                 // Re-select the previously selected bookmarks.
514                                                 for (int i = 0; i < selectedBookmarksPositionsSparseBooleanArray.size(); i++) {
515                                                     bookmarksListView.setItemChecked(selectedBookmarksPositionsSparseBooleanArray.keyAt(i), true);
516                                                 }
517                                                 break;
518
519                                                 // The Snackbar was dismissed without the `Undo` button being pushed.
520                                             default:
521                                                 // Delete each selected bookmark.
522                                                 for (long databaseIdLong : selectedBookmarksIdsLongArray) {
523                                                     // Convert `databaseIdLong` to an int.
524                                                     int databaseIdInt = (int) databaseIdLong;
525
526                                                     // Delete the selected bookmark.
527                                                     bookmarksDatabaseHelper.deleteBookmark(databaseIdInt);
528                                                 }
529                                         }
530
531                                         // Reset the deleting bookmarks flag.
532                                         deletingBookmarks = false;
533
534                                         // Enable the delete menu item.
535                                         deleteMenuItem.setEnabled(true);
536
537                                         // Close the activity if back has been pressed.
538                                         if (closeActivityAfterDismissingSnackbar) {
539                                             onBackPressed();
540                                         }
541                                     }
542                                 });
543
544                         // Show the Snackbar.
545                         bookmarksDeletedSnackbar.show();
546                         break;
547                 }
548
549                 // Consume the click.
550                 return false;
551             }
552
553             @Override
554             public void onDestroyActionMode(ActionMode mode) {
555                 // Do nothing.
556             }
557         });
558     }
559
560     @Override
561     public boolean onCreateOptionsMenu(Menu menu) {
562         // Inflate the menu.
563         getMenuInflater().inflate(R.menu.bookmarks_databaseview_options_menu, menu);
564
565         // Success.
566         return true;
567     }
568
569     @Override
570     public boolean onOptionsItemSelected(MenuItem menuItem) {
571         // Get the ID of the menu item that was selected.
572         int menuItemId = menuItem.getItemId();
573
574         switch (menuItemId) {
575             case android.R.id.home:  // The home arrow is identified as `android.R.id.home`, not just `R.id.home`.
576                 // Exit the activity.
577                 onBackPressed();
578                 break;
579
580             case R.id.options_menu_sort:
581                 // Update the sort by display order tracker.
582                 sortByDisplayOrder = !sortByDisplayOrder;
583
584                 // Get a handle for the bookmarks `ListView`.
585                 ListView bookmarksListView = findViewById(R.id.bookmarks_databaseview_listview);
586
587                 // Update the icon and display a snackbar.
588                 if (sortByDisplayOrder) {  // Sort by display order.
589                     // Update the icon according to the theme.
590                     if (MainWebViewActivity.darkTheme) {
591                         menuItem.setIcon(R.drawable.sort_selected_dark);
592                     } else {
593                         menuItem.setIcon(R.drawable.sort_selected_light);
594                     }
595
596                     // Display a Snackbar indicating the current sort type.
597                     Snackbar.make(bookmarksListView, R.string.sorted_by_display_order, Snackbar.LENGTH_SHORT).show();
598                 } else {  // Sort by database id.
599                     // Update the icon according to the theme.
600                     if (MainWebViewActivity.darkTheme) {
601                         menuItem.setIcon(R.drawable.sort_dark);
602                     } else {
603                         menuItem.setIcon(R.drawable.sort_light);
604                     }
605
606                     // Display a Snackbar indicating the current sort type.
607                     Snackbar.make(bookmarksListView, R.string.sorted_by_database_id, Snackbar.LENGTH_SHORT).show();
608                 }
609
610                 // Update the list view.
611                 updateBookmarksListView();
612                 break;
613         }
614         return true;
615     }
616
617     @Override
618     public void onBackPressed() {
619         // Check to see if a snackbar is currently displayed.  If so, it must be closed before existing so that a pending delete is completed before reloading the list view in the bookmarks activity.
620         if ((bookmarksDeletedSnackbar != null) && bookmarksDeletedSnackbar.isShown()) { // Close the bookmarks deleted snackbar before going home.
621             // Set the close flag.
622             closeActivityAfterDismissingSnackbar = true;
623
624             // Dismiss the snackbar.
625             bookmarksDeletedSnackbar.dismiss();
626         } else {  // Go home immediately.
627             // Update the current folder in the bookmarks activity.
628             switch (currentFolderDatabaseId) {
629                 case ALL_FOLDERS_DATABASE_ID:
630                     // Load the home folder.
631                     BookmarksActivity.currentFolder = "";
632                     break;
633
634                 case HOME_FOLDER_DATABASE_ID:
635                     // Load the home folder.
636                     BookmarksActivity.currentFolder = "";
637                     break;
638
639                 default:
640                     // Load the current folder.
641                     BookmarksActivity.currentFolder = currentFolderName;
642             }
643
644             // Reload the bookmarks list view when returning to the bookmarks activity.
645             BookmarksActivity.restartFromBookmarksDatabaseViewActivity = true;
646
647             // Exit the bookmarks database view activity.
648             super.onBackPressed();
649         }
650     }
651
652     private void updateBookmarksListView() {
653         // Populate the bookmarks list view based on the spinner selection.
654         switch (currentFolderDatabaseId) {
655             // Get a cursor with all the folders.
656             case ALL_FOLDERS_DATABASE_ID:
657                 if (sortByDisplayOrder) {
658                     bookmarksCursor = bookmarksDatabaseHelper.getAllBookmarksByDisplayOrder();
659                 } else {
660                     bookmarksCursor = bookmarksDatabaseHelper.getAllBookmarks();
661                 }
662                 break;
663
664             // Get a cursor for the home folder.
665             case HOME_FOLDER_DATABASE_ID:
666                 if (sortByDisplayOrder) {
667                     bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder("");
668                 } else {
669                     bookmarksCursor = bookmarksDatabaseHelper.getBookmarks("");
670                 }
671                 break;
672
673             // Display the selected folder.
674             default:
675                 // Get a cursor for the selected folder.
676                 if (sortByDisplayOrder) {
677                     bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderName);
678                 } else {
679                     bookmarksCursor = bookmarksDatabaseHelper.getBookmarks(currentFolderName);
680                 }
681         }
682
683         // Update the list view.
684         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
685     }
686
687     private void selectAllBookmarksInFolder(int folderId) {
688         // Get a handle for the bookmarks list view.
689         ListView bookmarksListView = findViewById(R.id.bookmarks_databaseview_listview);
690
691         // Get the folder name.
692         String folderName = bookmarksDatabaseHelper.getFolderName(folderId);
693
694         // Get a cursor with the contents of the folder.
695         Cursor folderCursor = bookmarksDatabaseHelper.getBookmarks(folderName);
696
697         // Move to the beginning of the cursor.
698         folderCursor.moveToFirst();
699
700         while (folderCursor.getPosition() < folderCursor.getCount()) {
701             // Get the bookmark database ID.
702             int bookmarkId = folderCursor.getInt(folderCursor.getColumnIndex(BookmarksDatabaseHelper._ID));
703
704             // Move the bookmarks cursor to the first position.
705             bookmarksCursor.moveToFirst();
706
707             // Initialize the bookmark position variable.
708             int bookmarkPosition = -1;
709
710             // Get the position of this bookmark in the bookmarks cursor.
711             while ((bookmarkPosition < 0) && (bookmarksCursor.getPosition() < bookmarksCursor.getCount())) {
712                 // Check if the bookmark IDs match.
713                 if (bookmarkId == bookmarksCursor.getInt(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper._ID))) {
714                     // Get the bookmark position.
715                     bookmarkPosition = bookmarksCursor.getPosition();
716
717                     // If this bookmark is a folder, select all the bookmarks inside it.
718                     if (bookmarksDatabaseHelper.isFolder(bookmarkId)) {
719                         selectAllBookmarksInFolder(bookmarkId);
720                     }
721
722                     // Select the bookmark.
723                     bookmarksListView.setItemChecked(bookmarkPosition, true);
724                 }
725
726                 // Increment the bookmarks cursor position.
727                 bookmarksCursor.moveToNext();
728             }
729
730             // Move to the next position.
731             folderCursor.moveToNext();
732         }
733     }
734
735     @Override
736     public void onSaveBookmark(AppCompatDialogFragment dialogFragment, int selectedBookmarkDatabaseId) {
737         // Get handles for the views from dialog fragment.
738         RadioButton currentBookmarkIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_current_icon_radiobutton);
739         EditText editBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_name_edittext);
740         EditText editBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_url_edittext);
741         Spinner folderSpinner = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_folder_spinner);
742         EditText displayOrderEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_display_order_edittext);
743
744         // Extract the bookmark information.
745         String bookmarkNameString = editBookmarkNameEditText.getText().toString();
746         String bookmarkUrlString = editBookmarkUrlEditText.getText().toString();
747         int folderDatabaseId = (int) folderSpinner.getSelectedItemId();
748         int displayOrderInt = Integer.valueOf(displayOrderEditText.getText().toString());
749
750         // Instantiate the parent folder name `String`.
751         String parentFolderNameString;
752
753         // Set the parent folder name.
754         if (folderDatabaseId == EditBookmarkDatabaseViewDialog.HOME_FOLDER_DATABASE_ID) {  // The home folder is selected.  Use `""`.
755             parentFolderNameString = "";
756         } else {  // Get the parent folder name from the database.
757             parentFolderNameString = bookmarksDatabaseHelper.getFolderName(folderDatabaseId);
758         }
759
760         // Update the bookmark.
761         if (currentBookmarkIconRadioButton.isChecked()) {  // Update the bookmark without changing the favorite icon.
762             bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, parentFolderNameString, displayOrderInt);
763         } else {  // Update the bookmark using the `WebView` favorite icon.
764             // Convert the favorite icon to a byte array.  `0` is for lossless compression (the only option for a PNG).
765             ByteArrayOutputStream newFavoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
766             MainWebViewActivity.favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFavoriteIconByteArrayOutputStream);
767             byte[] newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray();
768
769             //  Update the bookmark and the favorite icon.
770             bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, parentFolderNameString, displayOrderInt, newFavoriteIconByteArray);
771         }
772
773         // Update the list view.
774         updateBookmarksListView();
775     }
776
777     @Override
778     public void onSaveBookmarkFolder(AppCompatDialogFragment dialogFragment, int selectedBookmarkDatabaseId) {
779         // Get handles for the views from dialog fragment.
780         RadioButton currentBookmarkIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_current_icon_radiobutton);
781         RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_radiobutton);
782         ImageView defaultFolderIconImageView = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_imageview);
783         EditText editBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_folder_name_edittext);
784         Spinner parentFolderSpinner = dialogFragment.getDialog().findViewById(R.id.edit_folder_parent_folder_spinner);
785         EditText displayOrderEditText = dialogFragment.getDialog().findViewById(R.id.edit_folder_display_order_edittext);
786
787         // Extract the folder information.
788         String newFolderNameString = editBookmarkNameEditText.getText().toString();
789         int parentFolderDatabaseId = (int) parentFolderSpinner.getSelectedItemId();
790         int displayOrderInt = Integer.valueOf(displayOrderEditText.getText().toString());
791
792         // Instantiate the parent folder name `String`.
793         String parentFolderNameString;
794
795         // Set the parent folder name.
796         if (parentFolderDatabaseId == EditBookmarkFolderDatabaseViewDialog.HOME_FOLDER_DATABASE_ID) {  // The home folder is selected.  Use `""`.
797             parentFolderNameString = "";
798         } else {  // Get the parent folder name from the database.
799             parentFolderNameString = bookmarksDatabaseHelper.getFolderName(parentFolderDatabaseId);
800         }
801
802         // Update the folder.
803         if (currentBookmarkIconRadioButton.isChecked()) {  // Update the folder without changing the favorite icon.
804             bookmarksDatabaseHelper.updateFolder(selectedBookmarkDatabaseId, oldFolderNameString, newFolderNameString, parentFolderNameString, displayOrderInt);
805         } else {  // Update the folder and the icon.
806             // Instantiate the new folder icon `Bitmap`.
807             Bitmap folderIconBitmap;
808
809             // Populate the new folder icon bitmap.
810             if (defaultFolderIconRadioButton.isChecked()) {
811                 // Get the default folder icon and convert it to a `Bitmap`.
812                 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
813                 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
814                 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
815             } else {  // Use the `WebView` favorite icon.
816                 folderIconBitmap = MainWebViewActivity.favoriteIconBitmap;
817             }
818
819             // Convert the folder icon to a byte array.  `0` is for lossless compression (the only option for a PNG).
820             ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
821             folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
822             byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
823
824             //  Update the folder and the icon.
825             bookmarksDatabaseHelper.updateFolder(selectedBookmarkDatabaseId, oldFolderNameString, newFolderNameString, parentFolderNameString, displayOrderInt, newFolderIconByteArray);
826         }
827
828         // Update the list view.
829         updateBookmarksListView();
830     }
831
832     @Override
833     public void onDestroy() {
834         // Close the bookmarks cursor and database.
835         bookmarksCursor.close();
836         bookmarksDatabaseHelper.close();
837
838         // Run the default commands.
839         super.onDestroy();
840     }
841 }