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