Merge pull request #264 from feature/168-sort-vault-list

Feature/168 sort vault list
This commit is contained in:
Julian Raufelder 2021-02-12 18:52:51 +01:00 committed by GitHub
commit 5b1d7cfefb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 461 additions and 24 deletions

View File

@ -74,7 +74,7 @@ android {
} }
greendao { greendao {
schemaVersion 3 schemaVersion 4
} }
configurations.all { configurations.all {

View File

@ -21,12 +21,14 @@ class DatabaseUpgrades {
public DatabaseUpgrades( // public DatabaseUpgrades( //
Upgrade0To1 upgrade0To1, // Upgrade0To1 upgrade0To1, //
Upgrade1To2 upgrade1To2, // Upgrade1To2 upgrade1To2, //
Upgrade2To3 upgrade2To3) { Upgrade2To3 upgrade2To3, //
Upgrade3To4 upgrade3To4) {
availableUpgrades = defineUpgrades( // availableUpgrades = defineUpgrades( //
upgrade0To1, // upgrade0To1, //
upgrade1To2, // upgrade1To2, //
upgrade2To3); upgrade2To3, //
upgrade3To4);
} }
private Map<Integer, List<DatabaseUpgrade>> defineUpgrades(DatabaseUpgrade... upgrades) { private Map<Integer, List<DatabaseUpgrade>> defineUpgrades(DatabaseUpgrade... upgrades) {

View File

@ -0,0 +1,66 @@
package org.cryptomator.data.db
import org.cryptomator.data.db.Sql.SqlCreateTableBuilder.ForeignKeyBehaviour
import org.cryptomator.data.db.entities.CloudEntityDao
import org.cryptomator.data.db.entities.VaultEntityDao
import org.greenrobot.greendao.database.Database
import org.greenrobot.greendao.internal.DaoConfig
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
internal class Upgrade3To4 @Inject constructor() : DatabaseUpgrade(3, 4) {
override fun internalApplyTo(db: Database, origin: Int) {
db.beginTransaction()
try {
addPositionToVaultSchema(db)
initVaultPositionUsingCurrentSortOrder(db)
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
}
private fun addPositionToVaultSchema(db: Database) {
Sql.alterTable("VAULT_ENTITY").renameTo("VAULT_ENTITY_OLD").executeOn(db)
Sql.createTable("VAULT_ENTITY") //
.id() //
.optionalInt("FOLDER_CLOUD_ID") //
.optionalText("FOLDER_PATH") //
.optionalText("FOLDER_NAME") //
.requiredText("CLOUD_TYPE") //
.optionalText("PASSWORD") //
.optionalInt("POSITION") //
.foreignKey("FOLDER_CLOUD_ID", "CLOUD_ENTITY", ForeignKeyBehaviour.ON_DELETE_SET_NULL) //
.executeOn(db)
Sql.insertInto("VAULT_ENTITY") //
.select("_id", "FOLDER_CLOUD_ID", "FOLDER_PATH", "FOLDER_NAME", "PASSWORD", "CLOUD_ENTITY.TYPE") //
.columns("_id", "FOLDER_CLOUD_ID", "FOLDER_PATH", "FOLDER_NAME", "PASSWORD", "CLOUD_TYPE") //
.from("VAULT_ENTITY_OLD") //
.join("CLOUD_ENTITY", "VAULT_ENTITY_OLD.FOLDER_CLOUD_ID") //
.executeOn(db)
Sql.dropIndex("IDX_VAULT_ENTITY_FOLDER_PATH_FOLDER_CLOUD_ID").executeOn(db)
Sql.createUniqueIndex("IDX_VAULT_ENTITY_FOLDER_PATH_FOLDER_CLOUD_ID") //
.on("VAULT_ENTITY") //
.asc("FOLDER_PATH") //
.asc("FOLDER_CLOUD_ID") //
.executeOn(db)
Sql.dropTable("VAULT_ENTITY_OLD").executeOn(db)
}
private fun initVaultPositionUsingCurrentSortOrder(db: Database) {
CloudEntityDao(DaoConfig(db, VaultEntityDao::class.java)) //
.loadAll() //
.map {
Sql.update("VAULT_ENTITY") //
.where("_id", Sql.eq(it.id)) //
.set("POSITION", Sql.toInteger(it.id - 1)) //
.executeOn(db)
}
}
}

View File

@ -28,6 +28,8 @@ public class VaultEntity extends DatabaseEntity {
private String password; private String password;
private Integer position;
/** /**
* Convenient call for {@link org.greenrobot.greendao.AbstractDao#refresh(Object)}. * Convenient call for {@link org.greenrobot.greendao.AbstractDao#refresh(Object)}.
* Entity must attached to an entity context. * Entity must attached to an entity context.
@ -152,6 +154,14 @@ public class VaultEntity extends DatabaseEntity {
this.password = password; this.password = password;
} }
public Integer getPosition() {
return this.position;
}
public void setPosition(Integer position) {
this.position = position;
}
/** called by internal mechanisms, do not call yourself. */ /** called by internal mechanisms, do not call yourself. */
@Generated(hash = 674742652) @Generated(hash = 674742652)
public void __setDaoSession(DaoSession daoSession) { public void __setDaoSession(DaoSession daoSession) {
@ -159,14 +169,16 @@ public class VaultEntity extends DatabaseEntity {
myDao = daoSession != null ? daoSession.getVaultEntityDao() : null; myDao = daoSession != null ? daoSession.getVaultEntityDao() : null;
} }
@Generated(hash = 1196809909) @Generated(hash = 825602374)
public VaultEntity(Long id, Long folderCloudId, String folderPath, String folderName, @NotNull String cloudType, String password) { public VaultEntity(Long id, Long folderCloudId, String folderPath, String folderName, @NotNull String cloudType, String password,
Integer position) {
this.id = id; this.id = id;
this.folderCloudId = folderCloudId; this.folderCloudId = folderCloudId;
this.folderPath = folderPath; this.folderPath = folderPath;
this.folderName = folderName; this.folderName = folderName;
this.cloudType = cloudType; this.cloudType = cloudType;
this.password = password; this.password = password;
this.position = position;
} }
@Generated(hash = 691253864) @Generated(hash = 691253864)

View File

@ -30,6 +30,7 @@ public class VaultEntityMapper extends EntityMapper<VaultEntity, Vault> {
.withCloud(cloudFrom(entity)) // .withCloud(cloudFrom(entity)) //
.withCloudType(CloudType.valueOf(entity.getCloudType())) // .withCloudType(CloudType.valueOf(entity.getCloudType())) //
.withSavedPassword(entity.getPassword()) // .withSavedPassword(entity.getPassword()) //
.withPosition(entity.getPosition()) //
.build(); .build();
} }
@ -51,6 +52,7 @@ public class VaultEntityMapper extends EntityMapper<VaultEntity, Vault> {
} }
entity.setCloudType(domainObject.getCloudType().name()); entity.setCloudType(domainObject.getCloudType().name());
entity.setPassword(domainObject.getPassword()); entity.setPassword(domainObject.getPassword());
entity.setPosition(domainObject.getPosition());
return entity; return entity;
} }
} }

View File

@ -19,7 +19,8 @@ public class Vault implements Serializable {
.withPath(vault.getPath()) // .withPath(vault.getPath()) //
.withUnlocked(vault.isUnlocked()) // .withUnlocked(vault.isUnlocked()) //
.withSavedPassword(vault.getPassword()) // .withSavedPassword(vault.getPassword()) //
.withVersion(vault.getVersion()); .withVersion(vault.getVersion()) //
.withPosition(vault.getPosition());
} }
private final Long id; private final Long id;
@ -30,6 +31,7 @@ public class Vault implements Serializable {
private final boolean unlocked; private final boolean unlocked;
private final String password; private final String password;
private final int version; private final int version;
private final int position;
private Vault(Builder builder) { private Vault(Builder builder) {
this.id = builder.id; this.id = builder.id;
@ -40,6 +42,7 @@ public class Vault implements Serializable {
this.cloudType = builder.cloudType; this.cloudType = builder.cloudType;
this.password = builder.password; this.password = builder.password;
this.version = builder.version; this.version = builder.version;
this.position = builder.position;
} }
public Long getId() { public Long getId() {
@ -74,6 +77,10 @@ public class Vault implements Serializable {
return version; return version;
} }
public int getPosition() {
return position;
}
public static class Builder { public static class Builder {
private Long id = NOT_SET; private Long id = NOT_SET;
@ -84,6 +91,7 @@ public class Vault implements Serializable {
private boolean unlocked; private boolean unlocked;
private String password; private String password;
private int version = -1; private int version = -1;
private int position = -1;
private Builder() { private Builder() {
} }
@ -154,6 +162,11 @@ public class Vault implements Serializable {
return this; return this;
} }
public Builder withPosition(int position) {
this.position = position;
return this;
}
public Vault build() { public Vault build() {
validate(); validate();
return new Vault(this); return new Vault(this);
@ -172,6 +185,9 @@ public class Vault implements Serializable {
if (cloudType == null) { if (cloudType == null) {
throw new IllegalStateException("cloudtype must be set"); throw new IllegalStateException("cloudtype must be set");
} }
if (position == -1) {
throw new IllegalStateException("position must be set");
}
} }
} }

View File

@ -35,7 +35,10 @@ class CreateVault {
CloudFolder vaultFolder = cloudContentRepository.folder(folder, vaultName); CloudFolder vaultFolder = cloudContentRepository.folder(folder, vaultName);
vaultFolder = cloudContentRepository.create(vaultFolder); vaultFolder = cloudContentRepository.create(vaultFolder);
cloudRepository.create(vaultFolder, password); cloudRepository.create(vaultFolder, password);
return vaultRepository.store(aVault().thatIsNew().withNamePathAndCloudFrom(vaultFolder).build()); return vaultRepository.store(aVault() //
.thatIsNew() //
.withNamePathAndCloudFrom(vaultFolder) //
.withPosition(vaultRepository.vaults().size()) //
.build());
} }
} }

View File

@ -6,6 +6,8 @@ import org.cryptomator.domain.repository.VaultRepository;
import org.cryptomator.generator.Parameter; import org.cryptomator.generator.Parameter;
import org.cryptomator.generator.UseCase; import org.cryptomator.generator.UseCase;
import java.util.List;
@UseCase @UseCase
class DeleteVault { class DeleteVault {
@ -18,7 +20,12 @@ class DeleteVault {
} }
public Long execute() throws BackendException { public Long execute() throws BackendException {
return vaultRepository.delete(vault); Long vaultId = vaultRepository.delete(vault);
List<Vault> reorderVaults = MoveVaultHelper.Companion.reorderVaults(vaultRepository);
MoveVaultHelper.Companion.updateVaultsInDatabase(reorderVaults, vaultRepository);
return vaultId;
} }
} }

View File

@ -0,0 +1,51 @@
package org.cryptomator.domain.usecases.vault;
import org.cryptomator.domain.Vault
import org.cryptomator.domain.repository.VaultRepository
import java.util.*
class MoveVaultHelper {
companion object {
fun updateVaultPosition(fromPosition: Int, toPosition: Int, vaultRepository: VaultRepository): List<Vault> {
val vaults = vaultRepository.vaults()
vaults.sortWith(VaultComparator())
if (fromPosition < toPosition) {
for (i in fromPosition until toPosition) {
Collections.swap(vaults, i, i + 1)
}
} else {
for (i in fromPosition downTo toPosition + 1) {
Collections.swap(vaults, i, i - 1)
}
}
return reorderVaults(vaults)
}
private fun reorderVaults(vaults: MutableList<Vault>) : List<Vault> {
for (i in 0 until vaults.size) {
vaults[i] = Vault.aCopyOf(vaults[i]).withPosition(i).build()
}
return vaults;
}
fun reorderVaults(vaultRepository: VaultRepository) : List<Vault> {
val vaults = vaultRepository.vaults()
vaults.sortWith(VaultComparator())
return reorderVaults(vaults)
}
fun updateVaultsInDatabase(vaults: List<Vault>, vaultRepository: VaultRepository): List<Vault> {
vaults.forEach { vault -> vaultRepository.store(vault) }
return vaultRepository.vaults()
}
}
internal class VaultComparator : Comparator<Vault> {
override fun compare(o1: Vault, o2: Vault): Int {
return o1.position - o2.position
}
}
}

View File

@ -0,0 +1,28 @@
package org.cryptomator.domain.usecases.vault;
import org.cryptomator.domain.Vault;
import org.cryptomator.domain.exception.BackendException;
import org.cryptomator.domain.repository.VaultRepository;
import org.cryptomator.generator.Parameter;
import org.cryptomator.generator.UseCase;
import java.util.List;
@UseCase
class MoveVaultPosition {
private final VaultRepository vaultRepository;
private final int fromPosition;
private final int toPosition;
public MoveVaultPosition(VaultRepository vaultRepository, @Parameter Integer fromPosition, @Parameter Integer toPosition) {
this.vaultRepository = vaultRepository;
this.fromPosition = fromPosition;
this.toPosition = toPosition;
}
public List<Vault> execute() throws BackendException {
List<Vault> vaults = MoveVaultHelper.Companion.updateVaultPosition(fromPosition, toPosition, vaultRepository);
return MoveVaultHelper.Companion.updateVaultsInDatabase(vaults, vaultRepository);
}
}

View File

@ -0,0 +1,100 @@
package org.cryptomator.domain.usecases.vault
import org.cryptomator.domain.CloudType
import org.cryptomator.domain.Vault
import org.cryptomator.domain.repository.VaultRepository
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.Mockito
class MoveVaultHelperTest {
private lateinit var orderedVaults: ArrayList<Vault>
private lateinit var unorderedVaults: ArrayList<Vault>
private lateinit var vaultRepository: VaultRepository
private lateinit var cloudType: CloudType
@Test
fun reorderVaults() {
Mockito.`when`(vaultRepository.vaults()).thenReturn(unorderedVaults)
assertEquals(orderedVaults, MoveVaultHelper.Companion.reorderVaults(vaultRepository), "Failed to reorderVaults")
}
@Test
fun movePositionUp() {
Mockito.`when`(vaultRepository.vaults()).thenReturn(orderedVaults)
val resultList = ArrayList<Vault>()
resultList.add(Vault.aVault().withId(2).withPath("").withCloudType(cloudType).withName("foo 5").withPosition(0).build())
resultList.add(Vault.aVault().withId(3).withPath("").withCloudType(cloudType).withName("foo 10").withPosition(1).build())
resultList.add(Vault.aVault().withId(24).withPath("").withCloudType(cloudType).withName("foo 1").withPosition(2).build())
resultList.add(Vault.aVault().withId(4).withPath("").withCloudType(cloudType).withName("foo 15").withPosition(3).build())
assertEquals(resultList, MoveVaultHelper.Companion.updateVaultPosition(0, 2, vaultRepository), "Failed to movePositionUp")
}
@Test
fun movePositionDown() {
Mockito.`when`(vaultRepository.vaults()).thenReturn(orderedVaults)
val resultList = ArrayList<Vault>()
resultList.add(Vault.aVault().withId(3).withPath("").withCloudType(cloudType).withName("foo 10").withPosition(0).build())
resultList.add(Vault.aVault().withId(24).withPath("").withCloudType(cloudType).withName("foo 1").withPosition(1).build())
resultList.add(Vault.aVault().withId(2).withPath("").withCloudType(cloudType).withName("foo 5").withPosition(2).build())
resultList.add(Vault.aVault().withId(4).withPath("").withCloudType(cloudType).withName("foo 15").withPosition(3).build())
assertEquals(resultList, MoveVaultHelper.Companion.updateVaultPosition(2, 0, vaultRepository), "Failed to movePositionDown")
}
@Test
fun movePositionToSelf() {
Mockito.`when`(vaultRepository.vaults()).thenReturn(orderedVaults)
val resultList = ArrayList<Vault>()
resultList.add(Vault.aVault().withId(24).withPath("").withCloudType(cloudType).withName("foo 1").withPosition(0).build())
resultList.add(Vault.aVault().withId(2).withPath("").withCloudType(cloudType).withName("foo 5").withPosition(1).build())
resultList.add(Vault.aVault().withId(3).withPath("").withCloudType(cloudType).withName("foo 10").withPosition(2).build())
resultList.add(Vault.aVault().withId(4).withPath("").withCloudType(cloudType).withName("foo 15").withPosition(3).build())
assertEquals(resultList, MoveVaultHelper.Companion.updateVaultPosition(1, 1, vaultRepository), "Failed to movePositionToSelf")
}
@Test
fun movePositionOutOfBounds() {
Mockito.`when`(vaultRepository.vaults()).thenReturn(orderedVaults)
Assertions.assertThrows(IndexOutOfBoundsException::class.java) { MoveVaultHelper.Companion.updateVaultPosition(1, 4, vaultRepository) }
}
@Test
fun verifyStoreInVaultRepo() {
Mockito.`when`(vaultRepository.vaults()).thenReturn(orderedVaults)
val result = MoveVaultHelper.Companion.updateVaultsInDatabase(orderedVaults, vaultRepository)
assertEquals(orderedVaults, result, "Failed to verifyStoreInVaultRepo")
orderedVaults.forEach {
Mockito.verify(vaultRepository).store(Mockito.eq(it))
}
}
@BeforeEach
fun setup() {
vaultRepository = Mockito.mock(VaultRepository::class.java)
cloudType = CloudType.LOCAL
unorderedVaults = ArrayList()
unorderedVaults.add(Vault.aVault().withId(24).withPath("").withCloudType(cloudType).withName("foo 1").withPosition(1).build())
unorderedVaults.add(Vault.aVault().withId(3).withPath("").withCloudType(cloudType).withName("foo 10").withPosition(10).build())
unorderedVaults.add(Vault.aVault().withId(2).withPath("").withCloudType(cloudType).withName("foo 5").withPosition(5).build())
unorderedVaults.add(Vault.aVault().withId(4).withPath("").withCloudType(cloudType).withName("foo 15").withPosition(15).build())
orderedVaults = ArrayList()
orderedVaults.add(Vault.aVault().withId(24).withPath("").withCloudType(cloudType).withName("foo 1").withPosition(0).build())
orderedVaults.add(Vault.aVault().withId(2).withPath("").withCloudType(cloudType).withName("foo 5").withPosition(1).build())
orderedVaults.add(Vault.aVault().withId(3).withPath("").withCloudType(cloudType).withName("foo 10").withPosition(2).build())
orderedVaults.add(Vault.aVault().withId(4).withPath("").withCloudType(cloudType).withName("foo 15").withPosition(3).build())
}
}

View File

@ -13,6 +13,8 @@ class VaultModel(private val vault: Vault) : Serializable {
get() = vault.path get() = vault.path
val isLocked: Boolean val isLocked: Boolean
get() = !vault.isUnlocked get() = !vault.isUnlocked
val position: Int
get() = vault.position
fun toVault(): Vault { fun toVault(): Vault {
return vault return vault

View File

@ -53,6 +53,7 @@ class VaultListPresenter @Inject constructor( //
private val addExistingVaultWorkflow: AddExistingVaultWorkflow, // private val addExistingVaultWorkflow: AddExistingVaultWorkflow, //
private val createNewVaultWorkflow: CreateNewVaultWorkflow, // private val createNewVaultWorkflow: CreateNewVaultWorkflow, //
private val saveVaultUseCase: SaveVaultUseCase, // private val saveVaultUseCase: SaveVaultUseCase, //
private val moveVaultPositionUseCase: MoveVaultPositionUseCase, //
private val changePasswordUseCase: ChangePasswordUseCase, // private val changePasswordUseCase: ChangePasswordUseCase, //
private val removeStoredVaultPasswordsUseCase: RemoveStoredVaultPasswordsUseCase, // private val removeStoredVaultPasswordsUseCase: RemoveStoredVaultPasswordsUseCase, //
private val licenseCheckUseCase: DoLicenseCheckUseCase, // private val licenseCheckUseCase: DoLicenseCheckUseCase, //
@ -603,6 +604,25 @@ class VaultListPresenter @Inject constructor( //
view?.showDialog(AppIsObscuredInfoDialog.newInstance()) view?.showDialog(AppIsObscuredInfoDialog.newInstance())
} }
fun onRowMoved(fromPosition: Int, toPosition: Int) {
view?.rowMoved(fromPosition, toPosition)
}
fun onVaultMoved(fromPosition: Int, toPosition: Int) {
moveVaultPositionUseCase
.withFromPosition(fromPosition) //
.andToPosition(toPosition) //
.run(object : DefaultResultHandler<List<Vault>>() {
override fun onSuccess(vaults: List<Vault>) {
view?.vaultMoved(vaults.mapTo(ArrayList()) { VaultModel(it) })
}
override fun onError(e: Throwable) {
Timber.tag("VaultListPresenter").e(e, "Failed to execute MoveVaultUseCase")
}
})
}
fun onBiometricAuthenticationSucceeded(vaultModel: VaultModel) { fun onBiometricAuthenticationSucceeded(vaultModel: VaultModel) {
if (changedVaultPassword) { if (changedVaultPassword) {
changedVaultPassword = false changedVaultPassword = false
@ -690,6 +710,7 @@ class VaultListPresenter @Inject constructor( //
lockVaultUseCase, // lockVaultUseCase, //
getVaultListUseCase, // getVaultListUseCase, //
saveVaultUseCase, // saveVaultUseCase, //
moveVaultPositionUseCase, //
removeStoredVaultPasswordsUseCase, // removeStoredVaultPasswordsUseCase, //
unlockVaultUseCase, // unlockVaultUseCase, //
prepareUnlockUseCase, // prepareUnlockUseCase, //

View File

@ -150,6 +150,14 @@ class VaultListActivity : BaseActivity(), //
return biometricAuthentication?.stoppedBiometricAuthDuringCloudAuthentication() == true return biometricAuthentication?.stoppedBiometricAuthDuringCloudAuthentication() == true
} }
override fun rowMoved(fromPosition: Int, toPosition: Int) {
vaultListFragment().rowMoved(fromPosition, toPosition)
}
override fun vaultMoved(vaults: List<VaultModel>) {
vaultListFragment().vaultMoved(vaults)
}
override fun showVaultSettingsDialog(vaultModel: VaultModel) { override fun showVaultSettingsDialog(vaultModel: VaultModel) {
val vaultSettingDialog = // val vaultSettingDialog = //
SettingsVaultBottomSheet.newInstance(vaultModel) SettingsVaultBottomSheet.newInstance(vaultModel)

View File

@ -23,5 +23,7 @@ interface VaultListView : View {
fun isVaultLocked(vaultModel: VaultModel): Boolean fun isVaultLocked(vaultModel: VaultModel): Boolean
fun cancelBasicAuthIfRunning() fun cancelBasicAuthIfRunning()
fun stoppedBiometricAuthDuringCloudAuthentication(): Boolean fun stoppedBiometricAuthDuringCloudAuthentication(): Boolean
fun rowMoved(fromPosition: Int, toPosition: Int)
fun vaultMoved(vaults: List<VaultModel>)
} }

View File

@ -8,14 +8,17 @@ import org.cryptomator.presentation.ui.adapter.VaultsAdapter.VaultViewHolder
import javax.inject.Inject import javax.inject.Inject
class VaultsAdapter @Inject class VaultsAdapter @Inject
internal constructor() : RecyclerViewBaseAdapter<VaultModel, VaultsAdapter.OnItemClickListener, VaultViewHolder>() { internal constructor() : RecyclerViewBaseAdapter<VaultModel, VaultsAdapter.OnItemInteractionListener, VaultViewHolder>(VaultModelComparator()), VaultsMoveListener.Listener {
interface OnItemInteractionListener {
interface OnItemClickListener {
fun onVaultClicked(vaultModel: VaultModel) fun onVaultClicked(vaultModel: VaultModel)
fun onVaultSettingsClicked(vaultModel: VaultModel) fun onVaultSettingsClicked(vaultModel: VaultModel)
fun onVaultLockClicked(vaultModel: VaultModel) fun onVaultLockClicked(vaultModel: VaultModel)
fun onRowMoved(fromPosition: Int, toPosition: Int)
fun onVaultMoved(fromPosition: Int, toPosition: Int)
} }
override fun getItemLayout(viewType: Int): Int { override fun getItemLayout(viewType: Int): Int {
@ -65,4 +68,18 @@ internal constructor() : RecyclerViewBaseAdapter<VaultModel, VaultsAdapter.OnIte
itemView.settings.setOnClickListener { callback.onVaultSettingsClicked(vaultModel) } itemView.settings.setOnClickListener { callback.onVaultSettingsClicked(vaultModel) }
} }
} }
override fun onVaultMoved(fromPosition: Int, toPosition: Int) {
callback.onVaultMoved(fromPosition, toPosition)
}
override fun onRowMoved(fromPosition: Int, toPosition: Int) {
callback.onRowMoved(fromPosition, toPosition)
}
internal class VaultModelComparator : java.util.Comparator<VaultModel> {
override fun compare(o1: VaultModel, o2: VaultModel): Int {
return o1.position - o2.position
}
}
} }

View File

@ -0,0 +1,57 @@
package org.cryptomator.presentation.ui.adapter
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
class VaultsMoveListener(val adapter: VaultsAdapter) : ItemTouchHelper.Callback() {
var dragFrom = -1
var dragTo = -1
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
return makeMovementFlags(dragFlags, 0)
}
override fun isItemViewSwipeEnabled(): Boolean {
return false
}
override fun isLongPressDragEnabled(): Boolean {
return true
}
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
val fromPosition = viewHolder.adapterPosition
val toPosition = target.adapterPosition
if (dragFrom == -1) {
dragFrom = fromPosition;
}
dragTo = toPosition;
adapter.onRowMoved(viewHolder.adapterPosition, target.adapterPosition)
return true
}
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
super.clearView(recyclerView, viewHolder)
if (dragFrom != -1 && dragTo != -1 && dragFrom != dragTo) {
adapter.onVaultMoved(dragFrom, dragTo)
}
dragTo = -1
dragFrom = -1
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
}
interface Listener {
fun onRowMoved(fromPosition: Int, toPosition: Int)
fun onVaultMoved(fromPosition: Int, toPosition: Int)
}
}

View File

@ -2,6 +2,7 @@ package org.cryptomator.presentation.ui.fragment
import android.util.TypedValue import android.util.TypedValue
import android.view.View import android.view.View
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.fragment_vault_list.* import kotlinx.android.synthetic.main.fragment_vault_list.*
import kotlinx.android.synthetic.main.recycler_view_layout.* import kotlinx.android.synthetic.main.recycler_view_layout.*
@ -11,6 +12,7 @@ import org.cryptomator.presentation.R
import org.cryptomator.presentation.model.VaultModel import org.cryptomator.presentation.model.VaultModel
import org.cryptomator.presentation.presenter.VaultListPresenter import org.cryptomator.presentation.presenter.VaultListPresenter
import org.cryptomator.presentation.ui.adapter.VaultsAdapter import org.cryptomator.presentation.ui.adapter.VaultsAdapter
import org.cryptomator.presentation.ui.adapter.VaultsMoveListener
import javax.inject.Inject import javax.inject.Inject
@Fragment(R.layout.fragment_vault_list) @Fragment(R.layout.fragment_vault_list)
@ -22,7 +24,9 @@ class VaultListFragment : BaseFragment() {
@Inject @Inject
lateinit var vaultsAdapter: VaultsAdapter lateinit var vaultsAdapter: VaultsAdapter
private val onItemClickListener = object : VaultsAdapter.OnItemClickListener { lateinit var touchHelper: ItemTouchHelper
private val onItemClickListener = object : VaultsAdapter.OnItemInteractionListener {
override fun onVaultClicked(vaultModel: VaultModel) { override fun onVaultClicked(vaultModel: VaultModel) {
vaultListPresenter.onVaultClicked(vaultModel) vaultListPresenter.onVaultClicked(vaultModel)
} }
@ -34,8 +38,17 @@ class VaultListFragment : BaseFragment() {
override fun onVaultLockClicked(vaultModel: VaultModel) { override fun onVaultLockClicked(vaultModel: VaultModel) {
vaultListPresenter.onVaultLockClicked(vaultModel) vaultListPresenter.onVaultLockClicked(vaultModel)
} }
override fun onRowMoved(fromPosition: Int, toPosition: Int) {
vaultListPresenter.onRowMoved(fromPosition, toPosition)
} }
override fun onVaultMoved(fromPosition: Int, toPosition: Int) {
vaultListPresenter.onVaultMoved(fromPosition, toPosition)
}
}
override fun setupView() { override fun setupView() {
setupRecyclerView() setupRecyclerView()
fab_vault.setOnClickListener { vaultListPresenter.onCreateVaultClicked() } fab_vault.setOnClickListener { vaultListPresenter.onCreateVaultClicked() }
@ -48,6 +61,9 @@ class VaultListFragment : BaseFragment() {
private fun setupRecyclerView() { private fun setupRecyclerView() {
vaultsAdapter.setCallback(onItemClickListener) vaultsAdapter.setCallback(onItemClickListener)
touchHelper = ItemTouchHelper(VaultsMoveListener(vaultsAdapter))
touchHelper.attachToRecyclerView(recyclerView)
recyclerView.layoutManager = LinearLayoutManager(context()) recyclerView.layoutManager = LinearLayoutManager(context())
recyclerView.adapter = vaultsAdapter recyclerView.adapter = vaultsAdapter
recyclerView.setHasFixedSize(true) // smoother scrolling recyclerView.setHasFixedSize(true) // smoother scrolling
@ -83,5 +99,14 @@ class VaultListFragment : BaseFragment() {
vaultsAdapter.addOrUpdateVault(vaultModel) vaultsAdapter.addOrUpdateVault(vaultModel)
} }
fun vaultMoved(vaults: List<VaultModel>) {
vaultsAdapter.clear()
vaultsAdapter.addAll(vaults)
}
fun rowMoved(fromPosition: Int, toPosition: Int) {
vaultsAdapter.notifyItemMoved(fromPosition, toPosition)
}
fun rootView(): View = coordinatorLayout fun rootView(): View = coordinatorLayout
} }

View File

@ -7,6 +7,7 @@ import org.cryptomator.domain.CloudFolder;
import org.cryptomator.domain.Vault; import org.cryptomator.domain.Vault;
import org.cryptomator.domain.di.PerView; import org.cryptomator.domain.di.PerView;
import org.cryptomator.domain.usecases.cloud.GetRootFolderUseCase; import org.cryptomator.domain.usecases.cloud.GetRootFolderUseCase;
import org.cryptomator.domain.usecases.vault.GetVaultListUseCase;
import org.cryptomator.domain.usecases.vault.SaveVaultUseCase; import org.cryptomator.domain.usecases.vault.SaveVaultUseCase;
import org.cryptomator.generator.Callback; import org.cryptomator.generator.Callback;
import org.cryptomator.presentation.R; import org.cryptomator.presentation.R;
@ -18,6 +19,7 @@ import org.cryptomator.presentation.model.mappers.CloudModelMapper;
import org.cryptomator.presentation.presenter.VaultListPresenter; import org.cryptomator.presentation.presenter.VaultListPresenter;
import java.io.Serializable; import java.io.Serializable;
import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
@ -30,6 +32,7 @@ import static org.cryptomator.presentation.intent.Intents.chooseCloudServiceInte
public class AddExistingVaultWorkflow extends Workflow<AddExistingVaultWorkflow.State> { public class AddExistingVaultWorkflow extends Workflow<AddExistingVaultWorkflow.State> {
private final SaveVaultUseCase saveVaultUseCase; private final SaveVaultUseCase saveVaultUseCase;
private final GetVaultListUseCase getVaultListUseCase;
private final GetRootFolderUseCase getRootFolderUseCase; private final GetRootFolderUseCase getRootFolderUseCase;
private final CloudModelMapper cloudModelMapper; private final CloudModelMapper cloudModelMapper;
private final AuthenticationExceptionHandler authenticationExceptionHandler; private final AuthenticationExceptionHandler authenticationExceptionHandler;
@ -39,12 +42,14 @@ public class AddExistingVaultWorkflow extends Workflow<AddExistingVaultWorkflow.
public AddExistingVaultWorkflow( // public AddExistingVaultWorkflow( //
Context context, // Context context, //
SaveVaultUseCase saveVaultUseCase, // SaveVaultUseCase saveVaultUseCase, //
GetVaultListUseCase getVaultListUseCase, //
GetRootFolderUseCase getRootFolderUseCase, // GetRootFolderUseCase getRootFolderUseCase, //
CloudModelMapper cloudModelMapper, // CloudModelMapper cloudModelMapper, //
AuthenticationExceptionHandler authenticationExceptionHandler) { AuthenticationExceptionHandler authenticationExceptionHandler) {
super(new State()); super(new State());
this.context = context; this.context = context;
this.saveVaultUseCase = saveVaultUseCase; this.saveVaultUseCase = saveVaultUseCase;
this.getVaultListUseCase = getVaultListUseCase;
this.getRootFolderUseCase = getRootFolderUseCase; this.getRootFolderUseCase = getRootFolderUseCase;
this.cloudModelMapper = cloudModelMapper; this.cloudModelMapper = cloudModelMapper;
this.authenticationExceptionHandler = authenticationExceptionHandler; this.authenticationExceptionHandler = authenticationExceptionHandler;
@ -117,9 +122,13 @@ public class AddExistingVaultWorkflow extends Workflow<AddExistingVaultWorkflow.
@Override @Override
void completed() { void completed() {
presenter().getView().showProgress(ProgressModel.GENERIC); presenter().getView().showProgress(ProgressModel.GENERIC);
getVaultListUseCase.run(presenter().new ProgressCompletingResultHandler<List<Vault>>() {
@Override
public void onSuccess(List<Vault> vaults) {
saveVaultUseCase// saveVaultUseCase//
.withVault(aVault() // .withVault(aVault() //
.withNamePathAndCloudFrom(state().masterkeyFile.getParent()) // .withNamePathAndCloudFrom(state().masterkeyFile.getParent()) //
.withPosition(vaults.size()) //
.thatIsNew() // .thatIsNew() //
.build()) // .build()) //
.run(presenter().new ProgressCompletingResultHandler<Vault>() { .run(presenter().new ProgressCompletingResultHandler<Vault>() {
@ -129,6 +138,8 @@ public class AddExistingVaultWorkflow extends Workflow<AddExistingVaultWorkflow.
} }
}); });
} }
});
}
public static class State implements Serializable { public static class State implements Serializable {

View File

@ -17,6 +17,7 @@ import org.cryptomator.domain.usecases.vault.ChangePasswordUseCase;
import org.cryptomator.domain.usecases.vault.DeleteVaultUseCase; import org.cryptomator.domain.usecases.vault.DeleteVaultUseCase;
import org.cryptomator.domain.usecases.vault.GetVaultListUseCase; import org.cryptomator.domain.usecases.vault.GetVaultListUseCase;
import org.cryptomator.domain.usecases.vault.LockVaultUseCase; import org.cryptomator.domain.usecases.vault.LockVaultUseCase;
import org.cryptomator.domain.usecases.vault.MoveVaultPositionUseCase;
import org.cryptomator.domain.usecases.vault.PrepareUnlockUseCase; import org.cryptomator.domain.usecases.vault.PrepareUnlockUseCase;
import org.cryptomator.domain.usecases.vault.RemoveStoredVaultPasswordsUseCase; import org.cryptomator.domain.usecases.vault.RemoveStoredVaultPasswordsUseCase;
import org.cryptomator.domain.usecases.vault.RenameVaultUseCase; import org.cryptomator.domain.usecases.vault.RenameVaultUseCase;
@ -56,6 +57,7 @@ public class VaultListPresenterTest {
private static final Vault AN_UNLOCKED_VAULT = Vault.aVault() // private static final Vault AN_UNLOCKED_VAULT = Vault.aVault() //
.withId(1L) // .withId(1L) //
.withPosition(1) //
.withName("Top Secret") // .withName("Top Secret") //
.withPath("/top secret") // .withPath("/top secret") //
.withCloudType(CloudType.DROPBOX) // .withCloudType(CloudType.DROPBOX) //
@ -63,6 +65,7 @@ public class VaultListPresenterTest {
private static final Vault ANOTHER_VAULT_WITH_CLOUD = Vault.aVault() // private static final Vault ANOTHER_VAULT_WITH_CLOUD = Vault.aVault() //
.withId(2L) // .withId(2L) //
.withPosition(2) //
.withName("Trip to the moon") // .withName("Trip to the moon") //
.withPath("/trip to the moon") // .withPath("/trip to the moon") //
.withCloudType(CloudType.ONEDRIVE) // .withCloudType(CloudType.ONEDRIVE) //
@ -70,6 +73,7 @@ public class VaultListPresenterTest {
private static final Vault A_VAULT_WITH_NEW_NAME = Vault.aVault() // private static final Vault A_VAULT_WITH_NEW_NAME = Vault.aVault() //
.withId(3L) // .withId(3L) //
.withPosition(3) //
.withName(A_NEW_VAULT_NAME) // .withName(A_NEW_VAULT_NAME) //
.withPath("/trip to the moon") // .withPath("/trip to the moon") //
.withCloudType(CloudType.GOOGLE_DRIVE) // .withCloudType(CloudType.GOOGLE_DRIVE) //
@ -120,6 +124,8 @@ public class VaultListPresenterTest {
private SaveVaultUseCase saveVaultUseCase = Mockito.mock(SaveVaultUseCase.class); private SaveVaultUseCase saveVaultUseCase = Mockito.mock(SaveVaultUseCase.class);
private MoveVaultPositionUseCase moveVaultPositionUseCase = Mockito.mock(MoveVaultPositionUseCase.class);
private ChangePasswordUseCase changePasswordUseCase = Mockito.mock(ChangePasswordUseCase.class); private ChangePasswordUseCase changePasswordUseCase = Mockito.mock(ChangePasswordUseCase.class);
private RemoveStoredVaultPasswordsUseCase removeStoredVaultPasswordsUseCase = Mockito.mock(RemoveStoredVaultPasswordsUseCase.class); private RemoveStoredVaultPasswordsUseCase removeStoredVaultPasswordsUseCase = Mockito.mock(RemoveStoredVaultPasswordsUseCase.class);
@ -157,6 +163,7 @@ public class VaultListPresenterTest {
addExistingVaultWorkflow, // addExistingVaultWorkflow, //
createNewVaultWorkflow, // createNewVaultWorkflow, //
saveVaultUseCase, // saveVaultUseCase, //
moveVaultPositionUseCase, //
changePasswordUseCase, // changePasswordUseCase, //
removeStoredVaultPasswordsUseCase, // removeStoredVaultPasswordsUseCase, //
doLicenceCheckUsecase, // doLicenceCheckUsecase, //