From e89b263c281a0b555cf43604b013b85b40d81c61 Mon Sep 17 00:00:00 2001 From: Soren Stoutner Date: Sat, 16 Jul 2016 04:43:51 -0700 Subject: [PATCH] Add the ability to move bookmarks to folders. --- .idea/dictionaries/soren.xml | 2 + .../privacybrowser/BookmarksActivity.java | 130 ++++++++-- .../BookmarksDatabaseHandler.java | 128 +++++++++- .../BookmarksDatabaseViewActivity.java | 5 +- .../stoutner/privacybrowser/MoveToFolder.java | 241 ++++++++++++++++++ .../res/drawable-hdpi/folder_grey_bitmap.png | Bin 0 -> 585 bytes .../res/drawable-mdpi/folder_dark_blue.xml | 14 + .../res/drawable-mdpi/folder_grey_bitmap.png | Bin 0 -> 463 bytes .../res/drawable-xhdpi/folder_grey_bitmap.png | Bin 0 -> 866 bytes .../drawable-xxhdpi/folder_grey_bitmap.png | Bin 0 -> 1142 bytes .../drawable/{folder.xml => folder_grey.xml} | 2 +- ...marks_database_view_item_linearlayout.xml} | 3 +- .../res/layout/main_coordinatorlayout.xml | 2 +- .../main/res/layout/move_to_folder_dialog.xml | 30 +++ .../move_to_folder_item_linearlayout.xml | 46 ++++ app/src/main/res/values/colors.xml | 2 +- app/src/main/res/values/strings.xml | 4 +- 17 files changed, 578 insertions(+), 31 deletions(-) create mode 100644 app/src/main/res/drawable-hdpi/folder_grey_bitmap.png create mode 100644 app/src/main/res/drawable-mdpi/folder_dark_blue.xml create mode 100644 app/src/main/res/drawable-mdpi/folder_grey_bitmap.png create mode 100644 app/src/main/res/drawable-xhdpi/folder_grey_bitmap.png create mode 100644 app/src/main/res/drawable-xxhdpi/folder_grey_bitmap.png rename app/src/main/res/drawable/{folder.xml => folder_grey.xml} (83%) rename app/src/main/res/layout/{bookmarks_database_view_linearlayout.xml => bookmarks_database_view_item_linearlayout.xml} (96%) create mode 100644 app/src/main/res/layout/move_to_folder_dialog.xml create mode 100644 app/src/main/res/layout/move_to_folder_item_linearlayout.xml diff --git a/.idea/dictionaries/soren.xml b/.idea/dictionaries/soren.xml index f6e91ca9..8a435c03 100644 --- a/.idea/dictionaries/soren.xml +++ b/.idea/dictionaries/soren.xml @@ -35,6 +35,7 @@ orbot panopticlick parentfolder + programatically radiobutton radiogroup redmine @@ -46,6 +47,7 @@ securitypatch snackbar snackbars + subfolders tablayout techrepublic textview diff --git a/app/src/main/java/com/stoutner/privacybrowser/BookmarksActivity.java b/app/src/main/java/com/stoutner/privacybrowser/BookmarksActivity.java index ca004072..f228c526 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/BookmarksActivity.java +++ b/app/src/main/java/com/stoutner/privacybrowser/BookmarksActivity.java @@ -24,7 +24,6 @@ import android.app.DialogFragment; import android.content.Context; import android.content.Intent; import android.database.Cursor; -import android.database.DatabaseUtils; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Typeface; @@ -35,7 +34,6 @@ import android.support.design.widget.FloatingActionButton; import android.support.design.widget.Snackbar; import android.support.v4.app.NavUtils; import android.support.v4.content.ContextCompat; -import android.support.v4.widget.CursorAdapter; import android.support.v7.app.ActionBar; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; @@ -47,7 +45,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.AdapterView; -import android.widget.CheckBox; +import android.widget.CursorAdapter; import android.widget.EditText; import android.widget.ImageView; import android.widget.ListView; @@ -58,17 +56,18 @@ import java.io.ByteArrayOutputStream; public class BookmarksActivity extends AppCompatActivity implements CreateBookmark.CreateBookmarkListener, CreateBookmarkFolder.CreateBookmarkFolderListener, EditBookmark.EditBookmarkListener, - EditBookmarkFolder.EditBookmarkFolderListener { - // `bookmarksDatabaseHandler` is public static so it can be accessed from EditBookmark. It is also used in `onCreate()`, + EditBookmarkFolder.EditBookmarkFolderListener, MoveToFolder.MoveToFolderListener { + // `bookmarksDatabaseHandler` is public static so it can be accessed from `EditBookmark` and `MoveToFolder`. It is also used in `onCreate()`, // `onCreateBookmarkCreate()`, `updateBookmarksListView()`, and `updateBookmarksListViewExcept()`. public static BookmarksDatabaseHandler bookmarksDatabaseHandler; - // `bookmarksListView` is public static so it can be accessed from EditBookmark. + // `bookmarksListView` is public static so it can be accessed from `EditBookmark`. // It is also used in `onCreate()`, `updateBookmarksListView()`, and `updateBookmarksListViewExcept()`. public static ListView bookmarksListView; - // `currentFolder` is used in `onCreate`, `onOptionsItemSelected()`, `onCreateBookmarkCreate`, `onCreateBookmarkFolderCreate`, and `onEditBookmarkSave`. - private String currentFolder; + // `currentFolder` is public static so it can be accessed from `MoveToFolder`. + // It is used in `onCreate`, `onOptionsItemSelected()`, `onCreateBookmarkCreate`, `onCreateBookmarkFolderCreate`, and `onEditBookmarkSave`. + public static String currentFolder; // `contextualActionMode` is used in `onCreate()` and `onEditBookmarkSave()`. private ActionMode contextualActionMode; @@ -179,6 +178,9 @@ public class BookmarksActivity extends AppCompatActivity implements CreateBookma editBookmarkMenuItem = menu.findItem(R.id.edit_bookmark); selectAllBookmarksMenuItem = menu.findItem(R.id.context_menu_select_all_bookmarks); + // Get a handle for `contextualActionMode` so we can close it programatically. + contextualActionMode = mode; + return true; } @@ -195,6 +197,11 @@ public class BookmarksActivity extends AppCompatActivity implements CreateBookma // Calculate the number of selected bookmarks. int numberOfSelectedBookmarks = selectedBookmarksLongArray.length; + // Sometimes Android forgets to close the contextual app bar when all the items are deselected. + if (numberOfSelectedBookmarks == 0) { + mode.finish(); + } + // List the number of selected bookmarks in the subtitle. mode.setSubtitle(numberOfSelectedBookmarks + " " + getString(R.string.selected)); @@ -331,13 +338,20 @@ public class BookmarksActivity extends AppCompatActivity implements CreateBookma bookmarksListView.setSelection(selectedBookmarkNewPosition - 5); break; - case R.id.edit_bookmark: - // Get a handle for `contextualActionMode` so we can close it when `editBookmarkDialog` is finished. - contextualActionMode = mode; + case R.id.move_to_folder: + // Show the `MoveToFolder` `AlertDialog` and name the instance `@string/move_to_folder + DialogFragment moveToFolderDialog = new MoveToFolder(); + moveToFolderDialog.show(getFragmentManager(), getResources().getString(R.string.move_to_folder)); + break; + case R.id.edit_bookmark: // Get a handle for `selectedBookmarkPosition` so we can scroll to it after refreshing the ListView. bookmarkPositionSparseBooleanArray = bookmarksListView.getCheckedItemPositions(); - selectedBookmarkPosition = bookmarkPositionSparseBooleanArray.keyAt(0); + for (int i = 0; i < bookmarkPositionSparseBooleanArray.size(); i++) { + // Find the bookmark that is selected and save the position to `selectedBookmarkPosition`. + if (bookmarkPositionSparseBooleanArray.valueAt(i)) + selectedBookmarkPosition = bookmarkPositionSparseBooleanArray.keyAt(i); + } // Move to the selected database ID and find out if it is a folder. bookmarksCursor.moveToPosition(selectedBookmarkPosition); @@ -363,12 +377,16 @@ public class BookmarksActivity extends AppCompatActivity implements CreateBookma // Get a handle for `selectedBookmarkPosition` so we can scroll to it after refreshing the ListView. bookmarkPositionSparseBooleanArray = bookmarksListView.getCheckedItemPositions(); - selectedBookmarkPosition = bookmarkPositionSparseBooleanArray.keyAt(0); + for (int i = 0; i < bookmarkPositionSparseBooleanArray.size(); i++) { + // Find the bookmark that is selected and save the position to `selectedBookmarkPosition`. + if (bookmarkPositionSparseBooleanArray.valueAt(i)) + selectedBookmarkPosition = bookmarkPositionSparseBooleanArray.keyAt(i); + } updateBookmarksListViewExcept(selectedBookmarksLongArray, currentFolder); // Scroll to where the deleted bookmark was located. - bookmarksListView.setSelection(selectedBookmarkPosition); + bookmarksListView.setSelection(selectedBookmarkPosition - 5); String snackbarMessage; @@ -396,6 +414,9 @@ public class BookmarksActivity extends AppCompatActivity implements CreateBookma // Refresh the ListView to show the rows again. updateBookmarksListView(currentFolder); + // Scroll to where the deleted bookmark was located. + bookmarksListView.setSelection(selectedBookmarkPosition - 5); + break; // The Snackbar was dismissed without the "Undo" button being pushed. @@ -405,7 +426,11 @@ public class BookmarksActivity extends AppCompatActivity implements CreateBookma // Convert `databaseIdLong` to an int. int databaseIdInt = (int) databaseIdLong; - // Delete the database row. + if (bookmarksDatabaseHandler.isFolder(databaseIdInt)) { + deleteBookmarkFolderContents(databaseIdInt); + } + + // Delete `databaseIdInt`. bookmarksDatabaseHandler.deleteBookmark(databaseIdInt); } break; @@ -644,7 +669,7 @@ public class BookmarksActivity extends AppCompatActivity implements CreateBookma int existingFoldersWithNewName = bookmarkFolderCursor.getCount(); bookmarkFolderCursor.close(); if ( ((existingFoldersWithNewName == 0) || newFolderNameString.equals(oldFolderNameString)) && !newFolderNameString.isEmpty()) { - // Get a long array with the the databaseId of the selected folder and convert it to an `int`. + // Get a long array with the the database ID of the selected folder and convert it to an `int`. long[] selectedFolderLongArray = bookmarksListView.getCheckedItemIds(); int selectedFolderDatabaseId = (int) selectedFolderLongArray[0]; @@ -690,11 +715,57 @@ public class BookmarksActivity extends AppCompatActivity implements CreateBookma } } + @Override + public void onCancelMoveToFolder(DialogFragment dialogFragment) { + // Do nothing because the user selected `Cancel`. + } + + @Override + public void onMoveToFolder(DialogFragment dialogFragment) { + // Get the new folder database id. + ListView folderListView = (ListView) dialogFragment.getDialog().findViewById(R.id.move_to_folder_listview); + long[] newFolderLongArray = folderListView.getCheckedItemIds(); + + if (newFolderLongArray.length == 0) { // No new folder was selected. + Snackbar.make(findViewById(R.id.bookmarks_coordinatorlayout), getString(R.string.cannot_move_bookmarks), Snackbar.LENGTH_LONG).show(); + } else { // Move the selected bookmarks. + // Get the new folder database ID. + int newFolderDatabaseId = (int) newFolderLongArray[0]; + + // Instantiate `newFolderName`. + String newFolderName; + + if (newFolderDatabaseId == 0) { + // The new folder is the home folder, represented as `""` in the database. + newFolderName = ""; + } else { + // Get the new folder name from the database. + newFolderName = bookmarksDatabaseHandler.getFolderName(newFolderDatabaseId); + } + + // Get a long array with the the database ID of the selected bookmarks. + long[] selectedBookmarksLongArray = bookmarksListView.getCheckedItemIds(); + for (long databaseIdLong : selectedBookmarksLongArray) { + // Get `databaseIdInt` for each selected bookmark. + int databaseIdInt = (int) databaseIdLong; + + // Move the selected bookmark to the new folder. + bookmarksDatabaseHandler.moveToFolder(databaseIdInt, newFolderName); + } + + // Refresh the `ListView`. + updateBookmarksListView(currentFolder); + + // Close the contextual app bar. + contextualActionMode.finish(); + } + } + private void updateBookmarksListView(String folderName) { // Get a `Cursor` with the current contents of the bookmarks database. bookmarksCursor = bookmarksDatabaseHandler.getAllBookmarksCursorByDisplayOrder(folderName); - // Setup `bookmarksCursorAdapter` with `this` context. The `false` disables autoRequery. + // Setup `bookmarksCursorAdapter` with `this` context. `false` disables autoRequery. CursorAdapter bookmarksCursorAdapter = new CursorAdapter(this, bookmarksCursor, false) { @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { @@ -745,7 +816,7 @@ public class BookmarksActivity extends AppCompatActivity implements CreateBookma // Get a `Cursor` with the current contents of the bookmarks database except for the specified database IDs. bookmarksCursor = bookmarksDatabaseHandler.getBookmarksCursorExcept(exceptIdLongArray, folderName); - // Setup `bookmarksCursorAdapter` with `this` context. The `false` disables autoRequery. + // Setup `bookmarksCursorAdapter` with `this` context. `false` disables autoRequery. CursorAdapter bookmarksCursorAdapter = new CursorAdapter(this, bookmarksCursor, false) { @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { @@ -784,4 +855,27 @@ public class BookmarksActivity extends AppCompatActivity implements CreateBookma // Update the ListView. bookmarksListView.setAdapter(bookmarksCursorAdapter); } + + private void deleteBookmarkFolderContents(int databaseId) { + // Get the name of the folder. + String folderName = bookmarksDatabaseHandler.getFolderName(databaseId); + + // Get the contents of the folder. + Cursor folderCursor = bookmarksDatabaseHandler.getAllBookmarksCursorByDisplayOrder(folderName); + + for (int i = 0; i < folderCursor.getCount(); i++) { + // Move `folderCursor` to the current row. + folderCursor.moveToPosition(i); + + // Get the database ID of the item. + int itemDatabaseId = folderCursor.getInt(folderCursor.getColumnIndex(BookmarksDatabaseHandler._ID)); + + // If this is a folder, delete the contents first. + if (bookmarksDatabaseHandler.isFolder(itemDatabaseId)) { + deleteBookmarkFolderContents(itemDatabaseId); + } + + bookmarksDatabaseHandler.deleteBookmark(itemDatabaseId); + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/stoutner/privacybrowser/BookmarksDatabaseHandler.java b/app/src/main/java/com/stoutner/privacybrowser/BookmarksDatabaseHandler.java index bc17bc7d..2a81097e 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/BookmarksDatabaseHandler.java +++ b/app/src/main/java/com/stoutner/privacybrowser/BookmarksDatabaseHandler.java @@ -26,6 +26,8 @@ import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.provider.ContactsContract; +import android.support.design.widget.Snackbar; +import android.support.v4.content.ContextCompat; public class BookmarksDatabaseHandler extends SQLiteOpenHelper { private static final int SCHEMA_VERSION = 1; @@ -118,6 +120,29 @@ public class BookmarksDatabaseHandler extends SQLiteOpenHelper { return bookmarksDatabase.rawQuery(GET_ONE_BOOKMARK, null); } + public String getFolderName (int databaseId) { + // Get a readable database handle. + SQLiteDatabase bookmarksDatabase = this.getReadableDatabase(); + + // Prepare the SQL statement to get the `Cursor` for the folder. + final String GET_FOLDER = "Select * FROM " + BOOKMARKS_TABLE + + " WHERE " + _ID + " = " + databaseId; + + // Get `folderCursor`. The second argument is `null` because there are no `selectionArgs`. + Cursor folderCursor = bookmarksDatabase.rawQuery(GET_FOLDER, null); + + // Get `folderName`. + folderCursor.moveToFirst(); + String folderName = folderCursor.getString(folderCursor.getColumnIndex(BOOKMARK_NAME)); + + // Close the cursor and the database handle. + folderCursor.close(); + bookmarksDatabase.close(); + + // Return the folder name. + return folderName; + } + public Cursor getFolderCursor(String folderName) { // Get a readable database handle. SQLiteDatabase bookmarksDatabase = this.getReadableDatabase(); @@ -135,6 +160,38 @@ public class BookmarksDatabaseHandler extends SQLiteOpenHelper { return bookmarksDatabase.rawQuery(GET_FOLDER, null); } + public Cursor getFoldersCursorExcept(String exceptFolders) { + // Get a readable database handle. + SQLiteDatabase bookmarksDatabase = this.getReadableDatabase(); + + // Prepare the SQL statement to get the `Cursor` for the folders. + final String GET_FOLDERS_EXCEPT = "Select * FROM " + BOOKMARKS_TABLE + + " WHERE " + IS_FOLDER + " = " + 1 + + " AND " + BOOKMARK_NAME + " NOT IN (" + exceptFolders + + ") ORDER BY " + BOOKMARK_NAME + " ASC"; + + // Return the results as a `Cursor`. The second argument is `null` because there are no `selectionArgs`. + // We can't close the `Cursor` because we need to use it in the parent activity. + return bookmarksDatabase.rawQuery(GET_FOLDERS_EXCEPT, null); + } + + public Cursor getSubfoldersCursor(String currentFolder) { + // Get a readable database handle. + SQLiteDatabase bookmarksDatabase = this.getReadableDatabase(); + + // SQL escape `currentFolder. + currentFolder = DatabaseUtils.sqlEscapeString(currentFolder); + + // Prepare the SQL statement to get the `Cursor` for the subfolders. + final String GET_SUBFOLDERS = "Select * FROM " + BOOKMARKS_TABLE + + " WHERE " + PARENT_FOLDER + " = " + currentFolder + + " AND " + IS_FOLDER + " = " + 1; + + // Return the results as a `Cursor`. The second argument is `null` because there are no `selectionArgs`. + // We can't close the `Cursor` because we need to use it in the parent activity. + return bookmarksDatabase.rawQuery(GET_SUBFOLDERS, null); + } + public String getParentFolder(String currentFolder) { // Get a readable database handle. SQLiteDatabase bookmarksDatabase = this.getReadableDatabase(); @@ -144,7 +201,8 @@ public class BookmarksDatabaseHandler extends SQLiteOpenHelper { // Prepare the SQL statement to get the parent folder. final String GET_PARENT_FOLDER = "Select * FROM " + BOOKMARKS_TABLE + - " WHERE " + IS_FOLDER + " = " + 1 + " AND " + BOOKMARK_NAME + " = " + currentFolder; + " WHERE " + IS_FOLDER + " = " + 1 + + " AND " + BOOKMARK_NAME + " = " + currentFolder; // The second argument is `null` because there are no `selectionArgs`. Cursor bookmarkCursor = bookmarksDatabase.rawQuery(GET_PARENT_FOLDER, null); @@ -180,7 +238,8 @@ public class BookmarksDatabaseHandler extends SQLiteOpenHelper { // Get everything in the BOOKMARKS_TABLE. final String GET_ALL_BOOKMARKS = "Select * FROM " + BOOKMARKS_TABLE + - " WHERE " + PARENT_FOLDER + " = " + folderName + " ORDER BY " + DISPLAY_ORDER + " ASC"; + " WHERE " + PARENT_FOLDER + " = " + folderName + + " ORDER BY " + DISPLAY_ORDER + " ASC"; // Return the results as a Cursor. The second argument is `null` because there are no selectionArgs. // We can't close the Cursor because we need to use it in the parent activity. @@ -208,13 +267,37 @@ public class BookmarksDatabaseHandler extends SQLiteOpenHelper { // Prepare the SQL statement to select all items except those with the specified IDs. final String GET_All_BOOKMARKS_EXCEPT_SPECIFIED = "Select * FROM " + BOOKMARKS_TABLE + - " WHERE " + PARENT_FOLDER + " = " + folderName + " AND " + _ID + " NOT IN (" + doNotGetIdsString + ") ORDER BY " + DISPLAY_ORDER + " ASC"; + " WHERE " + PARENT_FOLDER + " = " + folderName + + " AND " + _ID + " NOT IN (" + doNotGetIdsString + + ") ORDER BY " + DISPLAY_ORDER + " ASC"; - // Return the results as a `Cursor`. The second argument is `null` because there are no selectionArgs. + // Return the results as a `Cursor`. The second argument is `null` because there are no `selectionArgs`. // We can't close the `Cursor` because we need to use it in the parent activity. return bookmarksDatabase.rawQuery(GET_All_BOOKMARKS_EXCEPT_SPECIFIED, null); } + public boolean isFolder(int databaseId) { + // Get a readable database handle. + SQLiteDatabase bookmarksDatabase = this.getReadableDatabase(); + + // Prepare the SQL statement to determine if `databaseId` is a folder. + final String CHECK_IF_FOLDER = "Select * FROM " + BOOKMARKS_TABLE + + " WHERE " + _ID + " = " + databaseId; + + // Populate folderCursor. The second argument is `null` because there are no `selectionArgs`. + Cursor folderCursor = bookmarksDatabase.rawQuery(CHECK_IF_FOLDER, null); + + // Ascertain if this database ID is a folder. + folderCursor.moveToFirst(); + boolean isFolder = (folderCursor.getInt(folderCursor.getColumnIndex(IS_FOLDER)) == 1); + + // Close the `Cursor` and the database handle. + folderCursor.close(); + bookmarksDatabase.close(); + + return isFolder; + } + public void updateBookmark(int databaseId, String bookmarkName, String bookmarkUrl) { // Store the updated values in `bookmarkContentValues`. ContentValues bookmarkContentValues = new ContentValues(); @@ -308,15 +391,46 @@ public class BookmarksDatabaseHandler extends SQLiteOpenHelper { } public void updateBookmarkDisplayOrder(int databaseId, int displayOrder) { - // Store the updated values in `bookmarkContentValues`. - ContentValues bookmarkContentValues = new ContentValues(); + // Get a writable database handle. + SQLiteDatabase bookmarksDatabase = this.getWritableDatabase(); + // Store the new display order in `bookmarkContentValues`. + ContentValues bookmarkContentValues = new ContentValues(); bookmarkContentValues.put(DISPLAY_ORDER, displayOrder); + // Update the database. The last argument is `null` because there are no `whereArgs`. + bookmarksDatabase.update(BOOKMARKS_TABLE, bookmarkContentValues, _ID + " = " + databaseId, null); + + // Close the database handle. + bookmarksDatabase.close(); + } + + public void moveToFolder(int databaseId, String newFolder) { // Get a writable database handle. SQLiteDatabase bookmarksDatabase = this.getWritableDatabase(); - // Update the database. The last argument is `null` because there are no `whereArgs`. + // Get the highest `DISPLAY_ORDER` in the new folder + String newFolderSqlEscaped = DatabaseUtils.sqlEscapeString(newFolder); + final String NEW_FOLDER = "Select * FROM " + BOOKMARKS_TABLE + + " WHERE " + PARENT_FOLDER + " = " + newFolderSqlEscaped + + " ORDER BY " + DISPLAY_ORDER + " ASC"; + // The second argument is `null` because there are no `selectionArgs`. + Cursor newFolderCursor = bookmarksDatabase.rawQuery(NEW_FOLDER, null); + int displayOrder; + if (newFolderCursor.getCount() > 0) { + newFolderCursor.moveToLast(); + displayOrder = newFolderCursor.getInt(newFolderCursor.getColumnIndex(DISPLAY_ORDER)) + 1; + } else { + displayOrder = 0; + } + newFolderCursor.close(); + + // Store the new values in `bookmarkContentValues`. + ContentValues bookmarkContentValues = new ContentValues(); + bookmarkContentValues.put(DISPLAY_ORDER, displayOrder); + bookmarkContentValues.put(PARENT_FOLDER, newFolder); + + // Update the database. The last argument is 'null' because there are no 'whereArgs'. bookmarksDatabase.update(BOOKMARKS_TABLE, bookmarkContentValues, _ID + " = " + databaseId, null); // Close the database handle. diff --git a/app/src/main/java/com/stoutner/privacybrowser/BookmarksDatabaseViewActivity.java b/app/src/main/java/com/stoutner/privacybrowser/BookmarksDatabaseViewActivity.java index 00bad900..db5b198e 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/BookmarksDatabaseViewActivity.java +++ b/app/src/main/java/com/stoutner/privacybrowser/BookmarksDatabaseViewActivity.java @@ -77,7 +77,7 @@ public class BookmarksDatabaseViewActivity extends AppCompatActivity { @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { // Inflate the individual item layout. `false` does not attach it to the root. - return getLayoutInflater().inflate(R.layout.bookmarks_database_view_linearlayout, parent, false); + return getLayoutInflater().inflate(R.layout.bookmarks_database_view_item_linearlayout, parent, false); } @Override @@ -116,12 +116,15 @@ public class BookmarksDatabaseViewActivity extends AppCompatActivity { // Get the parent folder from the `Cursor` and display it in `bookmarkParentFolder`. String bookmarkParentFolder = cursor.getString(cursor.getColumnIndex(BookmarksDatabaseHandler.PARENT_FOLDER)); + ImageView parentFolderImageView = (ImageView) view.findViewById(R.id.bookmarks_database_view_parent_folder_icon); TextView bookmarkParentFolderTextView = (TextView) view.findViewById(R.id.bookmarks_database_view_parent_folder); // Make the folder name gray if it is the home folder. if (bookmarkParentFolder.isEmpty()) { + parentFolderImageView.setImageDrawable(ContextCompat.getDrawable(getApplicationContext(), R.drawable.folder_grey)); bookmarkParentFolderTextView.setText(R.string.home_folder); bookmarkParentFolderTextView.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.grey)); } else { + parentFolderImageView.setImageDrawable(ContextCompat.getDrawable(getApplicationContext(), R.drawable.folder_dark_blue)); bookmarkParentFolderTextView.setText(bookmarkParentFolder); bookmarkParentFolderTextView.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.black)); } diff --git a/app/src/main/java/com/stoutner/privacybrowser/MoveToFolder.java b/app/src/main/java/com/stoutner/privacybrowser/MoveToFolder.java index 2ae2cc65..7ecde8f5 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/MoveToFolder.java +++ b/app/src/main/java/com/stoutner/privacybrowser/MoveToFolder.java @@ -19,7 +19,248 @@ package com.stoutner.privacybrowser; +import android.app.Activity; +import android.app.Dialog; import android.app.DialogFragment; +import android.content.Context; +import android.content.DialogInterface; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.MatrixCursor; +import android.database.MergeCursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +// If we don't use `android.support.v7.app.AlertDialog` instead of `android.app.AlertDialog` then the dialog will be covered by the keyboard. +import android.support.design.widget.Snackbar; +import android.support.v4.content.ContextCompat; +import android.support.v7.app.AlertDialog; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; + +import java.io.ByteArrayOutputStream; public class MoveToFolder extends DialogFragment { + // The public interface is used to send information back to the parent activity. + public interface MoveToFolderListener { + void onCancelMoveToFolder(DialogFragment dialogFragment); + + void onMoveToFolder(DialogFragment dialogFragment); + } + + // `moveToFolderListener` is used in `onAttach()` and `onCreateDialog`. + private MoveToFolderListener moveToFolderListener; + + public void onAttach(Activity parentActivity) { + super.onAttach(parentActivity); + + // Get a handle for `MoveToFolderListener` from `parentActivity`. + try { + moveToFolderListener = (MoveToFolderListener) parentActivity; + } catch(ClassCastException exception) { + throw new ClassCastException(parentActivity.toString() + " must implement EditBookmarkFolderListener."); + } + } + + // `exceptFolders` is used in `onCreateDialog()` and `addSubfoldersToExceptFolders()`. + private String exceptFolders; + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + // Use `AlertDialog.Builder` to create the `AlertDialog`. The style formats the color of the button text. + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity(), R.style.LightAlertDialog); + dialogBuilder.setTitle(R.string.move_to_folder); + // The parent view is `null` because it will be assigned by `AlertDialog`. + dialogBuilder.setView(getActivity().getLayoutInflater().inflate(R.layout.move_to_folder_dialog, null)); + + // Set an `onClick()` listener for the negative button. + dialogBuilder.setNegativeButton(R.string.cancel, new Dialog.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // Return the `DialogFragment` to the parent activity on cancel. + moveToFolderListener.onCancelMoveToFolder(MoveToFolder.this); + } + }); + + // Set the `onClick()` listener fo the positive button. + dialogBuilder.setPositiveButton(R.string.move, new Dialog.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // Return the `DialogFragment` to the parent activity on save. + moveToFolderListener.onMoveToFolder(MoveToFolder.this); + } + }); + + // Create an `AlertDialog` from the `AlertDialog.Builder`. + final AlertDialog alertDialog = dialogBuilder.create(); + + // We need to show the `AlertDialog` before we can modify items in the layout. + alertDialog.show(); + + // Initialize the `Cursor` and `CursorAdapter` variables. + Cursor foldersCursor; + CursorAdapter foldersCursorAdapter; + + // Check to see if we are in the `Home Folder`. + if (BookmarksActivity.currentFolder.isEmpty()) { // Don't display `Home Folder` at the top of the `ListView`. + // Initialize `exceptFolders`. + exceptFolders = ""; + + // If a folder is selected, add it and all children to the list of folders not to display. + long[] selectedBookmarksLongArray = BookmarksActivity.bookmarksListView.getCheckedItemIds(); + for (long databaseIdLong : selectedBookmarksLongArray) { + // Get `databaseIdInt` for each selected bookmark. + int databaseIdInt = (int) databaseIdLong; + + // If `databaseIdInt` is a folder. + if (BookmarksActivity.bookmarksDatabaseHandler.isFolder(databaseIdInt)) { + // Get the name of the selected folder. + String folderName = BookmarksActivity.bookmarksDatabaseHandler.getFolderName(databaseIdInt); + + if (exceptFolders.isEmpty()){ + // Add the selected folder to the list of folders not to display. + exceptFolders = DatabaseUtils.sqlEscapeString(folderName); + } else { + // Add the selected folder to the end of the list of folders not to display. + exceptFolders = exceptFolders + "," + DatabaseUtils.sqlEscapeString(folderName); + } + + // Add the selected folder's subfolders to the list of folders not to display. + addSubfoldersToExceptFolders(folderName); + } + } + + // Get a `Cursor` containing the folders to display. + foldersCursor = BookmarksActivity.bookmarksDatabaseHandler.getFoldersCursorExcept(exceptFolders); + + // Setup `foldersCursorAdaptor` with `this` context. `false` disables autoRequery. + foldersCursorAdapter = new CursorAdapter(alertDialog.getContext(), foldersCursor, false) { + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + // Inflate the individual item layout. `false` does not attach it to the root. + return getActivity().getLayoutInflater().inflate(R.layout.move_to_folder_item_linearlayout, parent, false); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + // Get the folder icon from `cursor`. + byte[] folderIconByteArray = cursor.getBlob(cursor.getColumnIndex(BookmarksDatabaseHandler.FAVORITE_ICON)); + // Convert the byte array to a `Bitmap` beginning at the first byte and ending at the last. + Bitmap folderIconBitmap = BitmapFactory.decodeByteArray(folderIconByteArray, 0, folderIconByteArray.length); + // Display `folderIconBitmap` in `move_to_folder_icon`. + ImageView folderIconImageView = (ImageView) view.findViewById(R.id.move_to_folder_icon); + assert folderIconImageView != null; // Remove the warning below that `currentIconImageView` might be null; + folderIconImageView.setImageBitmap(folderIconBitmap); + + // Get the folder name from `cursor` and display it in `move_to_folder_name_textview`. + String folderName = cursor.getString(cursor.getColumnIndex(BookmarksDatabaseHandler.BOOKMARK_NAME)); + TextView folderNameTextView = (TextView) view.findViewById(R.id.move_to_folder_name_textview); + folderNameTextView.setText(folderName); + } + }; + } else { // Display `Home Folder` at the top of the `ListView`. + // Get the home folder icon drawable and convert it to a `Bitmap`. `this` specifies the current context. + Drawable homeFolderIconDrawable = ContextCompat.getDrawable(getActivity().getApplicationContext(), R.drawable.folder_grey_bitmap); + BitmapDrawable homeFolderIconBitmapDrawable = (BitmapDrawable) homeFolderIconDrawable; + Bitmap homeFolderIconBitmap = homeFolderIconBitmapDrawable.getBitmap(); + // Convert the folder `Bitmap` to a byte array. `0` is for lossless compression (the only option for a PNG). + ByteArrayOutputStream homeFolderIconByteArrayOutputStream = new ByteArrayOutputStream(); + homeFolderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, homeFolderIconByteArrayOutputStream); + byte[] homeFolderIconByteArray = homeFolderIconByteArrayOutputStream.toByteArray(); + + // Setup a `MatrixCursor` for the `Home Folder`. + String[] homeFolderMatrixCursorColumnNames = {BookmarksDatabaseHandler._ID, BookmarksDatabaseHandler.BOOKMARK_NAME, BookmarksDatabaseHandler.FAVORITE_ICON}; + MatrixCursor homeFolderMatrixCursor = new MatrixCursor(homeFolderMatrixCursorColumnNames); + homeFolderMatrixCursor.addRow(new Object[]{0, getString(R.string.home_folder), homeFolderIconByteArray}); + + // Add the parent folder to the list of folders not to display. + exceptFolders = DatabaseUtils.sqlEscapeString(BookmarksActivity.currentFolder); + + // If a folder is selected, add it and all children to the list of folders not to display. + long[] selectedBookmarksLongArray = BookmarksActivity.bookmarksListView.getCheckedItemIds(); + for (long databaseIdLong : selectedBookmarksLongArray) { + // Get `databaseIdInt` for each selected bookmark. + int databaseIdInt = (int) databaseIdLong; + + // If `databaseIdInt` is a folder. + if (BookmarksActivity.bookmarksDatabaseHandler.isFolder(databaseIdInt)) { + // Get the name of the selected folder. + String folderName = BookmarksActivity.bookmarksDatabaseHandler.getFolderName(databaseIdInt); + + // Add the selected folder to the end of the list of folders not to display. + exceptFolders = exceptFolders + "," + DatabaseUtils.sqlEscapeString(folderName); + + // Add the selected folder's subfolders to the list of folders not to display. + addSubfoldersToExceptFolders(folderName); + } + } + + // Get a `foldersCursor`. + foldersCursor = BookmarksActivity.bookmarksDatabaseHandler.getFoldersCursorExcept(exceptFolders); + + // Combine `homeFolderMatrixCursor` and `foldersCursor`. + MergeCursor foldersMergeCursor = new MergeCursor(new Cursor[]{homeFolderMatrixCursor, foldersCursor}); + + // Setup `foldersCursorAdaptor` with `this` context. `false` disables autoRequery. + foldersCursorAdapter = new CursorAdapter(alertDialog.getContext(), foldersMergeCursor, false) { + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + // Inflate the individual item layout. `false` does not attach it to the root. + return getActivity().getLayoutInflater().inflate(R.layout.move_to_folder_item_linearlayout, parent, false); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + // Get the folder icon from `cursor`. + byte[] folderIconByteArray = cursor.getBlob(cursor.getColumnIndex(BookmarksDatabaseHandler.FAVORITE_ICON)); + // Convert the byte array to a `Bitmap` beginning at the first byte and ending at the last. + Bitmap folderIconBitmap = BitmapFactory.decodeByteArray(folderIconByteArray, 0, folderIconByteArray.length); + // Display `folderIconBitmap` in `move_to_folder_icon`. + ImageView folderIconImageView = (ImageView) view.findViewById(R.id.move_to_folder_icon); + assert folderIconImageView != null; // Remove the warning below that `currentIconImageView` might be null; + folderIconImageView.setImageBitmap(folderIconBitmap); + + // Get the folder name from `cursor` and display it in `move_to_folder_name_textview`. + String folderName = cursor.getString(cursor.getColumnIndex(BookmarksDatabaseHandler.BOOKMARK_NAME)); + TextView folderNameTextView = (TextView) view.findViewById(R.id.move_to_folder_name_textview); + folderNameTextView.setText(folderName); + } + }; + } + + // Display the ListView + ListView foldersListView = (ListView) alertDialog.findViewById(R.id.move_to_folder_listview); + assert foldersListView != null; // Remove the warning below that `foldersListView` might be null. + foldersListView.setAdapter(foldersCursorAdapter); + + // `onCreateDialog` requires the return of an `AlertDialog`. + return alertDialog; + } + + public void addSubfoldersToExceptFolders(String folderName) { + // Get a `Cursor` will all the immediate subfolders. + Cursor subfoldersCursor = BookmarksActivity.bookmarksDatabaseHandler.getSubfoldersCursor(folderName); + + for (int i = 0; i < subfoldersCursor.getCount(); i++) { + // Move `subfolderCursor` to the current item. + subfoldersCursor.moveToPosition(i); + + // Get the name of the subfolder. + String subfolderName = subfoldersCursor.getString(subfoldersCursor.getColumnIndex(BookmarksDatabaseHandler.BOOKMARK_NAME)); + + // Run the same tasks for any subfolders of the subfolder. + addSubfoldersToExceptFolders(subfolderName); + + // Add the subfolder to `exceptFolders`. + subfolderName = DatabaseUtils.sqlEscapeString(subfolderName); + exceptFolders = exceptFolders + "," + subfolderName; + } + + } } diff --git a/app/src/main/res/drawable-hdpi/folder_grey_bitmap.png b/app/src/main/res/drawable-hdpi/folder_grey_bitmap.png new file mode 100644 index 0000000000000000000000000000000000000000..cf7650192b943c154d3a8f699a3ed79f459473b9 GIT binary patch literal 585 zcmV-P0=E5$P)6&^-_gvDL~p7J^vDm?o`UDeNxC zrOjWtDv`SxgRQ+m6Vk-g>L0KblZeGFhQQ`aksQQBi@@D}gqf$@fqmb77`~k?R=CD+ zI9wHxb>KO$2nf^RxUTCL-EQ~WlUk#-EQx6I-RreMe*MM0p=SjVUCD=2YPv) ze}b#GKuoCWBUSz2y?+IlJKz+UGwx+U)oQgKN4Ep6aksAPjhF=hti>+ip<{XJlLS~7uqFur&aF!ra_)d5>lTLmbiko?3quZ^s%~1hFl5V>W%*H6 z-&r>?V7n-aT?bHARW~LHBJy@L8ueh}7#u=)sjA~YMjlMA-(plmegKE6x>*#(?s(w> X`44sKjf7GL00000NkvXXu0mjfaI^P_ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/folder_dark_blue.xml b/app/src/main/res/drawable-mdpi/folder_dark_blue.xml new file mode 100644 index 00000000..ffa94773 --- /dev/null +++ b/app/src/main/res/drawable-mdpi/folder_dark_blue.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/app/src/main/res/drawable-mdpi/folder_grey_bitmap.png b/app/src/main/res/drawable-mdpi/folder_grey_bitmap.png new file mode 100644 index 0000000000000000000000000000000000000000..c37add7474e6ef431c5bea023ba16bd50f5b8a0a GIT binary patch literal 463 zcmV;=0WkiFP)_Vi&QB@1UhWZy_QG$)a2vQwSnte{$I{U%9Yn&tY~50IJF~O*;S%0jvR7 zLOwUnxt%DAvYhWvjX&it0PL2OdFz~uW;LSGY&JV2k4wy4&9ZEx)oKlf!{JwnxqlK7 zIjA6a%Q-jb_xp<#WdFh>N!|f0R}rqNcO*|r%=-|LmpG2!CeN58$r!*wSs$5!PgT8E z)n2#TeXZkwiAPo46achtlG8eXBe8B#1j+4M4*;-I=U)f_8g)MbgaOv-eh22`1Zph| zuof<{wJ^Y1xC7S00BdtGAY5WgVSu%{7%;XjQNTz5JX^OY;7LfHS+~fGoc5!lIL8@MUQTpt6Hc|`>O!qxq z978JRyuE9iA)F||{^9@UW%<)%TB4%WAHO($X_v*$t=$>!{5lI8^|tShn6qo;gGVZj zcO1D_-a2@Y|IeE@YHPbA3JL>`Xw6_#*~7WsIm^@gPm@~x?CQGcNzH$D+Ml=G8Ljjp zp|G&zB*>69$wzejgP1vh8)hl&H-1_~@21ocli-#G{Jd?69;?E(zP2oO! zu1y`;1W2lfk{&D8~{1+@=?lB7P4A-09&t%1@qkL?} zY=>;l>kKI;8-qMu{9dyNRK9n<%NQdZ#c*9vw;?;CoMHM4mI;}RDNzhF-!T}?WiZlh z@ZnatEbFjL*kKtfR7NimA;WZ{gmdysm>w~QW!^J<)WLd~PHbT?ianHK1vU*RW8HvD zhIPVAA+^h#3YWopbQ{hv6x7>zm3A95*li46a)bTx-ulyi6O&#alla^)SLx_G>Gywy z8hpzpiTrvtXLoX1db&OHj&pvK&6J8vyB6^>GdK#w2v2l>6QKFYWcJy64BOr@A2P^^ z%3i+ryZ?@zK@UGZc#t5`mMEODTToks>uQW%x!eI!hS0EGc449ucPel!<7z$h?dRJc zDkh$j1R2CH{>tC@n1OHc@&Bqf*=3@>N8~fuuoP5h-+%XkIbr!y+lzMtV}bdB!PC{x JWt~$(696icZTbKJ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/folder_grey_bitmap.png b/app/src/main/res/drawable-xxhdpi/folder_grey_bitmap.png new file mode 100644 index 0000000000000000000000000000000000000000..4ad866ed3b67e8e0c30cf5a39e9d6cfcd7a7165c GIT binary patch literal 1142 zcmeAS@N?(olHy`uVBq!ia0vp^4M3d6!3HFmm1l+kDVAa<&kznEsNqQI0P;BtJR*x3 z7<9Zrm~pA?w3R?X$r9IylHmNblJdl&REF~Ma=pyF?Be9af>gcyqV(DCY@~qZefD&5 z45^s&_Lgmabg2m2hvzXhzKqg}(-_tsX^3FpOkC*q(D&5^jwNgYFQ>iG+Gyi)s^gUV zi31L1+%MzQg_!x4d=wDuY~57C6vc91L9;|DH0REPC-oAGrhHKq3wm6AXU=o;o8NzV z^*?bxe*C)FmxZe?WtiOKQS9kHahu^BLj~V~m37x8cinmYRqtBX)_J89+*@Tzm3%K3 zGdyDq`PBXSgG)+ob@lJZJ9qAseXY~q^qXtLmMvedynC0&_CUDV=D1U6`01ygj&^o- z*00&jvwJsJ!20X+`yW^E9*}mc>YC9RhMuWjOP{7~j@-g$w1$OYlTNjFX#EyGV~6{lC)_Lr+xuiOC&v!moR=6x z6`y#1ZLy4a=-VTOdv>M?gZ1E zntJ9Ri>|*udp&Dw^!Ir^7oLkdS_#f$`4A~{+}>2GH~hV=r0oMHtC?|JiZN$5y}5gs z-IB3JbM>q3k<<3p6eut)XlvA{Kfmgg*WS2(Mn8u2aoS?tp$Y$^59l)ZOS3Qfb2n%E zZT%w7FWF0C^!6WTvRJn2@g2sR)dfsH%4V^C$ZcW$@Ye7h!?{8x3!o$j$sVxuV|X3` zLpxZkj&nYc;fA5<4$mzZ=On|>W1$x^>J7y@FciZy_c2RBA1fGHHx$odIJXgo3OP&q zgb!GX!H`}flC?maRWBfH-ob1^inW3bvabbF($twgw3l)jZI)d5Y8jYj+>tc5DW~HE z|HM_>)-+Gx70m2h7Js(TiNZu%`3Ga-~Ir3@VFd*WGthR{hJG-dR+%H$X#$-T$3PLRr~1S*Hs51ErHArnF~QS69pX z`1zIb@!cua-VwDnuVnY#K!*EImdKI;Vst0I2-#2LJ#7 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/folder.xml b/app/src/main/res/drawable/folder_grey.xml similarity index 83% rename from app/src/main/res/drawable/folder.xml rename to app/src/main/res/drawable/folder_grey.xml index a6d3d31b..5710e29a 100644 --- a/app/src/main/res/drawable/folder.xml +++ b/app/src/main/res/drawable/folder_grey.xml @@ -1,4 +1,4 @@ - + app:itemIconTint="@color/blue_grey" /> + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/move_to_folder_item_linearlayout.xml b/app/src/main/res/layout/move_to_folder_item_linearlayout.xml new file mode 100644 index 00000000..be277d9b --- /dev/null +++ b/app/src/main/res/layout/move_to_folder_item_linearlayout.xml @@ -0,0 +1,46 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 598c51b7..423664c5 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -23,7 +23,7 @@ #FF000000 #FF1976D2 #FF0D47A1 - #FF607D8B + #FF607D8B #FF9E9E9E #FF64DD17 #FFBBDEFB diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 42396d59..24b5fbaf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -80,9 +80,11 @@ Folder names must be unique Cannot create the folder because the name is not unique: Cannot rename the folder because the new name is not unique: + Cannot move the selected bookmarks because no new folder was selected. Edit bookmark Edit folder Move to Folder + Move Save Bookmark name Bookmark URL @@ -101,7 +103,7 @@ Bookmarks Database View - Home folder + Home Folder Privacy Browser Guide -- 2.43.0