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