Merge branch 'feature/RecyclerBin' into develop

This commit is contained in:
J-Jamet
2019-11-14 15:57:09 +01:00
14 changed files with 260 additions and 49 deletions

View File

@@ -42,10 +42,7 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.FragmentManager
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
import com.kunzisoft.keepass.activities.dialogs.ReadOnlyDialog
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
import com.kunzisoft.keepass.activities.dialogs.*
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.lock.LockingActivity
@@ -77,6 +74,7 @@ class GroupActivity : LockingActivity(),
IconPickerDialogFragment.IconPickerListener,
ListNodesFragment.NodeClickListener,
ListNodesFragment.NodesActionMenuListener,
DeleteNodesDialogFragment.DeleteNodeListener,
ListNodesFragment.OnScrollListener,
SortDialogFragment.SortSelectionListener {
@@ -234,29 +232,28 @@ class GroupActivity : LockingActivity(),
ACTION_DATABASE_DELETE_NODES_TASK -> {
if (result.isSuccess) {
// Rebuild all the list the avoid bug when delete node from db sort
if (PreferencesUtil.getListSort(this@GroupActivity) == SortNodeEnum.DB) {
// Rebuild all the list the avoid bug when delete node from sort
mListNodesFragment?.rebuildList()
} else {
// Use the old Nodes / entries unchanged with the old parent
mListNodesFragment?.removeNodes(oldNodes)
}
// Add trash in views list if it doesn't exists
if (database.isRecycleBinEnabled) {
val recycleBin = database.recycleBin
if (mCurrentGroup != null && recycleBin != null
&& mCurrentGroup!!.parent == null
&& mCurrentGroup != recycleBin) {
if (mListNodesFragment?.contains(recycleBin) == true)
val currentGroup = mCurrentGroup
if (currentGroup != null && recycleBin != null
&& currentGroup != recycleBin) {
// Recycle bin already here, simply update it
if (mListNodesFragment?.contains(recycleBin) == true) {
mListNodesFragment?.updateNode(recycleBin)
else
}
// Recycle bin not here, verify if parents are similar to add it
else if (currentGroup.parent == recycleBin.parent) {
mListNodesFragment?.addNode(recycleBin)
}
}
}
}
}
}
if (!result.isSuccess) {
result.exception?.errorId?.let { errorId ->
@@ -596,14 +593,31 @@ class GroupActivity : LockingActivity(),
}
override fun onDeleteMenuClick(nodes: List<NodeVersioned>): Boolean {
val database = mDatabase
if (database != null
&& database.isRecycleBinEnabled
&& database.recycleBin != mCurrentGroup) {
// If recycle bin enabled and not in recycle bin, move in recycle bin
progressDialogThread?.startDatabaseDeleteNodes(
nodes,
!mReadOnly
)
} else {
// open the dialog to confirm deletion
DeleteNodesDialogFragment.getInstance(nodes)
.show(supportFragmentManager, "deleteNodesDialogFragment")
}
finishNodeAction()
return true
}
override fun permanentlyDeleteNodes(nodes: List<NodeVersioned>) {
progressDialogThread?.startDatabaseDeleteNodes(
nodes,
!mReadOnly
)
}
override fun onResume() {
super.onResume()
// Refresh the elements
@@ -632,6 +646,12 @@ class GroupActivity : LockingActivity(),
MenuUtil.contributionMenuInflater(inflater, menu)
}
// Menu for recycle bin
if (mDatabase?.isRecycleBinEnabled == true
&& mDatabase?.recycleBin == mCurrentGroup) {
inflater.inflate(R.menu.recycle_bin, menu)
}
// Get the SearchView and set the searchable configuration
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
@@ -740,6 +760,13 @@ class GroupActivity : LockingActivity(),
lockAndExit()
return true
}
R.id.menu_empty_recycle_bin -> {
mCurrentGroup?.getChildren()?.let { listChildren ->
// Automatically delete all elements
onDeleteMenuClick(listChildren)
}
return true
}
else -> {
// Check the time lock before launching settings
MenuUtil.onDefaultMenuOptionsItemSelected(this, item, mReadOnly, true)

View File

@@ -228,8 +228,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
R.id.menu_sort -> {
context?.let { context ->
val sortDialogFragment: SortDialogFragment =
if (Database.getInstance().allowRecycleBin
&& Database.getInstance().isRecycleBinEnabled) {
if (Database.getInstance().isRecycleBinEnabled) {
SortDialogFragment.getInstance(
PreferencesUtil.getListSort(context),
PreferencesUtil.getAscendingSort(context),
@@ -276,7 +275,8 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
// Open and Edit for a single item
if (nodes.size == 1) {
// Edition
if (readOnly || nodes[0] == database.recycleBin) {
if (readOnly
|| (database.isRecycleBinEnabled && nodes[0] == database.recycleBin)) {
menu?.removeItem(R.id.menu_edit)
}
} else {
@@ -287,7 +287,6 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
// Copy and Move (not for groups)
if (readOnly
|| isASearchResult
|| nodes.any { it == database.recycleBin }
|| nodes.any { it.type == Type.GROUP }) {
// TODO COPY For Group
menu?.removeItem(R.id.menu_copy)
@@ -295,7 +294,8 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
}
// Deletion
if (readOnly || nodes.any { it == database.recycleBin }) {
if (readOnly
|| (database.isRecycleBinEnabled && nodes.any { it == database.recycleBin })) {
menu?.removeItem(R.id.menu_delete)
}
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.NodeVersioned
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
class DeleteNodesDialogFragment : DialogFragment() {
private var mNodesToDelete: List<NodeVersioned> = ArrayList()
private var mListener: DeleteNodeListener? = null
override fun onAttach(context: Context) {
super.onAttach(context)
try {
mListener = context as DeleteNodeListener
} catch (e: ClassCastException) {
throw ClassCastException(context.toString()
+ " must implement " + DeleteNodeListener::class.java.name)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
arguments?.apply {
if (containsKey(DatabaseTaskNotificationService.GROUPS_ID_KEY)
&& containsKey(DatabaseTaskNotificationService.ENTRIES_ID_KEY)) {
mNodesToDelete = getListNodesFromBundle(Database.getInstance(), this)
}
} ?: savedInstanceState?.apply {
if (containsKey(DatabaseTaskNotificationService.GROUPS_ID_KEY)
&& containsKey(DatabaseTaskNotificationService.ENTRIES_ID_KEY)) {
mNodesToDelete = getListNodesFromBundle(Database.getInstance(), savedInstanceState)
}
}
activity?.let { activity ->
// Use the Builder class for convenient dialog construction
val builder = AlertDialog.Builder(activity)
builder.setMessage(getString(R.string.warning_permanently_delete_nodes))
builder.setPositiveButton(android.R.string.yes) { _, _ ->
mListener?.permanentlyDeleteNodes(mNodesToDelete)
}
builder.setNegativeButton(android.R.string.no) { _, _ -> dismiss() }
// Create the AlertDialog object and return it
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putAll(getBundleFromListNodes(mNodesToDelete))
}
interface DeleteNodeListener {
fun permanentlyDeleteNodes(nodes: List<NodeVersioned>)
}
companion object {
fun getInstance(nodesToDelete: List<NodeVersioned>): DeleteNodesDialogFragment {
return DeleteNodesDialogFragment().apply {
arguments = getBundleFromListNodes(nodesToDelete)
}
}
}
}

View File

@@ -59,13 +59,13 @@ enum class SortNodeEnum {
if (object1.type == Type.GROUP) {
return if (object2.type == Type.GROUP) {
// RecycleBin at end of groups
if (recycleBinBottom) {
if (Database.getInstance().recycleBin == object1)
val database = Database.getInstance()
if (database.isRecycleBinEnabled && recycleBinBottom) {
if (database.recycleBin == object1)
return 1
if (Database.getInstance().recycleBin == object2)
if (database.recycleBin == object2)
return -1
}
specificOrderOrHashIfEquals(object1, object2)
} else if (object2.type == Type.ENTRY) {
if (groupsBefore)

View File

@@ -20,6 +20,7 @@ import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Compa
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_COLOR_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_COMPRESSION_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_DEFAULT_USERNAME_TASK
@@ -461,4 +462,14 @@ class ProgressDialogThread(private val activity: FragmentActivity,
}
, ACTION_DATABASE_SAVE_PARALLELISM_TASK)
}
/**
* Save Database without parameter
*/
fun startDatabaseSave(save: Boolean) {
start(Bundle().apply {
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_SAVE)
}
}

View File

@@ -24,7 +24,6 @@ import android.content.res.Resources
import android.database.Cursor
import android.net.Uri
import android.util.Log
import android.webkit.URLUtil
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
import com.kunzisoft.keepass.database.NodeHandler
import com.kunzisoft.keepass.database.cursor.EntryCursorV3
@@ -253,8 +252,11 @@ class Database {
val allowRecycleBin: Boolean
get() = pwDatabaseV4 != null
val isRecycleBinEnabled: Boolean
var isRecycleBinEnabled: Boolean
get() = pwDatabaseV4?.isRecycleBinEnabled ?: false
set(value) {
pwDatabaseV4?.isRecycleBinEnabled = value
}
val recycleBin: GroupVersioned?
get() {
@@ -264,6 +266,14 @@ class Database {
return null
}
fun ensureRecycleBinExists(resources: Resources) {
pwDatabaseV4?.ensureRecycleBinExists(resources)
}
fun removeRecycleBin() {
pwDatabaseV4?.removeRecycleBin()
}
private fun setDatabaseV3(pwDatabaseV3: PwDatabaseV3) {
this.pwDatabaseV3 = pwDatabaseV3
this.pwDatabaseV4 = null

View File

@@ -209,7 +209,7 @@ class PwDatabaseV4 : PwDatabase<UUID, UUID, PwGroupV4, PwEntryV4> {
// Retrieve recycle bin in index
val recycleBin: PwGroupV4?
get() = getGroupByUUID(recycleBinUUID)
get() = if (recycleBinUUID == UUID_ZERO) null else getGroupByUUID(recycleBinUUID)
val lastSelectedGroup: PwGroupV4?
get() = getGroupByUUID(lastSelectedGroupUUID)
@@ -397,7 +397,7 @@ class PwDatabaseV4 : PwDatabase<UUID, UUID, PwGroupV4, PwEntryV4> {
* Ensure that the recycle bin tree exists, if enabled and create it
* if it doesn't exist
*/
private fun ensureRecycleBin(resources: Resources) {
fun ensureRecycleBinExists(resources: Resources) {
if (recycleBin == null) {
// Create recycle bin
val recycleBinGroup = createGroup().apply {
@@ -413,6 +413,13 @@ class PwDatabaseV4 : PwDatabase<UUID, UUID, PwGroupV4, PwEntryV4> {
}
}
fun removeRecycleBin() {
if (recycleBin != null) {
recycleBinUUID = UUID_ZERO
recycleBinChanged = PwDate().date
}
}
/**
* Define if a Node must be delete or recycle when remove action is called
* @param node Node to remove
@@ -422,21 +429,21 @@ class PwDatabaseV4 : PwDatabase<UUID, UUID, PwGroupV4, PwEntryV4> {
if (!isRecycleBinEnabled)
return false
if (recycleBin == null)
return true
return false
if (!node.isContainedIn(recycleBin!!))
return true
return false
}
fun recycle(group: PwGroupV4, resources: Resources) {
ensureRecycleBin(resources)
ensureRecycleBinExists(resources)
removeGroupFrom(group, group.parent)
addGroupTo(group, recycleBin)
group.afterAssignNewParent()
}
fun recycle(entry: PwEntryV4, resources: Resources) {
ensureRecycleBin(resources)
ensureRecycleBinExists(resources)
removeEntryFrom(entry, entry.parent)
addEntryTo(entry, recycleBin)
entry.afterAssignNewParent()

View File

@@ -108,7 +108,7 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
ACTION_DATABASE_SAVE_MEMORY_USAGE_TASK,
ACTION_DATABASE_SAVE_PARALLELISM_TASK,
ACTION_DATABASE_SAVE_ITERATIONS_TASK -> buildDatabaseSaveElementActionTask(intent)
else -> null
else -> buildDatabaseSave(intent)
}
actionRunnable?.let { actionRunnableNotNull ->
@@ -412,6 +412,19 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
}
}
/**
* Save database without parameter
*/
private fun buildDatabaseSave(intent: Intent): ActionRunnable? {
return if (intent.hasExtra(SAVE_DATABASE_KEY)) {
SaveDatabaseRunnable(this,
Database.getInstance(),
intent.getBooleanExtra(SAVE_DATABASE_KEY, false))
} else {
null
}
}
private class ActionRunnableAsyncTask(private val progressTaskUpdater: ProgressTaskUpdater,
private val onPreExecute: () -> Unit,
private val onPostExecute: (result: ActionRunnable.Result) -> Unit)
@@ -471,6 +484,7 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
const val ACTION_DATABASE_SAVE_MEMORY_USAGE_TASK = "ACTION_DATABASE_SAVE_MEMORY_USAGE_TASK"
const val ACTION_DATABASE_SAVE_PARALLELISM_TASK = "ACTION_DATABASE_SAVE_PARALLELISM_TASK"
const val ACTION_DATABASE_SAVE_ITERATIONS_TASK = "ACTION_DATABASE_SAVE_ITERATIONS_TASK"
const val ACTION_DATABASE_SAVE = "ACTION_DATABASE_SAVE"
const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
const val MASTER_PASSWORD_CHECKED_KEY = "MASTER_PASSWORD_CHECKED_KEY"
@@ -509,23 +523,23 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
}
fun getBundleFromListNodes(nodes: List<NodeVersioned>): Bundle {
val groupsIdToCopy = ArrayList<PwNodeId<*>>()
val entriesIdToCopy = ArrayList<PwNodeId<UUID>>()
val groupsId = ArrayList<PwNodeId<*>>()
val entriesId = ArrayList<PwNodeId<UUID>>()
nodes.forEach { nodeVersioned ->
when (nodeVersioned.type) {
Type.GROUP -> {
(nodeVersioned as GroupVersioned).nodeId?.let { groupId ->
groupsIdToCopy.add(groupId)
groupsId.add(groupId)
}
}
Type.ENTRY -> {
entriesIdToCopy.add((nodeVersioned as EntryVersioned).nodeId)
entriesId.add((nodeVersioned as EntryVersioned).nodeId)
}
}
}
return Bundle().apply {
putParcelableArrayList(GROUPS_ID_KEY, groupsIdToCopy)
putParcelableArrayList(ENTRIES_ID_KEY, entriesIdToCopy)
putParcelableArrayList(GROUPS_ID_KEY, groupsId)
putParcelableArrayList(ENTRIES_ID_KEY, entriesId)
}
}
}

View File

@@ -85,6 +85,7 @@ class NestedSettingsFragment : PreferenceFragmentCompat() {
private var dbDefaultUsername: InputTextPreference? = null
private var dbCustomColorPref: DialogColorPreference? = null
private var dbDataCompressionPref: Preference? = null
private var recycleBinGroupPref: Preference? = null
private var dbMaxHistoryItemsPref: InputNumberPreference? = null
private var dbMaxHistorySizePref: InputNumberPreference? = null
private var mEncryptionAlgorithmPref: DialogListExplanationPreference? = null
@@ -452,17 +453,35 @@ class NestedSettingsFragment : PreferenceFragmentCompat() {
}
val dbRecycleBinPrefCategory: PreferenceCategory? = findPreference(getString(R.string.database_category_recycle_bin_key))
recycleBinGroupPref = findPreference(getString(R.string.recycle_bin_group_key))
// Recycle bin
val recycleBinPref: SwitchPreference? = findPreference(getString(R.string.recycle_bin_key))
if (mDatabase.allowRecycleBin) {
recycleBinPref?.isChecked = mDatabase.isRecycleBinEnabled
// TODO Recycle Bin
recycleBinPref?.isEnabled = false
val recycleBinEnablePref: SwitchPreference? = findPreference(getString(R.string.recycle_bin_enable_key))
recycleBinEnablePref?.apply {
isChecked = mDatabase.isRecycleBinEnabled
setOnPreferenceChangeListener { _, newValue ->
val recycleBinEnabled = newValue as Boolean
mDatabase.isRecycleBinEnabled = recycleBinEnabled
if (recycleBinEnabled) {
mDatabase.ensureRecycleBinExists(resources)
} else {
mDatabase.removeRecycleBin()
}
refreshRecycleBinGroup()
// Save the database
// TODO manual save
(context as SettingsActivity?)?.progressDialogThread?.startDatabaseSave(true)
true
}
}
// Recycle Bin group
refreshRecycleBinGroup()
} else {
dbRecycleBinPrefCategory?.isVisible = false
}
// History
findPreference<PreferenceCategory>(getString(R.string.database_category_history_key))
?.isVisible = mDatabase.manageHistory == true
@@ -481,6 +500,18 @@ class NestedSettingsFragment : PreferenceFragmentCompat() {
}
}
private fun refreshRecycleBinGroup() {
recycleBinGroupPref?.apply {
if (mDatabase.isRecycleBinEnabled) {
summary = mDatabase.recycleBin?.title
isEnabled = true
} else {
summary = null
isEnabled = false
}
}
}
private fun onCreateDatabaseSecurityPreference(rootKey: String?) {
setPreferencesFromResource(R.xml.preferences_database_security, rootKey)

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_empty_recycle_bin"
android:icon="@drawable/ic_content_delete_white_24dp"
android:title="@string/menu_empty_recycle_bin"
android:orderInCategory="35"
app:showAsAction="ifRoom" />
</menu>

View File

@@ -220,7 +220,7 @@
<string name="full_file_path_enable_title">Chemin de fichier</string>
<string name="full_file_path_enable_summary">Afficher le chemin complet du fichier</string>
<string name="recycle_bin_title">Utiliser la corbeille</string>
<string name="recycle_bin_summary">Déplace les groupes et les entrées dans la « Corbeille » avant suppression</string>
<string name="recycle_bin_summary">Déplace les groupes et les entrées dans le groupe \"Corbeille\" avant suppression</string>
<string name="monospace_font_fields_enable_title">Police de champ</string>
<string name="monospace_font_fields_enable_summary">Changer la police utilisée dans les champs pour une meilleure visibilité des caractères</string>
<string name="allow_copy_password_title">Confiance dans le presse-papier</string>

View File

@@ -160,7 +160,8 @@
<string name="database_data_compression_key" translatable="false">database_data_compression_key</string>
<string name="database_category_recycle_bin_key" translatable="false">database_category_recycle_bin_key</string>
<string name="recycle_bin_key" translatable="false">recycle_bin_key</string>
<string name="recycle_bin_enable_key" translatable="false">recycle_bin_enable_key</string>
<string name="recycle_bin_group_key" translatable="false">recycle_bin_group_key</string>
<string name="database_category_history_key" translatable="false">database_category_history_key</string>
<string name="max_history_items_key" translatable="false">max_history_items_key</string>

View File

@@ -183,6 +183,7 @@
<string name="menu_url">Go to URL</string>
<string name="menu_file_selection_read_only">Write-protected</string>
<string name="menu_open_file_read_and_write">Modifiable</string>
<string name="menu_empty_recycle_bin">Empty Recycle Bin</string>
<string name="minus">Minus</string>
<string name="never">Never</string>
<string name="no_results">No search results</string>
@@ -242,6 +243,7 @@
<string name="warning_unmounted">Mount the SD card to create or load a database.</string>
<string name="warning_empty_password">Do you really want no password unlocking protection?</string>
<string name="warning_no_encryption_key">Are you sure you do not want to use any encryption key?</string>
<string name="warning_permanently_delete_nodes">Are you sure you want to permanently delete the selected nodes?</string>
<string name="version_label">Version %1$s</string>
<string name="build_label">Build %1$s</string>
<string name="configure_biometric">Biometric prompt is supported but not set up.</string>
@@ -304,7 +306,8 @@
<string name="database_data_compression_title">Data compression</string>
<string name="database_data_compression_summary">Data compression reduces the size of the database.</string>
<string name="recycle_bin_title">Use recycle bin</string>
<string name="recycle_bin_summary">Moves groups and entries to \"Recycle bin\" before deleting</string>
<string name="recycle_bin_summary">Moves groups and entries to \"Recycle bin\" group before deleting</string>
<string name="recycle_bin_group_title">Recycle bin group</string>
<string name="max_history_items_title">Max. history items</string>
<string name="max_history_items_summary">Limit number of history items per entry</string>
<string name="max_history_size_title">Max. history size</string>

View File

@@ -74,11 +74,15 @@
android:title="@string/recycle_bin">
<SwitchPreference
android:key="@string/recycle_bin_key"
android:key="@string/recycle_bin_enable_key"
android:persistent="false"
android:title="@string/recycle_bin_title"
android:summary="@string/recycle_bin_summary"
android:checked="false"/>
<Preference
android:key="@string/recycle_bin_group_key"
android:persistent="false"
android:title="@string/recycle_bin_group_title"/>
</PreferenceCategory>