2 * Copyright © 2016-2022 Soren Stoutner <soren@stoutner.com>.
4 * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
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.
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.
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/>.
20 package com.stoutner.privacybrowser.activities;
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.database.Cursor;
28 import android.graphics.Bitmap;
29 import android.graphics.BitmapFactory;
30 import android.graphics.Typeface;
31 import android.graphics.drawable.BitmapDrawable;
32 import android.graphics.drawable.Drawable;
33 import android.os.Bundle;
34 import android.preference.PreferenceManager;
35 import android.util.SparseBooleanArray;
36 import android.view.ActionMode;
37 import android.view.Menu;
38 import android.view.MenuItem;
39 import android.view.View;
40 import android.view.ViewGroup;
41 import android.view.Window;
42 import android.view.WindowManager;
43 import android.widget.AbsListView;
44 import android.widget.CursorAdapter;
45 import android.widget.EditText;
46 import android.widget.ImageView;
47 import android.widget.ListView;
48 import android.widget.RadioButton;
49 import android.widget.TextView;
51 import androidx.annotation.NonNull;
52 import androidx.appcompat.app.ActionBar;
53 import androidx.appcompat.app.AppCompatActivity;
54 import androidx.appcompat.widget.Toolbar;
55 import androidx.fragment.app.DialogFragment;
57 import com.google.android.material.floatingactionbutton.FloatingActionButton;
58 import com.google.android.material.snackbar.Snackbar;
60 import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog;
61 import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog;
62 import com.stoutner.privacybrowser.dialogs.EditBookmarkDialog;
63 import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDialog;
64 import com.stoutner.privacybrowser.dialogs.MoveToFolderDialog;
65 import com.stoutner.privacybrowser.R;
66 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper;
68 import java.io.ByteArrayOutputStream;
69 import java.util.ArrayList;
71 public class BookmarksActivity extends AppCompatActivity implements CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener, EditBookmarkDialog.EditBookmarkListener,
72 EditBookmarkFolderDialog.EditBookmarkFolderListener, MoveToFolderDialog.MoveToFolderListener {
74 // `currentFolder` is public static so it can be accessed from `BookmarksDatabaseViewActivity`.
75 public static String currentFolder;
77 // `restartFromBookmarksDatabaseViewActivity` is public static so it can be accessed from `BookmarksDatabaseViewActivity`. It is also used in `onRestart()`.
78 public static boolean restartFromBookmarksDatabaseViewActivity;
81 // Define the saved instance state constants.
82 private final String CHECKED_BOOKMARKS_ARRAY_LIST = "checked_bookmarks_array_list";
84 // Define the class menu items.
85 private MenuItem moveBookmarkUpMenuItem;
86 private MenuItem moveBookmarkDownMenuItem;
88 // `bookmarksDatabaseHelper` is used in `onCreate()`, `onOptionsItemSelected()`, `onBackPressed()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveBookmark()`, `onSaveBookmarkFolder()`,
89 // `onMoveToFolder()`, `deleteBookmarkFolderContents()`, `loadFolder()`, and `onDestroy()`.
90 private BookmarksDatabaseHelper bookmarksDatabaseHelper;
92 // `bookmarksListView` is used in `onCreate()`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveBookmark()`, `onSaveBookmarkFolder()`, `onMoveToFolder()`,
93 // `updateMoveIcons()`, `scrollBookmarks()`, and `loadFolder()`.
94 private ListView bookmarksListView;
96 // `bookmarksCursor` is used in `onCreate()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveBookmark()`, `onSaveBookmarkFolder()`, `onMoveToFolder()`, `deleteBookmarkFolderContents()`,
97 // `loadFolder()`, and `onDestroy()`.
98 private Cursor bookmarksCursor;
100 // `bookmarksCursorAdapter` is used in `onCreate(), `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveBookmark()`, `onSaveBookmarkFolder()`, `onMoveToFolder()`, and `onLoadFolder()`.
101 private CursorAdapter bookmarksCursorAdapter;
103 // `contextualActionMode` is used in `onCreate()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()` and `onMoveToFolder()`.
104 private ActionMode contextualActionMode;
106 // `appBar` is used in `onCreate()` and `loadFolder()`.
107 private ActionBar appBar;
109 // `oldFolderName` is used in `onCreate()` and `onSaveBookmarkFolder()`.
110 private String oldFolderNameString;
112 // `bookmarksDeletedSnackbar` is used in `onCreate()`, `onOptionsItemSelected()`, and `onBackPressed()`.
113 private Snackbar bookmarksDeletedSnackbar;
115 // `closeActivityAfterDismissingSnackbar` is used in `onCreate()`, `onOptionsItemSelected()`, and `onBackPressed()`.
116 private boolean closeActivityAfterDismissingSnackbar;
118 // The favorite icon byte array is populated in `onCreate()` and used in `onOptionsItemSelected()`.
119 private byte[] favoriteIconByteArray;
122 protected void onCreate(Bundle savedInstanceState) {
123 // Get a handle for the shared preferences.
124 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
126 // Get the preferences.
127 boolean allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false);
128 boolean bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false);
130 // Disable screenshots if not allowed.
131 if (!allowScreenshots) {
132 getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
135 // Run the default commands.
136 super.onCreate(savedInstanceState);
138 // Get the intent that launched the activity.
139 Intent launchingIntent = getIntent();
141 // Store the current URL and title.
142 String currentUrl = launchingIntent.getStringExtra("current_url");
143 String currentTitle = launchingIntent.getStringExtra("current_title");
145 // Set the current folder variable.
146 if (launchingIntent.getStringExtra("current_folder") != null) { // Set the current folder from the intent.
147 currentFolder = launchingIntent.getStringExtra("current_folder");
148 } else { // Set the current folder to be `""`, which is the home folder.
152 // Get the favorite icon byte array.
153 favoriteIconByteArray = launchingIntent.getByteArrayExtra("favorite_icon_byte_array");
155 // Remove the incorrect lint warning that the favorite icon byte array might be null.
156 assert favoriteIconByteArray != null;
158 // Convert the favorite icon byte array to a bitmap.
159 Bitmap favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.length);
161 // Set the content according to the app bar position.
163 // Set the content view.
164 setContentView(R.layout.bookmarks_bottom_appbar);
166 // `Window.FEATURE_ACTION_MODE_OVERLAY` makes the contextual action mode cover the support action bar. It must be requested before the content is set.
167 supportRequestWindowFeature(Window.FEATURE_ACTION_MODE_OVERLAY);
169 // Set the content view.
170 setContentView(R.layout.bookmarks_top_appbar);
173 // Get a handle for the toolbar.
174 final Toolbar toolbar = findViewById(R.id.bookmarks_toolbar);
176 // Set the support action bar.
177 setSupportActionBar(toolbar);
179 // Get handles for the views.
180 appBar = getSupportActionBar();
181 bookmarksListView = findViewById(R.id.bookmarks_listview);
183 // Remove the incorrect lint warning that `appBar` might be null.
184 assert appBar != null;
186 // Display the home arrow on the app bar.
187 appBar.setDisplayHomeAsUpEnabled(true);
189 // Initialize the database helper.
190 bookmarksDatabaseHelper = new BookmarksDatabaseHelper(this);
192 // Load the home folder.
195 // Set a listener so that tapping a list item loads the URL or folder.
196 bookmarksListView.setOnItemClickListener((parent, view, position, id) -> {
197 // Convert the id from long to int to match the format of the bookmarks database.
198 int databaseId = (int) id;
200 // Get the bookmark cursor for this ID.
201 Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseId);
203 // Move the cursor to the first entry.
204 bookmarkCursor.moveToFirst();
206 // Act upon the bookmark according to the type.
207 if (bookmarkCursor.getInt(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.IS_FOLDER)) == 1) { // The selected bookmark is a folder.
208 // Update the current folder.
209 currentFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME));
211 // Load the new folder.
213 } else { // The selected bookmark is not a folder.
214 // Instantiate the edit bookmark dialog.
215 DialogFragment editBookmarkDialog = EditBookmarkDialog.bookmarkDatabaseId(databaseId, favoriteIconBitmap);
218 editBookmarkDialog.show(getSupportFragmentManager(), getResources().getString(R.string.edit_bookmark));
222 bookmarkCursor.close();
225 // Handle long presses on the list view.
226 bookmarksListView.setMultiChoiceModeListener(new AbsListView.MultiChoiceModeListener() {
227 // Instantiate the common variables.
228 MenuItem editBookmarkMenuItem;
229 MenuItem deleteBookmarksMenuItem;
230 MenuItem selectAllBookmarksMenuItem;
231 boolean deletingBookmarks;
234 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
235 // Inflate the menu for the contextual app bar.
236 getMenuInflater().inflate(R.menu.bookmarks_context_menu, menu);
239 if (currentFolder.isEmpty()) { // Use `R.string.bookmarks` if in the home folder.
240 mode.setTitle(R.string.bookmarks);
241 } else { // Use the current folder name as the title.
242 mode.setTitle(currentFolder);
245 // Get handles for menu items that need to be selectively disabled.
246 moveBookmarkUpMenuItem = menu.findItem(R.id.move_bookmark_up);
247 moveBookmarkDownMenuItem = menu.findItem(R.id.move_bookmark_down);
248 editBookmarkMenuItem = menu.findItem(R.id.edit_bookmark);
249 deleteBookmarksMenuItem = menu.findItem(R.id.delete_bookmark);
250 selectAllBookmarksMenuItem = menu.findItem(R.id.context_menu_select_all_bookmarks);
252 // Disable the delete bookmarks menu item if a delete is pending.
253 deleteBookmarksMenuItem.setEnabled(!deletingBookmarks);
255 // Store a handle for the contextual action mode so it can be closed programatically.
256 contextualActionMode = mode;
263 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
264 // Get a handle for the move to folder menu item.
265 MenuItem moveToFolderMenuItem = menu.findItem(R.id.move_to_folder);
267 // Get a Cursor with all of the folders.
268 Cursor folderCursor = bookmarksDatabaseHelper.getAllFolders();
270 // Enable the move to folder menu item if at least one folder exists.
271 moveToFolderMenuItem.setVisible(folderCursor.getCount() > 0);
278 public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
279 // Get the number of selected bookmarks.
280 int numberOfSelectedBookmarks = bookmarksListView.getCheckedItemCount();
282 // Only process commands if at least one bookmark is selected. Otherwise, a context menu with 0 selected bookmarks is briefly displayed.
283 if (numberOfSelectedBookmarks > 0) {
284 // Adjust the ActionMode and the menu according to the number of selected bookmarks.
285 if (numberOfSelectedBookmarks == 1) { // One bookmark is selected.
286 // List the number of selected bookmarks in the subtitle.
287 mode.setSubtitle(getString(R.string.selected) + " 1");
289 // Show the `Move Up`, `Move Down`, and `Edit` options.
290 moveBookmarkUpMenuItem.setVisible(true);
291 moveBookmarkDownMenuItem.setVisible(true);
292 editBookmarkMenuItem.setVisible(true);
294 // Update the enabled status of the move icons.
296 } else { // More than one bookmark is selected.
297 // List the number of selected bookmarks in the subtitle.
298 mode.setSubtitle(getString(R.string.selected) + " " + numberOfSelectedBookmarks);
300 // Hide non-applicable `MenuItems`.
301 moveBookmarkUpMenuItem.setVisible(false);
302 moveBookmarkDownMenuItem.setVisible(false);
303 editBookmarkMenuItem.setVisible(false);
306 // Show the select all menu item if all the bookmarks are not selected.
307 selectAllBookmarksMenuItem.setVisible(bookmarksListView.getCheckedItemCount() != bookmarksListView.getCount());
312 public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
313 // Declare the common variables.
314 int selectedBookmarkNewPosition;
315 final SparseBooleanArray selectedBookmarksPositionsSparseBooleanArray;
317 // Initialize the selected bookmark position.
318 int selectedBookmarkPosition = 0;
320 // Get the menu item ID.
321 int menuItemId = menuItem.getItemId();
323 // Run the commands according to the selected action item.
324 if (menuItemId == R.id.move_bookmark_up) { // Move the bookmark up.
325 // Get the array of checked bookmark positions.
326 selectedBookmarksPositionsSparseBooleanArray = bookmarksListView.getCheckedItemPositions();
328 // Get the position of the bookmark that is selected. If other bookmarks have previously been selected they will be included in the sparse boolean array with a value of `false`.
329 for (int i = 0; i < selectedBookmarksPositionsSparseBooleanArray.size(); i++) {
330 // Check to see if the value for the bookmark is true, meaning it is currently selected.
331 if (selectedBookmarksPositionsSparseBooleanArray.valueAt(i)) {
332 // Only one bookmark should have a value of `true` when move bookmark up is enabled.
333 selectedBookmarkPosition = selectedBookmarksPositionsSparseBooleanArray.keyAt(i);
337 // Calculate the new position of the selected bookmark.
338 selectedBookmarkNewPosition = selectedBookmarkPosition - 1;
340 // Iterate through the bookmarks.
341 for (int i = 0; i < bookmarksListView.getCount(); i++) {
342 // Get the database ID for the current bookmark.
343 int currentBookmarkDatabaseId = (int) bookmarksListView.getItemIdAtPosition(i);
345 // Update the display order for the current bookmark.
346 if (i == selectedBookmarkPosition) { // The current bookmark is the selected bookmark.
347 // Move the current bookmark up one.
348 bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i - 1);
349 } else if ((i + 1) == selectedBookmarkPosition) { // The current bookmark is immediately above the selected bookmark.
350 // Move the current bookmark down one.
351 bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i + 1);
352 } else { // The current bookmark is not changing positions.
353 // Move `bookmarksCursor` to the current bookmark position.
354 bookmarksCursor.moveToPosition(i);
356 // Update the display order only if it is not correct in the database.
357 if (bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.DISPLAY_ORDER)) != i) {
358 bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i);
363 // Update the bookmarks cursor with the current contents of the bookmarks database.
364 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
366 // Update the list view.
367 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
369 // Scroll with the bookmark.
370 scrollBookmarks(selectedBookmarkNewPosition);
372 // Update the enabled status of the move icons.
374 } else if (menuItemId == R.id.move_bookmark_down) { // Move the bookmark down.
375 // Get the array of checked bookmark positions.
376 selectedBookmarksPositionsSparseBooleanArray = bookmarksListView.getCheckedItemPositions();
378 // Get the position of the bookmark that is selected. If other bookmarks have previously been selected they will be included in the sparse boolean array with a value of `false`.
379 for (int i = 0; i < selectedBookmarksPositionsSparseBooleanArray.size(); i++) {
380 // Check to see if the value for the bookmark is true, meaning it is currently selected.
381 if (selectedBookmarksPositionsSparseBooleanArray.valueAt(i)) {
382 // Only one bookmark should have a value of `true` when move bookmark down is enabled.
383 selectedBookmarkPosition = selectedBookmarksPositionsSparseBooleanArray.keyAt(i);
387 // Calculate the new position of the selected bookmark.
388 selectedBookmarkNewPosition = selectedBookmarkPosition + 1;
390 // Iterate through the bookmarks.
391 for (int i = 0; i < bookmarksListView.getCount(); i++) {
392 // Get the database ID for the current bookmark.
393 int currentBookmarkDatabaseId = (int) bookmarksListView.getItemIdAtPosition(i);
395 // Update the display order for the current bookmark.
396 if (i == selectedBookmarkPosition) { // The current bookmark is the selected bookmark.
397 // Move the current bookmark down one.
398 bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i + 1);
399 } else if ((i - 1) == selectedBookmarkPosition) { // The current bookmark is immediately below the selected bookmark.
400 // Move the bookmark below the selected bookmark up one.
401 bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i - 1);
402 } else { // The current bookmark is not changing positions.
403 // Move `bookmarksCursor` to the current bookmark position.
404 bookmarksCursor.moveToPosition(i);
406 // Update the display order only if it is not correct in the database.
407 if (bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.DISPLAY_ORDER)) != i) {
408 bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i);
413 // Update the bookmarks cursor with the current contents of the bookmarks database.
414 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
416 // Update the list view.
417 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
419 // Scroll with the bookmark.
420 scrollBookmarks(selectedBookmarkNewPosition);
422 // Update the enabled status of the move icons.
424 } else if (menuItemId == R.id.move_to_folder) { // Move to folder.
425 // Instantiate the move to folder alert dialog.
426 DialogFragment moveToFolderDialog = MoveToFolderDialog.moveBookmarks(currentFolder, bookmarksListView.getCheckedItemIds());
428 // Show the move to folder alert dialog.
429 moveToFolderDialog.show(getSupportFragmentManager(), getResources().getString(R.string.move_to_folder));
430 } else if (menuItemId == R.id.edit_bookmark) {
431 // Get the array of checked bookmark positions.
432 selectedBookmarksPositionsSparseBooleanArray = bookmarksListView.getCheckedItemPositions();
434 // Get the position of the bookmark that is selected. If other bookmarks have previously been selected they will be included in the sparse boolean array with a value of `false`.
435 for (int i = 0; i < selectedBookmarksPositionsSparseBooleanArray.size(); i++) {
436 // Check to see if the value for the bookmark is true, meaning it is currently selected.
437 if (selectedBookmarksPositionsSparseBooleanArray.valueAt(i)) {
438 // Only one bookmark should have a value of `true` when move edit bookmark is enabled.
439 selectedBookmarkPosition = selectedBookmarksPositionsSparseBooleanArray.keyAt(i);
443 // Move the cursor to the selected position.
444 bookmarksCursor.moveToPosition(selectedBookmarkPosition);
446 // Find out if this bookmark is a folder.
447 boolean isFolder = (bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.IS_FOLDER)) == 1);
449 // Get the selected bookmark database ID.
450 int databaseId = bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.ID));
452 // Show the edit bookmark or edit bookmark folder dialog.
454 // Save the current folder name, which is used in `onSaveBookmarkFolder()`.
455 oldFolderNameString = bookmarksCursor.getString(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME));
457 // Instantiate the edit bookmark folder dialog.
458 DialogFragment editFolderDialog = EditBookmarkFolderDialog.folderDatabaseId(databaseId, favoriteIconBitmap);
461 editFolderDialog.show(getSupportFragmentManager(), getResources().getString(R.string.edit_folder));
463 // Instantiate the edit bookmark dialog.
464 DialogFragment editBookmarkDialog = EditBookmarkDialog.bookmarkDatabaseId(databaseId, favoriteIconBitmap);
467 editBookmarkDialog.show(getSupportFragmentManager(), getResources().getString(R.string.edit_bookmark));
469 } else if (menuItemId == R.id.delete_bookmark) { // Delete bookmark.
470 // Set the deleting bookmarks flag, which prevents the delete menu item from being enabled until the current process finishes.
471 deletingBookmarks = true;
473 // Get an array of the selected row IDs.
474 final long[] selectedBookmarksIdsLongArray = bookmarksListView.getCheckedItemIds();
476 // Initialize a variable to count the number of bookmarks to delete.
477 int numberOfBookmarksToDelete = 0;
479 // Count the number of bookmarks.
480 for (long databaseIdLong : selectedBookmarksIdsLongArray) {
481 // Convert the database ID long to an int.
482 int databaseIdInt = (int) databaseIdLong;
484 // Count the contents of the folder if the selected bookmark is a folder.
485 if (bookmarksDatabaseHelper.isFolder(databaseIdInt)) {
486 // Add the bookmarks from the folder to the running total.
487 numberOfBookmarksToDelete = numberOfBookmarksToDelete + countBookmarkFolderContents(databaseIdInt);
490 // Increment the count of the number of bookmarks to delete.
491 numberOfBookmarksToDelete++;
494 // 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.
495 selectedBookmarksPositionsSparseBooleanArray = bookmarksListView.getCheckedItemPositions().clone();
497 // Update the bookmarks cursor with the current contents of the bookmarks database except for the specified database IDs.
498 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrderExcept(selectedBookmarksIdsLongArray, currentFolder);
500 // Update the list view.
501 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
503 // Create a Snackbar with the number of deleted bookmarks.
504 bookmarksDeletedSnackbar = Snackbar.make(findViewById(R.id.bookmarks_coordinatorlayout), getString(R.string.bookmarks_deleted) + " " + numberOfBookmarksToDelete,
505 Snackbar.LENGTH_LONG)
506 .setAction(R.string.undo, view -> {
507 // Do nothing because everything will be handled by `onDismissed()` below.
509 .addCallback(new Snackbar.Callback() {
510 @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
512 public void onDismissed(Snackbar snackbar, int event) {
513 if (event == Snackbar.Callback.DISMISS_EVENT_ACTION) { // The user pushed the undo button.
514 // Update the bookmarks cursor with the current contents of the bookmarks database, including the "deleted" bookmarks.
515 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
517 // Update the list view.
518 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
520 // Re-select the previously selected bookmarks.
521 for (int i = 0; i < selectedBookmarksPositionsSparseBooleanArray.size(); i++) {
522 bookmarksListView.setItemChecked(selectedBookmarksPositionsSparseBooleanArray.keyAt(i), true);
524 } else { // The snackbar was dismissed without the undo button being pushed.
525 // Delete each selected bookmark.
526 for (long databaseIdLong : selectedBookmarksIdsLongArray) {
527 // Convert `databaseIdLong` to an int.
528 int databaseIdInt = (int) databaseIdLong;
530 // Delete the contents of the folder if the selected bookmark is a folder.
531 if (bookmarksDatabaseHelper.isFolder(databaseIdInt)) {
532 deleteBookmarkFolderContents(databaseIdInt);
535 // Delete the selected bookmark.
536 bookmarksDatabaseHelper.deleteBookmark(databaseIdInt);
539 // Update the display order.
540 for (int i = 0; i < bookmarksListView.getCount(); i++) {
541 // Get the database ID for the current bookmark.
542 int currentBookmarkDatabaseId = (int) bookmarksListView.getItemIdAtPosition(i);
544 // Move `bookmarksCursor` to the current bookmark position.
545 bookmarksCursor.moveToPosition(i);
547 // Update the display order only if it is not correct in the database.
548 if (bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.DISPLAY_ORDER)) != i) {
549 bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i);
554 // Reset the deleting bookmarks flag.
555 deletingBookmarks = false;
557 // Enable the delete bookmarks menu item.
558 deleteBookmarksMenuItem.setEnabled(true);
560 // Close the activity if back has been pressed.
561 if (closeActivityAfterDismissingSnackbar) {
568 bookmarksDeletedSnackbar.show();
569 } else if (menuItemId == R.id.context_menu_select_all_bookmarks) { // Select all.
570 // Get the total number of bookmarks.
571 int numberOfBookmarks = bookmarksListView.getCount();
574 for (int i = 0; i < numberOfBookmarks; i++) {
575 bookmarksListView.setItemChecked(i, true);
579 // Consume the click.
584 public void onDestroyActionMode(ActionMode mode) {
589 // Get handles for the floating action buttons.
590 FloatingActionButton createBookmarkFolderFab = findViewById(R.id.create_bookmark_folder_fab);
591 FloatingActionButton createBookmarkFab = findViewById(R.id.create_bookmark_fab);
593 // Set the create new bookmark folder FAB to display the `AlertDialog`.
594 createBookmarkFolderFab.setOnClickListener(v -> {
595 // Create a create bookmark folder dialog.
596 DialogFragment createBookmarkFolderDialog = CreateBookmarkFolderDialog.createBookmarkFolder(favoriteIconBitmap);
598 // Show the create bookmark folder dialog.
599 createBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.create_folder));
602 // Set the create new bookmark FAB to display the alert dialog.
603 createBookmarkFab.setOnClickListener(view -> {
604 // Remove the incorrect lint warning below.
605 assert currentUrl != null;
606 assert currentTitle != null;
608 // Instantiate the create bookmark dialog.
609 DialogFragment createBookmarkDialog = CreateBookmarkDialog.createBookmark(currentUrl, currentTitle, favoriteIconBitmap);
611 // Display the create bookmark dialog.
612 createBookmarkDialog.show(getSupportFragmentManager(), getResources().getString(R.string.create_bookmark));
615 // Restore the state if the app has been restarted.
616 if (savedInstanceState != null) {
617 // Update the bookmarks list view after it has loaded.
618 bookmarksListView.post(() -> {
619 // Get the checked bookmarks array list.
620 ArrayList<Integer> checkedBookmarksArrayList = savedInstanceState.getIntegerArrayList(CHECKED_BOOKMARKS_ARRAY_LIST);
622 // Check each previously checked bookmark in the list view. When the minimum API >= 24 a `forEach()` command can be used instead.
623 if (checkedBookmarksArrayList != null) {
624 for (int i = 0; i < checkedBookmarksArrayList.size(); i++) {
625 bookmarksListView.setItemChecked(checkedBookmarksArrayList.get(i), true);
633 public void onRestart() {
634 // Run the default commands.
637 // Update the list view if returning from the bookmarks database view activity.
638 if (restartFromBookmarksDatabaseViewActivity) {
639 // Load the current folder in the list view.
642 // Reset `restartFromBookmarksDatabaseViewActivity`.
643 restartFromBookmarksDatabaseViewActivity = false;
648 public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
649 // Run the default commands.
650 super.onSaveInstanceState(savedInstanceState);
652 // Get the array of the checked items.
653 SparseBooleanArray checkedBookmarksSparseBooleanArray = bookmarksListView.getCheckedItemPositions();
655 // Create a checked items array list.
656 ArrayList<Integer> checkedBookmarksArrayList = new ArrayList<>();
658 // Add each checked bookmark position to the array list.
659 for (int i = 0; i < checkedBookmarksSparseBooleanArray.size(); i++) {
660 // Check to see if the bookmark is currently checked. Bookmarks that have previously been checked but currently aren't will be populated in the sparse boolean array, but will return false.
661 if (checkedBookmarksSparseBooleanArray.valueAt(i)) {
662 // Add the bookmark position to the checked bookmarks array list.
663 checkedBookmarksArrayList.add(checkedBookmarksSparseBooleanArray.keyAt(i));
667 // Store the checked items array list in the saved instance state.
668 savedInstanceState.putIntegerArrayList(CHECKED_BOOKMARKS_ARRAY_LIST, checkedBookmarksArrayList);
672 public boolean onCreateOptionsMenu(Menu menu) {
674 getMenuInflater().inflate(R.menu.bookmarks_options_menu, menu);
681 public boolean onOptionsItemSelected(MenuItem menuItem) {
682 // Get a handle for the menu item ID.
683 int menuItemId = menuItem.getItemId();
685 // Run the command according to the selected option.
686 if (menuItemId == android.R.id.home) { // Home. The home arrow is identified as `android.R.id.home`, not just `R.id.home`.
687 if (currentFolder.isEmpty()) { // Currently in the home folder.
688 // Run the back commands.
690 } else { // Currently in a subfolder.
691 // Place the former parent folder in `currentFolder`.
692 currentFolder = bookmarksDatabaseHelper.getParentFolderName(currentFolder);
694 // Load the new folder.
697 } else if (menuItemId == R.id.options_menu_select_all_bookmarks) { // Select all.
698 // Get the total number of bookmarks.
699 int numberOfBookmarks = bookmarksListView.getCount();
702 for (int i = 0; i < numberOfBookmarks; i++) {
703 bookmarksListView.setItemChecked(i, true);
705 } else if (menuItemId == R.id.bookmarks_database_view) {
706 // Close the contextual action bar if it is displayed. This can happen if the bottom app bar is enabled.
707 if (contextualActionMode != null) {
708 contextualActionMode.finish();
711 // Create an intent to launch the bookmarks database view activity.
712 Intent bookmarksDatabaseViewIntent = new Intent(this, BookmarksDatabaseViewActivity.class);
714 // Include the favorite icon byte array to the intent.
715 bookmarksDatabaseViewIntent.putExtra("favorite_icon_byte_array", favoriteIconByteArray);
718 startActivity(bookmarksDatabaseViewIntent);
724 public void onBackPressed() {
725 // Check to see if a snackbar is currently displayed. If so, it must be closed before exiting so that a pending delete is completed before reloading the list view in the bookmarks drawer.
726 if ((bookmarksDeletedSnackbar != null) && bookmarksDeletedSnackbar.isShown()) { // Close the bookmarks deleted snackbar before going home.
727 // Set the close flag.
728 closeActivityAfterDismissingSnackbar = true;
730 // Dismiss the snackbar.
731 bookmarksDeletedSnackbar.dismiss();
732 } else { // Go home immediately.
733 // Update the bookmarks folder for the bookmarks drawer in the main WebView activity.
734 MainWebViewActivity.currentBookmarksFolder = currentFolder;
736 // Close the bookmarks drawer and reload the bookmarks ListView when returning to the main WebView activity.
737 MainWebViewActivity.restartFromBookmarksActivity = true;
739 // Exit the bookmarks activity.
740 super.onBackPressed();
745 public void onCreateBookmark(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) {
746 // Get the alert dialog from the fragment.
747 Dialog dialog = dialogFragment.getDialog();
749 // Remove the incorrect lint warning below that the dialog might be null.
750 assert dialog != null;
752 // Get the views from the dialog fragment.
753 EditText createBookmarkNameEditText = dialog.findViewById(R.id.create_bookmark_name_edittext);
754 EditText createBookmarkUrlEditText = dialog.findViewById(R.id.create_bookmark_url_edittext);
756 // Extract the strings from the edit texts.
757 String bookmarkNameString = createBookmarkNameEditText.getText().toString();
758 String bookmarkUrlString = createBookmarkUrlEditText.getText().toString();
760 // Create a favorite icon byte array output stream.
761 ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
763 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
764 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
766 // Convert the favorite icon byte array stream to a byte array.
767 byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
769 // Display the new bookmark below the current items in the (0 indexed) list.
770 int newBookmarkDisplayOrder = bookmarksListView.getCount();
772 // Create the bookmark.
773 bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentFolder, newBookmarkDisplayOrder, favoriteIconByteArray);
775 // Update the bookmarks cursor with the current contents of this folder.
776 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
778 // Update the `ListView`.
779 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
781 // Scroll to the new bookmark.
782 bookmarksListView.setSelection(newBookmarkDisplayOrder);
786 public void onCreateBookmarkFolder(DialogFragment dialogFragment, @NonNull Bitmap favoriteIconBitmap) {
787 // Get the dialog from the dialog fragment.
788 Dialog dialog = dialogFragment.getDialog();
790 // Remove the incorrect lint warning below that the dialog might be null.
791 assert dialog != null;
793 // Get handles for the views in the dialog fragment.
794 EditText folderNameEditText = dialog.findViewById(R.id.folder_name_edittext);
795 RadioButton defaultIconRadioButton = dialog.findViewById(R.id.default_icon_radiobutton);
796 ImageView defaultIconImageView = dialog.findViewById(R.id.default_icon_imageview);
798 // Get new folder name string.
799 String folderNameString = folderNameEditText.getText().toString();
801 // Create a folder icon bitmap.
802 Bitmap folderIconBitmap;
804 // Set the folder icon bitmap according to the dialog.
805 if (defaultIconRadioButton.isChecked()) { // Use the default folder icon.
806 // Get the default folder icon drawable.
807 Drawable folderIconDrawable = defaultIconImageView.getDrawable();
809 // Convert the folder icon drawable to a bitmap drawable.
810 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
812 // Convert the folder icon bitmap drawable to a bitmap.
813 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
814 } else { // Use the WebView favorite icon.
815 // Copy the favorite icon bitmap to the folder icon bitmap.
816 folderIconBitmap = favoriteIconBitmap;
819 // Create a folder icon byte array output stream.
820 ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream();
822 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
823 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream);
825 // Convert the folder icon byte array stream to a byte array.
826 byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray();
828 // Move all the bookmarks down one in the display order.
829 for (int i = 0; i < bookmarksListView.getCount(); i++) {
830 int databaseId = (int) bookmarksListView.getItemIdAtPosition(i);
831 bookmarksDatabaseHelper.updateDisplayOrder(databaseId, i + 1);
834 // Create the folder, which will be placed at the top of the `ListView`.
835 bookmarksDatabaseHelper.createFolder(folderNameString, currentFolder, folderIconByteArray);
837 // Update the bookmarks cursor with the current contents of this folder.
838 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
840 // Update the list view.
841 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
843 // Scroll to the new folder.
844 bookmarksListView.setSelection(0);
848 public void onSaveBookmark(DialogFragment dialogFragment, int selectedBookmarkDatabaseId, @NonNull Bitmap favoriteIconBitmap) {
849 // Get the dialog from the dialog fragment.
850 Dialog dialog = dialogFragment.getDialog();
852 // Remove the incorrect lint warning below that the dialog might be null.
853 assert dialog != null;
855 // Get handles for the views from the dialog fragment.
856 EditText bookmarkNameEditText = dialog.findViewById(R.id.bookmark_name_edittext);
857 EditText bookmarkUrlEditText = dialog.findViewById(R.id.bookmark_url_edittext);
858 RadioButton currentIconRadioButton = dialog.findViewById(R.id.current_icon_radiobutton);
860 // Store the bookmark strings.
861 String bookmarkNameString = bookmarkNameEditText.getText().toString();
862 String bookmarkUrlString = bookmarkUrlEditText.getText().toString();
864 // Update the bookmark.
865 if (currentIconRadioButton.isChecked()) { // Update the bookmark without changing the favorite icon.
866 bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString);
867 } else { // Update the bookmark using the WebView favorite icon.
868 // Create a favorite icon byte array output stream.
869 ByteArrayOutputStream newFavoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
871 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
872 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFavoriteIconByteArrayOutputStream);
874 // Convert the favorite icon byte array stream to a byte array.
875 byte[] newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray();
877 // Update the bookmark and the favorite icon.
878 bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, newFavoriteIconByteArray);
881 // Check to see if the contextual action mode has been created.
882 if (contextualActionMode != null) {
883 // Close the contextual action mode if it is open.
884 contextualActionMode.finish();
887 // Update the bookmarks cursor with the contents of the current folder.
888 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
890 // Update the `ListView`.
891 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
895 public void onSaveBookmarkFolder(DialogFragment dialogFragment, int selectedFolderDatabaseId, @NonNull Bitmap favoriteIconBitmap) {
896 // Get the dialog from the dialog fragment.
897 Dialog dialog = dialogFragment.getDialog();
899 // Remove the incorrect lint warning below that the dialog might be null.
900 assert dialog != null;
902 // Get handles for the views from the dialog fragment.
903 RadioButton currentFolderIconRadioButton = dialog.findViewById(R.id.current_icon_radiobutton);
904 RadioButton defaultFolderIconRadioButton = dialog.findViewById(R.id.default_icon_radiobutton);
905 ImageView defaultFolderIconImageView = dialog.findViewById(R.id.default_icon_imageview);
906 EditText editFolderNameEditText = dialog.findViewById(R.id.folder_name_edittext);
908 // Get the new folder name.
909 String newFolderNameString = editFolderNameEditText.getText().toString();
911 // Check if the favorite icon has changed.
912 if (currentFolderIconRadioButton.isChecked()) { // Only the name has changed.
913 // Update the name in the database.
914 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString);
915 } else if (!currentFolderIconRadioButton.isChecked() && newFolderNameString.equals(oldFolderNameString)) { // Only the icon has changed.
916 // Create the new folder icon Bitmap.
917 Bitmap folderIconBitmap;
919 // Populate the new folder icon bitmap.
920 if (defaultFolderIconRadioButton.isChecked()) {
921 // Get the default folder icon drawable.
922 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
924 // Convert the folder icon drawable to a bitmap drawable.
925 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
927 // Convert the folder icon bitmap drawable to a bitmap.
928 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
929 } else { // Use the WebView favorite icon.
930 // Copy the favorite icon bitmap to the folder icon bitmap.
931 folderIconBitmap = favoriteIconBitmap;
934 // Create a folder icon byte array output stream.
935 ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
937 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
938 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
940 // Convert the folder icon byte array stream to a byte array.
941 byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
943 // Update the folder icon in the database.
944 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderIconByteArray);
945 } else { // The folder icon and the name have changed.
946 // Instantiate the new folder icon `Bitmap`.
947 Bitmap folderIconBitmap;
949 // Populate the new folder icon bitmap.
950 if (defaultFolderIconRadioButton.isChecked()) {
951 // Get the default folder icon drawable.
952 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
954 // Convert the folder icon drawable to a bitmap drawable.
955 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
957 // Convert the folder icon bitmap drawable to a bitmap.
958 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
959 } else { // Use the WebView favorite icon.
960 // Copy the favorite icon bitmap to the folder icon bitmap.
961 folderIconBitmap = favoriteIconBitmap;
964 // Create a folder icon byte array output stream.
965 ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
967 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
968 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
970 // Convert the folder icon byte array stream to a byte array.
971 byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
973 // Update the folder name and icon in the database.
974 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, newFolderIconByteArray);
977 // Update the bookmarks cursor with the current contents of this folder.
978 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
980 // Update the `ListView`.
981 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
983 // Close the contextual action mode.
984 contextualActionMode.finish();
988 public void onMoveToFolder(DialogFragment dialogFragment) {
989 // Get the dialog from the dialog fragment.
990 Dialog dialog = dialogFragment.getDialog();
992 // Remove the incorrect lint warning below that the dialog might be null.
993 assert dialog != null;
995 // Get a handle for the list view from the dialog.
996 ListView folderListView = dialog.findViewById(R.id.move_to_folder_listview);
998 // Store a long array of the selected folders.
999 long[] newFolderLongArray = folderListView.getCheckedItemIds();
1001 // Get the new folder database ID. Only one folder will be selected.
1002 int newFolderDatabaseId = (int) newFolderLongArray[0];
1004 // Instantiate `newFolderName`.
1005 String newFolderName;
1007 // Set the new folder name.
1008 if (newFolderDatabaseId == 0) {
1009 // The new folder is the home folder, represented as `""` in the database.
1012 // Get the new folder name from the database.
1013 newFolderName = bookmarksDatabaseHelper.getFolderName(newFolderDatabaseId);
1016 // Get a long array with the the database ID of the selected bookmarks.
1017 long[] selectedBookmarksLongArray = bookmarksListView.getCheckedItemIds();
1019 // Move each of the selected bookmarks to the new folder.
1020 for (long databaseIdLong : selectedBookmarksLongArray) {
1021 // Get `databaseIdInt` for each selected bookmark.
1022 int databaseIdInt = (int) databaseIdLong;
1024 // Move the selected bookmark to the new folder.
1025 bookmarksDatabaseHelper.moveToFolder(databaseIdInt, newFolderName);
1028 // Update the bookmarks cursor with the current contents of this folder.
1029 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
1031 // Update the `ListView`.
1032 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
1034 // Close the contextual app bar.
1035 contextualActionMode.finish();
1038 private int countBookmarkFolderContents(int databaseId) {
1039 // Initialize the bookmark counter.
1040 int bookmarkCounter = 0;
1042 // Get the name of the folder.
1043 String folderName = bookmarksDatabaseHelper.getFolderName(databaseId);
1045 // Get the contents of the folder.
1046 Cursor folderCursor = bookmarksDatabaseHelper.getBookmarkIds(folderName);
1048 // Count each of the bookmarks in the folder.
1049 for (int i = 0; i < folderCursor.getCount(); i++) {
1050 // Move the folder cursor to the current row.
1051 folderCursor.moveToPosition(i);
1053 // Get the database ID of the item.
1054 int itemDatabaseId = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.ID));
1056 // If this is a folder, recursively count the contents first.
1057 if (bookmarksDatabaseHelper.isFolder(itemDatabaseId)) {
1058 // Add the bookmarks from the folder to the running total.
1059 bookmarkCounter = bookmarkCounter + countBookmarkFolderContents(itemDatabaseId);
1062 // Add the bookmark to the running total.
1066 // Return the bookmark counter.
1067 return bookmarkCounter;
1070 private void deleteBookmarkFolderContents(int databaseId) {
1071 // Get the name of the folder.
1072 String folderName = bookmarksDatabaseHelper.getFolderName(databaseId);
1074 // Get the contents of the folder.
1075 Cursor folderCursor = bookmarksDatabaseHelper.getBookmarkIds(folderName);
1077 // Delete each of the bookmarks in the folder.
1078 for (int i = 0; i < folderCursor.getCount(); i++) {
1079 // Move the folder cursor to the current row.
1080 folderCursor.moveToPosition(i);
1082 // Get the database ID of the item.
1083 int itemDatabaseId = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.ID));
1085 // If this is a folder, recursively delete the contents first.
1086 if (bookmarksDatabaseHelper.isFolder(itemDatabaseId)) {
1087 deleteBookmarkFolderContents(itemDatabaseId);
1090 // Delete the bookmark.
1091 bookmarksDatabaseHelper.deleteBookmark(itemDatabaseId);
1095 private void updateMoveIcons() {
1096 // Get a long array of the selected bookmarks.
1097 long[] selectedBookmarksLongArray = bookmarksListView.getCheckedItemIds();
1099 // Get the database IDs for the first, last, and selected bookmarks.
1100 int selectedBookmarkDatabaseId = (int) selectedBookmarksLongArray[0];
1101 int firstBookmarkDatabaseId = (int) bookmarksListView.getItemIdAtPosition(0);
1102 // bookmarksListView is 0 indexed.
1103 int lastBookmarkDatabaseId = (int) bookmarksListView.getItemIdAtPosition(bookmarksListView.getCount() - 1);
1105 // Update the move bookmark up menu item.
1106 if (selectedBookmarkDatabaseId == firstBookmarkDatabaseId) { // The selected bookmark is in the first position.
1107 // Disable the move bookmark up menu item.
1108 moveBookmarkUpMenuItem.setEnabled(false);
1111 moveBookmarkUpMenuItem.setIcon(R.drawable.move_up_disabled);
1112 } else { // The selected bookmark is not in the first position.
1113 // Enable the move bookmark up menu item.
1114 moveBookmarkUpMenuItem.setEnabled(true);
1116 // Set the icon according to the theme.
1117 moveBookmarkUpMenuItem.setIcon(R.drawable.move_up_enabled);
1120 // Update the move bookmark down menu item.
1121 if (selectedBookmarkDatabaseId == lastBookmarkDatabaseId) { // The selected bookmark is in the last position.
1122 // Disable the move bookmark down menu item.
1123 moveBookmarkDownMenuItem.setEnabled(false);
1126 moveBookmarkDownMenuItem.setIcon(R.drawable.move_down_disabled);
1127 } else { // The selected bookmark is not in the last position.
1128 // Enable the move bookmark down menu item.
1129 moveBookmarkDownMenuItem.setEnabled(true);
1132 moveBookmarkDownMenuItem.setIcon(R.drawable.move_down_enabled);
1136 private void scrollBookmarks(int selectedBookmarkPosition) {
1137 // Get the first and last visible bookmark positions.
1138 int firstVisibleBookmarkPosition = bookmarksListView.getFirstVisiblePosition();
1139 int lastVisibleBookmarkPosition = bookmarksListView.getLastVisiblePosition();
1141 // Calculate the number of bookmarks per screen.
1142 int numberOfBookmarksPerScreen = lastVisibleBookmarkPosition - firstVisibleBookmarkPosition;
1144 // Scroll with the moved bookmark if necessary.
1145 if (selectedBookmarkPosition <= firstVisibleBookmarkPosition) { // The selected bookmark position is at or above the top of the screen.
1146 // Scroll to the selected bookmark position.
1147 bookmarksListView.setSelection(selectedBookmarkPosition);
1148 } else if (selectedBookmarkPosition >= (lastVisibleBookmarkPosition - 1)) { // The selected bookmark is at or below the bottom of the screen.
1149 // The `-1` handles partial bookmarks displayed at the bottom of the list view. This command scrolls to display the selected bookmark at the bottom of the screen.
1150 // `+1` assures that the entire bookmark will be displayed in situations where only a partial bookmark fits at the bottom of the list view.
1151 bookmarksListView.setSelection(selectedBookmarkPosition - numberOfBookmarksPerScreen + 1);
1155 private void loadFolder() {
1156 // Update bookmarks cursor with the contents of the bookmarks database for the current folder.
1157 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
1159 // Setup a `CursorAdapter`. `this` specifies the `Context`. `false` disables `autoRequery`.
1160 bookmarksCursorAdapter = new CursorAdapter(this, bookmarksCursor, false) {
1162 public View newView(Context context, Cursor cursor, ViewGroup parent) {
1163 // Inflate the individual item layout. `false` does not attach it to the root.
1164 return getLayoutInflater().inflate(R.layout.bookmarks_activity_item_linearlayout, parent, false);
1168 public void bindView(View view, Context context, Cursor cursor) {
1169 // Get handles for the views.
1170 ImageView bookmarkFavoriteIcon = view.findViewById(R.id.bookmark_favorite_icon);
1171 TextView bookmarkNameTextView = view.findViewById(R.id.bookmark_name);
1173 // Get the favorite icon byte array from the `Cursor`.
1174 byte[] favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.FAVORITE_ICON));
1176 // Convert the byte array to a `Bitmap` beginning at the first byte and ending at the last.
1177 Bitmap favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.length);
1179 // Display the bitmap in `bookmarkFavoriteIcon`.
1180 bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap);
1182 // Get the bookmark name from the cursor and display it in `bookmarkNameTextView`.
1183 String bookmarkNameString = cursor.getString(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME));
1184 bookmarkNameTextView.setText(bookmarkNameString);
1186 // Make the font bold for folders.
1187 if (cursor.getInt(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {
1188 bookmarkNameTextView.setTypeface(Typeface.DEFAULT_BOLD);
1189 } else { // Reset the font to default for normal bookmarks.
1190 bookmarkNameTextView.setTypeface(Typeface.DEFAULT);
1195 // Populate the list view with the adapter.
1196 bookmarksListView.setAdapter(bookmarksCursorAdapter);
1198 // Set the `AppBar` title.
1199 if (currentFolder.isEmpty()) {
1200 appBar.setTitle(R.string.bookmarks);
1202 appBar.setTitle(currentFolder);
1207 public void onDestroy() {
1208 // Close the bookmarks cursor and database.
1209 bookmarksCursor.close();
1210 bookmarksDatabaseHelper.close();
1212 // Run the default commands.