From: Soren Stoutner Date: Thu, 16 Nov 2023 22:09:45 +0000 (-0700) Subject: Add import and export of bookmarks to HTML file. https://redmine.stoutner.com/issues/91 X-Git-Tag: v3.16~1 X-Git-Url: https://gitweb.stoutner.com/?p=PrivacyBrowserAndroid.git;a=commitdiff_plain;h=f9f282da2dfc2539c0880a6f9a07e17fa2e003d1 Add import and export of bookmarks to HTML file. https://redmine.stoutner.com/issues/91 --- diff --git a/app/src/main/java/com/stoutner/privacybrowser/activities/BookmarksActivity.kt b/app/src/main/java/com/stoutner/privacybrowser/activities/BookmarksActivity.kt index b5b3992b..737d6393 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/BookmarksActivity.kt +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/BookmarksActivity.kt @@ -90,6 +90,7 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookma // Define the class variables. private var bookmarksDeletedSnackbar: Snackbar? = null + private var checkingManyBookmarks = false private var closeActivityAfterDismissingSnackbar = false private var contextualActionMode: ActionMode? = null @@ -250,46 +251,49 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookma return true } - override fun onItemCheckedStateChanged(mode: ActionMode, position: Int, id: Long, checked: Boolean) { - // Get the number of selected bookmarks. - val numberOfSelectedBookmarks = bookmarksListView.checkedItemCount - - // Only process commands if at least one bookmark is selected. Otherwise, a context menu with 0 selected bookmarks is briefly displayed. - if (numberOfSelectedBookmarks > 0) { - // Adjust the action mode and the menu according to the number of selected bookmarks. - if (numberOfSelectedBookmarks == 1) { // One bookmark is selected. - // Show the applicable menu items. - moveBookmarkUpMenuItem.isVisible = true - moveBookmarkDownMenuItem.isVisible = true - editBookmarkMenuItem.isVisible = true - - // Update the enabled status of the move icons. - updateMoveIcons() - } else { // More than one bookmark is selected. - // Hide non-applicable `MenuItems`. - moveBookmarkUpMenuItem.isVisible = false - moveBookmarkDownMenuItem.isVisible = false - editBookmarkMenuItem.isVisible = false - } + override fun onItemCheckedStateChanged(actionMode: ActionMode, position: Int, id: Long, checked: Boolean) { + // Only update the UI if not checking many bookmarks. In that case, the flag will be reset on the last bookmark so the UI is only updated once. + if (!checkingManyBookmarks) { + // Get the number of selected bookmarks. + val numberOfSelectedBookmarks = bookmarksListView.checkedItemCount + + // Only process commands if at least one bookmark is selected. Otherwise, a context menu with 0 selected bookmarks is briefly displayed. + if (numberOfSelectedBookmarks > 0) { + // Adjust the action mode and the menu according to the number of selected bookmarks. + if (numberOfSelectedBookmarks == 1) { // One bookmark is selected. + // Show the applicable menu items. + moveBookmarkUpMenuItem.isVisible = true + moveBookmarkDownMenuItem.isVisible = true + editBookmarkMenuItem.isVisible = true + + // Update the enabled status of the move icons. + updateMoveIcons() + } else { // More than one bookmark is selected. + // Hide non-applicable `MenuItems`. + moveBookmarkUpMenuItem.isVisible = false + moveBookmarkDownMenuItem.isVisible = false + editBookmarkMenuItem.isVisible = false + } - // Display the move to folder menu item if at least one other folder exists. - moveToFolderMenuItem.isVisible = bookmarksDatabaseHelper.hasFoldersExceptDatabaseId(bookmarksListView.checkedItemIds) + // Display the move to folder menu item if at least one other folder exists. + moveToFolderMenuItem.isVisible = bookmarksDatabaseHelper.hasFoldersExceptDatabaseId(bookmarksListView.checkedItemIds) - // List the number of selected bookmarks in the subtitle. - mode.subtitle = getString(R.string.selected, numberOfSelectedBookmarks) + // List the number of selected bookmarks in the subtitle. + actionMode.subtitle = getString(R.string.selected, numberOfSelectedBookmarks) - // Show the select all menu item if all the bookmarks are not selected. - selectAllBookmarksMenuItem.isVisible = (numberOfSelectedBookmarks != bookmarksListView.count) + // Show the select all menu item if all the bookmarks are not selected. + selectAllBookmarksMenuItem.isVisible = (numberOfSelectedBookmarks != bookmarksListView.count) + } } } override fun onActionItemClicked(actionMode: ActionMode, menuItem: MenuItem): Boolean { // Declare the variables. - val selectedBookmarkNewPosition: Int - val selectedBookmarksPositionsSparseBooleanArray: SparseBooleanArray + val checkedBookmarkNewPosition: Int + val checkedBookmarksPositionsSparseBooleanArray: SparseBooleanArray - // Initialize the selected bookmark position. - var selectedBookmarkPosition = 0 + // Initialize the checked bookmark position. + var checkedBookmarkPosition = 0 // Get the menu item ID. val menuItemId = menuItem.itemId @@ -297,30 +301,33 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookma // Run the commands according to the selected action item. if (menuItemId == R.id.move_bookmark_up) { // Move the bookmark up. // Get the array of checked bookmark positions. - selectedBookmarksPositionsSparseBooleanArray = bookmarksListView.checkedItemPositions + checkedBookmarksPositionsSparseBooleanArray = bookmarksListView.checkedItemPositions // 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`. - for (i in 0 until selectedBookmarksPositionsSparseBooleanArray.size()) { + for (i in 0 until checkedBookmarksPositionsSparseBooleanArray.size()) { // Check to see if the value for the bookmark is true, meaning it is currently selected. - if (selectedBookmarksPositionsSparseBooleanArray.valueAt(i)) { + if (checkedBookmarksPositionsSparseBooleanArray.valueAt(i)) { // Only one bookmark should have a value of `true` when move bookmark up is enabled. - selectedBookmarkPosition = selectedBookmarksPositionsSparseBooleanArray.keyAt(i) + checkedBookmarkPosition = checkedBookmarksPositionsSparseBooleanArray.keyAt(i) } } - // Calculate the new position of the selected bookmark. - selectedBookmarkNewPosition = selectedBookmarkPosition - 1 + // Calculate the new position of the checked bookmark. + checkedBookmarkNewPosition = checkedBookmarkPosition - 1 + + // Get the bookmarks count. + val bookmarksCount = bookmarksListView.count // Iterate through the bookmarks. - for (i in 0 until bookmarksListView.count) { + for (i in 0 until bookmarksCount) { // Get the database ID for the current bookmark. val currentBookmarkDatabaseId = bookmarksListView.getItemIdAtPosition(i).toInt() // Update the display order for the current bookmark. - if (i == selectedBookmarkPosition) { // The current bookmark is the selected bookmark. + if (i == checkedBookmarkPosition) { // The current bookmark is the selected bookmark. // Move the current bookmark up one. bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i - 1) - } else if ((i + 1) == selectedBookmarkPosition) { // The current bookmark is immediately above the selected bookmark. + } else if ((i + 1) == checkedBookmarkPosition) { // The current bookmark is immediately above the selected bookmark. // Move the current bookmark down one. bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i + 1) } else { // The current bookmark is not changing positions. @@ -340,25 +347,25 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookma bookmarksCursorAdapter.changeCursor(bookmarksCursor) // Scroll to the new bookmark position. - scrollBookmarks(selectedBookmarkNewPosition) + scrollBookmarks(checkedBookmarkNewPosition) // Update the enabled status of the move icons. updateMoveIcons() } else if (menuItemId == R.id.move_bookmark_down) { // Move the bookmark down. // Get the array of checked bookmark positions. - selectedBookmarksPositionsSparseBooleanArray = bookmarksListView.checkedItemPositions + checkedBookmarksPositionsSparseBooleanArray = bookmarksListView.checkedItemPositions - // 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`. - for (i in 0 until selectedBookmarksPositionsSparseBooleanArray.size()) { + // Get the position of the bookmark that is selected. If other bookmarks have previously been checked they will be included in the sparse boolean array with a value of `false`. + for (i in 0 until checkedBookmarksPositionsSparseBooleanArray.size()) { // Check to see if the value for the bookmark is true, meaning it is currently selected. - if (selectedBookmarksPositionsSparseBooleanArray.valueAt(i)) { + if (checkedBookmarksPositionsSparseBooleanArray.valueAt(i)) { // Only one bookmark should have a value of `true` when move bookmark down is enabled. - selectedBookmarkPosition = selectedBookmarksPositionsSparseBooleanArray.keyAt(i) + checkedBookmarkPosition = checkedBookmarksPositionsSparseBooleanArray.keyAt(i) } } - // Calculate the new position of the selected bookmark. - selectedBookmarkNewPosition = selectedBookmarkPosition + 1 + // Calculate the new position of the checked bookmark. + checkedBookmarkNewPosition = checkedBookmarkPosition + 1 // Iterate through the bookmarks. for (i in 0 until bookmarksListView.count) { @@ -366,10 +373,10 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookma val currentBookmarkDatabaseId = bookmarksListView.getItemIdAtPosition(i).toInt() // Update the display order for the current bookmark. - if (i == selectedBookmarkPosition) { // The current bookmark is the selected bookmark. + if (i == checkedBookmarkPosition) { // The current bookmark is the checked bookmark. // Move the current bookmark down one. bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i + 1) - } else if (i - 1 == selectedBookmarkPosition) { // The current bookmark is immediately below the selected bookmark. + } else if ((i - 1) == checkedBookmarkPosition) { // The current bookmark is immediately below the checked bookmark. // Move the bookmark below the selected bookmark up one. bookmarksDatabaseHelper.updateDisplayOrder(currentBookmarkDatabaseId, i - 1) } else { // The current bookmark is not changing positions. @@ -390,7 +397,7 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookma bookmarksCursorAdapter.changeCursor(bookmarksCursor) // Scroll to the new bookmark position. - scrollBookmarks(selectedBookmarkNewPosition) + scrollBookmarks(checkedBookmarkNewPosition) // Update the enabled status of the move icons. updateMoveIcons() @@ -402,19 +409,19 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookma moveToFolderDialog.show(supportFragmentManager, resources.getString(R.string.move_to_folder)) } else if (menuItemId == R.id.edit_bookmark) { // Get the array of checked bookmark positions. - selectedBookmarksPositionsSparseBooleanArray = bookmarksListView.checkedItemPositions + checkedBookmarksPositionsSparseBooleanArray = bookmarksListView.checkedItemPositions // 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`. - for (i in 0 until selectedBookmarksPositionsSparseBooleanArray.size()) { + for (i in 0 until checkedBookmarksPositionsSparseBooleanArray.size()) { // Check to see if the value for the bookmark is true, meaning it is currently selected. - if (selectedBookmarksPositionsSparseBooleanArray.valueAt(i)) { + if (checkedBookmarksPositionsSparseBooleanArray.valueAt(i)) { // Only one bookmark should have a value of `true` when move edit bookmark is enabled. - selectedBookmarkPosition = selectedBookmarksPositionsSparseBooleanArray.keyAt(i) + checkedBookmarkPosition = checkedBookmarksPositionsSparseBooleanArray.keyAt(i) } } // Move the cursor to the selected position. - bookmarksCursor.moveToPosition(selectedBookmarkPosition) + bookmarksCursor.moveToPosition(checkedBookmarkPosition) // Get the selected bookmark database ID. val databaseId = bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(ID)) @@ -437,14 +444,14 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookma // Set the deleting bookmarks flag, which prevents the delete menu item from being enabled until the current process finishes. deletingBookmarks = true - // Get an array of the selected row IDs. - val selectedBookmarksIdsLongArray = bookmarksListView.checkedItemIds + // Get an array of the checked row IDs. + val checkedBookmarksIdsLongArray = bookmarksListView.checkedItemIds // Initialize a variable to count the number of bookmarks to delete. var numberOfBookmarksToDelete = 0 // Count the number of bookmarks to delete. - for (databaseIdLong in selectedBookmarksIdsLongArray) { + for (databaseIdLong in checkedBookmarksIdsLongArray) { // Convert the database ID long to an int. val databaseIdInt = databaseIdLong.toInt() @@ -459,10 +466,10 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookma } // 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. - selectedBookmarksPositionsSparseBooleanArray = bookmarksListView.checkedItemPositions.clone() + checkedBookmarksPositionsSparseBooleanArray = bookmarksListView.checkedItemPositions.clone() // Update the bookmarks cursor with the current contents of the bookmarks database except for the specified database IDs. - bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrderExcept(selectedBookmarksIdsLongArray, currentFolderId) + bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrderExcept(checkedBookmarksIdsLongArray, currentFolderId) // Update the list view. bookmarksCursorAdapter.changeCursor(bookmarksCursor) @@ -479,12 +486,24 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookma // Update the list view. bookmarksCursorAdapter.changeCursor(bookmarksCursor) - // Re-select the previously selected bookmarks. - for (i in 0 until selectedBookmarksPositionsSparseBooleanArray.size()) - bookmarksListView.setItemChecked(selectedBookmarksPositionsSparseBooleanArray.keyAt(i), true) + // Get the number of checked bookmarks. + val numberOfCheckedBookmarks = checkedBookmarksPositionsSparseBooleanArray.size() + + // Set the checking many bookmarks flag. + checkingManyBookmarks = true + + // Re-check the previously checked bookmarks. + for (i in 0 until numberOfCheckedBookmarks) { + // Reset the checking many bookmarks flag on the last bookmark so the UI is updated. + if (i == (numberOfCheckedBookmarks - 1)) + checkingManyBookmarks = false + + // Check the bookmark. + bookmarksListView.setItemChecked(checkedBookmarksPositionsSparseBooleanArray.keyAt(i), true) + } } else { // The snackbar was dismissed without the undo button being pushed. // Delete each selected bookmark. - for (databaseIdLong in selectedBookmarksIdsLongArray) { + for (databaseIdLong in checkedBookmarksIdsLongArray) { // Convert the database long ID to an int. val databaseIdInt = databaseIdLong.toInt() @@ -533,8 +552,16 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookma // Get the total number of bookmarks. val numberOfBookmarks = bookmarksListView.count + // Set the checking many bookmarks flag. + checkingManyBookmarks = true + // Select them all. for (i in 0 until numberOfBookmarks) { + // Reset the checking many bookmarks flag on the last bookmark so the UI is updated. + if (i == (numberOfBookmarks - 1)) + checkingManyBookmarks = false + + // Check the bookmark. bookmarksListView.setItemChecked(i, true) } } @@ -651,8 +678,16 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookma // Get the total number of bookmarks. val numberOfBookmarks = bookmarksListView.count + // Set the checking many bookmarks flag. + checkingManyBookmarks = true + // Select them all. for (i in 0 until numberOfBookmarks) { + // Reset the checking many bookmarks flag on the last bookmark so the UI is updated. + if (i == (numberOfBookmarks - 1)) + checkingManyBookmarks = false + + // Check the bookmark. bookmarksListView.setItemChecked(i, true) } } else if (menuItemId == R.id.bookmarks_database_view) { @@ -747,11 +782,11 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookma // Move all the bookmarks down one in the display order. for (i in 0 until bookmarksListView.count) { val databaseId = bookmarksListView.getItemIdAtPosition(i).toInt() - bookmarksDatabaseHelper.updateDisplayOrder(databaseId, i + 1) + bookmarksDatabaseHelper.updateDisplayOrder(databaseId, displayOrder = i + 1) } // Create the folder, which will be placed at the top of the list view. - bookmarksDatabaseHelper.createFolder(folderNameString, currentFolderId, folderIconByteArray) + bookmarksDatabaseHelper.createFolder(folderNameString, currentFolderId, displayOrder = 0, folderIconByteArray) // Update the bookmarks cursor with the contents of the current folder. bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentFolderId) @@ -907,7 +942,7 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookma val folderId = bookmarksDatabaseHelper.getFolderId(folderDatabaseId) // Get the contents of the folder. - val folderCursor = bookmarksDatabaseHelper.getBookmarkIds(folderId) + val folderCursor = bookmarksDatabaseHelper.getBookmarkAndFolderIds(folderId) // Initialize the bookmark counter. var bookmarkCounter = 0 @@ -937,7 +972,7 @@ class BookmarksActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookma val folderId = bookmarksDatabaseHelper.getFolderId(folderDatabaseId) // Get the contents of the folder. - val folderCursor = bookmarksDatabaseHelper.getBookmarkIds(folderId) + val folderCursor = bookmarksDatabaseHelper.getBookmarkAndFolderIds(folderId) // Delete each of the bookmarks in the folder. for (i in 0 until folderCursor.count) { diff --git a/app/src/main/java/com/stoutner/privacybrowser/activities/ImportExportActivity.kt b/app/src/main/java/com/stoutner/privacybrowser/activities/ImportExportActivity.kt index 8e4fff48..35f4321a 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/ImportExportActivity.kt +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/ImportExportActivity.kt @@ -34,6 +34,7 @@ import android.widget.Button import android.widget.EditText import android.widget.LinearLayout import android.widget.RadioButton +import android.widget.ScrollView import android.widget.Spinner import android.widget.TextView @@ -52,6 +53,7 @@ import com.stoutner.privacybrowser.BuildConfig import com.stoutner.privacybrowser.helpers.EXPORT_SUCCESSFUL import com.stoutner.privacybrowser.helpers.IMPORT_EXPORT_SCHEMA_VERSION import com.stoutner.privacybrowser.helpers.IMPORT_SUCCESSFUL +import com.stoutner.privacybrowser.helpers.ImportExportBookmarksHelper import com.stoutner.privacybrowser.helpers.ImportExportDatabaseHelper import java.io.File @@ -79,25 +81,34 @@ private const val OPENPGP_ENCRYPTION = 2 // Define the saved instance state constants. private const val ENCRYPTION_PASSWORD_TEXTINPUTLAYOUT_VISIBILITY = "A" private const val OPEN_KEYCHAIN_REQUIRED_TEXTVIEW_VISIBILITY = "B" -private const val FILE_LOCATION_CARD_VIEW = "C" -private const val FILE_NAME_LINEARLAYOUT_VISIBILITY = "D" +private const val SETTINGS_FILE_LOCATION_CARDVIEW_VISIBILITY = "C" +private const val SETTINGS_FILE_NAME_LINEARLAYOUT_VISIBILITY = "D" private const val OPEN_KEYCHAIN_IMPORT_INSTRUCTIONS_TEXTVIEW_VISIBILITY = "E" -private const val IMPORT_EXPORT_BUTTON_VISIBILITY = "F" -private const val FILE_NAME_TEXT = "G" -private const val IMPORT_EXPORT_BUTTON_TEXT = "H" +private const val SETTINGS_IMPORT_EXPORT_BUTTON_VISIBILITY = "F" +private const val SETTINGS_FILE_NAME_TEXT = "G" +private const val SETTINGS_IMPORT_EXPORT_BUTTON_TEXT = "H" +private const val BOOKMARKS_FILE_NAME_LINEARLAYOUT_VISIBILITY = "I" +private const val BOOKMARKS_IMPORT_EXPORT_BUTTON_VISIBILITY = "J" +private const val BOOKMARKS_FILE_NAME_TEXT = "K" +private const val BOOKMARKS_IMPORT_EXPORT_BUTTON_TEXT = "L" class ImportExportActivity : AppCompatActivity() { // Define the class views. + private lateinit var scrollView: ScrollView private lateinit var encryptionSpinner: Spinner private lateinit var encryptionPasswordTextInputLayout: TextInputLayout private lateinit var encryptionPasswordEditText: EditText private lateinit var openKeychainRequiredTextView: TextView - private lateinit var fileLocationCardView: CardView - private lateinit var importRadioButton: RadioButton - private lateinit var fileNameLinearLayout: LinearLayout - private lateinit var fileNameEditText: EditText + private lateinit var settingsFileLocationCardView: CardView + private lateinit var settingsImportRadioButton: RadioButton + private lateinit var settingsFileNameLinearLayout: LinearLayout + private lateinit var settingsFileNameEditText: EditText private lateinit var openKeychainImportInstructionsTextView: TextView - private lateinit var importExportButton: Button + private lateinit var settingsImportExportButton: Button + private lateinit var bookmarksImportRadioButton: RadioButton + private lateinit var bookmarksFileNameLinearLayout: LinearLayout + private lateinit var bookmarksFileNameEditText: EditText + private lateinit var bookmarksImportExportButton: Button // Define the class variables. private lateinit var fileProviderDirectory: File @@ -105,32 +116,70 @@ class ImportExportActivity : AppCompatActivity() { private lateinit var temporaryPgpEncryptedImportFile: File private lateinit var temporaryPreEncryptedExportFile: File - // Define the browse for import activity result launcher. It must be defined before `onCreate()` is run or the app will crash. - private val browseForImportActivityResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { fileUri: Uri? -> + // Define the result launchers. They must be defined before `onCreate()` is run or the app will crash. + private val settingsBrowseForImportActivityResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { fileUri: Uri? -> // Only do something if the user didn't press back from the file picker. if (fileUri != null) { // Get the file name string from the URI. val fileNameString = fileUri.toString() - // Set the file name name text. - fileNameEditText.setText(fileNameString) + // Set the settings file name text. + settingsFileNameEditText.setText(fileNameString) // Move the cursor to the end of the file name edit text. - fileNameEditText.setSelection(fileNameString.length) + settingsFileNameEditText.setSelection(fileNameString.length) } } - private val browseForExportActivityResultLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("*/*")) { fileUri: Uri? -> + private val settingsBrowseForExportActivityResultLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("*/*")) { fileUri: Uri? -> // Only do something if the user didn't press back from the file picker. if (fileUri != null) { // Get the file name string from the URI. val fileNameString = fileUri.toString() - // Set the file name name text. - fileNameEditText.setText(fileNameString) + // Set the settings file name text. + settingsFileNameEditText.setText(fileNameString) // Move the cursor to the end of the file name edit text. - fileNameEditText.setSelection(fileNameString.length) + settingsFileNameEditText.setSelection(fileNameString.length) + } + } + + private val bookmarksBrowseForImportActivityResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { fileUri: Uri? -> + // Only do something if the user didn't press back from the file picker. + if (fileUri != null) { + // Get the file name string from the URI. + val fileNameString = fileUri.toString() + + // Set the bookmarks file name text. + bookmarksFileNameEditText.setText(fileNameString) + + // Move the cursor to the end of the file name edit text. + bookmarksFileNameEditText.setSelection(fileNameString.length) + + // Scroll to the bottom. + scrollView.post { + scrollView.scrollY = scrollView.height + } + } + } + + private val bookmarksBrowseForExportActivityResultLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("*/*")) { fileUri: Uri? -> + // Only do something if the user didn't press back from the file picker. + if (fileUri != null) { + // Get the file name string from the URI. + val fileNameString = fileUri.toString() + + // Set the bookmarks file name text. + bookmarksFileNameEditText.setText(fileNameString) + + // Move the cursor to the end of the file name edit text. + bookmarksFileNameEditText.setSelection(fileNameString.length) + + // Scroll to the bottom. + scrollView.post { + scrollView.scrollY = scrollView.height + } } } @@ -195,17 +244,22 @@ class ImportExportActivity : AppCompatActivity() { } // Get handles for the views. + scrollView = findViewById(R.id.scrollview) encryptionSpinner = findViewById(R.id.encryption_spinner) encryptionPasswordTextInputLayout = findViewById(R.id.encryption_password_textinputlayout) encryptionPasswordEditText = findViewById(R.id.encryption_password_edittext) openKeychainRequiredTextView = findViewById(R.id.openkeychain_required_textview) - fileLocationCardView = findViewById(R.id.file_location_cardview) - importRadioButton = findViewById(R.id.import_radiobutton) - val exportRadioButton = findViewById(R.id.export_radiobutton) - fileNameLinearLayout = findViewById(R.id.file_name_linearlayout) - fileNameEditText = findViewById(R.id.file_name_edittext) + settingsFileLocationCardView = findViewById(R.id.settings_file_location_cardview) + settingsImportRadioButton = findViewById(R.id.settings_import_radiobutton) + val settingsExportRadioButton = findViewById(R.id.settings_export_radiobutton) + settingsFileNameLinearLayout = findViewById(R.id.settings_file_name_linearlayout) + settingsFileNameEditText = findViewById(R.id.settings_file_name_edittext) openKeychainImportInstructionsTextView = findViewById(R.id.openkeychain_import_instructions_textview) - importExportButton = findViewById(R.id.import_export_button) + settingsImportExportButton = findViewById(R.id.settings_import_export_button) + bookmarksImportRadioButton = findViewById(R.id.bookmarks_import_radiobutton) + bookmarksFileNameLinearLayout = findViewById(R.id.bookmarks_file_name_linearlayout) + bookmarksFileNameEditText = findViewById(R.id.bookmarks_file_name_edittext) + bookmarksImportExportButton = findViewById(R.id.bookmarks_import_export_button) // Create an array adapter for the spinner. val encryptionArrayAdapter = ArrayAdapter.createFromResource(this, R.array.encryption_type, R.layout.spinner_item) @@ -218,7 +272,7 @@ class ImportExportActivity : AppCompatActivity() { // Update the UI when the spinner changes. encryptionSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected(parent: AdapterView<*>?, view: View, position: Int, id: Long) { + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { when (position) { NO_ENCRYPTION -> { // Hide the unneeded layout items. @@ -227,21 +281,21 @@ class ImportExportActivity : AppCompatActivity() { openKeychainImportInstructionsTextView.visibility = View.GONE // Show the file location card. - fileLocationCardView.visibility = View.VISIBLE + settingsFileLocationCardView.visibility = View.VISIBLE // Show the file name linear layout if either import or export is checked. - if (importRadioButton.isChecked || exportRadioButton.isChecked) - fileNameLinearLayout.visibility = View.VISIBLE + if (settingsImportRadioButton.isChecked || settingsExportRadioButton.isChecked) + settingsFileNameLinearLayout.visibility = View.VISIBLE // Reset the text of the import button, which may have been changed to `Decrypt`. - if (importRadioButton.isChecked) - importExportButton.setText(R.string.import_button) + if (settingsImportRadioButton.isChecked) + settingsImportExportButton.setText(R.string.import_button) // Clear the file name edit text. - fileNameEditText.text.clear() + settingsFileNameEditText.text.clear() // Disable the import/export button. - importExportButton.isEnabled = false + settingsImportExportButton.isEnabled = false } PASSWORD_ENCRYPTION -> { @@ -253,21 +307,21 @@ class ImportExportActivity : AppCompatActivity() { encryptionPasswordTextInputLayout.visibility = View.VISIBLE // Show the file location card. - fileLocationCardView.visibility = View.VISIBLE + settingsFileLocationCardView.visibility = View.VISIBLE // Show the file name linear layout if either import or export is checked. - if (importRadioButton.isChecked || exportRadioButton.isChecked) - fileNameLinearLayout.visibility = View.VISIBLE + if (settingsImportRadioButton.isChecked || settingsExportRadioButton.isChecked) + settingsFileNameLinearLayout.visibility = View.VISIBLE // Reset the text of the import button, which may have been changed to `Decrypt`. - if (importRadioButton.isChecked) - importExportButton.setText(R.string.import_button) + if (settingsImportRadioButton.isChecked) + settingsImportExportButton.setText(R.string.import_button) // Clear the file name edit text. - fileNameEditText.text.clear() + settingsFileNameEditText.text.clear() // Disable the import/export button. - importExportButton.isEnabled = false + settingsImportExportButton.isEnabled = false } OPENPGP_ENCRYPTION -> { @@ -277,36 +331,36 @@ class ImportExportActivity : AppCompatActivity() { // Updated items based on the installation status of OpenKeychain. if (openKeychainInstalled) { // OpenKeychain is installed. // Show the file location card. - fileLocationCardView.visibility = View.VISIBLE + settingsFileLocationCardView.visibility = View.VISIBLE // Update the layout based on the checked radio button. - if (importRadioButton.isChecked) { + if (settingsImportRadioButton.isChecked) { // Show the file name linear layout and the OpenKeychain import instructions. - fileNameLinearLayout.visibility = View.VISIBLE + settingsFileNameLinearLayout.visibility = View.VISIBLE openKeychainImportInstructionsTextView.visibility = View.VISIBLE // Set the text of the import button to be `Decrypt`. - importExportButton.setText(R.string.decrypt) + settingsImportExportButton.setText(R.string.decrypt) // Clear the file name edit text. - fileNameEditText.text.clear() + settingsFileNameEditText.text.clear() // Disable the import/export button. - importExportButton.isEnabled = false - } else if (exportRadioButton.isChecked) { + settingsImportExportButton.isEnabled = false + } else if (settingsExportRadioButton.isChecked) { // Hide the file name linear layout and the OpenKeychain import instructions. - fileNameLinearLayout.visibility = View.GONE + settingsFileNameLinearLayout.visibility = View.GONE openKeychainImportInstructionsTextView.visibility = View.GONE // Enable the export button. - importExportButton.isEnabled = true + settingsImportExportButton.isEnabled = true } } else { // OpenKeychain is not installed. // Show the OpenPGP required layout item. openKeychainRequiredTextView.visibility = View.VISIBLE // Hide the file location card. - fileLocationCardView.visibility = View.GONE + settingsFileLocationCardView.visibility = View.GONE } } } @@ -327,12 +381,12 @@ class ImportExportActivity : AppCompatActivity() { override fun afterTextChanged(s: Editable) { // Enable the import/export button if both the file string and the password are populated. - importExportButton.isEnabled = fileNameEditText.text.toString().isNotEmpty() && encryptionPasswordEditText.text.toString().isNotEmpty() + settingsImportExportButton.isEnabled = settingsFileNameEditText.text.toString().isNotEmpty() && encryptionPasswordEditText.text.toString().isNotEmpty() } }) - // Update the UI when the file name edit text changes. - fileNameEditText.addTextChangedListener(object : TextWatcher { + // Update the UI when the settings file name edit text changes. + settingsFileNameEditText.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { // Do nothing. } @@ -344,35 +398,57 @@ class ImportExportActivity : AppCompatActivity() { override fun afterTextChanged(s: Editable) { // Adjust the UI according to the encryption spinner position. if (encryptionSpinner.selectedItemPosition == PASSWORD_ENCRYPTION) { - // Enable the import/export button if both the file name and the password are populated. - importExportButton.isEnabled = fileNameEditText.text.toString().isNotEmpty() && encryptionPasswordEditText.text.toString().isNotEmpty() + // Enable the settings import/export button if both the file name and the password are populated. + settingsImportExportButton.isEnabled = settingsFileNameEditText.text.toString().isNotEmpty() && encryptionPasswordEditText.text.toString().isNotEmpty() } else { - // Enable the export button if the file name is populated. - importExportButton.isEnabled = fileNameEditText.text.toString().isNotEmpty() + // Enable the settings import/export button if the file name is populated. + settingsImportExportButton.isEnabled = settingsFileNameEditText.text.toString().isNotEmpty() } } }) + // Update the UI when the bookmarks file name edit text changes. + bookmarksFileNameEditText.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { + // Do nothing. + } + + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + // Do nothing. + } + + override fun afterTextChanged(s: Editable) { + // Enable the bookmarks import/export button if the file name is populated. + bookmarksImportExportButton.isEnabled = bookmarksFileNameEditText.text.toString().isNotEmpty() + } + }) + // Check to see if the activity has been restarted. if (savedInstanceState == null) { // The app has not been restarted. // Initially hide the unneeded views. encryptionPasswordTextInputLayout.visibility = View.GONE openKeychainRequiredTextView.visibility = View.GONE - fileNameLinearLayout.visibility = View.GONE + settingsFileNameLinearLayout.visibility = View.GONE openKeychainImportInstructionsTextView.visibility = View.GONE - importExportButton.visibility = View.GONE + settingsImportExportButton.visibility = View.GONE + bookmarksFileNameLinearLayout.visibility = View.GONE + bookmarksImportExportButton.visibility = View.GONE } else { // The app has been restarted. // Restore the visibility of the views. encryptionPasswordTextInputLayout.visibility = savedInstanceState.getInt(ENCRYPTION_PASSWORD_TEXTINPUTLAYOUT_VISIBILITY) openKeychainRequiredTextView.visibility = savedInstanceState.getInt(OPEN_KEYCHAIN_REQUIRED_TEXTVIEW_VISIBILITY) - fileLocationCardView.visibility = savedInstanceState.getInt(FILE_LOCATION_CARD_VIEW) - fileNameLinearLayout.visibility = savedInstanceState.getInt(FILE_NAME_LINEARLAYOUT_VISIBILITY) + settingsFileLocationCardView.visibility = savedInstanceState.getInt(SETTINGS_FILE_LOCATION_CARDVIEW_VISIBILITY) + settingsFileNameLinearLayout.visibility = savedInstanceState.getInt(SETTINGS_FILE_NAME_LINEARLAYOUT_VISIBILITY) openKeychainImportInstructionsTextView.visibility = savedInstanceState.getInt(OPEN_KEYCHAIN_IMPORT_INSTRUCTIONS_TEXTVIEW_VISIBILITY) - importExportButton.visibility = savedInstanceState.getInt(IMPORT_EXPORT_BUTTON_VISIBILITY) + settingsImportExportButton.visibility = savedInstanceState.getInt(SETTINGS_IMPORT_EXPORT_BUTTON_VISIBILITY) + bookmarksFileNameLinearLayout.visibility = savedInstanceState.getInt(BOOKMARKS_FILE_NAME_LINEARLAYOUT_VISIBILITY) + bookmarksImportExportButton.visibility = savedInstanceState.getInt(BOOKMARKS_IMPORT_EXPORT_BUTTON_VISIBILITY) // Restore the text. - fileNameEditText.post { fileNameEditText.setText(savedInstanceState.getString(FILE_NAME_TEXT)) } - importExportButton.text = savedInstanceState.getString(IMPORT_EXPORT_BUTTON_TEXT) + settingsFileNameEditText.post { settingsFileNameEditText.setText(savedInstanceState.getString(SETTINGS_FILE_NAME_TEXT)) } + settingsImportExportButton.text = savedInstanceState.getString(SETTINGS_IMPORT_EXPORT_BUTTON_TEXT) + bookmarksFileNameEditText.post { bookmarksFileNameEditText.setText(savedInstanceState.getString(BOOKMARKS_FILE_NAME_TEXT)) } + bookmarksImportExportButton.text = savedInstanceState.getString(BOOKMARKS_IMPORT_EXPORT_BUTTON_TEXT) } } @@ -383,98 +459,159 @@ class ImportExportActivity : AppCompatActivity() { // Save the visibility of the views. savedInstanceState.putInt(ENCRYPTION_PASSWORD_TEXTINPUTLAYOUT_VISIBILITY, encryptionPasswordTextInputLayout.visibility) savedInstanceState.putInt(OPEN_KEYCHAIN_REQUIRED_TEXTVIEW_VISIBILITY, openKeychainRequiredTextView.visibility) - savedInstanceState.putInt(FILE_LOCATION_CARD_VIEW, fileLocationCardView.visibility) - savedInstanceState.putInt(FILE_NAME_LINEARLAYOUT_VISIBILITY, fileNameLinearLayout.visibility) + savedInstanceState.putInt(SETTINGS_FILE_LOCATION_CARDVIEW_VISIBILITY, settingsFileLocationCardView.visibility) + savedInstanceState.putInt(SETTINGS_FILE_NAME_LINEARLAYOUT_VISIBILITY, settingsFileNameLinearLayout.visibility) savedInstanceState.putInt(OPEN_KEYCHAIN_IMPORT_INSTRUCTIONS_TEXTVIEW_VISIBILITY, openKeychainImportInstructionsTextView.visibility) - savedInstanceState.putInt(IMPORT_EXPORT_BUTTON_VISIBILITY, importExportButton.visibility) + savedInstanceState.putInt(SETTINGS_IMPORT_EXPORT_BUTTON_VISIBILITY, settingsImportExportButton.visibility) + savedInstanceState.putInt(BOOKMARKS_FILE_NAME_LINEARLAYOUT_VISIBILITY, bookmarksFileNameLinearLayout.visibility) + savedInstanceState.putInt(BOOKMARKS_IMPORT_EXPORT_BUTTON_VISIBILITY, bookmarksImportExportButton.visibility) // Save the text. - savedInstanceState.putString(FILE_NAME_TEXT, fileNameEditText.text.toString()) - savedInstanceState.putString(IMPORT_EXPORT_BUTTON_TEXT, importExportButton.text.toString()) + savedInstanceState.putString(SETTINGS_FILE_NAME_TEXT, settingsFileNameEditText.text.toString()) + savedInstanceState.putString(SETTINGS_IMPORT_EXPORT_BUTTON_TEXT, settingsImportExportButton.text.toString()) + savedInstanceState.putString(BOOKMARKS_FILE_NAME_TEXT, bookmarksFileNameEditText.text.toString()) + savedInstanceState.putString(BOOKMARKS_IMPORT_EXPORT_BUTTON_VISIBILITY, bookmarksImportExportButton.text.toString()) + } + + fun onClickBookmarksRadioButton(view: View) { + // Check to see if import or export was selected. + if (view.id == R.id.bookmarks_import_radiobutton) { // The bookmarks import radio button was selected. + // Set the text on the bookmarks import/export button to be `Import`. + bookmarksImportExportButton.setText(R.string.import_button) + } else { // The bookmarks export radio button was selected. + // Set the text on the bookmarks import/export button to be `Export`. + bookmarksImportExportButton.setText(R.string.export) + } + + // Display the bookmarks views. + bookmarksFileNameLinearLayout.visibility = View.VISIBLE + bookmarksImportExportButton.visibility = View.VISIBLE + + // Clear the bookmarks file name edit text. + bookmarksFileNameEditText.text.clear() + + // Disable the bookmarks import/export button. + bookmarksImportExportButton.isEnabled = false + + // Scroll to the bottom of the screen. + scrollView.post { + scrollView.scrollY = scrollView.height + } } - fun onClickRadioButton(view: View) { + fun onClickSettingsRadioButton(view: View) { // Check to see if import or export was selected. - if (view.id == R.id.import_radiobutton) { // The import radio button is selected. + if (view.id == R.id.settings_import_radiobutton) { // The settings import radio button was selected. // Check to see if OpenPGP encryption is selected. if (encryptionSpinner.selectedItemPosition == OPENPGP_ENCRYPTION) { // OpenPGP encryption selected. // Show the OpenKeychain import instructions. openKeychainImportInstructionsTextView.visibility = View.VISIBLE - // Set the text on the import/export button to be `Decrypt`. - importExportButton.setText(R.string.decrypt) + // Set the text on the settings import/export button to be `Decrypt`. + settingsImportExportButton.setText(R.string.decrypt) } else { // OpenPGP encryption not selected. // Hide the OpenKeychain import instructions. openKeychainImportInstructionsTextView.visibility = View.GONE - // Set the text on the import/export button to be `Import`. - importExportButton.setText(R.string.import_button) + // Set the text on the settings import/export button to be `Import`. + settingsImportExportButton.setText(R.string.import_button) } - // Display the file name views. - fileNameLinearLayout.visibility = View.VISIBLE - importExportButton.visibility = View.VISIBLE + // Display the views. + settingsFileNameLinearLayout.visibility = View.VISIBLE + settingsImportExportButton.visibility = View.VISIBLE - // Clear the file name edit text. - fileNameEditText.text.clear() + // Clear the settings file name edit text. + settingsFileNameEditText.text.clear() - // Disable the import/export button. - importExportButton.isEnabled = false - } else { // The export radio button is selected. + // Disable the settings import/export button. + settingsImportExportButton.isEnabled = false + } else { // The settings export radio button was selected. // Hide the OpenKeychain import instructions. openKeychainImportInstructionsTextView.visibility = View.GONE - // Set the text on the import/export button to be `Export`. - importExportButton.setText(R.string.export) + // Set the text on the settings import/export button to be `Export`. + settingsImportExportButton.setText(R.string.export) - // Show the import/export button. - importExportButton.visibility = View.VISIBLE + // Show the settings import/export button. + settingsImportExportButton.visibility = View.VISIBLE // Check to see if OpenPGP encryption is selected. if (encryptionSpinner.selectedItemPosition == OPENPGP_ENCRYPTION) { // OpenPGP encryption is selected. - // Hide the file name views. - fileNameLinearLayout.visibility = View.GONE + // Hide the settings file name views. + settingsFileNameLinearLayout.visibility = View.GONE - // Enable the export button. - importExportButton.isEnabled = true + // Enable the settings export button. + settingsImportExportButton.isEnabled = true } else { // OpenPGP encryption is not selected. - // Show the file name view. - fileNameLinearLayout.visibility = View.VISIBLE + // Show the settings file name view. + settingsFileNameLinearLayout.visibility = View.VISIBLE - // Clear the file name edit text. - fileNameEditText.text.clear() + // Clear the settings file name edit text. + settingsFileNameEditText.text.clear() - // Disable the import/export button. - importExportButton.isEnabled = false + // Disable the settings import/export button. + settingsImportExportButton.isEnabled = false } } } - fun browse(@Suppress("UNUSED_PARAMETER") view: View) { + fun settingsBrowse(@Suppress("UNUSED_PARAMETER") view: View) { // Check to see if import or export is selected. - if (importRadioButton.isChecked) { // Import is selected. + if (settingsImportRadioButton.isChecked) { // Import is selected. // Open the file picker. - browseForImportActivityResultLauncher.launch("*/*") + settingsBrowseForImportActivityResultLauncher.launch("*/*") } else { // Export is selected // Open the file picker with the export name according to the encryption type. if (encryptionSpinner.selectedItemPosition == NO_ENCRYPTION) // No encryption is selected. - browseForExportActivityResultLauncher.launch(getString(R.string.privacy_browser_settings_pbs, BuildConfig.VERSION_NAME, IMPORT_EXPORT_SCHEMA_VERSION)) + settingsBrowseForExportActivityResultLauncher.launch(getString(R.string.privacy_browser_settings_pbs, BuildConfig.VERSION_NAME, IMPORT_EXPORT_SCHEMA_VERSION)) else // Password encryption is selected. - browseForExportActivityResultLauncher.launch(getString(R.string.privacy_browser_settings_pbs_aes, BuildConfig.VERSION_NAME, IMPORT_EXPORT_SCHEMA_VERSION)) + settingsBrowseForExportActivityResultLauncher.launch(getString(R.string.privacy_browser_settings_pbs_aes, BuildConfig.VERSION_NAME, IMPORT_EXPORT_SCHEMA_VERSION)) + } + } + + fun bookmarksBrowse(@Suppress("UNUSED_PARAMETER") view: View) { + // Check to see if import or export is selected. + if (bookmarksImportRadioButton.isChecked) { // Import is selected. + // Open the file picker. + bookmarksBrowseForImportActivityResultLauncher.launch("*/*") + } else { // Export is selected. + // Open the file picker. + bookmarksBrowseForExportActivityResultLauncher.launch(getString(R.string.privacy_browser_bookmarks_html)) + } + } + + fun importExportBookmarks(@Suppress("UNUSED_PARAMETER") view: View) { + // Instantiate the import/export bookmarks helper. + val importExportBookmarksHelper = ImportExportBookmarksHelper() + + // Get the file name string. + val fileNameString = bookmarksFileNameEditText.text.toString() + + // Check to see if import or export is selected. + if (bookmarksImportRadioButton.isChecked) { // Import is selected. + // Import the bookmarks. + importExportBookmarksHelper.importBookmarks(fileNameString, context = this, scrollView) + + // Repopulate the bookmarks in the main WebView activity. + MainWebViewActivity.restartFromBookmarksActivity = true + } else { // Export is selected. + // Export the bookmarks. + importExportBookmarksHelper.exportBookmarks(fileNameString, context = this, scrollView) } } - fun importExport(@Suppress("UNUSED_PARAMETER") view: View) { - // Instantiate the import export database helper. + fun importExportSettings(@Suppress("UNUSED_PARAMETER") view: View) { + // Instantiate the import/export database helper. val importExportDatabaseHelper = ImportExportDatabaseHelper() // Check to see if import or export is selected. - if (importRadioButton.isChecked) { // Import is selected. + if (settingsImportRadioButton.isChecked) { // Import is selected. // Initialize the import status string var importStatus = "" // Get the file name string. - val fileNameString = fileNameEditText.text.toString() + val fileNameString = settingsFileNameEditText.text.toString() // Import according to the encryption type. when (encryptionSpinner.selectedItemPosition) { @@ -669,18 +806,18 @@ class ImportExportActivity : AppCompatActivity() { // Display a snack bar with the import error if it was unsuccessful. if (importStatus != IMPORT_SUCCESSFUL) - Snackbar.make(fileNameEditText, getString(R.string.import_failed, importStatus), Snackbar.LENGTH_INDEFINITE).show() + Snackbar.make(settingsFileNameEditText, getString(R.string.import_failed, importStatus), Snackbar.LENGTH_INDEFINITE).show() } else { // Export is selected. // Export according to the encryption type. when (encryptionSpinner.selectedItemPosition) { NO_ENCRYPTION -> { // Get the file name string. - val noEncryptionFileNameString = fileNameEditText.text.toString() + val noEncryptionFileNameString = settingsFileNameEditText.text.toString() try { - // Get the export file output stream. + // Get the export file output stream, truncating any existing content. // A file may be opened directly once the minimum API >= 29. - val exportFileOutputStream = contentResolver.openOutputStream(Uri.parse(noEncryptionFileNameString))!! + val exportFileOutputStream = contentResolver.openOutputStream(Uri.parse(noEncryptionFileNameString), "wt")!! // Export the unencrypted file. val noEncryptionExportStatus = importExportDatabaseHelper.exportUnencrypted(exportFileOutputStream, this) @@ -690,12 +827,12 @@ class ImportExportActivity : AppCompatActivity() { // Display an export disposition snackbar. if (noEncryptionExportStatus == EXPORT_SUCCESSFUL) - Snackbar.make(fileNameEditText, getString(R.string.export_successful), Snackbar.LENGTH_SHORT).show() + Snackbar.make(settingsFileNameEditText, getString(R.string.export_successful), Snackbar.LENGTH_SHORT).show() else - Snackbar.make(fileNameEditText, getString(R.string.export_failed, noEncryptionExportStatus), Snackbar.LENGTH_INDEFINITE).show() + Snackbar.make(settingsFileNameEditText, getString(R.string.export_failed, noEncryptionExportStatus), Snackbar.LENGTH_INDEFINITE).show() } catch (fileNotFoundException: FileNotFoundException) { // Display a snackbar with the exception. - Snackbar.make(fileNameEditText, getString(R.string.export_failed, fileNotFoundException), Snackbar.LENGTH_INDEFINITE).show() + Snackbar.make(settingsFileNameEditText, getString(R.string.export_failed, fileNotFoundException), Snackbar.LENGTH_INDEFINITE).show() } } @@ -768,10 +905,10 @@ class ImportExportActivity : AppCompatActivity() { cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmParameterSpec) // Get the file name string. - val passwordEncryptionFileNameString = fileNameEditText.text.toString() + val passwordEncryptionFileNameString = settingsFileNameEditText.text.toString() - // Get the export file output stream. - val exportFileOutputStream = contentResolver.openOutputStream(Uri.parse(passwordEncryptionFileNameString))!! + // Get the export file output stream, truncating any existing content. + val exportFileOutputStream = contentResolver.openOutputStream(Uri.parse(passwordEncryptionFileNameString), "wt")!! // Add the salt and the initialization vector to the export file output stream. exportFileOutputStream.write(saltByteArray) @@ -809,12 +946,12 @@ class ImportExportActivity : AppCompatActivity() { // Display an export disposition snackbar. if (passwordEncryptionExportStatus == EXPORT_SUCCESSFUL) - Snackbar.make(fileNameEditText, getString(R.string.export_successful), Snackbar.LENGTH_SHORT).show() + Snackbar.make(settingsFileNameEditText, getString(R.string.export_successful), Snackbar.LENGTH_SHORT).show() else - Snackbar.make(fileNameEditText, getString(R.string.export_failed, passwordEncryptionExportStatus), Snackbar.LENGTH_INDEFINITE).show() + Snackbar.make(settingsFileNameEditText, getString(R.string.export_failed, passwordEncryptionExportStatus), Snackbar.LENGTH_INDEFINITE).show() } catch (exception: Exception) { // Display a snackbar with the exception. - Snackbar.make(fileNameEditText, getString(R.string.export_failed, exception), Snackbar.LENGTH_INDEFINITE).show() + Snackbar.make(settingsFileNameEditText, getString(R.string.export_failed, exception), Snackbar.LENGTH_INDEFINITE).show() } } @@ -848,7 +985,7 @@ class ImportExportActivity : AppCompatActivity() { // Display an export error snackbar if the temporary pre-encrypted export failed. if (openpgpEncryptionExportStatus != EXPORT_SUCCESSFUL) - Snackbar.make(fileNameEditText, getString(R.string.export_failed, openpgpEncryptionExportStatus), Snackbar.LENGTH_INDEFINITE).show() + Snackbar.make(settingsFileNameEditText, getString(R.string.export_failed, openpgpEncryptionExportStatus), Snackbar.LENGTH_INDEFINITE).show() // Create an encryption intent for OpenKeychain. val openKeychainEncryptIntent = Intent("org.sufficientlysecure.keychain.action.ENCRYPT_DATA") @@ -866,7 +1003,7 @@ class ImportExportActivity : AppCompatActivity() { openKeychainEncryptActivityResultLauncher.launch(openKeychainEncryptIntent) } catch (exception: Exception) { // Display a snackbar with the exception. - Snackbar.make(fileNameEditText, getString(R.string.export_failed, exception), Snackbar.LENGTH_INDEFINITE).show() + Snackbar.make(settingsFileNameEditText, getString(R.string.export_failed, exception), Snackbar.LENGTH_INDEFINITE).show() } } } diff --git a/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.kt b/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.kt index 13ae3b78..8d2a7a07 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.kt +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.kt @@ -2996,7 +2996,9 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook // Scroll to the new tab position if moving to the new tab. if (moveToTab) - tabLayout.post { tabLayout.setScrollPosition(newTabPosition, 0F, false, false) } + tabLayout.post { + tabLayout.setScrollPosition(newTabPosition, 0F, false, false) + } // Show the app bar if it is at the bottom of the screen and the new tab is taking focus. if (bottomAppBar && moveToTab && appBarLayout.translationY != 0f) { @@ -4100,11 +4102,11 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook val databaseId = bookmarksListView.getItemIdAtPosition(i).toInt() // Move the bookmark down one slot. - bookmarksDatabaseHelper!!.updateDisplayOrder(databaseId, i + 1) + bookmarksDatabaseHelper!!.updateDisplayOrder(databaseId, displayOrder = i + 1) } // Create the folder, which will be placed at the top of the list view. - bookmarksDatabaseHelper!!.createFolder(folderNameString, currentBookmarksFolderId, folderIconByteArray) + bookmarksDatabaseHelper!!.createFolder(folderNameString, currentBookmarksFolderId, displayOrder = 0, folderIconByteArray) // Update the bookmarks cursor with the current contents of this folder. bookmarksCursor = bookmarksDatabaseHelper!!.getBookmarksByDisplayOrder(currentBookmarksFolderId) @@ -5901,17 +5903,10 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook // Request focus for the URL text box. urlEditText.requestFocus() - // Create a display keyboard handler. - val displayKeyboardHandler = Handler(Looper.getMainLooper()) - - // Create a display keyboard runnable. - val displayKeyboardRunnable = Runnable { - // Display the keyboard. + // Display the keyboard once the tab layout has settled. + tabLayout.post { inputMethodManager.showSoftInput(urlEditText, 0) } - - // Display the keyboard after 100 milliseconds, which leaves enough time for the tab to transition. - displayKeyboardHandler.postDelayed(displayKeyboardRunnable, 100) } } } @@ -6270,7 +6265,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook urlRelativeLayout.background = AppCompatResources.getDrawable(this, R.color.transparent) } } catch (exception: Exception) { - // Try again in 100 milliseconds if the WebView has not yet been populated. + // Try again in 50 milliseconds if the WebView has not yet been populated. // Create a handler to set the current WebView. val setCurrentWebViewHandler = Handler(Looper.getMainLooper()) diff --git a/app/src/main/java/com/stoutner/privacybrowser/dialogs/EditBookmarkDialog.kt b/app/src/main/java/com/stoutner/privacybrowser/dialogs/EditBookmarkDialog.kt index 4b0b2c27..0a8be3ec 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/dialogs/EditBookmarkDialog.kt +++ b/app/src/main/java/com/stoutner/privacybrowser/dialogs/EditBookmarkDialog.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2023 Soren Stoutner . + * Copyright 2016-2023 Soren Stoutner . * * This file is part of Privacy Browser Android . * @@ -308,4 +308,4 @@ class EditBookmarkDialog : DialogFragment() { // Update the enabled status of the save button. saveButton.isEnabled = iconChanged || nameChanged || urlChanged } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/stoutner/privacybrowser/helpers/BookmarksDatabaseHelper.kt b/app/src/main/java/com/stoutner/privacybrowser/helpers/BookmarksDatabaseHelper.kt index 6241b49f..1d198840 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/helpers/BookmarksDatabaseHelper.kt +++ b/app/src/main/java/com/stoutner/privacybrowser/helpers/BookmarksDatabaseHelper.kt @@ -172,6 +172,16 @@ class BookmarksDatabaseHelper(context: Context) : SQLiteOpenHelper(context, BOOK return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE", null) } + // Get a cursor with just the database IDs of all the bookmarks and folders. This is useful for counting the number of bookmarks imported. + val allBookmarkAndFolderIds: Cursor + get() { + // Get a readable database handle. + val bookmarksDatabase = this.readableDatabase + + // Return a cursor with all the database IDs. The cursor cannot be closed because it is used in the parent activity. + return bookmarksDatabase.rawQuery("SELECT $ID FROM $BOOKMARKS_TABLE", null) + } + // Get a cursor for all bookmarks and folders ordered by display order. val allBookmarksByDisplayOrder: Cursor get() { @@ -218,16 +228,19 @@ class BookmarksDatabaseHelper(context: Context) : SQLiteOpenHelper(context, BOOK } // Create a folder. - fun createFolder(folderName: String, parentFolderId: Long, favoriteIcon: ByteArray) { + fun createFolder(folderName: String, parentFolderId: Long, displayOrder: Int, favoriteIcon: ByteArray): Long { // Create a bookmark folder content values. val bookmarkFolderContentValues = ContentValues() + // Generate the folder ID. + val folderId = generateFolderId() + // The ID is created automatically. Folders are always created at the top of the list. bookmarkFolderContentValues.put(BOOKMARK_NAME, folderName) bookmarkFolderContentValues.put(PARENT_FOLDER_ID, parentFolderId) - bookmarkFolderContentValues.put(DISPLAY_ORDER, 0) + bookmarkFolderContentValues.put(DISPLAY_ORDER, displayOrder) bookmarkFolderContentValues.put(IS_FOLDER, true) - bookmarkFolderContentValues.put(FOLDER_ID, generateFolderId()) + bookmarkFolderContentValues.put(FOLDER_ID, folderId) bookmarkFolderContentValues.put(FAVORITE_ICON, favoriteIcon) // Get a writable database handle. @@ -238,6 +251,9 @@ class BookmarksDatabaseHelper(context: Context) : SQLiteOpenHelper(context, BOOK // Close the database handle. bookmarksDatabase.close() + + // Return the new folder ID. + return folderId } // Delete one bookmark. @@ -309,8 +325,8 @@ class BookmarksDatabaseHelper(context: Context) : SQLiteOpenHelper(context, BOOK return bookmarksDatabase.rawQuery("SELECT * FROM $BOOKMARKS_TABLE WHERE $ID NOT IN ($idsNotToGetStringBuilder)", null) } - // Get a cursor with just database ID of bookmarks and folders in the specified folder. This is useful for deleting folders with bookmarks that have favorite icons too large to fit in a cursor. - fun getBookmarkIds(parentFolderId: Long): Cursor { + // Get a cursor with just the database IDs of bookmarks and folders in the specified folder. This is useful for deleting folders with bookmarks that have favorite icons too large to fit in a cursor. + fun getBookmarkAndFolderIds(parentFolderId: Long): Cursor { // Get a readable database handle. val bookmarksDatabase = this.readableDatabase diff --git a/app/src/main/java/com/stoutner/privacybrowser/helpers/ImportExportBookmarksHelper.kt b/app/src/main/java/com/stoutner/privacybrowser/helpers/ImportExportBookmarksHelper.kt new file mode 100644 index 00000000..92d4c7cc --- /dev/null +++ b/app/src/main/java/com/stoutner/privacybrowser/helpers/ImportExportBookmarksHelper.kt @@ -0,0 +1,307 @@ +/* + * Copyright 2023 Soren Stoutner . + * + * This file is part of Privacy Browser Android . + * + * Privacy Browser Android is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Privacy Browser Android is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Privacy Browser Android. If not, see . + */ + +package com.stoutner.privacybrowser.helpers + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import android.net.Uri +import android.util.Base64 +import android.widget.ScrollView + +import androidx.appcompat.content.res.AppCompatResources + +import com.google.android.material.snackbar.Snackbar +import com.stoutner.privacybrowser.BuildConfig + +import com.stoutner.privacybrowser.R + +import java.io.BufferedReader +import java.io.ByteArrayOutputStream +import java.io.InputStreamReader +import java.lang.Exception +import java.nio.charset.StandardCharsets + +class ImportExportBookmarksHelper { + // Define the class variables. + private var bookmarksAndFolderExported = 0 + + fun importBookmarks(fileNameString: String, context: Context, scrollView: ScrollView) { + try { + // Get an input stream for the file name. + val inputStream = context.contentResolver.openInputStream(Uri.parse(fileNameString))!! + + // Load the bookmarks input stream into a buffered reader. + val bufferedReader = BufferedReader(InputStreamReader(inputStream)) + + // Instantiate the bookmarks database helper. + val bookmarksDatabaseHelper = BookmarksDatabaseHelper(context) + + // Get the default icon drawables. + val defaultFavoriteIconDrawable = AppCompatResources.getDrawable(context, R.drawable.world) + val defaultFolderIconDrawable = AppCompatResources.getDrawable(context, R.drawable.folder_blue_bitmap) + + // Cast the default icon drawables to bitmap drawables. + val defaultFavoriteIconBitmapDrawable = (defaultFavoriteIconDrawable as BitmapDrawable?)!! + val defaultFolderIconBitmapDrawable = (defaultFolderIconDrawable as BitmapDrawable) + + // Get the default icon bitmaps. + val defaultFavoriteIconBitmap = defaultFavoriteIconBitmapDrawable.bitmap + val defaultFolderIconBitmap = defaultFolderIconBitmapDrawable.bitmap + + // Create the default icon byte array output streams. + val defaultFavoriteIconByteArrayOutputStream = ByteArrayOutputStream() + val defaultFolderIconByteArrayOutputStream = ByteArrayOutputStream() + + // Convert the default icon bitmaps to byte array streams. `0` is for lossless compression (the only option for a PNG). + defaultFavoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, defaultFavoriteIconByteArrayOutputStream) + defaultFolderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, defaultFolderIconByteArrayOutputStream) + + // Convert the icon byte array streams to byte array stream to byte arrays. + val defaultFavoriteIconByteArray = defaultFavoriteIconByteArrayOutputStream.toByteArray() + val defaultFolderIconByteArray = defaultFolderIconByteArrayOutputStream.toByteArray() + + // Get a cursor with all the bookmarks. + val initialNumberOfBookmarksAndFoldersCursor = bookmarksDatabaseHelper.allBookmarkAndFolderIds + + // Get an initial count of the folders and bookmarks. + val initialNumberOfFoldersAndBookmarks = initialNumberOfBookmarksAndFoldersCursor.count + + // Close the cursor. + initialNumberOfBookmarksAndFoldersCursor.close() + + // Get a cursor with the contents of the home folder. + val homeFolderContentCursor = bookmarksDatabaseHelper.getBookmarkAndFolderIds(0L) + + // Initialize the variables. + var parentFolderId = 0L + var displayOrder = homeFolderContentCursor.count + + // Close the cursor. + homeFolderContentCursor.close() + + // Parse the bookmarks. + bufferedReader.forEachLine { + // Trim the string. + var line = it.trimStart() + + // Only process interesting lines. + if (line.startsWith("
")) { // This is a bookmark or a folder. + // Remove the initial `
` + line = line.substring(4) + + // Check to see if this is a bookmark or a folder. + if (line.contains("HREF=\"")) { // This is a bookmark. + // Remove the text before the bookmark name. + var bookmarkName = line.substring(line.indexOf(">") + 1) + + // Remove the text after the bookmark name. + bookmarkName = bookmarkName.substring(0, bookmarkName.indexOf("<")) + + // Remove the text before the bookmark URL. + var bookmarkUrl = line.substring(line.indexOf("HREF=\"") + 6) + + // Remove the text after the bookmark URL. + bookmarkUrl = bookmarkUrl.substring(0, bookmarkUrl.indexOf("\"")) + + // Initialize the favorite icon string. + var favoriteIconString = "" + + // Populate the favorite icon string. + if (line.contains("ICON=\"data:image/png;base64,")) { // The `ICON` attribute contains a Base64 encoded icon. + // Remove the text before the icon string. + favoriteIconString = line.substring(line.indexOf("ICON=\"data:image/png;base64,") + 28) + + // Remove the text after the icon string. + favoriteIconString = favoriteIconString.substring(0, favoriteIconString.indexOf("\"")) + } else if (line.contains("ICON_URI=\"data:image/png;base64,")) { // The `ICON_URI` attribute contains a Base64 encoded icon. + // Remove the text before the icon string. + favoriteIconString = line.substring(line.indexOf("ICON_URI=\"data:image/png;base64,") + 32) + + // Remove the text after the icon string. + favoriteIconString = favoriteIconString.substring(0, favoriteIconString.indexOf("\"")) + } + + // Populate the favorite icon byte array. + val favoriteIconByteArray = if (favoriteIconString.isEmpty()) // The favorite icon string is empty. Use the default favorite icon. + defaultFavoriteIconByteArray + else // The favorite icon string is populated. Decode it to a byte array. + Base64.decode(favoriteIconString, Base64.DEFAULT) + + // Add the bookmark. + bookmarksDatabaseHelper.createBookmark(bookmarkName, bookmarkUrl, parentFolderId, displayOrder, favoriteIconByteArray) + + // Increment the display order. + ++displayOrder + } else { // This is a folder. The following lines will be contain in this folder until a `` is encountered. + // Remove the text before the folder name. + var folderName = line.substring(line.indexOf(">") + 1) + + // Remove the text after the folder name. + folderName = folderName.substring(0, folderName.indexOf("<")) + + // Add the folder and set it as the new parent folder ID. + parentFolderId = bookmarksDatabaseHelper.createFolder(folderName, parentFolderId, displayOrder, defaultFolderIconByteArray) + + // Reset the display order. + displayOrder = 0 + } + } else if (line.startsWith("")) { // This is the end of a folder's contents. + // Reset the parent folder id if it isn't 0. + if (parentFolderId != 0L) + parentFolderId = bookmarksDatabaseHelper.getParentFolderId(parentFolderId) + + // Get a cursor with the contents of the new parent folder. + val folderContentCursor = bookmarksDatabaseHelper.getBookmarkAndFolderIds(parentFolderId) + + // Reset the display order. + displayOrder = folderContentCursor.count + + // Close the cursor. + folderContentCursor.close() + } + } + + // Get a cursor with all the bookmarks. + val finalNumberOfBookmarksAndFoldersCursor = bookmarksDatabaseHelper.allBookmarkAndFolderIds + + // Get the final count of the folders and bookmarks. + val finalNumberOfFoldersAndBookmarks = finalNumberOfBookmarksAndFoldersCursor.count + + // Close the cursor. + finalNumberOfBookmarksAndFoldersCursor.close() + + // Close the bookmarks database helper. + bookmarksDatabaseHelper.close() + + // Close the input stream. + inputStream.close() + + // Display a snackbar with the number of folders and bookmarks imported. + Snackbar.make(scrollView, context.getString(R.string.bookmarks_imported, finalNumberOfFoldersAndBookmarks - initialNumberOfFoldersAndBookmarks), Snackbar.LENGTH_LONG).show() + } catch (exception: Exception) { + // Display a snackbar with the error message. + Snackbar.make(scrollView, context.getString(R.string.error, exception), Snackbar.LENGTH_INDEFINITE).show() + } + } + + fun exportBookmarks(fileNameString: String, context: Context, scrollView: ScrollView) { + // Create a bookmarks string builder + val bookmarksStringBuilder = StringBuilder() + + // Populate the headers. + bookmarksStringBuilder.append("") + bookmarksStringBuilder.append("\n\n") + bookmarksStringBuilder.append("") + bookmarksStringBuilder.append("\n\n") + bookmarksStringBuilder.append("") + bookmarksStringBuilder.append("\n\n") + + // Begin the bookmarks. + bookmarksStringBuilder.append("

") + bookmarksStringBuilder.append("\n") + + // Instantiate the bookmarks database helper. + val bookmarksDatabaseHelper = BookmarksDatabaseHelper(context) + + // Initialize the indent string. + val indentString = " " + + // Populate the bookmarks, starting with the home folder. + bookmarksStringBuilder.append(populateBookmarks(bookmarksDatabaseHelper, 0L, indentString)) + + // End the bookmarks. + bookmarksStringBuilder.append("

") + bookmarksStringBuilder.append("\n") + + try { + // Get an output stream for the file name, truncating any existing content. + val outputStream = context.contentResolver.openOutputStream(Uri.parse(fileNameString), "wt")!! + + // Write the bookmarks string to the output stream. + outputStream.write(bookmarksStringBuilder.toString().toByteArray(StandardCharsets.UTF_8)) + + // Close the output stream. + outputStream.close() + + // Display a snackbar with the number of folders and bookmarks exported. + Snackbar.make(scrollView, context.getString(R.string.bookmarks_exported, bookmarksAndFolderExported), Snackbar.LENGTH_LONG).show() + } catch (exception: Exception) { + // Display a snackbar with the error message. + Snackbar.make(scrollView, context.getString(R.string.error, exception), Snackbar.LENGTH_INDEFINITE).show() + } + + // Close the bookmarks database helper. + bookmarksDatabaseHelper.close() + } + + private fun populateBookmarks(bookmarksDatabaseHelper: BookmarksDatabaseHelper, folderId: Long, indentString: String): String { + // Create a bookmarks string builder. + val bookmarksStringBuilder = StringBuilder() + + // Get all the bookmarks in the current folder. + val bookmarksCursor = bookmarksDatabaseHelper.getBookmarks(folderId) + + // Process each bookmark. + while (bookmarksCursor.moveToNext()) { + if (bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(IS_FOLDER)) == 1) { // This is a folder. + // Export the folder. + bookmarksStringBuilder.append(indentString) + bookmarksStringBuilder.append("

") + bookmarksStringBuilder.append(bookmarksCursor.getString(bookmarksCursor.getColumnIndexOrThrow(BOOKMARK_NAME))) + bookmarksStringBuilder.append("

") + bookmarksStringBuilder.append("\n") + + // Begin the folder contents. + bookmarksStringBuilder.append(indentString) + bookmarksStringBuilder.append("

") + bookmarksStringBuilder.append("\n") + + // Populate the folder contents. + bookmarksStringBuilder.append(populateBookmarks(bookmarksDatabaseHelper, bookmarksCursor.getLong(bookmarksCursor.getColumnIndexOrThrow(FOLDER_ID)), " $indentString")) + + // End the folder contents. + bookmarksStringBuilder.append(indentString) + bookmarksStringBuilder.append("

") + bookmarksStringBuilder.append("\n") + + // Increment the bookmarks and folders exported counter. + ++bookmarksAndFolderExported + } else { // This is a bookmark. + // Export the bookmark. + bookmarksStringBuilder.append(indentString) + bookmarksStringBuilder.append("

") + bookmarksStringBuilder.append(bookmarksCursor.getString(bookmarksCursor.getColumnIndexOrThrow(BOOKMARK_NAME))) + bookmarksStringBuilder.append("") + bookmarksStringBuilder.append("\n") + + // Increment the bookmarks and folders exported counter. + ++bookmarksAndFolderExported + } + } + + // Return the bookmarks string. + return bookmarksStringBuilder.toString() + } +} diff --git a/app/src/main/res/layout/import_export_bottom_appbar.xml b/app/src/main/res/layout/import_export_bottom_appbar.xml index 2c5759c9..f72fe661 100644 --- a/app/src/main/res/layout/import_export_bottom_appbar.xml +++ b/app/src/main/res/layout/import_export_bottom_appbar.xml @@ -1,7 +1,7 @@ + + android:orientation="vertical" > - + + android:orientation="vertical" + android:layout_marginTop="10dp" > + + + + + + android:layout_marginEnd="10dp" + android:layout_marginBottom="5dp" > - + + android:onClick="onClickSettingsRadioButton" /> + android:onClick="onClickSettingsRadioButton" />