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