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);
136 setTheme(R.style.PrivacyBrowser);
138 // Run the default commands.
139 super.onCreate(savedInstanceState);
141 // Get the intent that launched the activity.
142 Intent launchingIntent = getIntent();
144 // Store the current URL and title.
145 String currentUrl = launchingIntent.getStringExtra("current_url");
146 String currentTitle = launchingIntent.getStringExtra("current_title");
148 // Set the current folder variable.
149 if (launchingIntent.getStringExtra("current_folder") != null) { // Set the current folder from the intent.
150 currentFolder = launchingIntent.getStringExtra("current_folder");
151 } else { // Set the current folder to be `""`, which is the home folder.
155 // Get the favorite icon byte array.
156 favoriteIconByteArray = launchingIntent.getByteArrayExtra("favorite_icon_byte_array");
158 // Remove the incorrect lint warning that the favorite icon byte array might be null.
159 assert favoriteIconByteArray != null;
161 // Convert the favorite icon byte array to a bitmap.
162 Bitmap favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.length);
164 // Set the content according to the app bar position.
166 // Set the content view.
167 setContentView(R.layout.bookmarks_bottom_appbar);
169 // `Window.FEATURE_ACTION_MODE_OVERLAY` makes the contextual action mode cover the support action bar. It must be requested before the content is set.
170 supportRequestWindowFeature(Window.FEATURE_ACTION_MODE_OVERLAY);
172 // Set the content view.
173 setContentView(R.layout.bookmarks_top_appbar);
176 // Get a handle for the toolbar.
177 final Toolbar toolbar = findViewById(R.id.bookmarks_toolbar);
179 // Set the support action bar.
180 setSupportActionBar(toolbar);
182 // Get handles for the views.
183 appBar = getSupportActionBar();
184 bookmarksListView = findViewById(R.id.bookmarks_listview);
186 // Remove the incorrect lint warning that `appBar` might be null.
187 assert appBar != null;
189 // Display the home arrow on the app bar.
190 appBar.setDisplayHomeAsUpEnabled(true);
192 // Initialize the database helper. `this` specifies the context. The two `nulls` do not specify the database name or a `CursorFactory`.
193 // The `0` specifies a database version, but that is ignored and set instead using a constant in `BookmarksDatabaseHelper`.
194 bookmarksDatabaseHelper = new BookmarksDatabaseHelper(this, null, null, 0);
196 // Load the home folder.
199 // Set a listener so that tapping a list item loads the URL or folder.
200 bookmarksListView.setOnItemClickListener((parent, view, position, id) -> {
201 // Convert the id from long to int to match the format of the bookmarks database.
202 int databaseId = (int) id;
204 // Get the bookmark cursor for this ID.
205 Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseId);
207 // Move the cursor to the first entry.
208 bookmarkCursor.moveToFirst();
210 // Act upon the bookmark according to the type.
211 if (bookmarkCursor.getInt(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.IS_FOLDER)) == 1) { // The selected bookmark is a folder.
212 // Update the current folder.
213 currentFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME));
215 // Load the new folder.
217 } else { // The selected bookmark is not a folder.
218 // Instantiate the edit bookmark dialog.
219 DialogFragment editBookmarkDialog = EditBookmarkDialog.bookmarkDatabaseId(databaseId, favoriteIconBitmap);
222 editBookmarkDialog.show(getSupportFragmentManager(), getResources().getString(R.string.edit_bookmark));
226 bookmarkCursor.close();
229 // Handle long presses on the list view.
230 bookmarksListView.setMultiChoiceModeListener(new AbsListView.MultiChoiceModeListener() {
231 // Instantiate the common variables.
232 MenuItem editBookmarkMenuItem;
233 MenuItem deleteBookmarksMenuItem;
234 MenuItem selectAllBookmarksMenuItem;
235 boolean deletingBookmarks;
238 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
239 // Inflate the menu for the contextual app bar.
240 getMenuInflater().inflate(R.menu.bookmarks_context_menu, menu);
243 if (currentFolder.isEmpty()) { // Use `R.string.bookmarks` if in the home folder.
244 mode.setTitle(R.string.bookmarks);
245 } else { // Use the current folder name as the title.
246 mode.setTitle(currentFolder);
249 // Get handles for menu items that need to be selectively disabled.
250 moveBookmarkUpMenuItem = menu.findItem(R.id.move_bookmark_up);
251 moveBookmarkDownMenuItem = menu.findItem(R.id.move_bookmark_down);
252 editBookmarkMenuItem = menu.findItem(R.id.edit_bookmark);
253 deleteBookmarksMenuItem = menu.findItem(R.id.delete_bookmark);
254 selectAllBookmarksMenuItem = menu.findItem(R.id.context_menu_select_all_bookmarks);
256 // Disable the delete bookmarks menu item if a delete is pending.
257 deleteBookmarksMenuItem.setEnabled(!deletingBookmarks);
259 // Store a handle for the contextual action mode so it can be closed programatically.
260 contextualActionMode = mode;
267 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
268 // Get a handle for the move to folder menu item.
269 MenuItem moveToFolderMenuItem = menu.findItem(R.id.move_to_folder);
271 // Get a Cursor with all of the folders.
272 Cursor folderCursor = bookmarksDatabaseHelper.getAllFolders();
274 // Enable the move to folder menu item if at least one folder exists.
275 moveToFolderMenuItem.setVisible(folderCursor.getCount() > 0);
282 public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
283 // Get the number of selected bookmarks.
284 int numberOfSelectedBookmarks = bookmarksListView.getCheckedItemCount();
286 // Only process commands if at least one bookmark is selected. Otherwise, a context menu with 0 selected bookmarks is briefly displayed.
287 if (numberOfSelectedBookmarks > 0) {
288 // Adjust the ActionMode and the menu according to the number of selected bookmarks.
289 if (numberOfSelectedBookmarks == 1) { // One bookmark is selected.
290 // List the number of selected bookmarks in the subtitle.
291 mode.setSubtitle(getString(R.string.selected) + " 1");
293 // Show the `Move Up`, `Move Down`, and `Edit` options.
294 moveBookmarkUpMenuItem.setVisible(true);
295 moveBookmarkDownMenuItem.setVisible(true);
296 editBookmarkMenuItem.setVisible(true);
298 // Update the enabled status of the move icons.
300 } else { // More than one bookmark is selected.
301 // List the number of selected bookmarks in the subtitle.
302 mode.setSubtitle(getString(R.string.selected) + " " + numberOfSelectedBookmarks);
304 // Hide non-applicable `MenuItems`.
305 moveBookmarkUpMenuItem.setVisible(false);
306 moveBookmarkDownMenuItem.setVisible(false);
307 editBookmarkMenuItem.setVisible(false);
310 // Show the select all menu item if all the bookmarks are not selected.
311 selectAllBookmarksMenuItem.setVisible(bookmarksListView.getCheckedItemCount() != bookmarksListView.getCount());
316 public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
317 // Declare the common variables.
318 int selectedBookmarkNewPosition;
319 final SparseBooleanArray selectedBookmarksPositionsSparseBooleanArray;
321 // Initialize the selected bookmark position.
322 int selectedBookmarkPosition = 0;
324 // Get the menu item ID.
325 int menuItemId = menuItem.getItemId();
327 // Run the commands according to the selected action item.
328 if (menuItemId == R.id.move_bookmark_up) { // Move the bookmark up.
329 // Get the array of checked bookmark positions.
330 selectedBookmarksPositionsSparseBooleanArray = bookmarksListView.getCheckedItemPositions();
332 // 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`.
333 for (int i = 0; i < selectedBookmarksPositionsSparseBooleanArray.size(); i++) {
334 // Check to see if the value for the bookmark is true, meaning it is currently selected.
335 if (selectedBookmarksPositionsSparseBooleanArray.valueAt(i)) {
336 // Only one bookmark should have a value of `true` when move bookmark up is enabled.
337 selectedBookmarkPosition = selectedBookmarksPositionsSparseBooleanArray.keyAt(i);
341 // Calculate the new position of the selected bookmark.
342 selectedBookmarkNewPosition = selectedBookmarkPosition - 1;
344 // Iterate through the bookmarks.
345 for (int i = 0; i < bookmarksListView.getCount(); i++) {
346 // Get the database ID for the current bookmark.
347 int currentBookmarkDatabaseId = (int) bookmarksListView.getItemIdAtPosition(i);
349 // Update the display order for the current bookmark.
350 if (i == selectedBookmarkPosition) { // The current bookmark is the selected bookmark.
351 // Move the current bookmark up one.
352 bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i - 1);
353 } else if ((i + 1) == selectedBookmarkPosition) { // The current bookmark is immediately above the selected bookmark.
354 // Move the current bookmark down one.
355 bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i + 1);
356 } else { // The current bookmark is not changing positions.
357 // Move `bookmarksCursor` to the current bookmark position.
358 bookmarksCursor.moveToPosition(i);
360 // Update the display order only if it is not correct in the database.
361 if (bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.DISPLAY_ORDER)) != i) {
362 bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i);
367 // Update the bookmarks cursor with the current contents of the bookmarks database.
368 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
370 // Update the list view.
371 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
373 // Scroll with the bookmark.
374 scrollBookmarks(selectedBookmarkNewPosition);
376 // Update the enabled status of the move icons.
378 } else if (menuItemId == R.id.move_bookmark_down) { // Move the bookmark down.
379 // Get the array of checked bookmark positions.
380 selectedBookmarksPositionsSparseBooleanArray = bookmarksListView.getCheckedItemPositions();
382 // 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`.
383 for (int i = 0; i < selectedBookmarksPositionsSparseBooleanArray.size(); i++) {
384 // Check to see if the value for the bookmark is true, meaning it is currently selected.
385 if (selectedBookmarksPositionsSparseBooleanArray.valueAt(i)) {
386 // Only one bookmark should have a value of `true` when move bookmark down is enabled.
387 selectedBookmarkPosition = selectedBookmarksPositionsSparseBooleanArray.keyAt(i);
391 // Calculate the new position of the selected bookmark.
392 selectedBookmarkNewPosition = selectedBookmarkPosition + 1;
394 // Iterate through the bookmarks.
395 for (int i = 0; i < bookmarksListView.getCount(); i++) {
396 // Get the database ID for the current bookmark.
397 int currentBookmarkDatabaseId = (int) bookmarksListView.getItemIdAtPosition(i);
399 // Update the display order for the current bookmark.
400 if (i == selectedBookmarkPosition) { // The current bookmark is the selected bookmark.
401 // Move the current bookmark down one.
402 bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i + 1);
403 } else if ((i - 1) == selectedBookmarkPosition) { // The current bookmark is immediately below the selected bookmark.
404 // Move the bookmark below the selected bookmark up one.
405 bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i - 1);
406 } else { // The current bookmark is not changing positions.
407 // Move `bookmarksCursor` to the current bookmark position.
408 bookmarksCursor.moveToPosition(i);
410 // Update the display order only if it is not correct in the database.
411 if (bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.DISPLAY_ORDER)) != i) {
412 bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i);
417 // Update the bookmarks cursor with the current contents of the bookmarks database.
418 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
420 // Update the list view.
421 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
423 // Scroll with the bookmark.
424 scrollBookmarks(selectedBookmarkNewPosition);
426 // Update the enabled status of the move icons.
428 } else if (menuItemId == R.id.move_to_folder) { // Move to folder.
429 // Instantiate the move to folder alert dialog.
430 DialogFragment moveToFolderDialog = MoveToFolderDialog.moveBookmarks(currentFolder, bookmarksListView.getCheckedItemIds());
432 // Show the move to folder alert dialog.
433 moveToFolderDialog.show(getSupportFragmentManager(), getResources().getString(R.string.move_to_folder));
434 } else if (menuItemId == R.id.edit_bookmark) {
435 // Get the array of checked bookmark positions.
436 selectedBookmarksPositionsSparseBooleanArray = bookmarksListView.getCheckedItemPositions();
438 // 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`.
439 for (int i = 0; i < selectedBookmarksPositionsSparseBooleanArray.size(); i++) {
440 // Check to see if the value for the bookmark is true, meaning it is currently selected.
441 if (selectedBookmarksPositionsSparseBooleanArray.valueAt(i)) {
442 // Only one bookmark should have a value of `true` when move edit bookmark is enabled.
443 selectedBookmarkPosition = selectedBookmarksPositionsSparseBooleanArray.keyAt(i);
447 // Move the cursor to the selected position.
448 bookmarksCursor.moveToPosition(selectedBookmarkPosition);
450 // Find out if this bookmark is a folder.
451 boolean isFolder = (bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.IS_FOLDER)) == 1);
453 // Get the selected bookmark database ID.
454 int databaseId = bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper._ID));
456 // Show the edit bookmark or edit bookmark folder dialog.
458 // Save the current folder name, which is used in `onSaveBookmarkFolder()`.
459 oldFolderNameString = bookmarksCursor.getString(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME));
461 // Instantiate the edit bookmark folder dialog.
462 DialogFragment editFolderDialog = EditBookmarkFolderDialog.folderDatabaseId(databaseId, favoriteIconBitmap);
465 editFolderDialog.show(getSupportFragmentManager(), getResources().getString(R.string.edit_folder));
467 // Instantiate the edit bookmark dialog.
468 DialogFragment editBookmarkDialog = EditBookmarkDialog.bookmarkDatabaseId(databaseId, favoriteIconBitmap);
471 editBookmarkDialog.show(getSupportFragmentManager(), getResources().getString(R.string.edit_bookmark));
473 } else if (menuItemId == R.id.delete_bookmark) { // Delete bookmark.
474 // Set the deleting bookmarks flag, which prevents the delete menu item from being enabled until the current process finishes.
475 deletingBookmarks = true;
477 // Get an array of the selected row IDs.
478 final long[] selectedBookmarksIdsLongArray = bookmarksListView.getCheckedItemIds();
480 // Initialize a variable to count the number of bookmarks to delete.
481 int numberOfBookmarksToDelete = 0;
483 // Count the number of bookmarks.
484 for (long databaseIdLong : selectedBookmarksIdsLongArray) {
485 // Convert the database ID long to an int.
486 int databaseIdInt = (int) databaseIdLong;
488 // Count the contents of the folder if the selected bookmark is a folder.
489 if (bookmarksDatabaseHelper.isFolder(databaseIdInt)) {
490 // Add the bookmarks from the folder to the running total.
491 numberOfBookmarksToDelete = numberOfBookmarksToDelete + countBookmarkFolderContents(databaseIdInt);
494 // Increment the count of the number of bookmarks to delete.
495 numberOfBookmarksToDelete++;
498 // 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.
499 selectedBookmarksPositionsSparseBooleanArray = bookmarksListView.getCheckedItemPositions().clone();
501 // Update the bookmarks cursor with the current contents of the bookmarks database except for the specified database IDs.
502 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrderExcept(selectedBookmarksIdsLongArray, currentFolder);
504 // Update the list view.
505 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
507 // Create a Snackbar with the number of deleted bookmarks.
508 bookmarksDeletedSnackbar = Snackbar.make(findViewById(R.id.bookmarks_coordinatorlayout), getString(R.string.bookmarks_deleted) + " " + numberOfBookmarksToDelete,
509 Snackbar.LENGTH_LONG)
510 .setAction(R.string.undo, view -> {
511 // Do nothing because everything will be handled by `onDismissed()` below.
513 .addCallback(new Snackbar.Callback() {
514 @SuppressLint("SwitchIntDef") // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
516 public void onDismissed(Snackbar snackbar, int event) {
517 if (event == Snackbar.Callback.DISMISS_EVENT_ACTION) { // The user pushed the undo button.
518 // Update the bookmarks cursor with the current contents of the bookmarks database, including the "deleted" bookmarks.
519 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
521 // Update the list view.
522 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
524 // Re-select the previously selected bookmarks.
525 for (int i = 0; i < selectedBookmarksPositionsSparseBooleanArray.size(); i++) {
526 bookmarksListView.setItemChecked(selectedBookmarksPositionsSparseBooleanArray.keyAt(i), true);
528 } else { // The snackbar was dismissed without the undo button being pushed.
529 // Delete each selected bookmark.
530 for (long databaseIdLong : selectedBookmarksIdsLongArray) {
531 // Convert `databaseIdLong` to an int.
532 int databaseIdInt = (int) databaseIdLong;
534 // Delete the contents of the folder if the selected bookmark is a folder.
535 if (bookmarksDatabaseHelper.isFolder(databaseIdInt)) {
536 deleteBookmarkFolderContents(databaseIdInt);
539 // Delete the selected bookmark.
540 bookmarksDatabaseHelper.deleteBookmark(databaseIdInt);
543 // Update the display order.
544 for (int i = 0; i < bookmarksListView.getCount(); i++) {
545 // Get the database ID for the current bookmark.
546 int currentBookmarkDatabaseId = (int) bookmarksListView.getItemIdAtPosition(i);
548 // Move `bookmarksCursor` to the current bookmark position.
549 bookmarksCursor.moveToPosition(i);
551 // Update the display order only if it is not correct in the database.
552 if (bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.DISPLAY_ORDER)) != i) {
553 bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i);
558 // Reset the deleting bookmarks flag.
559 deletingBookmarks = false;
561 // Enable the delete bookmarks menu item.
562 deleteBookmarksMenuItem.setEnabled(true);
564 // Close the activity if back has been pressed.
565 if (closeActivityAfterDismissingSnackbar) {
572 bookmarksDeletedSnackbar.show();
573 } else if (menuItemId == R.id.context_menu_select_all_bookmarks) { // Select all.
574 // Get the total number of bookmarks.
575 int numberOfBookmarks = bookmarksListView.getCount();
578 for (int i = 0; i < numberOfBookmarks; i++) {
579 bookmarksListView.setItemChecked(i, true);
583 // Consume the click.
588 public void onDestroyActionMode(ActionMode mode) {
593 // Get handles for the floating action buttons.
594 FloatingActionButton createBookmarkFolderFab = findViewById(R.id.create_bookmark_folder_fab);
595 FloatingActionButton createBookmarkFab = findViewById(R.id.create_bookmark_fab);
597 // Set the create new bookmark folder FAB to display the `AlertDialog`.
598 createBookmarkFolderFab.setOnClickListener(v -> {
599 // Create a create bookmark folder dialog.
600 DialogFragment createBookmarkFolderDialog = CreateBookmarkFolderDialog.createBookmarkFolder(favoriteIconBitmap);
602 // Show the create bookmark folder dialog.
603 createBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.create_folder));
606 // Set the create new bookmark FAB to display the alert dialog.
607 createBookmarkFab.setOnClickListener(view -> {
608 // Remove the incorrect lint warning below.
609 assert currentUrl != null;
610 assert currentTitle != null;
612 // Instantiate the create bookmark dialog.
613 DialogFragment createBookmarkDialog = CreateBookmarkDialog.createBookmark(currentUrl, currentTitle, favoriteIconBitmap);
615 // Display the create bookmark dialog.
616 createBookmarkDialog.show(getSupportFragmentManager(), getResources().getString(R.string.create_bookmark));
619 // Restore the state if the app has been restarted.
620 if (savedInstanceState != null) {
621 // Update the bookmarks list view after it has loaded.
622 bookmarksListView.post(() -> {
623 // Get the checked bookmarks array list.
624 ArrayList<Integer> checkedBookmarksArrayList = savedInstanceState.getIntegerArrayList(CHECKED_BOOKMARKS_ARRAY_LIST);
626 // Check each previously checked bookmark in the list view. When the minimum API >= 24 a `forEach()` command can be used instead.
627 if (checkedBookmarksArrayList != null) {
628 for (int i = 0; i < checkedBookmarksArrayList.size(); i++) {
629 bookmarksListView.setItemChecked(checkedBookmarksArrayList.get(i), true);
637 public void onRestart() {
638 // Run the default commands.
641 // Update the list view if returning from the bookmarks database view activity.
642 if (restartFromBookmarksDatabaseViewActivity) {
643 // Load the current folder in the list view.
646 // Reset `restartFromBookmarksDatabaseViewActivity`.
647 restartFromBookmarksDatabaseViewActivity = false;
652 public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
653 // Run the default commands.
654 super.onSaveInstanceState(savedInstanceState);
656 // Get the array of the checked items.
657 SparseBooleanArray checkedBookmarksSparseBooleanArray = bookmarksListView.getCheckedItemPositions();
659 // Create a checked items array list.
660 ArrayList<Integer> checkedBookmarksArrayList = new ArrayList<>();
662 // Add each checked bookmark position to the array list.
663 for (int i = 0; i < checkedBookmarksSparseBooleanArray.size(); i++) {
664 // 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.
665 if (checkedBookmarksSparseBooleanArray.valueAt(i)) {
666 // Add the bookmark position to the checked bookmarks array list.
667 checkedBookmarksArrayList.add(checkedBookmarksSparseBooleanArray.keyAt(i));
671 // Store the checked items array list in the saved instance state.
672 savedInstanceState.putIntegerArrayList(CHECKED_BOOKMARKS_ARRAY_LIST, checkedBookmarksArrayList);
676 public boolean onCreateOptionsMenu(Menu menu) {
678 getMenuInflater().inflate(R.menu.bookmarks_options_menu, menu);
685 public boolean onOptionsItemSelected(MenuItem menuItem) {
686 // Get a handle for the menu item ID.
687 int menuItemId = menuItem.getItemId();
689 // Run the command according to the selected option.
690 if (menuItemId == android.R.id.home) { // Home. The home arrow is identified as `android.R.id.home`, not just `R.id.home`.
691 if (currentFolder.isEmpty()) { // Currently in the home folder.
692 // Run the back commands.
694 } else { // Currently in a subfolder.
695 // Place the former parent folder in `currentFolder`.
696 currentFolder = bookmarksDatabaseHelper.getParentFolderName(currentFolder);
698 // Load the new folder.
701 } else if (menuItemId == R.id.options_menu_select_all_bookmarks) { // Select all.
702 // Get the total number of bookmarks.
703 int numberOfBookmarks = bookmarksListView.getCount();
706 for (int i = 0; i < numberOfBookmarks; i++) {
707 bookmarksListView.setItemChecked(i, true);
709 } else if (menuItemId == R.id.bookmarks_database_view) {
710 // Close the contextual action bar if it is displayed. This can happen if the bottom app bar is enabled.
711 if (contextualActionMode != null) {
712 contextualActionMode.finish();
715 // Create an intent to launch the bookmarks database view activity.
716 Intent bookmarksDatabaseViewIntent = new Intent(this, BookmarksDatabaseViewActivity.class);
718 // Include the favorite icon byte array to the intent.
719 bookmarksDatabaseViewIntent.putExtra("favorite_icon_byte_array", favoriteIconByteArray);
722 startActivity(bookmarksDatabaseViewIntent);
728 public void onBackPressed() {
729 // 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.
730 if ((bookmarksDeletedSnackbar != null) && bookmarksDeletedSnackbar.isShown()) { // Close the bookmarks deleted snackbar before going home.
731 // Set the close flag.
732 closeActivityAfterDismissingSnackbar = true;
734 // Dismiss the snackbar.
735 bookmarksDeletedSnackbar.dismiss();
736 } else { // Go home immediately.
737 // Update the bookmarks folder for the bookmarks drawer in the main WebView activity.
738 MainWebViewActivity.currentBookmarksFolder = currentFolder;
740 // Close the bookmarks drawer and reload the bookmarks ListView when returning to the main WebView activity.
741 MainWebViewActivity.restartFromBookmarksActivity = true;
743 // Exit the bookmarks activity.
744 super.onBackPressed();
749 public void onCreateBookmark(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) {
750 // Get the alert dialog from the fragment.
751 Dialog dialog = dialogFragment.getDialog();
753 // Remove the incorrect lint warning below that the dialog might be null.
754 assert dialog != null;
756 // Get the views from the dialog fragment.
757 EditText createBookmarkNameEditText = dialog.findViewById(R.id.create_bookmark_name_edittext);
758 EditText createBookmarkUrlEditText = dialog.findViewById(R.id.create_bookmark_url_edittext);
760 // Extract the strings from the edit texts.
761 String bookmarkNameString = createBookmarkNameEditText.getText().toString();
762 String bookmarkUrlString = createBookmarkUrlEditText.getText().toString();
764 // Create a favorite icon byte array output stream.
765 ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
767 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
768 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
770 // Convert the favorite icon byte array stream to a byte array.
771 byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
773 // Display the new bookmark below the current items in the (0 indexed) list.
774 int newBookmarkDisplayOrder = bookmarksListView.getCount();
776 // Create the bookmark.
777 bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentFolder, newBookmarkDisplayOrder, favoriteIconByteArray);
779 // Update the bookmarks cursor with the current contents of this folder.
780 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
782 // Update the `ListView`.
783 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
785 // Scroll to the new bookmark.
786 bookmarksListView.setSelection(newBookmarkDisplayOrder);
790 public void onCreateBookmarkFolder(DialogFragment dialogFragment, @NonNull Bitmap favoriteIconBitmap) {
791 // Get the dialog from the dialog fragment.
792 Dialog dialog = dialogFragment.getDialog();
794 // Remove the incorrect lint warning below that the dialog might be null.
795 assert dialog != null;
797 // Get handles for the views in the dialog fragment.
798 EditText folderNameEditText = dialog.findViewById(R.id.folder_name_edittext);
799 RadioButton defaultIconRadioButton = dialog.findViewById(R.id.default_icon_radiobutton);
800 ImageView defaultIconImageView = dialog.findViewById(R.id.default_icon_imageview);
802 // Get new folder name string.
803 String folderNameString = folderNameEditText.getText().toString();
805 // Create a folder icon bitmap.
806 Bitmap folderIconBitmap;
808 // Set the folder icon bitmap according to the dialog.
809 if (defaultIconRadioButton.isChecked()) { // Use the default folder icon.
810 // Get the default folder icon drawable.
811 Drawable folderIconDrawable = defaultIconImageView.getDrawable();
813 // Convert the folder icon drawable to a bitmap drawable.
814 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
816 // Convert the folder icon bitmap drawable to a bitmap.
817 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
818 } else { // Use the WebView favorite icon.
819 // Copy the favorite icon bitmap to the folder icon bitmap.
820 folderIconBitmap = favoriteIconBitmap;
823 // Create a folder icon byte array output stream.
824 ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream();
826 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
827 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream);
829 // Convert the folder icon byte array stream to a byte array.
830 byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray();
832 // Move all the bookmarks down one in the display order.
833 for (int i = 0; i < bookmarksListView.getCount(); i++) {
834 int databaseId = (int) bookmarksListView.getItemIdAtPosition(i);
835 bookmarksDatabaseHelper.updateDisplayOrder(databaseId, i + 1);
838 // Create the folder, which will be placed at the top of the `ListView`.
839 bookmarksDatabaseHelper.createFolder(folderNameString, currentFolder, folderIconByteArray);
841 // Update the bookmarks cursor with the current contents of this folder.
842 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
844 // Update the list view.
845 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
847 // Scroll to the new folder.
848 bookmarksListView.setSelection(0);
852 public void onSaveBookmark(DialogFragment dialogFragment, int selectedBookmarkDatabaseId, @NonNull Bitmap favoriteIconBitmap) {
853 // Get the dialog from the dialog fragment.
854 Dialog dialog = dialogFragment.getDialog();
856 // Remove the incorrect lint warning below that the dialog might be null.
857 assert dialog != null;
859 // Get handles for the views from the dialog fragment.
860 EditText bookmarkNameEditText = dialog.findViewById(R.id.bookmark_name_edittext);
861 EditText bookmarkUrlEditText = dialog.findViewById(R.id.bookmark_url_edittext);
862 RadioButton currentIconRadioButton = dialog.findViewById(R.id.current_icon_radiobutton);
864 // Store the bookmark strings.
865 String bookmarkNameString = bookmarkNameEditText.getText().toString();
866 String bookmarkUrlString = bookmarkUrlEditText.getText().toString();
868 // Update the bookmark.
869 if (currentIconRadioButton.isChecked()) { // Update the bookmark without changing the favorite icon.
870 bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString);
871 } else { // Update the bookmark using the WebView favorite icon.
872 // Create a favorite icon byte array output stream.
873 ByteArrayOutputStream newFavoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
875 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
876 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFavoriteIconByteArrayOutputStream);
878 // Convert the favorite icon byte array stream to a byte array.
879 byte[] newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray();
881 // Update the bookmark and the favorite icon.
882 bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, newFavoriteIconByteArray);
885 // Check to see if the contextual action mode has been created.
886 if (contextualActionMode != null) {
887 // Close the contextual action mode if it is open.
888 contextualActionMode.finish();
891 // Update the bookmarks cursor with the contents of the current folder.
892 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
894 // Update the `ListView`.
895 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
899 public void onSaveBookmarkFolder(DialogFragment dialogFragment, int selectedFolderDatabaseId, @NonNull Bitmap favoriteIconBitmap) {
900 // Get the dialog from the dialog fragment.
901 Dialog dialog = dialogFragment.getDialog();
903 // Remove the incorrect lint warning below that the dialog might be null.
904 assert dialog != null;
906 // Get handles for the views from the dialog fragment.
907 RadioButton currentFolderIconRadioButton = dialog.findViewById(R.id.current_icon_radiobutton);
908 RadioButton defaultFolderIconRadioButton = dialog.findViewById(R.id.default_icon_radiobutton);
909 ImageView defaultFolderIconImageView = dialog.findViewById(R.id.default_icon_imageview);
910 EditText editFolderNameEditText = dialog.findViewById(R.id.folder_name_edittext);
912 // Get the new folder name.
913 String newFolderNameString = editFolderNameEditText.getText().toString();
915 // Check if the favorite icon has changed.
916 if (currentFolderIconRadioButton.isChecked()) { // Only the name has changed.
917 // Update the name in the database.
918 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString);
919 } else if (!currentFolderIconRadioButton.isChecked() && newFolderNameString.equals(oldFolderNameString)) { // Only the icon has changed.
920 // Create the new folder icon Bitmap.
921 Bitmap folderIconBitmap;
923 // Populate the new folder icon bitmap.
924 if (defaultFolderIconRadioButton.isChecked()) {
925 // Get the default folder icon drawable.
926 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
928 // Convert the folder icon drawable to a bitmap drawable.
929 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
931 // Convert the folder icon bitmap drawable to a bitmap.
932 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
933 } else { // Use the WebView favorite icon.
934 // Copy the favorite icon bitmap to the folder icon bitmap.
935 folderIconBitmap = favoriteIconBitmap;
938 // Create a folder icon byte array output stream.
939 ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
941 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
942 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
944 // Convert the folder icon byte array stream to a byte array.
945 byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
947 // Update the folder icon in the database.
948 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderIconByteArray);
949 } else { // The folder icon and the name have changed.
950 // Instantiate the new folder icon `Bitmap`.
951 Bitmap folderIconBitmap;
953 // Populate the new folder icon bitmap.
954 if (defaultFolderIconRadioButton.isChecked()) {
955 // Get the default folder icon drawable.
956 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
958 // Convert the folder icon drawable to a bitmap drawable.
959 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
961 // Convert the folder icon bitmap drawable to a bitmap.
962 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
963 } else { // Use the WebView favorite icon.
964 // Copy the favorite icon bitmap to the folder icon bitmap.
965 folderIconBitmap = favoriteIconBitmap;
968 // Create a folder icon byte array output stream.
969 ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
971 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
972 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
974 // Convert the folder icon byte array stream to a byte array.
975 byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
977 // Update the folder name and icon in the database.
978 bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, newFolderIconByteArray);
981 // Update the bookmarks cursor with the current contents of this folder.
982 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
984 // Update the `ListView`.
985 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
987 // Close the contextual action mode.
988 contextualActionMode.finish();
992 public void onMoveToFolder(DialogFragment dialogFragment) {
993 // Get the dialog from the dialog fragment.
994 Dialog dialog = dialogFragment.getDialog();
996 // Remove the incorrect lint warning below that the dialog might be null.
997 assert dialog != null;
999 // Get a handle for the list view from the dialog.
1000 ListView folderListView = dialog.findViewById(R.id.move_to_folder_listview);
1002 // Store a long array of the selected folders.
1003 long[] newFolderLongArray = folderListView.getCheckedItemIds();
1005 // Get the new folder database ID. Only one folder will be selected.
1006 int newFolderDatabaseId = (int) newFolderLongArray[0];
1008 // Instantiate `newFolderName`.
1009 String newFolderName;
1011 // Set the new folder name.
1012 if (newFolderDatabaseId == 0) {
1013 // The new folder is the home folder, represented as `""` in the database.
1016 // Get the new folder name from the database.
1017 newFolderName = bookmarksDatabaseHelper.getFolderName(newFolderDatabaseId);
1020 // Get a long array with the the database ID of the selected bookmarks.
1021 long[] selectedBookmarksLongArray = bookmarksListView.getCheckedItemIds();
1023 // Move each of the selected bookmarks to the new folder.
1024 for (long databaseIdLong : selectedBookmarksLongArray) {
1025 // Get `databaseIdInt` for each selected bookmark.
1026 int databaseIdInt = (int) databaseIdLong;
1028 // Move the selected bookmark to the new folder.
1029 bookmarksDatabaseHelper.moveToFolder(databaseIdInt, newFolderName);
1032 // Update the bookmarks cursor with the current contents of this folder.
1033 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
1035 // Update the `ListView`.
1036 bookmarksCursorAdapter.changeCursor(bookmarksCursor);
1038 // Close the contextual app bar.
1039 contextualActionMode.finish();
1042 private int countBookmarkFolderContents(int databaseId) {
1043 // Initialize the bookmark counter.
1044 int bookmarkCounter = 0;
1046 // Get the name of the folder.
1047 String folderName = bookmarksDatabaseHelper.getFolderName(databaseId);
1049 // Get the contents of the folder.
1050 Cursor folderCursor = bookmarksDatabaseHelper.getBookmarkIds(folderName);
1052 // Count each of the bookmarks in the folder.
1053 for (int i = 0; i < folderCursor.getCount(); i++) {
1054 // Move the folder cursor to the current row.
1055 folderCursor.moveToPosition(i);
1057 // Get the database ID of the item.
1058 int itemDatabaseId = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper._ID));
1060 // If this is a folder, recursively count the contents first.
1061 if (bookmarksDatabaseHelper.isFolder(itemDatabaseId)) {
1062 // Add the bookmarks from the folder to the running total.
1063 bookmarkCounter = bookmarkCounter + countBookmarkFolderContents(itemDatabaseId);
1066 // Add the bookmark to the running total.
1070 // Return the bookmark counter.
1071 return bookmarkCounter;
1074 private void deleteBookmarkFolderContents(int databaseId) {
1075 // Get the name of the folder.
1076 String folderName = bookmarksDatabaseHelper.getFolderName(databaseId);
1078 // Get the contents of the folder.
1079 Cursor folderCursor = bookmarksDatabaseHelper.getBookmarkIds(folderName);
1081 // Delete each of the bookmarks in the folder.
1082 for (int i = 0; i < folderCursor.getCount(); i++) {
1083 // Move the folder cursor to the current row.
1084 folderCursor.moveToPosition(i);
1086 // Get the database ID of the item.
1087 int itemDatabaseId = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper._ID));
1089 // If this is a folder, recursively delete the contents first.
1090 if (bookmarksDatabaseHelper.isFolder(itemDatabaseId)) {
1091 deleteBookmarkFolderContents(itemDatabaseId);
1094 // Delete the bookmark.
1095 bookmarksDatabaseHelper.deleteBookmark(itemDatabaseId);
1099 private void updateMoveIcons() {
1100 // Get a long array of the selected bookmarks.
1101 long[] selectedBookmarksLongArray = bookmarksListView.getCheckedItemIds();
1103 // Get the database IDs for the first, last, and selected bookmarks.
1104 int selectedBookmarkDatabaseId = (int) selectedBookmarksLongArray[0];
1105 int firstBookmarkDatabaseId = (int) bookmarksListView.getItemIdAtPosition(0);
1106 // bookmarksListView is 0 indexed.
1107 int lastBookmarkDatabaseId = (int) bookmarksListView.getItemIdAtPosition(bookmarksListView.getCount() - 1);
1109 // Update the move bookmark up menu item.
1110 if (selectedBookmarkDatabaseId == firstBookmarkDatabaseId) { // The selected bookmark is in the first position.
1111 // Disable the move bookmark up menu item.
1112 moveBookmarkUpMenuItem.setEnabled(false);
1115 moveBookmarkUpMenuItem.setIcon(R.drawable.move_up_disabled);
1116 } else { // The selected bookmark is not in the first position.
1117 // Enable the move bookmark up menu item.
1118 moveBookmarkUpMenuItem.setEnabled(true);
1120 // Set the icon according to the theme.
1121 moveBookmarkUpMenuItem.setIcon(R.drawable.move_up_enabled);
1124 // Update the move bookmark down menu item.
1125 if (selectedBookmarkDatabaseId == lastBookmarkDatabaseId) { // The selected bookmark is in the last position.
1126 // Disable the move bookmark down menu item.
1127 moveBookmarkDownMenuItem.setEnabled(false);
1130 moveBookmarkDownMenuItem.setIcon(R.drawable.move_down_disabled);
1131 } else { // The selected bookmark is not in the last position.
1132 // Enable the move bookmark down menu item.
1133 moveBookmarkDownMenuItem.setEnabled(true);
1136 moveBookmarkDownMenuItem.setIcon(R.drawable.move_down_enabled);
1140 private void scrollBookmarks(int selectedBookmarkPosition) {
1141 // Get the first and last visible bookmark positions.
1142 int firstVisibleBookmarkPosition = bookmarksListView.getFirstVisiblePosition();
1143 int lastVisibleBookmarkPosition = bookmarksListView.getLastVisiblePosition();
1145 // Calculate the number of bookmarks per screen.
1146 int numberOfBookmarksPerScreen = lastVisibleBookmarkPosition - firstVisibleBookmarkPosition;
1148 // Scroll with the moved bookmark if necessary.
1149 if (selectedBookmarkPosition <= firstVisibleBookmarkPosition) { // The selected bookmark position is at or above the top of the screen.
1150 // Scroll to the selected bookmark position.
1151 bookmarksListView.setSelection(selectedBookmarkPosition);
1152 } else if (selectedBookmarkPosition >= (lastVisibleBookmarkPosition - 1)) { // The selected bookmark is at or below the bottom of the screen.
1153 // 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.
1154 // `+1` assures that the entire bookmark will be displayed in situations where only a partial bookmark fits at the bottom of the list view.
1155 bookmarksListView.setSelection(selectedBookmarkPosition - numberOfBookmarksPerScreen + 1);
1159 private void loadFolder() {
1160 // Update bookmarks cursor with the contents of the bookmarks database for the current folder.
1161 bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolder);
1163 // Setup a `CursorAdapter`. `this` specifies the `Context`. `false` disables `autoRequery`.
1164 bookmarksCursorAdapter = new CursorAdapter(this, bookmarksCursor, false) {
1166 public View newView(Context context, Cursor cursor, ViewGroup parent) {
1167 // Inflate the individual item layout. `false` does not attach it to the root.
1168 return getLayoutInflater().inflate(R.layout.bookmarks_activity_item_linearlayout, parent, false);
1172 public void bindView(View view, Context context, Cursor cursor) {
1173 // Get handles for the views.
1174 ImageView bookmarkFavoriteIcon = view.findViewById(R.id.bookmark_favorite_icon);
1175 TextView bookmarkNameTextView = view.findViewById(R.id.bookmark_name);
1177 // Get the favorite icon byte array from the `Cursor`.
1178 byte[] favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.FAVORITE_ICON));
1180 // Convert the byte array to a `Bitmap` beginning at the first byte and ending at the last.
1181 Bitmap favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.length);
1183 // Display the bitmap in `bookmarkFavoriteIcon`.
1184 bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap);
1186 // Get the bookmark name from the cursor and display it in `bookmarkNameTextView`.
1187 String bookmarkNameString = cursor.getString(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME));
1188 bookmarkNameTextView.setText(bookmarkNameString);
1190 // Make the font bold for folders.
1191 if (cursor.getInt(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {
1192 bookmarkNameTextView.setTypeface(Typeface.DEFAULT_BOLD);
1193 } else { // Reset the font to default for normal bookmarks.
1194 bookmarkNameTextView.setTypeface(Typeface.DEFAULT);
1199 // Populate the list view with the adapter.
1200 bookmarksListView.setAdapter(bookmarksCursorAdapter);
1202 // Set the `AppBar` title.
1203 if (currentFolder.isEmpty()) {
1204 appBar.setTitle(R.string.bookmarks);
1206 appBar.setTitle(currentFolder);
1211 public void onDestroy() {
1212 // Close the bookmarks cursor and database.
1213 bookmarksCursor.close();
1214 bookmarksDatabaseHelper.close();
1216 // Run the default commands.