2 * Copyright © 2016-2021 Soren Stoutner <soren@stoutner.com>.
4 * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
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.
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.
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/>.
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.content.res.Configuration;
28 import android.database.Cursor;
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.Window;
43 import android.view.WindowManager;
44 import android.widget.AbsListView;
45 import android.widget.CursorAdapter;
46 import android.widget.EditText;
47 import android.widget.ImageView;
48 import android.widget.ListView;
49 import android.widget.RadioButton;
50 import android.widget.TextView;
52 import androidx.annotation.NonNull;
53 import androidx.appcompat.app.ActionBar;
54 import androidx.appcompat.app.AppCompatActivity;
55 import androidx.appcompat.widget.Toolbar;
56 import androidx.fragment.app.DialogFragment;
58 import com.google.android.material.floatingactionbutton.FloatingActionButton;
59 import com.google.android.material.snackbar.Snackbar;
61 import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog;
62 import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog;
63 import com.stoutner.privacybrowser.dialogs.EditBookmarkDialog;
64 import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDialog;
65 import com.stoutner.privacybrowser.dialogs.MoveToFolderDialog;
66 import com.stoutner.privacybrowser.R;
67 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper;
69 import java.io.ByteArrayOutputStream;
70 import java.util.ArrayList;
72 public class BookmarksActivity extends AppCompatActivity implements CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener, EditBookmarkDialog.EditBookmarkListener,
73 EditBookmarkFolderDialog.EditBookmarkFolderListener, MoveToFolderDialog.MoveToFolderListener {
75 // `currentFolder` is public static so it can be accessed from `BookmarksDatabaseViewActivity`.
76 public static String currentFolder;
78 // `restartFromBookmarksDatabaseViewActivity` is public static so it can be accessed from `BookmarksDatabaseViewActivity`. It is also used in `onRestart()`.
79 public static boolean restartFromBookmarksDatabaseViewActivity;
82 // Define the saved instance state constants.
83 private final String CHECKED_BOOKMARKS_ARRAY_LIST = "checked_bookmarks_array_list";
85 // Define the class menu items.
86 private MenuItem moveBookmarkUpMenuItem;
87 private MenuItem moveBookmarkDownMenuItem;
89 // `bookmarksDatabaseHelper` is used in `onCreate()`, `onOptionsItemSelected()`, `onBackPressed()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveBookmark()`, `onSaveBookmarkFolder()`,
90 // `onMoveToFolder()`, `deleteBookmarkFolderContents()`, `loadFolder()`, and `onDestroy()`.
91 private BookmarksDatabaseHelper bookmarksDatabaseHelper;
93 // `bookmarksListView` is used in `onCreate()`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveBookmark()`, `onSaveBookmarkFolder()`, `onMoveToFolder()`,
94 // `updateMoveIcons()`, `scrollBookmarks()`, and `loadFolder()`.
95 private ListView bookmarksListView;
97 // `bookmarksCursor` is used in `onCreate()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveBookmark()`, `onSaveBookmarkFolder()`, `onMoveToFolder()`, `deleteBookmarkFolderContents()`,
98 // `loadFolder()`, and `onDestroy()`.
99 private Cursor bookmarksCursor;
101 // `bookmarksCursorAdapter` is used in `onCreate(), `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveBookmark()`, `onSaveBookmarkFolder()`, `onMoveToFolder()`, and `onLoadFolder()`.
102 private CursorAdapter bookmarksCursorAdapter;
104 // `contextualActionMode` is used in `onCreate()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()` and `onMoveToFolder()`.
105 private ActionMode contextualActionMode;
107 // `appBar` is used in `onCreate()` and `loadFolder()`.
108 private ActionBar appBar;
110 // `oldFolderName` is used in `onCreate()` and `onSaveBookmarkFolder()`.
111 private String oldFolderNameString;
113 // `bookmarksDeletedSnackbar` is used in `onCreate()`, `onOptionsItemSelected()`, and `onBackPressed()`.
114 private Snackbar bookmarksDeletedSnackbar;
116 // `closeActivityAfterDismissingSnackbar` is used in `onCreate()`, `onOptionsItemSelected()`, and `onBackPressed()`.
117 private boolean closeActivityAfterDismissingSnackbar;
119 // The favorite icon byte array is populated in `onCreate()` and used in `onOptionsItemSelected()`.
120 private byte[] favoriteIconByteArray;
123 protected void onCreate(Bundle savedInstanceState) {
124 // Get a handle for the shared preferences.
125 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
127 // Get the preferences.
128 boolean allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false);
129 boolean bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false);
131 // Disable screenshots if not allowed.
132 if (!allowScreenshots) {
133 getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
137 setTheme(R.style.PrivacyBrowser);
139 // Run the default commands.
140 super.onCreate(savedInstanceState);
142 // Get the intent that launched the activity.
143 Intent launchingIntent = getIntent();
145 // Store the current URL and title.
146 String currentUrl = launchingIntent.getStringExtra("current_url");
147 String currentTitle = launchingIntent.getStringExtra("current_title");
149 // Set the current folder variable.
150 if (launchingIntent.getStringExtra("current_folder") != null) { // Set the current folder from the intent.
151 currentFolder = launchingIntent.getStringExtra("current_folder");
152 } else { // Set the current folder to be `""`, which is the home folder.
156 // Get the favorite icon byte array.
157 favoriteIconByteArray = launchingIntent.getByteArrayExtra("favorite_icon_byte_array");
159 // Remove the incorrect lint warning that the favorite icon byte array might be null.
160 assert favoriteIconByteArray != null;
162 // Convert the favorite icon byte array to a bitmap.
163 Bitmap favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.length);
165 // Set the content according to the app bar position.
167 // Set the content view.
168 setContentView(R.layout.bookmarks_bottom_appbar);
170 // `Window.FEATURE_ACTION_MODE_OVERLAY` makes the contextual action mode cover the support action bar. It must be requested before the content is set.
171 supportRequestWindowFeature(Window.FEATURE_ACTION_MODE_OVERLAY);
173 // Set the content view.
174 setContentView(R.layout.bookmarks_top_appbar);
177 // Get a handle for the toolbar.
178 final Toolbar toolbar = findViewById(R.id.bookmarks_toolbar);
180 // Set the support action bar.
181 setSupportActionBar(toolbar);
183 // Get handles for the views.
184 appBar = getSupportActionBar();
185 bookmarksListView = findViewById(R.id.bookmarks_listview);
187 // Remove the incorrect lint warning that `appBar` might be null.
188 assert appBar != null;
190 // Display the home arrow on the app bar.
191 appBar.setDisplayHomeAsUpEnabled(true);
193 // Initialize the database helper. `this` specifies the context. The two `nulls` do not specify the database name or a `CursorFactory`.
194 // The `0` specifies a database version, but that is ignored and set instead using a constant in `BookmarksDatabaseHelper`.
195 bookmarksDatabaseHelper = new BookmarksDatabaseHelper(this, null, null, 0);
197 // Load the home folder.
200 // Set a listener so that tapping a list item loads the URL or folder.
201 bookmarksListView.setOnItemClickListener((parent, view, position, id) -> {
202 // Convert the id from long to int to match the format of the bookmarks database.
203 int databaseId = (int) id;
205 // Get the bookmark cursor for this ID.
206 Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseId);
208 // Move the cursor to the first entry.
209 bookmarkCursor.moveToFirst();
211 // Act upon the bookmark according to the type.
212 if (bookmarkCursor.getInt(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) { // The selected bookmark is a folder.
213 // Update the current folder.
214 currentFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
216 // Load the new folder.
218 } else { // The selected bookmark is not a folder.
219 // Instantiate the edit bookmark dialog.
220 DialogFragment editBookmarkDialog = EditBookmarkDialog.bookmarkDatabaseId(databaseId, favoriteIconBitmap);
223 editBookmarkDialog.show(getSupportFragmentManager(), getResources().getString(R.string.edit_bookmark));
227 bookmarkCursor.close();
230 // Handle long presses on the list view.
231 bookmarksListView.setMultiChoiceModeListener(new AbsListView.MultiChoiceModeListener() {
232 // Instantiate the common variables.
233 MenuItem editBookmarkMenuItem;
234 MenuItem deleteBookmarksMenuItem;
235 MenuItem selectAllBookmarksMenuItem;
236 boolean deletingBookmarks;
239 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
240 // Inflate the menu for the contextual app bar.
241 getMenuInflater().inflate(R.menu.bookmarks_context_menu, menu);
244 if (currentFolder.isEmpty()) { // Use `R.string.bookmarks` if in the home folder.
245 mode.setTitle(R.string.bookmarks);
246 } else { // Use the current folder name as the title.
247 mode.setTitle(currentFolder);
250 // Get handles for menu items that need to be selectively disabled.
251 moveBookmarkUpMenuItem = menu.findItem(R.id.move_bookmark_up);
252 moveBookmarkDownMenuItem = menu.findItem(R.id.move_bookmark_down);
253 editBookmarkMenuItem = menu.findItem(R.id.edit_bookmark);
254 deleteBookmarksMenuItem = menu.findItem(R.id.delete_bookmark);
255 selectAllBookmarksMenuItem = menu.findItem(R.id.context_menu_select_all_bookmarks);
257 // Disable the delete bookmarks menu item if a delete is pending.
258 deleteBookmarksMenuItem.setEnabled(!deletingBookmarks);
260 // Store a handle for the contextual action mode so it can be closed programatically.
261 contextualActionMode = mode;
268 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
269 // Get a handle for the move to folder menu item.
270 MenuItem moveToFolderMenuItem = menu.findItem(R.id.move_to_folder);
272 // Get a Cursor with all of the folders.
273 Cursor folderCursor = bookmarksDatabaseHelper.getAllFolders();
275 // Enable the move to folder menu item if at least one folder exists.
276 moveToFolderMenuItem.setVisible(folderCursor.getCount() > 0);
283 public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
284 // Get the number of selected bookmarks.
285 int numberOfSelectedBookmarks = bookmarksListView.getCheckedItemCount();
287 // Only process commands if at least one bookmark is selected. Otherwise, a context menu with 0 selected bookmarks is briefly displayed.
288 if (numberOfSelectedBookmarks > 0) {
289 // Adjust the ActionMode and the menu according to the number of selected bookmarks.
290 if (numberOfSelectedBookmarks == 1) { // One bookmark is selected.
291 // List the number of selected bookmarks in the subtitle.
292 mode.setSubtitle(getString(R.string.selected) + " 1");
294 // Show the `Move Up`, `Move Down`, and `Edit` options.
295 moveBookmarkUpMenuItem.setVisible(true);
296 moveBookmarkDownMenuItem.setVisible(true);
297 editBookmarkMenuItem.setVisible(true);
299 // Update the enabled status of the move icons.
301 } else { // More than one bookmark is selected.
302 // List the number of selected bookmarks in the subtitle.
303 mode.setSubtitle(getString(R.string.selected) + " " + numberOfSelectedBookmarks);
305 // Hide non-applicable `MenuItems`.
306 moveBookmarkUpMenuItem.setVisible(false);
307 moveBookmarkDownMenuItem.setVisible(false);
308 editBookmarkMenuItem.setVisible(false);
311 // Show the select all menu item if all the bookmarks are not selected.
312 selectAllBookmarksMenuItem.setVisible(bookmarksListView.getCheckedItemCount() != bookmarksListView.getCount());
317 public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
318 // Declare the common variables.
319 int selectedBookmarkNewPosition;
320 final SparseBooleanArray selectedBookmarksPositionsSparseBooleanArray;
322 // Initialize the selected bookmark position.
323 int selectedBookmarkPosition = 0;
325 // Get the menu item ID.
326 int menuItemId = menuItem.getItemId();
328 // Run the commands according to the selected action item.
329 if (menuItemId == R.id.move_bookmark_up) { // Move the bookmark up.
330 // Get the array of checked bookmark positions.
331 selectedBookmarksPositionsSparseBooleanArray = bookmarksListView.getCheckedItemPositions();
333 // 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`.
334 for (int i = 0; i < selectedBookmarksPositionsSparseBooleanArray.size(); i++) {
335 // Check to see if the value for the bookmark is true, meaning it is currently selected.
336 if (selectedBookmarksPositionsSparseBooleanArray.valueAt(i)) {
337 // Only one bookmark should have a value of `true` when move bookmark up is enabled.
338 selectedBookmarkPosition = selectedBookmarksPositionsSparseBooleanArray.keyAt(i);
342 // Calculate the new position of the selected bookmark.
343 selectedBookmarkNewPosition = selectedBookmarkPosition - 1;
345 // Iterate through the bookmarks.
346 for (int i = 0; i < bookmarksListView.getCount(); i++) {
347 // Get the database ID for the current bookmark.
348 int currentBookmarkDatabaseId = (int) bookmarksListView.getItemIdAtPosition(i);
350 // Update the display order for the current bookmark.
351 if (i == selectedBookmarkPosition) { // The current bookmark is the selected bookmark.
352 // Move the current bookmark up one.
353 bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i - 1);
354 } else if ((i + 1) == selectedBookmarkPosition) { // The current bookmark is immediately above the selected bookmark.
355 // Move the current bookmark down one.
356 bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i + 1);
357 } else { // The current bookmark is not changing positions.
358 // Move `bookmarksCursor` to the current bookmark position.
359 bookmarksCursor.moveToPosition(i);
361 // Update the display order only if it is not correct in the database.
362 if (bookmarksCursor.getInt(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.DISPLAY_ORDER)) != i) {
363 bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i);
368 // Update the bookmarks cursor with the current contents of the bookmarks database.
369 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
371 // Update the list view.
372 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
374 // Scroll with the bookmark.
375 scrollBookmarks(selectedBookmarkNewPosition);
377 // Update the enabled status of the move icons.
379 } else if (menuItemId == R.id.move_bookmark_down) { // Move the bookmark down.
380 // Get the array of checked bookmark positions.
381 selectedBookmarksPositionsSparseBooleanArray = bookmarksListView.getCheckedItemPositions();
383 // 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`.
384 for (int i = 0; i < selectedBookmarksPositionsSparseBooleanArray.size(); i++) {
385 // Check to see if the value for the bookmark is true, meaning it is currently selected.
386 if (selectedBookmarksPositionsSparseBooleanArray.valueAt(i)) {
387 // Only one bookmark should have a value of `true` when move bookmark down is enabled.
388 selectedBookmarkPosition = selectedBookmarksPositionsSparseBooleanArray.keyAt(i);
392 // Calculate the new position of the selected bookmark.
393 selectedBookmarkNewPosition = selectedBookmarkPosition + 1;
395 // Iterate through the bookmarks.
396 for (int i = 0; i < bookmarksListView.getCount(); i++) {
397 // Get the database ID for the current bookmark.
398 int currentBookmarkDatabaseId = (int) bookmarksListView.getItemIdAtPosition(i);
400 // Update the display order for the current bookmark.
401 if (i == selectedBookmarkPosition) { // The current bookmark is the selected bookmark.
402 // Move the current bookmark down one.
403 bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i + 1);
404 } else if ((i - 1) == selectedBookmarkPosition) { // The current bookmark is immediately below the selected bookmark.
405 // Move the bookmark below the selected bookmark up one.
406 bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i - 1);
407 } else { // The current bookmark is not changing positions.
408 // Move `bookmarksCursor` to the current bookmark position.
409 bookmarksCursor.moveToPosition(i);
411 // Update the display order only if it is not correct in the database.
412 if (bookmarksCursor.getInt(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.DISPLAY_ORDER)) != i) {
413 bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i);
418 // Update the bookmarks cursor with the current contents of the bookmarks database.
419 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
421 // Update the list view.
422 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
424 // Scroll with the bookmark.
425 scrollBookmarks(selectedBookmarkNewPosition);
427 // Update the enabled status of the move icons.
429 } else if (menuItemId == R.id.move_to_folder) { // Move to folder.
430 // Instantiate the move to folder alert dialog.
431 DialogFragment moveToFolderDialog = MoveToFolderDialog.moveBookmarks(currentFolder, bookmarksListView.getCheckedItemIds());
433 // Show the move to folder alert dialog.
434 moveToFolderDialog.show(getSupportFragmentManager(), getResources().getString(R.string.move_to_folder));
435 } else if (menuItemId == R.id.edit_bookmark) {
436 // Get the array of checked bookmark positions.
437 selectedBookmarksPositionsSparseBooleanArray = bookmarksListView.getCheckedItemPositions();
439 // 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`.
440 for (int i = 0; i < selectedBookmarksPositionsSparseBooleanArray.size(); i++) {
441 // Check to see if the value for the bookmark is true, meaning it is currently selected.
442 if (selectedBookmarksPositionsSparseBooleanArray.valueAt(i)) {
443 // Only one bookmark should have a value of `true` when move edit bookmark is enabled.
444 selectedBookmarkPosition = selectedBookmarksPositionsSparseBooleanArray.keyAt(i);
448 // Move the cursor to the selected position.
449 bookmarksCursor.moveToPosition(selectedBookmarkPosition);
451 // Find out if this bookmark is a folder.
452 boolean isFolder = (bookmarksCursor.getInt(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1);
454 // Get the selected bookmark database ID.
455 int databaseId = bookmarksCursor.getInt(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper._ID));
457 // Show the edit bookmark or edit bookmark folder dialog.
459 // Save the current folder name, which is used in `onSaveBookmarkFolder()`.
460 oldFolderNameString = bookmarksCursor.getString(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
462 // Instantiate the edit bookmark folder dialog.
463 DialogFragment editFolderDialog = EditBookmarkFolderDialog.folderDatabaseId(databaseId, favoriteIconBitmap);
466 editFolderDialog.show(getSupportFragmentManager(), getResources().getString(R.string.edit_folder));
468 // Instantiate the edit bookmark dialog.
469 DialogFragment editBookmarkDialog = EditBookmarkDialog.bookmarkDatabaseId(databaseId, favoriteIconBitmap);
472 editBookmarkDialog.show(getSupportFragmentManager(), getResources().getString(R.string.edit_bookmark));
474 } else if (menuItemId == R.id.delete_bookmark) { // Delete bookmark.
475 // Set the deleting bookmarks flag, which prevents the delete menu item from being enabled until the current process finishes.
476 deletingBookmarks = true;
478 // Get an array of the selected row IDs.
479 final long[] selectedBookmarksIdsLongArray = bookmarksListView.getCheckedItemIds();
481 // Initialize a variable to count the number of bookmarks to delete.
482 int numberOfBookmarksToDelete = 0;
484 // Count the number of bookmarks.
485 for (long databaseIdLong : selectedBookmarksIdsLongArray) {
486 // Convert the database ID long to an int.
487 int databaseIdInt = (int) databaseIdLong;
489 // Count the contents of the folder if the selected bookmark is a folder.
490 if (bookmarksDatabaseHelper.isFolder(databaseIdInt)) {
491 // Add the bookmarks from the folder to the running total.
492 numberOfBookmarksToDelete = numberOfBookmarksToDelete + countBookmarkFolderContents(databaseIdInt);
495 // Increment the count of the number of bookmarks to delete.
496 numberOfBookmarksToDelete++;
499 // 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.
500 selectedBookmarksPositionsSparseBooleanArray = bookmarksListView.getCheckedItemPositions().clone();
502 // Update the bookmarks cursor with the current contents of the bookmarks database except for the specified database IDs.
503 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrderExcept(selectedBookmarksIdsLongArray, currentFolder);
505 // Update the list view.
506 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
508 // Create a Snackbar with the number of deleted bookmarks.
509 bookmarksDeletedSnackbar = Snackbar.make(findViewById(R.id.bookmarks_coordinatorlayout), getString(R.string.bookmarks_deleted) + " " + numberOfBookmarksToDelete,
510 Snackbar.LENGTH_LONG)
511 .setAction(R.string.undo, view -> {
512 // Do nothing because everything will be handled by `onDismissed()` below.
514 .addCallback(new Snackbar.Callback() {
515 @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
517 public void onDismissed(Snackbar snackbar, int event) {
518 if (event == Snackbar.Callback.DISMISS_EVENT_ACTION) { // The user pushed the undo button.
519 // Update the bookmarks cursor with the current contents of the bookmarks database, including the "deleted" bookmarks.
520 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
522 // Update the list view.
523 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
525 // Re-select the previously selected bookmarks.
526 for (int i = 0; i < selectedBookmarksPositionsSparseBooleanArray.size(); i++) {
527 bookmarksListView.setItemChecked(selectedBookmarksPositionsSparseBooleanArray.keyAt(i), true);
529 } else { // The snackbar was dismissed without the undo button being pushed.
530 // Delete each selected bookmark.
531 for (long databaseIdLong : selectedBookmarksIdsLongArray) {
532 // Convert `databaseIdLong` to an int.
533 int databaseIdInt = (int) databaseIdLong;
535 // Delete the contents of the folder if the selected bookmark is a folder.
536 if (bookmarksDatabaseHelper.isFolder(databaseIdInt)) {
537 deleteBookmarkFolderContents(databaseIdInt);
540 // Delete the selected bookmark.
541 bookmarksDatabaseHelper.deleteBookmark(databaseIdInt);
544 // Update the display order.
545 for (int i = 0; i < bookmarksListView.getCount(); i++) {
546 // Get the database ID for the current bookmark.
547 int currentBookmarkDatabaseId = (int) bookmarksListView.getItemIdAtPosition(i);
549 // Move `bookmarksCursor` to the current bookmark position.
550 bookmarksCursor.moveToPosition(i);
552 // Update the display order only if it is not correct in the database.
553 if (bookmarksCursor.getInt(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.DISPLAY_ORDER)) != i) {
554 bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i);
559 // Reset the deleting bookmarks flag.
560 deletingBookmarks = false;
562 // Enable the delete bookmarks menu item.
563 deleteBookmarksMenuItem.setEnabled(true);
565 // Close the activity if back has been pressed.
566 if (closeActivityAfterDismissingSnackbar) {
573 bookmarksDeletedSnackbar.show();
574 } else if (menuItemId == R.id.context_menu_select_all_bookmarks) { // Select all.
575 // Get the total number of bookmarks.
576 int numberOfBookmarks = bookmarksListView.getCount();
579 for (int i = 0; i < numberOfBookmarks; i++) {
580 bookmarksListView.setItemChecked(i, true);
584 // Consume the click.
589 public void onDestroyActionMode(ActionMode mode) {
594 // Get handles for the floating action buttons.
595 FloatingActionButton createBookmarkFolderFab = findViewById(R.id.create_bookmark_folder_fab);
596 FloatingActionButton createBookmarkFab = findViewById(R.id.create_bookmark_fab);
598 // Set the create new bookmark folder FAB to display the `AlertDialog`.
599 createBookmarkFolderFab.setOnClickListener(v -> {
600 // Create a create bookmark folder dialog.
601 DialogFragment createBookmarkFolderDialog = CreateBookmarkFolderDialog.createBookmarkFolder(favoriteIconBitmap);
603 // Show the create bookmark folder dialog.
604 createBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.create_folder));
607 // Set the create new bookmark FAB to display the alert dialog.
608 createBookmarkFab.setOnClickListener(view -> {
609 // Remove the incorrect lint warning below.
610 assert currentUrl != null;
611 assert currentTitle != null;
613 // Instantiate the create bookmark dialog.
614 DialogFragment createBookmarkDialog = CreateBookmarkDialog.createBookmark(currentUrl, currentTitle, favoriteIconBitmap);
616 // Display the create bookmark dialog.
617 createBookmarkDialog.show(getSupportFragmentManager(), getResources().getString(R.string.create_bookmark));
620 // Restore the state if the app has been restarted.
621 if (savedInstanceState != null) {
622 // Update the bookmarks list view after it has loaded.
623 bookmarksListView.post(() -> {
624 // Get the checked bookmarks array list.
625 ArrayList<Integer> checkedBookmarksArrayList = savedInstanceState.getIntegerArrayList(CHECKED_BOOKMARKS_ARRAY_LIST);
627 // Check each previously checked bookmark in the list view. When the minimum API >= 24 a `forEach()` command can be used instead.
628 if (checkedBookmarksArrayList != null) {
629 for (int i = 0; i < checkedBookmarksArrayList.size(); i++) {
630 bookmarksListView.setItemChecked(checkedBookmarksArrayList.get(i), true);
638 public void onRestart() {
639 // Run the default commands.
642 // Update the list view if returning from the bookmarks database view activity.
643 if (restartFromBookmarksDatabaseViewActivity) {
644 // Load the current folder in the list view.
647 // Reset `restartFromBookmarksDatabaseViewActivity`.
648 restartFromBookmarksDatabaseViewActivity = false;
653 public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
654 // Run the default commands.
655 super.onSaveInstanceState(savedInstanceState);
657 // Get the array of the checked items.
658 SparseBooleanArray checkedBookmarksSparseBooleanArray = bookmarksListView.getCheckedItemPositions();
660 // Create a checked items array list.
661 ArrayList<Integer> checkedBookmarksArrayList = new ArrayList<>();
663 // Add each checked bookmark position to the array list.
664 for (int i = 0; i < checkedBookmarksSparseBooleanArray.size(); i++) {
665 // 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.
666 if (checkedBookmarksSparseBooleanArray.valueAt(i)) {
667 // Add the bookmark position to the checked bookmarks array list.
668 checkedBookmarksArrayList.add(checkedBookmarksSparseBooleanArray.keyAt(i));
672 // Store the checked items array list in the saved instance state.
673 savedInstanceState.putIntegerArrayList(CHECKED_BOOKMARKS_ARRAY_LIST, checkedBookmarksArrayList);
677 public boolean onCreateOptionsMenu(Menu menu) {
679 getMenuInflater().inflate(R.menu.bookmarks_options_menu, menu);
686 public boolean onOptionsItemSelected(MenuItem menuItem) {
687 // Get a handle for the menu item ID.
688 int menuItemId = menuItem.getItemId();
690 // Run the command according to the selected option.
691 if (menuItemId == android.R.id.home) { // Home. The home arrow is identified as `android.R.id.home`, not just `R.id.home`.
692 if (currentFolder.isEmpty()) { // Currently in the home folder.
693 // Run the back commands.
695 } else { // Currently in a subfolder.
696 // Place the former parent folder in `currentFolder`.
697 currentFolder = bookmarksDatabaseHelper.getParentFolderName(currentFolder);
699 // Load the new folder.
702 } else if (menuItemId == R.id.options_menu_select_all_bookmarks) { // Select all.
703 // Get the total number of bookmarks.
704 int numberOfBookmarks = bookmarksListView.getCount();
707 for (int i = 0; i < numberOfBookmarks; i++) {
708 bookmarksListView.setItemChecked(i, true);
710 } else if (menuItemId == R.id.bookmarks_database_view) {
711 // Close the contextual action bar if it is displayed. This can happen if the bottom app bar is enabled.
712 if (contextualActionMode != null) {
713 contextualActionMode.finish();
716 // Create an intent to launch the bookmarks database view activity.
717 Intent bookmarksDatabaseViewIntent = new Intent(this, BookmarksDatabaseViewActivity.class);
719 // Include the favorite icon byte array to the intent.
720 bookmarksDatabaseViewIntent.putExtra("favorite_icon_byte_array", favoriteIconByteArray);
723 startActivity(bookmarksDatabaseViewIntent);
729 public void onBackPressed() {
730 // 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.
731 if ((bookmarksDeletedSnackbar != null) && bookmarksDeletedSnackbar.isShown()) { // Close the bookmarks deleted snackbar before going home.
732 // Set the close flag.
733 closeActivityAfterDismissingSnackbar = true;
735 // Dismiss the snackbar.
736 bookmarksDeletedSnackbar.dismiss();
737 } else { // Go home immediately.
738 // Update the bookmarks folder for the bookmarks drawer in the main WebView activity.
739 MainWebViewActivity.currentBookmarksFolder = currentFolder;
741 // Close the bookmarks drawer and reload the bookmarks ListView when returning to the main WebView activity.
742 MainWebViewActivity.restartFromBookmarksActivity = true;
744 // Exit the bookmarks activity.
745 super.onBackPressed();
750 public void onCreateBookmark(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) {
751 // Get the alert dialog from the fragment.
752 Dialog dialog = dialogFragment.getDialog();
754 // Remove the incorrect lint warning below that the dialog might be null.
755 assert dialog != null;
757 // Get the views from the dialog fragment.
758 EditText createBookmarkNameEditText = dialog.findViewById(R.id.create_bookmark_name_edittext);
759 EditText createBookmarkUrlEditText = dialog.findViewById(R.id.create_bookmark_url_edittext);
761 // Extract the strings from the edit texts.
762 String bookmarkNameString = createBookmarkNameEditText.getText().toString();
763 String bookmarkUrlString = createBookmarkUrlEditText.getText().toString();
765 // Create a favorite icon byte array output stream.
766 ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
768 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
769 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
771 // Convert the favorite icon byte array stream to a byte array.
772 byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
774 // Display the new bookmark below the current items in the (0 indexed) list.
775 int newBookmarkDisplayOrder = bookmarksListView.getCount();
777 // Create the bookmark.
778 bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentFolder, newBookmarkDisplayOrder, favoriteIconByteArray);
780 // Update the bookmarks cursor with the current contents of this folder.
781 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
783 // Update the `ListView`.
784 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
786 // Scroll to the new bookmark.
787 bookmarksListView.setSelection(newBookmarkDisplayOrder);
791 public void onCreateBookmarkFolder(DialogFragment dialogFragment, @NonNull Bitmap favoriteIconBitmap) {
792 // Get the dialog from the dialog fragment.
793 Dialog dialog = dialogFragment.getDialog();
795 // Remove the incorrect lint warning below that the dialog might be null.
796 assert dialog != null;
798 // Get handles for the views in the dialog fragment.
799 EditText folderNameEditText = dialog.findViewById(R.id.folder_name_edittext);
800 RadioButton defaultIconRadioButton = dialog.findViewById(R.id.default_icon_radiobutton);
801 ImageView defaultIconImageView = dialog.findViewById(R.id.default_icon_imageview);
803 // Get new folder name string.
804 String folderNameString = folderNameEditText.getText().toString();
806 // Create a folder icon bitmap.
807 Bitmap folderIconBitmap;
809 // Set the folder icon bitmap according to the dialog.
810 if (defaultIconRadioButton.isChecked()) { // Use the default folder icon.
811 // Get the default folder icon drawable.
812 Drawable folderIconDrawable = defaultIconImageView.getDrawable();
814 // Convert the folder icon drawable to a bitmap drawable.
815 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
817 // Convert the folder icon bitmap drawable to a bitmap.
818 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
819 } else { // Use the WebView favorite icon.
820 // Copy the favorite icon bitmap to the folder icon bitmap.
821 folderIconBitmap = favoriteIconBitmap;
824 // Create a folder icon byte array output stream.
825 ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream();
827 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
828 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream);
830 // Convert the folder icon byte array stream to a byte array.
831 byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray();
833 // Move all the bookmarks down one in the display order.
834 for (int i = 0; i < bookmarksListView.getCount(); i++) {
835 int databaseId = (int) bookmarksListView.getItemIdAtPosition(i);
836 bookmarksDatabaseHelper.updateDisplayOrder(databaseId, i + 1);
839 // Create the folder, which will be placed at the top of the `ListView`.
840 bookmarksDatabaseHelper.createFolder(folderNameString, currentFolder, folderIconByteArray);
842 // Update the bookmarks cursor with the current contents of this folder.
843 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
845 // Update the list view.
846 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
848 // Scroll to the new folder.
849 bookmarksListView.setSelection(0);
853 public void onSaveBookmark(DialogFragment dialogFragment, int selectedBookmarkDatabaseId, @NonNull Bitmap favoriteIconBitmap) {
854 // Get the dialog from the dialog fragment.
855 Dialog dialog = dialogFragment.getDialog();
857 // Remove the incorrect lint warning below that the dialog might be null.
858 assert dialog != null;
860 // Get handles for the views from the dialog fragment.
861 EditText bookmarkNameEditText = dialog.findViewById(R.id.bookmark_name_edittext);
862 EditText bookmarkUrlEditText = dialog.findViewById(R.id.bookmark_url_edittext);
863 RadioButton currentIconRadioButton = dialog.findViewById(R.id.current_icon_radiobutton);
865 // Store the bookmark strings.
866 String bookmarkNameString = bookmarkNameEditText.getText().toString();
867 String bookmarkUrlString = bookmarkUrlEditText.getText().toString();
869 // Update the bookmark.
870 if (currentIconRadioButton.isChecked()) { // Update the bookmark without changing the favorite icon.
871 bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString);
872 } else { // Update the bookmark using the WebView favorite icon.
873 // Create a favorite icon byte array output stream.
874 ByteArrayOutputStream newFavoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
876 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
877 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFavoriteIconByteArrayOutputStream);
879 // Convert the favorite icon byte array stream to a byte array.
880 byte[] newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray();
882 // Update the bookmark and the favorite icon.
883 bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, newFavoriteIconByteArray);
886 // Check to see if the contextual action mode has been created.
887 if (contextualActionMode != null) {
888 // Close the contextual action mode if it is open.
889 contextualActionMode.finish();
892 // Update the bookmarks cursor with the contents of the current folder.
893 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
895 // Update the `ListView`.
896 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
900 public void onSaveBookmarkFolder(DialogFragment dialogFragment, int selectedFolderDatabaseId, @NonNull Bitmap favoriteIconBitmap) {
901 // Get the dialog from the dialog fragment.
902 Dialog dialog = dialogFragment.getDialog();
904 // Remove the incorrect lint warning below that the dialog might be null.
905 assert dialog != null;
907 // Get handles for the views from the dialog fragment.
908 RadioButton currentFolderIconRadioButton = dialog.findViewById(R.id.current_icon_radiobutton);
909 RadioButton defaultFolderIconRadioButton = dialog.findViewById(R.id.default_icon_radiobutton);
910 ImageView defaultFolderIconImageView = dialog.findViewById(R.id.default_icon_imageview);
911 EditText editFolderNameEditText = dialog.findViewById(R.id.folder_name_edittext);
913 // Get the new folder name.
914 String newFolderNameString = editFolderNameEditText.getText().toString();
916 // Check if the favorite icon has changed.
917 if (currentFolderIconRadioButton.isChecked()) { // Only the name has changed.
918 // Update the name in the database.
919 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString);
920 } else if (!currentFolderIconRadioButton.isChecked() && newFolderNameString.equals(oldFolderNameString)) { // Only the icon has changed.
921 // Create the new folder icon Bitmap.
922 Bitmap folderIconBitmap;
924 // Populate the new folder icon bitmap.
925 if (defaultFolderIconRadioButton.isChecked()) {
926 // Get the default folder icon drawable.
927 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
929 // Convert the folder icon drawable to a bitmap drawable.
930 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
932 // Convert the folder icon bitmap drawable to a bitmap.
933 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
934 } else { // Use the WebView favorite icon.
935 // Copy the favorite icon bitmap to the folder icon bitmap.
936 folderIconBitmap = favoriteIconBitmap;
939 // Create a folder icon byte array output stream.
940 ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
942 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
943 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
945 // Convert the folder icon byte array stream to a byte array.
946 byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
948 // Update the folder icon in the database.
949 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderIconByteArray);
950 } else { // The folder icon and the name have changed.
951 // Instantiate the new folder icon `Bitmap`.
952 Bitmap folderIconBitmap;
954 // Populate the new folder icon bitmap.
955 if (defaultFolderIconRadioButton.isChecked()) {
956 // Get the default folder icon drawable.
957 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
959 // Convert the folder icon drawable to a bitmap drawable.
960 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
962 // Convert the folder icon bitmap drawable to a bitmap.
963 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
964 } else { // Use the WebView favorite icon.
965 // Copy the favorite icon bitmap to the folder icon bitmap.
966 folderIconBitmap = favoriteIconBitmap;
969 // Create a folder icon byte array output stream.
970 ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
972 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
973 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
975 // Convert the folder icon byte array stream to a byte array.
976 byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
978 // Update the folder name and icon in the database.
979 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, newFolderIconByteArray);
982 // Update the bookmarks cursor with the current contents of this folder.
983 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
985 // Update the `ListView`.
986 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
988 // Close the contextual action mode.
989 contextualActionMode.finish();
993 public void onMoveToFolder(DialogFragment dialogFragment) {
994 // Get the dialog from the dialog fragment.
995 Dialog dialog = dialogFragment.getDialog();
997 // Remove the incorrect lint warning below that the dialog might be null.
998 assert dialog != null;
1000 // Get a handle for the list view from the dialog.
1001 ListView folderListView = dialog.findViewById(R.id.move_to_folder_listview);
1003 // Store a long array of the selected folders.
1004 long[] newFolderLongArray = folderListView.getCheckedItemIds();
1006 // Get the new folder database ID. Only one folder will be selected.
1007 int newFolderDatabaseId = (int) newFolderLongArray[0];
1009 // Instantiate `newFolderName`.
1010 String newFolderName;
1012 // Set the new folder name.
1013 if (newFolderDatabaseId == 0) {
1014 // The new folder is the home folder, represented as `""` in the database.
1017 // Get the new folder name from the database.
1018 newFolderName = bookmarksDatabaseHelper.getFolderName(newFolderDatabaseId);
1021 // Get a long array with the the database ID of the selected bookmarks.
1022 long[] selectedBookmarksLongArray = bookmarksListView.getCheckedItemIds();
1024 // Move each of the selected bookmarks to the new folder.
1025 for (long databaseIdLong : selectedBookmarksLongArray) {
1026 // Get `databaseIdInt` for each selected bookmark.
1027 int databaseIdInt = (int) databaseIdLong;
1029 // Move the selected bookmark to the new folder.
1030 bookmarksDatabaseHelper.moveToFolder(databaseIdInt, newFolderName);
1033 // Update the bookmarks cursor with the current contents of this folder.
1034 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
1036 // Update the `ListView`.
1037 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
1039 // Close the contextual app bar.
1040 contextualActionMode.finish();
1043 private int countBookmarkFolderContents(int databaseId) {
1044 // Initialize the bookmark counter.
1045 int bookmarkCounter = 0;
1047 // Get the name of the folder.
1048 String folderName = bookmarksDatabaseHelper.getFolderName(databaseId);
1050 // Get the contents of the folder.
1051 Cursor folderCursor = bookmarksDatabaseHelper.getBookmarkIds(folderName);
1053 // Count each of the bookmarks in the folder.
1054 for (int i = 0; i < folderCursor.getCount(); i++) {
1055 // Move the folder cursor to the current row.
1056 folderCursor.moveToPosition(i);
1058 // Get the database ID of the item.
1059 int itemDatabaseId = folderCursor.getInt(folderCursor.getColumnIndex(BookmarksDatabaseHelper._ID));
1061 // If this is a folder, recursively count the contents first.
1062 if (bookmarksDatabaseHelper.isFolder(itemDatabaseId)) {
1063 // Add the bookmarks from the folder to the running total.
1064 bookmarkCounter = bookmarkCounter + countBookmarkFolderContents(itemDatabaseId);
1067 // Add the bookmark to the running total.
1071 // Return the bookmark counter.
1072 return bookmarkCounter;
1075 private void deleteBookmarkFolderContents(int databaseId) {
1076 // Get the name of the folder.
1077 String folderName = bookmarksDatabaseHelper.getFolderName(databaseId);
1079 // Get the contents of the folder.
1080 Cursor folderCursor = bookmarksDatabaseHelper.getBookmarkIds(folderName);
1082 // Delete each of the bookmarks in the folder.
1083 for (int i = 0; i < folderCursor.getCount(); i++) {
1084 // Move the folder cursor to the current row.
1085 folderCursor.moveToPosition(i);
1087 // Get the database ID of the item.
1088 int itemDatabaseId = folderCursor.getInt(folderCursor.getColumnIndex(BookmarksDatabaseHelper._ID));
1090 // If this is a folder, recursively delete the contents first.
1091 if (bookmarksDatabaseHelper.isFolder(itemDatabaseId)) {
1092 deleteBookmarkFolderContents(itemDatabaseId);
1095 // Delete the bookmark.
1096 bookmarksDatabaseHelper.deleteBookmark(itemDatabaseId);
1100 private void updateMoveIcons() {
1101 // Get a long array of the selected bookmarks.
1102 long[] selectedBookmarksLongArray = bookmarksListView.getCheckedItemIds();
1104 // Get the database IDs for the first, last, and selected bookmarks.
1105 int selectedBookmarkDatabaseId = (int) selectedBookmarksLongArray[0];
1106 int firstBookmarkDatabaseId = (int) bookmarksListView.getItemIdAtPosition(0);
1107 // bookmarksListView is 0 indexed.
1108 int lastBookmarkDatabaseId = (int) bookmarksListView.getItemIdAtPosition(bookmarksListView.getCount() - 1);
1110 // Get the current theme status.
1111 int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
1113 // Update the move bookmark up `MenuItem`.
1114 if (selectedBookmarkDatabaseId == firstBookmarkDatabaseId) { // The selected bookmark is in the first position.
1115 // Disable the move bookmark up `MenuItem`.
1116 moveBookmarkUpMenuItem.setEnabled(false);
1118 // Set the move bookmark up icon to be ghosted.
1119 moveBookmarkUpMenuItem.setIcon(R.drawable.move_up_disabled);
1120 } else { // The selected bookmark is not in the first position.
1121 // Enable the move bookmark up menu item.
1122 moveBookmarkUpMenuItem.setEnabled(true);
1124 // Set the icon according to the theme.
1125 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) {
1126 moveBookmarkUpMenuItem.setIcon(R.drawable.move_up_enabled_night);
1128 moveBookmarkUpMenuItem.setIcon(R.drawable.move_up_enabled_day);
1132 // Update the move bookmark down `MenuItem`.
1133 if (selectedBookmarkDatabaseId == lastBookmarkDatabaseId) { // The selected bookmark is in the last position.
1134 // Disable the move bookmark down `MenuItem`.
1135 moveBookmarkDownMenuItem.setEnabled(false);
1137 // Set the move bookmark down icon to be ghosted.
1138 moveBookmarkDownMenuItem.setIcon(R.drawable.move_down_disabled);
1139 } else { // The selected bookmark is not in the last position.
1140 // Enable the move bookmark down `MenuItem`.
1141 moveBookmarkDownMenuItem.setEnabled(true);
1143 // Set the icon according to the theme.
1144 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) {
1145 moveBookmarkDownMenuItem.setIcon(R.drawable.move_down_enabled_night);
1147 moveBookmarkDownMenuItem.setIcon(R.drawable.move_down_enabled_day);
1152 private void scrollBookmarks(int selectedBookmarkPosition) {
1153 // Get the first and last visible bookmark positions.
1154 int firstVisibleBookmarkPosition = bookmarksListView.getFirstVisiblePosition();
1155 int lastVisibleBookmarkPosition = bookmarksListView.getLastVisiblePosition();
1157 // Calculate the number of bookmarks per screen.
1158 int numberOfBookmarksPerScreen = lastVisibleBookmarkPosition - firstVisibleBookmarkPosition;
1160 // Scroll with the moved bookmark if necessary.
1161 if (selectedBookmarkPosition <= firstVisibleBookmarkPosition) { // The selected bookmark position is at or above the top of the screen.
1162 // Scroll to the selected bookmark position.
1163 bookmarksListView.setSelection(selectedBookmarkPosition);
1164 } else if (selectedBookmarkPosition >= (lastVisibleBookmarkPosition - 1)) { // The selected bookmark is at or below the bottom of the screen.
1165 // 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.
1166 // `+1` assures that the entire bookmark will be displayed in situations where only a partial bookmark fits at the bottom of the list view.
1167 bookmarksListView.setSelection(selectedBookmarkPosition - numberOfBookmarksPerScreen + 1);
1171 private void loadFolder() {
1172 // Update bookmarks cursor with the contents of the bookmarks database for the current folder.
1173 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
1175 // Setup a `CursorAdapter`. `this` specifies the `Context`. `false` disables `autoRequery`.
1176 bookmarksCursorAdapter = new CursorAdapter(this, bookmarksCursor, false) {
1178 public View newView(Context context, Cursor cursor, ViewGroup parent) {
1179 // Inflate the individual item layout. `false` does not attach it to the root.
1180 return getLayoutInflater().inflate(R.layout.bookmarks_activity_item_linearlayout, parent, false);
1184 public void bindView(View view, Context context, Cursor cursor) {
1185 // Get handles for the views.
1186 ImageView bookmarkFavoriteIcon = view.findViewById(R.id.bookmark_favorite_icon);
1187 TextView bookmarkNameTextView = view.findViewById(R.id.bookmark_name);
1189 // Get the favorite icon byte array from the `Cursor`.
1190 byte[] favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON));
1192 // Convert the byte array to a `Bitmap` beginning at the first byte and ending at the last.
1193 Bitmap favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.length);
1195 // Display the bitmap in `bookmarkFavoriteIcon`.
1196 bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap);
1198 // Get the bookmark name from the cursor and display it in `bookmarkNameTextView`.
1199 String bookmarkNameString = cursor.getString(cursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
1200 bookmarkNameTextView.setText(bookmarkNameString);
1202 // Make the font bold for folders.
1203 if (cursor.getInt(cursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {
1204 bookmarkNameTextView.setTypeface(Typeface.DEFAULT_BOLD);
1205 } else { // Reset the font to default for normal bookmarks.
1206 bookmarkNameTextView.setTypeface(Typeface.DEFAULT);
1211 // Populate the list view with the adapter.
1212 bookmarksListView.setAdapter(bookmarksCursorAdapter);
1214 // Set the `AppBar` title.
1215 if (currentFolder.isEmpty()) {
1216 appBar.setTitle(R.string.bookmarks);
1218 appBar.setTitle(currentFolder);
1223 public void onDestroy() {
1224 // Close the bookmarks cursor and database.
1225 bookmarksCursor.close();
1226 bookmarksDatabaseHelper.close();
1228 // Run the default commands.