Merge branch 'feature/251-update-to-API-level-30' into develop

This commit is contained in:
Julian Raufelder 2021-10-28 20:37:39 +02:00
commit c18632106e
No known key found for this signature in database
GPG Key ID: 17EE71F6634E381D
57 changed files with 432 additions and 676 deletions

View File

@ -8,8 +8,8 @@ allprojects {
ext { ext {
androidBuildToolsVersion = "30.0.2" androidBuildToolsVersion = "30.0.2"
androidMinSdkVersion = 24 androidMinSdkVersion = 24
androidTargetSdkVersion = 29 androidTargetSdkVersion = 30
androidCompileSdkVersion = 29 androidCompileSdkVersion = 30
// android and java libs // android and java libs
androidVersion = '4.1.1.4' androidVersion = '4.1.1.4'

View File

@ -82,7 +82,7 @@ android {
} }
greendao { greendao {
schemaVersion 9 schemaVersion 10
} }
configurations.all { configurations.all {

View File

@ -50,6 +50,7 @@ class UpgradeDatabaseTest {
Upgrade6To7().applyTo(db, 6) Upgrade6To7().applyTo(db, 6)
Upgrade7To8().applyTo(db, 7) Upgrade7To8().applyTo(db, 7)
Upgrade8To9(sharedPreferencesHandler).applyTo(db, 8) Upgrade8To9(sharedPreferencesHandler).applyTo(db, 8)
Upgrade9To10(sharedPreferencesHandler).applyTo(db, 9)
CloudEntityDao(DaoConfig(db, CloudEntityDao::class.java)).loadAll() CloudEntityDao(DaoConfig(db, CloudEntityDao::class.java)).loadAll()
VaultEntityDao(DaoConfig(db, VaultEntityDao::class.java)).loadAll() VaultEntityDao(DaoConfig(db, VaultEntityDao::class.java)).loadAll()
@ -407,4 +408,66 @@ class UpgradeDatabaseTest {
Assert.assertThat(sharedPreferencesHandler.isBetaModeAlreadyShown(), CoreMatchers.`is`(false)) Assert.assertThat(sharedPreferencesHandler.isBetaModeAlreadyShown(), CoreMatchers.`is`(false))
} }
@Test
fun upgrade9To10() {
Upgrade0To1().applyTo(db, 0)
Upgrade1To2().applyTo(db, 1)
Upgrade2To3(context).applyTo(db, 2)
Upgrade3To4().applyTo(db, 3)
Upgrade4To5().applyTo(db, 4)
Upgrade5To6().applyTo(db, 5)
Upgrade6To7().applyTo(db, 6)
Upgrade7To8().applyTo(db, 7)
Upgrade8To9(sharedPreferencesHandler).applyTo(db, 8)
Sql.insertInto("CLOUD_ENTITY") //
.integer("_id", 15) //
.text("TYPE", CloudType.LOCAL.name) //
.text("URL", "url") //
.text("USERNAME", "username") //
.text("WEBDAV_CERTIFICATE", "certificate") //
.text("ACCESS_TOKEN", "accessToken")
.text("S3_BUCKET", "s3Bucket") //
.text("S3_REGION", "s3Region") //
.text("S3_SECRET_KEY", "s3SecretKey") //
.executeOn(db)
Sql.insertInto("VAULT_ENTITY") //
.integer("_id", 25) //
.integer("FOLDER_CLOUD_ID", 15) //
.text("FOLDER_PATH", "path") //
.text("FOLDER_NAME", "name") //
.text("CLOUD_TYPE", CloudType.LOCAL.name) //
.text("PASSWORD", "password") //
.integer("POSITION", 10) //
.executeOn(db)
Sql.insertInto("VAULT_ENTITY") //
.integer("_id", 26) //
.integer("FOLDER_CLOUD_ID", 4) //
.text("FOLDER_PATH", "pathOfVault26") //
.text("FOLDER_NAME", "name") //
.text("CLOUD_TYPE", CloudType.LOCAL.name) //
.text("PASSWORD", "password") //
.integer("POSITION", 11) //
.executeOn(db)
Sql.query("CLOUD_ENTITY").executeOn(db).use {
Assert.assertThat(it.count, CoreMatchers.`is`(5))
}
Upgrade9To10(sharedPreferencesHandler).applyTo(db, 9)
Sql.query("VAULT_ENTITY").executeOn(db).use {
Assert.assertThat(it.count, CoreMatchers.`is`(1))
}
Sql.query("CLOUD_ENTITY").executeOn(db).use {
Assert.assertThat(it.count, CoreMatchers.`is`(4))
}
Assert.assertThat(sharedPreferencesHandler.vaultsRemovedDuringMigration(), CoreMatchers.`is`(Pair("LOCAL", arrayListOf("pathOfVault26"))))
}
} }

View File

@ -31,11 +31,6 @@ public class CryptoCloud implements Cloud {
return vault.equals(cloud.vault); return vault.equals(cloud.vault);
} }
@Override
public boolean predefined() {
return false;
}
@Override @Override
public boolean persistent() { public boolean persistent() {
return false; return false;

View File

@ -1,4 +1,4 @@
package org.cryptomator.data.cloud.local.storageaccessframework package org.cryptomator.data.cloud.local
import android.util.LruCache import android.util.LruCache
import org.cryptomator.domain.CloudFolder import org.cryptomator.domain.CloudFolder

View File

@ -1,4 +1,4 @@
package org.cryptomator.data.cloud.local.storageaccessframework package org.cryptomator.data.cloud.local
import android.net.Uri import android.net.Uri
import org.cryptomator.domain.Cloud import org.cryptomator.domain.Cloud

View File

@ -1,4 +1,4 @@
package org.cryptomator.data.cloud.local.storageaccessframework package org.cryptomator.data.cloud.local
import android.net.Uri import android.net.Uri
import org.cryptomator.domain.Cloud import org.cryptomator.domain.Cloud

View File

@ -1,4 +1,4 @@
package org.cryptomator.data.cloud.local.storageaccessframework package org.cryptomator.data.cloud.local
import android.content.Context import android.content.Context
import org.cryptomator.domain.LocalStorageCloud import org.cryptomator.domain.LocalStorageCloud

View File

@ -1,4 +1,4 @@
package org.cryptomator.data.cloud.local.storageaccessframework package org.cryptomator.data.cloud.local
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context import android.content.Context
@ -7,10 +7,10 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.provider.DocumentsContract import android.provider.DocumentsContract
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import org.cryptomator.data.cloud.local.storageaccessframework.LocalStorageAccessFrameworkNodeFactory.file import org.cryptomator.data.cloud.local.LocalStorageAccessFrameworkNodeFactory.file
import org.cryptomator.data.cloud.local.storageaccessframework.LocalStorageAccessFrameworkNodeFactory.folder import org.cryptomator.data.cloud.local.LocalStorageAccessFrameworkNodeFactory.folder
import org.cryptomator.data.cloud.local.storageaccessframework.LocalStorageAccessFrameworkNodeFactory.from import org.cryptomator.data.cloud.local.LocalStorageAccessFrameworkNodeFactory.from
import org.cryptomator.data.cloud.local.storageaccessframework.LocalStorageAccessFrameworkNodeFactory.getNodePath import org.cryptomator.data.cloud.local.LocalStorageAccessFrameworkNodeFactory.getNodePath
import org.cryptomator.data.util.CopyStream import org.cryptomator.data.util.CopyStream
import org.cryptomator.data.util.TransferredBytesAwareInputStream import org.cryptomator.data.util.TransferredBytesAwareInputStream
import org.cryptomator.data.util.TransferredBytesAwareOutputStream import org.cryptomator.data.util.TransferredBytesAwareOutputStream
@ -243,7 +243,8 @@ internal class LocalStorageAccessFrameworkImpl(context: Context, private val mim
private fun rename(source: LocalStorageAccessNode, name: String): LocalStorageAccessNode { private fun rename(source: LocalStorageAccessNode, name: String): LocalStorageAccessNode {
source.parent?.let { parent -> source.parent?.let { parent ->
var newUri = try { var newUri = try {
DocumentsContract.renameDocument(contentResolver(), source.uri, name) requireNotNull(source.uri)
DocumentsContract.renameDocument(contentResolver(), source.uri!!, name)
} catch (e: FileNotFoundException) { } catch (e: FileNotFoundException) {
/* Bug in Android 9 see #460 TLDR; In this renameDocument-method, Android 9 throws /* Bug in Android 9 see #460 TLDR; In this renameDocument-method, Android 9 throws
a `FileNotFoundException` although the file exists and is also renamed. */ a `FileNotFoundException` although the file exists and is also renamed. */
@ -336,11 +337,13 @@ internal class LocalStorageAccessFrameworkImpl(context: Context, private val mim
private fun createNewDocumentSupplier(file: LocalStorageAccessFile): Supplier<Uri?> { private fun createNewDocumentSupplier(file: LocalStorageAccessFile): Supplier<Uri?> {
return Supplier { return Supplier {
val mimeType = if (mimeTypes.fromFilename(file.name) == null) MimeType.APPLICATION_OCTET_STREAM else mimeTypes.fromFilename(file.name) file.parent.uri?.let {
try { val mimeType = if (mimeTypes.fromFilename(file.name) == null) MimeType.APPLICATION_OCTET_STREAM else mimeTypes.fromFilename(file.name)
DocumentsContract.createDocument(contentResolver(), file.parent.uri, mimeType.toString(), file.name) // FIXME try {
} catch (e: FileNotFoundException) { DocumentsContract.createDocument(contentResolver(), it, mimeType.toString(), file.name)
null } catch (e: FileNotFoundException) {
null
}
} }
} }
} }
@ -372,7 +375,7 @@ internal class LocalStorageAccessFrameworkImpl(context: Context, private val mim
fun delete(node: LocalStorageAccessNode) { fun delete(node: LocalStorageAccessNode) {
requireNotNull(node.uri) requireNotNull(node.uri)
try { try {
DocumentsContract.deleteDocument(contentResolver(), node.uri) DocumentsContract.deleteDocument(contentResolver(), node.uri!!)
} catch (e: FileNotFoundException) { } catch (e: FileNotFoundException) {
throw NoSuchCloudFileException(node.name) throw NoSuchCloudFileException(node.name)
} }

View File

@ -1,4 +1,4 @@
package org.cryptomator.data.cloud.local.storageaccessframework package org.cryptomator.data.cloud.local
import android.database.Cursor import android.database.Cursor
import android.provider.DocumentsContract import android.provider.DocumentsContract

View File

@ -1,4 +1,4 @@
package org.cryptomator.data.cloud.local.storageaccessframework package org.cryptomator.data.cloud.local
import android.net.Uri import android.net.Uri
import org.cryptomator.domain.CloudNode import org.cryptomator.domain.CloudNode

View File

@ -1,9 +1,10 @@
package org.cryptomator.data.cloud.local; package org.cryptomator.data.cloud.local;
import android.content.Context; import static org.cryptomator.domain.CloudType.LOCAL;
import android.content.Context;
import android.content.UriPermission;
import org.cryptomator.data.cloud.local.file.LocalStorageContentRepository;
import org.cryptomator.data.cloud.local.storageaccessframework.LocalStorageAccessFrameworkContentRepository;
import org.cryptomator.data.repository.CloudContentRepositoryFactory; import org.cryptomator.data.repository.CloudContentRepositoryFactory;
import org.cryptomator.domain.Cloud; import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.LocalStorageCloud; import org.cryptomator.domain.LocalStorageCloud;
@ -11,15 +12,11 @@ import org.cryptomator.domain.exception.authentication.NoAuthenticationProvidedE
import org.cryptomator.domain.repository.CloudContentRepository; import org.cryptomator.domain.repository.CloudContentRepository;
import org.cryptomator.util.file.MimeTypes; import org.cryptomator.util.file.MimeTypes;
import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static androidx.core.content.ContextCompat.checkSelfPermission;
import static org.cryptomator.domain.CloudType.LOCAL;
@Singleton @Singleton
public class LocalStorageContentRepositoryFactory implements CloudContentRepositoryFactory { public class LocalStorageContentRepositoryFactory implements CloudContentRepositoryFactory {
@ -39,23 +36,14 @@ public class LocalStorageContentRepositoryFactory implements CloudContentReposit
@Override @Override
public CloudContentRepository cloudContentRepositoryFor(Cloud cloud) { public CloudContentRepository cloudContentRepositoryFor(Cloud cloud) {
if (!hasPermissions(WRITE_EXTERNAL_STORAGE, READ_EXTERNAL_STORAGE)) { List<UriPermission> permissions = context.getContentResolver().getPersistedUriPermissions();
throw new NoAuthenticationProvidedException(cloud); for (UriPermission permission : permissions) {
} if(permission.getUri().toString().equals(((LocalStorageCloud) cloud).rootUri())) {
if (((LocalStorageCloud) cloud).rootUri() != null) { return new LocalStorageAccessFrameworkContentRepository(context, mimeTypes, (LocalStorageCloud) cloud);
return new LocalStorageAccessFrameworkContentRepository(context, mimeTypes, (LocalStorageCloud) cloud);
} else {
return new LocalStorageContentRepository(context, (LocalStorageCloud) cloud);
}
}
private boolean hasPermissions(String... permissions) {
for (String permission : permissions) {
if (checkSelfPermission(context, permission) != PERMISSION_GRANTED) {
return false;
} }
} }
return true;
throw new NoAuthenticationProvidedException(cloud);
} }
} }

View File

@ -1,4 +1,4 @@
package org.cryptomator.data.cloud.local.storageaccessframework package org.cryptomator.data.cloud.local
import android.net.Uri import android.net.Uri
import android.provider.DocumentsContract import android.provider.DocumentsContract

View File

@ -1,11 +0,0 @@
package org.cryptomator.data.cloud.local.file
import org.cryptomator.domain.Cloud
import org.cryptomator.domain.CloudFile
import java.util.Date
class LocalFile(override val parent: LocalFolder, override val name: String, override val path: String, override val size: Long?, override val modified: Date?) : CloudFile, LocalNode {
override val cloud: Cloud?
get() = parent.cloud
}

View File

@ -1,14 +0,0 @@
package org.cryptomator.data.cloud.local.file
import org.cryptomator.domain.Cloud
import org.cryptomator.domain.CloudFolder
open class LocalFolder(override val parent: LocalFolder?, override val name: String, override val path: String) : CloudFolder, LocalNode {
override val cloud: Cloud?
get() = parent?.cloud
override fun withCloud(cloud: Cloud?): LocalFolder? {
return LocalFolder(parent?.withCloud(cloud), name, path)
}
}

View File

@ -1,9 +0,0 @@
package org.cryptomator.data.cloud.local.file
import org.cryptomator.domain.CloudNode
interface LocalNode : CloudNode {
override val parent: LocalFolder?
}

View File

@ -1,111 +0,0 @@
package org.cryptomator.data.cloud.local.file
import android.content.Context
import org.cryptomator.domain.LocalStorageCloud
import org.cryptomator.domain.exception.BackendException
import org.cryptomator.domain.exception.FatalBackendException
import org.cryptomator.domain.exception.NoSuchCloudFileException
import org.cryptomator.domain.repository.CloudContentRepository
import org.cryptomator.domain.usecases.ProgressAware
import org.cryptomator.domain.usecases.cloud.DataSource
import org.cryptomator.domain.usecases.cloud.DownloadState
import org.cryptomator.domain.usecases.cloud.UploadState
import org.cryptomator.util.ExceptionUtil
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.io.OutputStream
class LocalStorageContentRepository(context: Context, localStorageCloud: LocalStorageCloud) : CloudContentRepository<LocalStorageCloud, LocalNode, LocalFolder, LocalFile> {
private val localStorageImpl: LocalStorageImpl = LocalStorageImpl(context, localStorageCloud)
@Throws(BackendException::class)
override fun root(cloud: LocalStorageCloud): LocalFolder {
return localStorageImpl.root()
}
override fun resolve(cloud: LocalStorageCloud, path: String): LocalFolder {
return localStorageImpl.resolve(path)
}
@Throws(BackendException::class)
override fun file(parent: LocalFolder, name: String): LocalFile {
return localStorageImpl.file(parent, name, null)
}
@Throws(BackendException::class)
override fun file(parent: LocalFolder, name: String, size: Long?): LocalFile {
return localStorageImpl.file(parent, name, size)
}
@Throws(BackendException::class)
override fun folder(parent: LocalFolder, name: String): LocalFolder {
return localStorageImpl.folder(parent, name)
}
@Throws(BackendException::class)
override fun exists(node: LocalNode): Boolean {
return localStorageImpl.exists(node)
}
@Throws(BackendException::class)
override fun list(folder: LocalFolder): List<LocalNode> {
return localStorageImpl.list(folder)
}
@Throws(BackendException::class)
override fun create(folder: LocalFolder): LocalFolder {
return localStorageImpl.create(folder)
}
@Throws(BackendException::class)
override fun move(source: LocalFolder, target: LocalFolder): LocalFolder {
return localStorageImpl.move(source, target) as LocalFolder
}
@Throws(BackendException::class)
override fun move(source: LocalFile, target: LocalFile): LocalFile {
return localStorageImpl.move(source, target) as LocalFile
}
@Throws(BackendException::class)
override fun write(file: LocalFile, data: DataSource, progressAware: ProgressAware<UploadState>, replace: Boolean, size: Long): LocalFile {
return try {
localStorageImpl.write(file, data, progressAware, replace, size)
} catch (e: IOException) {
if (ExceptionUtil.contains(e, FileNotFoundException::class.java)) {
throw NoSuchCloudFileException(file.name)
}
throw FatalBackendException(e)
}
}
@Throws(BackendException::class)
override fun read(file: LocalFile, encryptedTmpFile: File?, data: OutputStream, progressAware: ProgressAware<DownloadState>) {
try {
localStorageImpl.read(file, data, progressAware)
} catch (e: IOException) {
if (ExceptionUtil.contains(e, FileNotFoundException::class.java)) {
throw NoSuchCloudFileException(file.name)
}
throw FatalBackendException(e)
}
}
@Throws(BackendException::class)
override fun delete(node: LocalNode) {
localStorageImpl.delete(node)
}
@Throws(BackendException::class)
override fun checkAuthenticationAndRetrieveCurrentAccount(cloud: LocalStorageCloud): String {
return ""
}
@Throws(BackendException::class)
override fun logout(cloud: LocalStorageCloud) {
// empty
}
}

View File

@ -1,165 +0,0 @@
package org.cryptomator.data.cloud.local.file
import android.content.Context
import org.cryptomator.data.util.CopyStream
import org.cryptomator.data.util.TransferredBytesAwareInputStream
import org.cryptomator.data.util.TransferredBytesAwareOutputStream
import org.cryptomator.domain.CloudNode
import org.cryptomator.domain.LocalStorageCloud
import org.cryptomator.domain.exception.BackendException
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException
import org.cryptomator.domain.exception.FatalBackendException
import org.cryptomator.domain.exception.NoSuchCloudFileException
import org.cryptomator.domain.exception.ParentFolderIsNullException
import org.cryptomator.domain.usecases.ProgressAware
import org.cryptomator.domain.usecases.cloud.DataSource
import org.cryptomator.domain.usecases.cloud.DownloadState
import org.cryptomator.domain.usecases.cloud.Progress
import org.cryptomator.domain.usecases.cloud.UploadState
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream
import java.util.Date
internal class LocalStorageImpl(private val context: Context, localStorageCloud: LocalStorageCloud) {
private val root: RootLocalFolder = RootLocalFolder(localStorageCloud)
fun root(): LocalFolder {
return root
}
fun resolve(path: String): LocalFolder {
val names = path.substring(root.path.length + 1).split("/").toTypedArray()
var folder: LocalFolder = root
for (name in names) {
folder = folder(folder, name)
}
return folder
}
fun file(folder: LocalFolder, name: String, size: Long?): LocalFile {
return LocalStorageNodeFactory.file(folder, name, folder.path + '/' + name, size, null)
}
fun folder(folder: LocalFolder, name: String): LocalFolder {
return LocalStorageNodeFactory.folder(folder, name, folder.path + '/' + name)
}
fun exists(node: CloudNode): Boolean {
return File(node.path).exists()
}
@Throws(BackendException::class)
fun list(folder: LocalFolder): List<LocalNode> {
val localDirectory = File(folder.path)
if (!exists(folder)) {
throw NoSuchCloudFileException()
}
return localDirectory.listFiles()?.map { file -> LocalStorageNodeFactory.from(folder, file) }
?: throw FatalBackendException("listFiles() shouldn't return null")
}
@Throws(BackendException::class)
fun create(folder: LocalFolder): LocalFolder {
folder.parent?.let { parentFolder ->
val createFolder = File(folder.path)
if (createFolder.exists()) {
throw CloudNodeAlreadyExistsException(folder.name)
}
if (!createFolder.mkdirs()) {
throw FatalBackendException("Couldn't create a local folder at " + folder.path)
}
return LocalStorageNodeFactory.folder(parentFolder, createFolder)
} ?: throw ParentFolderIsNullException(folder.name)
}
@Throws(BackendException::class)
fun move(source: LocalNode, target: LocalNode): LocalNode {
target.parent?.let {
val sourceFile = File(source.path)
val targetFile = File(target.path)
if (targetFile.exists()) {
throw CloudNodeAlreadyExistsException(target.name)
}
if (!sourceFile.exists()) {
throw NoSuchCloudFileException(source.name)
}
if (!sourceFile.renameTo(targetFile)) {
throw FatalBackendException("Couldn't move " + source.path + " to " + target.path)
}
return LocalStorageNodeFactory.from(it, targetFile)
} ?: throw ParentFolderIsNullException(target.name)
}
fun delete(node: CloudNode) {
val fileOrDirectory = File(node.path)
if (!deleteRecursive(fileOrDirectory)) {
throw FatalBackendException("Couldn't delete local CloudNode $fileOrDirectory")
}
}
private fun deleteRecursive(fileOrDirectory: File): Boolean {
if (fileOrDirectory.isDirectory) {
fileOrDirectory.listFiles()?.forEach {
deleteRecursive(it)
}
}
return fileOrDirectory.delete()
}
@Throws(IOException::class, BackendException::class)
fun write(file: LocalFile, data: DataSource, progressAware: ProgressAware<UploadState>, replace: Boolean, size: Long): LocalFile {
if (!replace && exists(file)) {
throw CloudNodeAlreadyExistsException("CloudNode already exists and replace is false")
}
progressAware.onProgress(Progress.started(UploadState.upload(file)))
val localFile = File(file.path)
FileOutputStream(localFile).use { out ->
data.open(context)?.use { inputStream ->
object : TransferredBytesAwareInputStream(inputStream) {
override fun bytesTransferred(transferred: Long) {
progressAware.onProgress( //
Progress.progress(UploadState.upload(file)) //
.between(0) //
.and(size) //
.withValue(transferred)
)
}
}.use { CopyStream.copyStreamToStream(it, out) }
} ?: throw FatalBackendException("InputStream shouldn't be null")
}
progressAware.onProgress(Progress.completed(UploadState.upload(file)))
return LocalStorageNodeFactory.file( //
file.parent, //
file.name, //
localFile.path, //
localFile.length(), //
Date(localFile.lastModified())
)
}
@Throws(IOException::class)
fun read(file: LocalFile, data: OutputStream, progressAware: ProgressAware<DownloadState>) {
progressAware.onProgress(Progress.started(DownloadState.download(file)))
val localFile = File(file.path)
FileInputStream(localFile).use { inputStream ->
object : TransferredBytesAwareOutputStream(data) {
override fun bytesTransferred(transferred: Long) {
progressAware //
.onProgress(
Progress.progress(DownloadState.download(file)) //
.between(0) //
.and(localFile.length()) //
.withValue(transferred)
)
}
}.use { out -> CopyStream.copyStreamToStream(inputStream, out) }
}
progressAware.onProgress(Progress.completed(DownloadState.download(file)))
}
}

View File

@ -1,36 +0,0 @@
package org.cryptomator.data.cloud.local.file
import java.io.File
import java.util.Date
internal object LocalStorageNodeFactory {
@JvmStatic
fun from(parent: LocalFolder, file: File): LocalNode {
return if (file.isDirectory) {
folder(parent, file)
} else {
file( //
parent, //
file.name, //
file.path, //
file.length(), //
Date(file.lastModified())
)
}
}
fun folder(parent: LocalFolder, file: File): LocalFolder {
return folder(parent, file.name, file.path)
}
@JvmStatic
fun folder(parent: LocalFolder, name: String, path: String): LocalFolder {
return LocalFolder(parent, name, path)
}
@JvmStatic
fun file(folder: LocalFolder, name: String, path: String, size: Long?, modified: Date?): LocalFile {
return LocalFile(folder, name, path, size, modified)
}
}

View File

@ -1,15 +0,0 @@
package org.cryptomator.data.cloud.local.file
import android.os.Environment
import org.cryptomator.domain.Cloud
import org.cryptomator.domain.LocalStorageCloud
class RootLocalFolder(private val localStorageCloud: LocalStorageCloud) : LocalFolder(null, "", Environment.getExternalStorageDirectory().path) {
override val cloud: Cloud
get() = localStorageCloud
override fun withCloud(cloud: Cloud?): RootLocalFolder {
return RootLocalFolder(cloud as LocalStorageCloud)
}
}

View File

@ -27,7 +27,8 @@ class DatabaseUpgrades {
Upgrade5To6 upgrade5To6, // Upgrade5To6 upgrade5To6, //
Upgrade6To7 upgrade6To7, // Upgrade6To7 upgrade6To7, //
Upgrade7To8 upgrade7To8, // Upgrade7To8 upgrade7To8, //
Upgrade8To9 upgrade8To9) { Upgrade8To9 upgrade8To9, //
Upgrade9To10 upgrade9To10) {
availableUpgrades = defineUpgrades( // availableUpgrades = defineUpgrades( //
upgrade0To1, // upgrade0To1, //
@ -38,7 +39,8 @@ class DatabaseUpgrades {
upgrade5To6, // upgrade5To6, //
upgrade6To7, // upgrade6To7, //
upgrade7To8, // upgrade7To8, //
upgrade8To9); upgrade8To9, //
upgrade9To10);
} }
private static Comparator<DatabaseUpgrade> reverseOrder() { private static Comparator<DatabaseUpgrade> reverseOrder() {

View File

@ -0,0 +1,48 @@
package org.cryptomator.data.db
import org.cryptomator.domain.CloudType
import org.cryptomator.util.SharedPreferencesHandler
import org.greenrobot.greendao.database.Database
import javax.inject.Inject
import javax.inject.Singleton
import timber.log.Timber
@Singleton
internal class Upgrade9To10 @Inject constructor(private val sharedPreferencesHandler: SharedPreferencesHandler) : DatabaseUpgrade(9, 10) {
private val defaultLocalStorageCloudId = 4L
override fun internalApplyTo(db: Database, origin: Int) {
db.beginTransaction()
try {
Sql.query("VAULT_ENTITY")
.columns(listOf("FOLDER_PATH"))
.where("FOLDER_CLOUD_ID", Sql.eq(defaultLocalStorageCloudId))
.executeOn(db).use {
val vaultsToBeRemoved = ArrayList<String>()
while (it.moveToNext()) {
val folderPath = it.getString(it.getColumnIndex("FOLDER_PATH"))
vaultsToBeRemoved.add(folderPath)
}
if (vaultsToBeRemoved.isNotEmpty()) {
sharedPreferencesHandler.vaultsRemovedDuringMigration(Pair(CloudType.LOCAL.name, vaultsToBeRemoved))
Timber.tag("Upgrade9To10").i("Added %s to the removeDuringMigrations", vaultsToBeRemoved)
}
}
Sql.deleteFrom("VAULT_ENTITY")
.where("FOLDER_CLOUD_ID", Sql.eq(defaultLocalStorageCloudId))
.executeOn(db)
Sql.deleteFrom("CLOUD_ENTITY")
.where("_id", Sql.eq(defaultLocalStorageCloudId))
.where("TYPE", Sql.eq("LOCAL"))
.executeOn(db)
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
}
}

View File

@ -76,9 +76,6 @@ class CloudRepositoryImpl implements CloudRepository {
@Override @Override
public void delete(Cloud cloud) { public void delete(Cloud cloud) {
if (cloud.predefined()) {
throw new IllegalArgumentException("Can not delete predefined cloud");
}
if (!cloud.persistent()) { if (!cloud.persistent()) {
throw new IllegalArgumentException("Can not delete non persistent cloud"); throw new IllegalArgumentException("Can not delete non persistent cloud");
} }

View File

@ -28,6 +28,6 @@ class NetworkConnectionCheck @Inject internal constructor(private val context: C
fun checkWifiOnAndConnected(): Boolean { fun checkWifiOnAndConnected(): Boolean {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val activeNetwork = connectivityManager.activeNetwork val activeNetwork = connectivityManager.activeNetwork
return connectivityManager.getNetworkCapabilities(activeNetwork).hasTransport(NetworkCapabilities.TRANSPORT_WIFI) return connectivityManager.getNetworkCapabilities(activeNetwork)?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
} }
} }

View File

@ -7,7 +7,6 @@ interface Cloud : Serializable {
fun id(): Long? fun id(): Long?
fun type(): CloudType? fun type(): CloudType?
fun configurationMatches(cloud: Cloud?): Boolean fun configurationMatches(cloud: Cloud?): Boolean
fun predefined(): Boolean
fun persistent(): Boolean fun persistent(): Boolean
fun requiresNetwork(): Boolean fun requiresNetwork(): Boolean
} }

View File

@ -48,11 +48,6 @@ public class DropboxCloud implements Cloud {
return true; return true;
} }
@Override
public boolean predefined() {
return true;
}
@Override @Override
public boolean persistent() { public boolean persistent() {
return true; return true;

View File

@ -48,11 +48,6 @@ public class GoogleDriveCloud implements Cloud {
return true; return true;
} }
@Override
public boolean predefined() {
return true;
}
@Override @Override
public boolean persistent() { public boolean persistent() {
return true; return true;

View File

@ -1,6 +1,5 @@
package org.cryptomator.domain; package org.cryptomator.domain;
import android.os.Build;
import android.text.TextUtils; import android.text.TextUtils;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -48,11 +47,6 @@ public class LocalStorageCloud implements Cloud {
} }
@Override
public boolean predefined() {
return Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP;
}
@Override @Override
public boolean persistent() { public boolean persistent() {
return true; return true;

View File

@ -43,11 +43,6 @@ public class OnedriveCloud implements Cloud {
return CloudType.ONEDRIVE; return CloudType.ONEDRIVE;
} }
@Override
public boolean predefined() {
return true;
}
@Override @Override
public boolean persistent() { public boolean persistent() {
return true; return true;

View File

@ -59,12 +59,6 @@ public class PCloud implements Cloud {
return username.equals(cloud.username); return username.equals(cloud.username);
} }
@Override
public boolean predefined() {
return false;
}
@Override @Override
public boolean persistent() { public boolean persistent() {
return true; return true;

View File

@ -80,12 +80,6 @@ public class S3Cloud implements Cloud {
return s3Bucket.equals(cloud.s3Bucket) && s3Endpoint.equals(cloud.s3Endpoint) && s3Region.equals(cloud.s3Region); return s3Bucket.equals(cloud.s3Bucket) && s3Endpoint.equals(cloud.s3Endpoint) && s3Region.equals(cloud.s3Region);
} }
@Override
public boolean predefined() {
return false;
}
@Override @Override
public boolean persistent() { public boolean persistent() {
return true; return true;

View File

@ -66,11 +66,6 @@ public class WebDavCloud implements Cloud {
return certificate; return certificate;
} }
@Override
public boolean predefined() {
return false;
}
@Override @Override
public boolean persistent() { public boolean persistent() {
return true; return true;

View File

@ -1,7 +1,9 @@
package org.cryptomator.presentation.presenter package org.cryptomator.presentation.presenter
import android.Manifest
import android.accounts.AccountManager import android.accounts.AccountManager
import android.content.Intent
import android.content.Intent.ACTION_OPEN_DOCUMENT_TREE
import android.provider.DocumentsContract
import android.widget.Toast import android.widget.Toast
import com.dropbox.core.android.Auth import com.dropbox.core.android.Auth
import org.cryptomator.data.cloud.onedrive.OnedriveClientFactory import org.cryptomator.data.cloud.onedrive.OnedriveClientFactory
@ -35,6 +37,7 @@ import org.cryptomator.presentation.intent.AuthenticateCloudIntent
import org.cryptomator.presentation.intent.Intents import org.cryptomator.presentation.intent.Intents
import org.cryptomator.presentation.model.CloudModel import org.cryptomator.presentation.model.CloudModel
import org.cryptomator.presentation.model.CloudTypeModel import org.cryptomator.presentation.model.CloudTypeModel
import org.cryptomator.presentation.model.LocalStorageModel
import org.cryptomator.presentation.model.ProgressModel import org.cryptomator.presentation.model.ProgressModel
import org.cryptomator.presentation.model.ProgressStateModel import org.cryptomator.presentation.model.ProgressStateModel
import org.cryptomator.presentation.model.S3CloudModel import org.cryptomator.presentation.model.S3CloudModel
@ -44,7 +47,6 @@ import org.cryptomator.presentation.ui.activity.view.AuthenticateCloudView
import org.cryptomator.presentation.workflow.ActivityResult import org.cryptomator.presentation.workflow.ActivityResult
import org.cryptomator.presentation.workflow.AddExistingVaultWorkflow import org.cryptomator.presentation.workflow.AddExistingVaultWorkflow
import org.cryptomator.presentation.workflow.CreateNewVaultWorkflow import org.cryptomator.presentation.workflow.CreateNewVaultWorkflow
import org.cryptomator.presentation.workflow.PermissionsResult
import org.cryptomator.presentation.workflow.Workflow import org.cryptomator.presentation.workflow.Workflow
import org.cryptomator.util.ExceptionUtil import org.cryptomator.util.ExceptionUtil
import org.cryptomator.util.crypto.CredentialCryptor import org.cryptomator.util.crypto.CredentialCryptor
@ -433,6 +435,7 @@ class AuthenticateCloudPresenter @Inject constructor( //
private inner class LocalStorageAuthStrategy : AuthStrategy { private inner class LocalStorageAuthStrategy : AuthStrategy {
private var authenticationStarted = false private var authenticationStarted = false
override fun supports(cloud: CloudModel): Boolean { override fun supports(cloud: CloudModel): Boolean {
return cloud.cloudType() == CloudTypeModel.LOCAL return cloud.cloudType() == CloudTypeModel.LOCAL
} }
@ -445,22 +448,41 @@ class AuthenticateCloudPresenter @Inject constructor( //
private fun startAuthentication(cloud: CloudModel) { private fun startAuthentication(cloud: CloudModel) {
authenticationStarted = true authenticationStarted = true
requestPermissions(
PermissionsResultCallbacks.onLocalStorageAuthenticated(cloud), // val uri = (cloud as LocalStorageModel).uri()
R.string.permission_snackbar_auth_local_vault, //
Manifest.permission.READ_EXTERNAL_STORAGE, // val permissions = context().contentResolver.persistedUriPermissions
Manifest.permission.WRITE_EXTERNAL_STORAGE for (permission in permissions) {
) if (permission.uri.toString() == uri) {
succeedAuthenticationWith(cloud.toCloud())
}
}
Timber.tag("AuthicateCloudPrester").e("Permission revoked, ask to re-pick location")
Toast.makeText(context(), getString(R.string.permission_revoked_re_request_permission), Toast.LENGTH_LONG).show()
val openDocumentTree = Intent(ACTION_OPEN_DOCUMENT_TREE).apply {
putExtra(DocumentsContract.EXTRA_INITIAL_URI, uri)
}
requestActivityResult(ActivityResultCallbacks.rePickedLocalStorageLocation(cloud), openDocumentTree)
} }
} }
@Callback @Callback
fun onLocalStorageAuthenticated(result: PermissionsResult, cloud: CloudModel) { fun rePickedLocalStorageLocation(result: ActivityResult, cloud: LocalStorageModel) {
if (result.granted()) { val rootTreeUriOfLocalStorage = result.intent().data
succeedAuthenticationWith(cloud.toCloud()) rootTreeUriOfLocalStorage?.let {
} else { context() //
failAuthentication(PermissionNotGrantedException(R.string.permission_snackbar_auth_local_vault)) .contentResolver //
.takePersistableUriPermission( //
it, //
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
} }
Timber.tag("AuthicateCloudPrester").e("Permission granted again")
succeedAuthenticationWith(cloud.toCloud())
} }
private fun encrypt(password: String): String { private fun encrypt(password: String): String {

View File

@ -7,7 +7,6 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" /> <uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
@ -28,7 +27,6 @@
android:allowBackup="false" android:allowBackup="false"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
android:usesCleartextTraffic="true"> android:usesCleartextTraffic="true">

View File

@ -3,7 +3,6 @@ package org.cryptomator.presentation
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build
import org.cryptomator.presentation.service.CryptorsService import org.cryptomator.presentation.service.CryptorsService
import org.cryptomator.presentation.service.PhotoContentJob import org.cryptomator.presentation.service.PhotoContentJob
import org.cryptomator.util.SharedPreferencesHandler import org.cryptomator.util.SharedPreferencesHandler
@ -18,7 +17,7 @@ class BootAwareReceiver : BroadcastReceiver() {
context.stopService(CryptorsService.lockAllIntent(context)) context.stopService(CryptorsService.lockAllIntent(context))
} }
intent.action.equals(Intent.ACTION_BOOT_COMPLETED, ignoreCase = true) -> { intent.action.equals(Intent.ACTION_BOOT_COMPLETED, ignoreCase = true) -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && SharedPreferencesHandler(context).usePhotoUpload()) { if (SharedPreferencesHandler(context).usePhotoUpload()) {
Timber.tag("BootAwareReceiver").i("Starting AutoUploadJobScheduler") Timber.tag("BootAwareReceiver").i("Starting AutoUploadJobScheduler")
PhotoContentJob.scheduleJob(context) PhotoContentJob.scheduleJob(context)
} }

View File

@ -4,11 +4,8 @@ import android.Manifest
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.widget.Toast import android.widget.Toast
import androidx.annotation.RequiresApi
import org.cryptomator.domain.CloudFile import org.cryptomator.domain.CloudFile
import org.cryptomator.domain.CloudFolder import org.cryptomator.domain.CloudFolder
import org.cryptomator.domain.CloudNode import org.cryptomator.domain.CloudNode
@ -83,10 +80,8 @@ import org.cryptomator.util.SharedPreferencesHandler
import org.cryptomator.util.file.FileCacheUtils import org.cryptomator.util.file.FileCacheUtils
import org.cryptomator.util.file.MimeType import org.cryptomator.util.file.MimeType
import org.cryptomator.util.file.MimeTypes import org.cryptomator.util.file.MimeTypes
import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.Serializable import java.io.Serializable
import java.security.DigestInputStream import java.security.DigestInputStream
import java.security.MessageDigest import java.security.MessageDigest
@ -738,30 +733,6 @@ class BrowseFilesPresenter @Inject constructor( //
exportNodesToUserSelectedLocation(selectedCloudFiles, trigger) exportNodesToUserSelectedLocation(selectedCloudFiles, trigger)
} }
@Callback
fun exportFileToDownloadDirectory(result: PermissionsResult, fileToExport: CloudFileModel, exportOperation: ExportOperation) {
if (result.granted()) {
val downloads = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val cryptomatorDownloads = File(downloads, context().getString(R.string.download_subdirectory_name))
cryptomatorDownloads.mkdirs()
if (cryptomatorDownloads.isDirectory) {
val target = File(cryptomatorDownloads, fileToExport.name)
try {
val downloadFile = DownloadFile.Builder() //
.setDownloadFile(fileToExport.toCloudNode()) //
.setDataSink(FileOutputStream(target)) //
.build()
exportOperation.export(this, listOf(downloadFile))
} catch (e: FileNotFoundException) {
showError(e)
}
} else {
view?.showError(R.string.screen_file_browser_msg_creating_download_dir_failed)
}
}
}
@RequiresApi(Build.VERSION_CODES.KITKAT)
private fun exportFileToUserSelectedLocation(fileToExport: CloudFileModel, exportOperation: ExportOperation) { private fun exportFileToUserSelectedLocation(fileToExport: CloudFileModel, exportOperation: ExportOperation) {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE) intent.addCategory(Intent.CATEGORY_OPENABLE)
@ -789,7 +760,6 @@ class BrowseFilesPresenter @Inject constructor( //
} }
@Callback @Callback
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
fun pickedLocalStorageLocation( fun pickedLocalStorageLocation(
result: ActivityResult, // result: ActivityResult, //
nodesToExport: ArrayList<CloudNodeModel<*>>, // nodesToExport: ArrayList<CloudNodeModel<*>>, //
@ -809,7 +779,6 @@ class BrowseFilesPresenter @Inject constructor( //
disableSelectionMode() disableSelectionMode()
} }
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private fun collectNodesToExport( private fun collectNodesToExport(
parentUri: Uri, // parentUri: Uri, //
exportOperation: ExportOperation, // exportOperation: ExportOperation, //
@ -827,7 +796,6 @@ class BrowseFilesPresenter @Inject constructor( //
collectFolderContentForExport(parentUri, exportOperation, foldersForRecursiveDirListing, filesToExport) collectFolderContentForExport(parentUri, exportOperation, foldersForRecursiveDirListing, filesToExport)
} }
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private fun collectFolderContentForExport( private fun collectFolderContentForExport(
parentUri: Uri, exportOperation: ExportOperation, folders: List<CloudFolderModel>, // parentUri: Uri, exportOperation: ExportOperation, folders: List<CloudFolderModel>, //
filesToExport: List<CloudFileModel> filesToExport: List<CloudFileModel>
@ -847,7 +815,6 @@ class BrowseFilesPresenter @Inject constructor( //
}) })
} }
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private fun prepareExportingOf(parentUri: Uri, exportOperation: ExportOperation, filesToExport: List<CloudFileModel>, cloudNodeRecursiveListing: CloudNodeRecursiveListing) { private fun prepareExportingOf(parentUri: Uri, exportOperation: ExportOperation, filesToExport: List<CloudFileModel>, cloudNodeRecursiveListing: CloudNodeRecursiveListing) {
downloadFiles = ArrayList() downloadFiles = ArrayList()
downloadFiles.addAll(prepareFilesForExport(cloudFileModelMapper.fromModels(filesToExport), parentUri)) downloadFiles.addAll(prepareFilesForExport(cloudFileModelMapper.fromModels(filesToExport), parentUri))
@ -862,12 +829,10 @@ class BrowseFilesPresenter @Inject constructor( //
} }
} }
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private fun prepareFilesForExport(filesToExport: List<CloudFile>, parentUri: Uri): List<DownloadFile> { private fun prepareFilesForExport(filesToExport: List<CloudFile>, parentUri: Uri): List<DownloadFile> {
return filesToExport.mapTo(ArrayList()) { createDownloadFile(it, parentUri) } return filesToExport.mapTo(ArrayList()) { createDownloadFile(it, parentUri) }
} }
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private fun prepareFolderContentForExport(cloudFolderRecursiveListing: CloudFolderRecursiveListing, parentUri: Uri) { private fun prepareFolderContentForExport(cloudFolderRecursiveListing: CloudFolderRecursiveListing, parentUri: Uri) {
createFolder(parentUri, cloudFolderRecursiveListing.parent.name)?.let { createFolder(parentUri, cloudFolderRecursiveListing.parent.name)?.let {
downloadFiles.addAll(prepareFilesForExport(cloudFolderRecursiveListing.files, it)) downloadFiles.addAll(prepareFilesForExport(cloudFolderRecursiveListing.files, it))
@ -877,7 +842,6 @@ class BrowseFilesPresenter @Inject constructor( //
} ?: throw FatalBackendException("Failed to create parent folder for export") } ?: throw FatalBackendException("Failed to create parent folder for export")
} }
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private fun createFolder(parentUri: Uri, folderName: String): Uri? { private fun createFolder(parentUri: Uri, folderName: String): Uri? {
return try { return try {
DocumentsContract.createDocument( // DocumentsContract.createDocument( //
@ -892,7 +856,6 @@ class BrowseFilesPresenter @Inject constructor( //
} }
} }
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private fun createDownloadFile(file: CloudFile, documentUri: Uri): DownloadFile { private fun createDownloadFile(file: CloudFile, documentUri: Uri): DownloadFile {
return try { return try {
DownloadFile.Builder() // DownloadFile.Builder() //
@ -918,7 +881,6 @@ class BrowseFilesPresenter @Inject constructor( //
} }
} }
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Throws(IllegalFileNameException::class, NoSuchCloudFileException::class) @Throws(IllegalFileNameException::class, NoSuchCloudFileException::class)
private fun createNewDocumentUri(parentUri: Uri, fileName: String): Uri { private fun createNewDocumentUri(parentUri: Uri, fileName: String): Uri {
val mimeType = mimeTypes.fromFilename(fileName) ?: MimeType.APPLICATION_OCTET_STREAM val mimeType = mimeTypes.fromFilename(fileName) ?: MimeType.APPLICATION_OCTET_STREAM

View File

@ -3,9 +3,7 @@ package org.cryptomator.presentation.presenter
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build
import android.widget.Toast import android.widget.Toast
import androidx.annotation.RequiresApi
import org.cryptomator.domain.Cloud import org.cryptomator.domain.Cloud
import org.cryptomator.domain.LocalStorageCloud import org.cryptomator.domain.LocalStorageCloud
import org.cryptomator.domain.PCloud import org.cryptomator.domain.PCloud
@ -251,7 +249,6 @@ class CloudConnectionListPresenter @Inject constructor( //
} }
@Callback @Callback
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
fun pickedLocalStorageLocation(result: ActivityResult) { fun pickedLocalStorageLocation(result: ActivityResult) {
val rootTreeUriOfLocalStorage = result.intent().data val rootTreeUriOfLocalStorage = result.intent().data
persistUriPermission(rootTreeUriOfLocalStorage) persistUriPermission(rootTreeUriOfLocalStorage)
@ -266,7 +263,6 @@ class CloudConnectionListPresenter @Inject constructor( //
}) })
} }
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
private fun persistUriPermission(rootTreeUriOfLocalStorage: Uri?) { private fun persistUriPermission(rootTreeUriOfLocalStorage: Uri?) {
rootTreeUriOfLocalStorage?.let { rootTreeUriOfLocalStorage?.let {
context() // context() //
@ -278,7 +274,6 @@ class CloudConnectionListPresenter @Inject constructor( //
} }
} }
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
private fun releaseUriPermission(uri: String) { private fun releaseUriPermission(uri: String) {
context() // context() //
.contentResolver // .contentResolver //
@ -294,10 +289,6 @@ class CloudConnectionListPresenter @Inject constructor( //
} }
} }
fun onDefaultLocalCloudConnectionClicked() {
finishWithResult(SELECTED_CLOUD, defaultLocalStorageCloud)
}
companion object { companion object {
const val SELECTED_CLOUD = "selectedCloudConnection" const val SELECTED_CLOUD = "selectedCloudConnection"

View File

@ -3,8 +3,6 @@ package org.cryptomator.presentation.presenter
import android.Manifest import android.Manifest
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Environment
import org.cryptomator.domain.CloudFile import org.cryptomator.domain.CloudFile
import org.cryptomator.domain.CloudNode import org.cryptomator.domain.CloudNode
import org.cryptomator.domain.di.PerView import org.cryptomator.domain.di.PerView
@ -30,9 +28,7 @@ import org.cryptomator.presentation.util.ShareFileHelper
import org.cryptomator.presentation.workflow.ActivityResult import org.cryptomator.presentation.workflow.ActivityResult
import org.cryptomator.presentation.workflow.PermissionsResult import org.cryptomator.presentation.workflow.PermissionsResult
import org.cryptomator.util.ExceptionUtil import org.cryptomator.util.ExceptionUtil
import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
@ -58,37 +54,33 @@ class ImagePreviewPresenter @Inject constructor( //
@InstanceState @InstanceState
lateinit var pageIndexes: ArrayList<Int> lateinit var pageIndexes: ArrayList<Int>
fun onExportImageClicked(uri: Uri) { fun exportImageToUserSelectedLocation(uri: Uri) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
copyFileToDownloadDirectory(uri) intent.addCategory(Intent.CATEGORY_OPENABLE)
} else { intent.type = "*/*"
copyFileToUserSelectedLocation(uri) intent.putExtra(Intent.EXTRA_TITLE, contentResolverUtil.fileName(uri))
} requestActivityResult(ActivityResultCallbacks.exportImageToUserSelectedLocation(uri.toString()), intent)
} }
private fun copyFileToDownloadDirectory(uri: Uri) { @Callback
fun exportImageToUserSelectedLocation(result: ActivityResult, sourceUri: String?) {
requestPermissions( requestPermissions(
PermissionsResultCallbacks.copyFileToDownloadDirectory(uri.toString()), // PermissionsResultCallbacks.exportImageToUserSelectedLocation(result.intent()?.dataString, sourceUri), //
R.string.permission_message_export_file, Manifest.permission.WRITE_EXTERNAL_STORAGE R.string.permission_message_export_file, //
Manifest.permission.READ_EXTERNAL_STORAGE
) )
} }
@Callback @Callback
fun copyFileToDownloadDirectory(result: PermissionsResult, uriString: String?) { fun exportImageToUserSelectedLocation(result: PermissionsResult, targetUri: String?, sourceUri: String?) {
if (result.granted()) { if (result.granted()) {
val uriFile = Uri.parse(uriString) try {
val downloads = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) copyFile(
val cryptomatorDownloads = File(downloads, context().getString(R.string.download_subdirectory_name)) contentResolverUtil.openInputStream(Uri.parse(sourceUri)), //
cryptomatorDownloads.mkdirs() contentResolverUtil.openOutputStream(Uri.parse(targetUri))
if (cryptomatorDownloads.isDirectory) { )
val target = File(cryptomatorDownloads, contentResolverUtil.fileName(uriFile)) } catch (e: FileNotFoundException) {
try { showError(e)
copyFile(contentResolverUtil.openInputStream(uriFile), FileOutputStream(target))
} catch (e: FileNotFoundException) {
showError(e)
}
} else {
view?.showError(R.string.screen_file_browser_msg_creating_download_dir_failed)
} }
} }
} }
@ -107,37 +99,6 @@ class ImagePreviewPresenter @Inject constructor( //
}) })
} }
private fun copyFileToUserSelectedLocation(uri: Uri) {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "*/*"
intent.putExtra(Intent.EXTRA_TITLE, contentResolverUtil.fileName(uri))
requestActivityResult(ActivityResultCallbacks.copyFileToUserSelectedLocation(uri.toString()), intent)
}
@Callback
fun copyFileToUserSelectedLocation(result: ActivityResult, sourceUri: String?) {
requestPermissions(
PermissionsResultCallbacks.copyFileToUserSelectedLocation(result.intent()?.dataString, sourceUri), //
R.string.permission_message_export_file, //
Manifest.permission.READ_EXTERNAL_STORAGE
)
}
@Callback
fun copyFileToUserSelectedLocation(result: PermissionsResult, targetUri: String?, sourceUri: String?) {
if (result.granted()) {
try {
copyFile(
contentResolverUtil.openInputStream(Uri.parse(sourceUri)), //
contentResolverUtil.openOutputStream(Uri.parse(targetUri))
)
} catch (e: FileNotFoundException) {
showError(e)
}
}
}
fun onShareImageClicked(uri: Uri) { fun onShareImageClicked(uri: Uri) {
shareFileHelper.shareFile(this, uri) shareFileHelper.shareFile(this, uri)
} }

View File

@ -90,17 +90,14 @@ class SettingsPresenter @Inject internal constructor(
requestPermissions( requestPermissions(
PermissionsResultCallbacks.onLocalStoragePermissionGranted(), // PermissionsResultCallbacks.onLocalStoragePermissionGranted(), //
R.string.permission_snackbar_auth_auto_upload, // R.string.permission_snackbar_auth_auto_upload, //
Manifest.permission.READ_EXTERNAL_STORAGE, // Manifest.permission.READ_EXTERNAL_STORAGE
Manifest.permission.WRITE_EXTERNAL_STORAGE
) )
} }
@Callback @Callback
fun onLocalStoragePermissionGranted(result: PermissionsResult) { fun onLocalStoragePermissionGranted(result: PermissionsResult) {
if (result.granted()) { if (result.granted()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { scheduleJob(context())
scheduleJob(context())
}
} else { } else {
view?.disableAutoUpload() view?.disableAutoUpload()
} }

View File

@ -67,7 +67,7 @@ class UnlockVaultPresenter @Inject constructor(
super.destroyed() super.destroyed()
if (retryUnlockHandler != null) { if (retryUnlockHandler != null) {
running = false running = false
retryUnlockHandler?.removeCallbacks(null) retryUnlockHandler?.removeCallbacksAndMessages(null)
} }
} }
@ -140,11 +140,12 @@ class UnlockVaultPresenter @Inject constructor(
} }
} }
// FIXME why is this method not used?
fun onWindowFocusChanged(hasFocus: Boolean) { fun onWindowFocusChanged(hasFocus: Boolean) {
if (hasFocus) { if (hasFocus) {
if (retryUnlockHandler != null) { if (retryUnlockHandler != null) {
running = false running = false
retryUnlockHandler?.removeCallbacks(null) retryUnlockHandler?.removeCallbacksAndMessages(null)
} }
} }
} }

View File

@ -12,6 +12,7 @@ import org.cryptomator.data.cloud.crypto.CryptoCloud
import org.cryptomator.data.util.NetworkConnectionCheck import org.cryptomator.data.util.NetworkConnectionCheck
import org.cryptomator.domain.Cloud import org.cryptomator.domain.Cloud
import org.cryptomator.domain.CloudFolder import org.cryptomator.domain.CloudFolder
import org.cryptomator.domain.CloudType
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.exception.license.LicenseNotValidException import org.cryptomator.domain.exception.license.LicenseNotValidException
@ -46,10 +47,10 @@ import org.cryptomator.presentation.ui.activity.LicenseCheckActivity
import org.cryptomator.presentation.ui.activity.view.VaultListView import org.cryptomator.presentation.ui.activity.view.VaultListView
import org.cryptomator.presentation.ui.dialog.AppIsObscuredInfoDialog import org.cryptomator.presentation.ui.dialog.AppIsObscuredInfoDialog
import org.cryptomator.presentation.ui.dialog.AskForLockScreenDialog import org.cryptomator.presentation.ui.dialog.AskForLockScreenDialog
import org.cryptomator.presentation.ui.dialog.BetaConfirmationDialog
import org.cryptomator.presentation.ui.dialog.EnterPasswordDialog import org.cryptomator.presentation.ui.dialog.EnterPasswordDialog
import org.cryptomator.presentation.ui.dialog.UpdateAppAvailableDialog import org.cryptomator.presentation.ui.dialog.UpdateAppAvailableDialog
import org.cryptomator.presentation.ui.dialog.UpdateAppDialog import org.cryptomator.presentation.ui.dialog.UpdateAppDialog
import org.cryptomator.presentation.ui.dialog.VaultsRemovedDuringMigrationDialog
import org.cryptomator.presentation.util.FileUtil import org.cryptomator.presentation.util.FileUtil
import org.cryptomator.presentation.workflow.ActivityResult import org.cryptomator.presentation.workflow.ActivityResult
import org.cryptomator.presentation.workflow.AddExistingVaultWorkflow import org.cryptomator.presentation.workflow.AddExistingVaultWorkflow
@ -104,6 +105,12 @@ class VaultListPresenter @Inject constructor( //
sharedPreferencesHandler.setScreenLockDialogAlreadyShown() sharedPreferencesHandler.setScreenLockDialogAlreadyShown()
} }
sharedPreferencesHandler.vaultsRemovedDuringMigration()?.let {
val cloudNameString = getString(CloudTypeModel.valueOf(CloudType.valueOf(it.first)).displayNameResource)
view?.showDialog(VaultsRemovedDuringMigrationDialog.newInstance(Pair(cloudNameString, it.second)))
sharedPreferencesHandler.vaultsRemovedDuringMigration(null)
}
checkLicense() checkLicense()
} }
@ -119,9 +126,10 @@ class VaultListPresenter @Inject constructor( //
} }
override fun onError(e: Throwable) { override fun onError(e: Throwable) {
var license: String? = "" val license = if (e is LicenseNotValidException) {
if (e is LicenseNotValidException) { e.license
license = e.license } else {
""
} }
val intent = Intent(context(), LicenseCheckActivity::class.java) val intent = Intent(context(), LicenseCheckActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
@ -272,8 +280,8 @@ class VaultListPresenter @Inject constructor( //
view?.showVaultCreationHint() view?.showVaultCreationHint()
} else { } else {
view?.hideVaultCreationHint() view?.hideVaultCreationHint()
view?.renderVaultList(vaultModels)
} }
view?.renderVaultList(vaultModels)
} }
}) })
} }

View File

@ -66,7 +66,7 @@ class ImagePreviewActivity : BaseActivity(), ImagePreviewView, ConfirmDeleteClou
presenter.onDeleteImageClicked(imagePreviewFiles[imagePreviewSliderAdapter.getIndex(viewPager.currentItem)]) presenter.onDeleteImageClicked(imagePreviewFiles[imagePreviewSliderAdapter.getIndex(viewPager.currentItem)])
} }
exportImage.setOnClickListener { exportImage.setOnClickListener {
currentImageUri?.let { presenter.onExportImageClicked(it) } currentImageUri?.let { presenter.exportImageToUserSelectedLocation(it) }
} }
shareImage.setOnClickListener { shareImage.setOnClickListener {
currentImageUri?.let { presenter.onShareImageClicked(it) } currentImageUri?.let { presenter.onShareImageClicked(it) }

View File

@ -1,6 +1,5 @@
package org.cryptomator.presentation.ui.bottomsheet package org.cryptomator.presentation.ui.bottomsheet
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import org.cryptomator.generator.BottomSheet import org.cryptomator.generator.BottomSheet
@ -46,12 +45,10 @@ class FolderSettingsBottomSheet : BaseBottomSheet<FolderSettingsBottomSheet.Call
callback?.onMoveFolderClicked(cloudFolderModel) callback?.onMoveFolderClicked(cloudFolderModel)
dismiss() dismiss()
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { export_folder.visibility = View.VISIBLE
export_folder.visibility = View.VISIBLE export_folder.setOnClickListener {
export_folder.setOnClickListener { callback?.onExportFolderClicked(cloudFolderModel)
callback?.onExportFolderClicked(cloudFolderModel) dismiss()
dismiss()
}
} }
delete_folder.setOnClickListener { delete_folder.setOnClickListener {
callback?.onDeleteNodeClicked(cloudFolderModel) callback?.onDeleteNodeClicked(cloudFolderModel)

View File

@ -13,10 +13,10 @@ import kotlinx.android.synthetic.main.dialog_app_is_obscured_info.tv_app_is_obsc
class AppIsObscuredInfoDialog : BaseDialog<Activity>() { class AppIsObscuredInfoDialog : BaseDialog<Activity>() {
public override fun setupDialog(builder: AlertDialog.Builder): android.app.Dialog { public override fun setupDialog(builder: AlertDialog.Builder): android.app.Dialog {
builder // return builder //
.setTitle(R.string.dialog_app_is_obscured_info_title) // .setTitle(R.string.dialog_app_is_obscured_info_title) //
.setNeutralButton(R.string.dialog_app_is_obscured_info_neutral_button) { dialog: DialogInterface, _: Int -> dialog.dismiss() } .setNeutralButton(R.string.dialog_app_is_obscured_info_neutral_button) { dialog: DialogInterface, _: Int -> dialog.dismiss() } //
return builder.create() .create()
} }
override fun disableDialogWhenObscured(): Boolean { override fun disableDialogWhenObscured(): Boolean {

View File

@ -1,7 +1,6 @@
package org.cryptomator.presentation.ui.dialog package org.cryptomator.presentation.ui.dialog
import android.content.DialogInterface import android.content.DialogInterface
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.Html import android.text.Html
import android.view.View import android.view.View
@ -32,11 +31,7 @@ class UpdateAppAvailableDialog : BaseProgressErrorDialog<UpdateAppAvailableDialo
public override fun setupView() { public override fun setupView() {
val message = requireArguments().getSerializable(MESSAGE_ARG) as String val message = requireArguments().getSerializable(MESSAGE_ARG) as String
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { tv_message.text = Html.fromHtml(message, Html.FROM_HTML_MODE_COMPACT)
tv_message.text = Html.fromHtml(message, Html.FROM_HTML_MODE_COMPACT)
} else {
tv_message.text = Html.fromHtml(message)
}
} }
override fun enableViewAfterError(): View { override fun enableViewAfterError(): View {

View File

@ -0,0 +1,28 @@
package org.cryptomator.presentation.ui.dialog
import android.app.Activity
import android.content.DialogInterface
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import org.cryptomator.generator.Dialog
import org.cryptomator.presentation.R
@Dialog(R.layout.dialog_vault_is_root_folder_of_cloud)
class VaultIsRootFolderOfCloudDialog : BaseDialog<Activity>() {
public override fun setupDialog(builder: AlertDialog.Builder): android.app.Dialog {
return builder //
.setTitle(R.string.dialog_vault_is_root_folder_of_cloud_title) //
.setNeutralButton(R.string.dialog_vault_is_root_folder_of_cloud_neutral_button) { dialog: DialogInterface, _: Int -> dialog.dismiss() }
.create()
}
override fun setupView() {}
companion object {
fun newInstance(): DialogFragment {
return VaultIsRootFolderOfCloudDialog()
}
}
}

View File

@ -0,0 +1,47 @@
package org.cryptomator.presentation.ui.dialog
import android.app.Activity
import android.content.DialogInterface
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import org.cryptomator.generator.Dialog
import org.cryptomator.presentation.R
import kotlinx.android.synthetic.main.dialog_vaults_removed_during_migration.tv_message
@Dialog(R.layout.dialog_vaults_removed_during_migration)
class VaultsRemovedDuringMigrationDialog : BaseDialog<Activity>() {
public override fun setupDialog(builder: AlertDialog.Builder): android.app.Dialog {
val vaultsRemovedDuringMigration = requireArguments().getSerializable(VAULTS_REMOVED_ARG) as Pair<String, ArrayList<String>>
return builder //
.setTitle(String.format(getString(R.string.dialog_vaults_removed_during_migration_title), vaultsRemovedDuringMigration.first)) //
.setNeutralButton(R.string.dialog_vaults_removed_during_migration_neutral_button) { dialog: DialogInterface, _: Int -> dialog.dismiss() }
.create()
}
public override fun setupView() {
val vaultsRemovedDuringMigration = requireArguments().getSerializable(VAULTS_REMOVED_ARG) as Pair<String, ArrayList<String>>
val vaultsRemovedDuringMigrationString = vaultsRemovedDuringMigration
.second
.map { path -> "* $path" }
.reduce { acc, s -> "$acc\n$s" }
tv_message.text = String.format(getString(R.string.dialog_vaults_removed_during_migration_hint), vaultsRemovedDuringMigrationString)
}
companion object {
private const val VAULTS_REMOVED_ARG = "vaultsRemovedArg"
fun newInstance(vaultsRemovedDuringMigration: Pair<String, List<String>>): DialogFragment {
val args = Bundle()
args.putSerializable(VAULTS_REMOVED_ARG, vaultsRemovedDuringMigration)
val fragment = VaultsRemovedDuringMigrationDialog()
fragment.arguments = args
return fragment
}
}
}

View File

@ -1,6 +1,5 @@
package org.cryptomator.presentation.ui.fragment package org.cryptomator.presentation.ui.fragment
import android.os.Environment
import android.util.TypedValue import android.util.TypedValue
import android.view.View.GONE import android.view.View.GONE
import android.view.View.VISIBLE import android.view.View.VISIBLE
@ -13,10 +12,7 @@ import org.cryptomator.presentation.presenter.CloudConnectionListPresenter
import org.cryptomator.presentation.ui.adapter.CloudConnectionListAdapter import org.cryptomator.presentation.ui.adapter.CloudConnectionListAdapter
import javax.inject.Inject import javax.inject.Inject
import kotlinx.android.synthetic.main.fragment_browse_cloud_connections.floating_action_button import kotlinx.android.synthetic.main.fragment_browse_cloud_connections.floating_action_button
import kotlinx.android.synthetic.main.fragment_browse_cloud_connections.rv_local_default_cloud
import kotlinx.android.synthetic.main.recycler_view_layout.recyclerView import kotlinx.android.synthetic.main.recycler_view_layout.recyclerView
import kotlinx.android.synthetic.main.view_cloud_connection_content.cloudSubText
import kotlinx.android.synthetic.main.view_cloud_connection_content.cloudText
import kotlinx.android.synthetic.main.view_empty_cloud_connections.rl_creation_hint import kotlinx.android.synthetic.main.view_empty_cloud_connections.rl_creation_hint
@Fragment(R.layout.fragment_browse_cloud_connections) @Fragment(R.layout.fragment_browse_cloud_connections)
@ -42,7 +38,6 @@ class CloudConnectionListFragment : BaseFragment() {
override fun setupView() { override fun setupView() {
setupRecyclerView() setupRecyclerView()
rv_local_default_cloud.setOnClickListener { cloudConnectionListPresenter.onDefaultLocalCloudConnectionClicked() }
floating_action_button.setOnClickListener { cloudConnectionListPresenter.onAddConnectionClicked() } floating_action_button.setOnClickListener { cloudConnectionListPresenter.onAddConnectionClicked() }
} }
@ -71,11 +66,5 @@ class CloudConnectionListFragment : BaseFragment() {
fun setSelectedCloudType(selectedCloudType: CloudTypeModel) { fun setSelectedCloudType(selectedCloudType: CloudTypeModel) {
this.selectedCloudType = selectedCloudType this.selectedCloudType = selectedCloudType
if (CloudTypeModel.LOCAL == selectedCloudType) {
rv_local_default_cloud.visibility = VISIBLE
cloudText.text = getString(R.string.screen_cloud_local_default_storage_title)
cloudSubText.text = Environment.getExternalStorageDirectory().toString()
}
} }
} }

View File

@ -1,6 +1,5 @@
package org.cryptomator.presentation.ui.fragment package org.cryptomator.presentation.ui.fragment
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.SpannableString import android.text.SpannableString
import android.text.style.ForegroundColorSpan import android.text.style.ForegroundColorSpan
@ -259,9 +258,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
if (enabled) { if (enabled) {
activity().grantLocalStoragePermissionForAutoUpload() activity().grantLocalStoragePermissionForAutoUpload()
} else { } else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { PhotoContentJob.cancelJob(activity().applicationContext)
PhotoContentJob.cancelJob(activity().applicationContext)
}
} }
(findPreference(SharedPreferencesHandler.PHOTO_UPLOAD) as SwitchPreferenceCompat?)?.isChecked = enabled (findPreference(SharedPreferencesHandler.PHOTO_UPLOAD) as SwitchPreferenceCompat?)?.isChecked = enabled
} }

View File

@ -18,6 +18,7 @@ import org.cryptomator.presentation.model.ProgressModel;
import org.cryptomator.presentation.model.mappers.CloudModelMapper; import org.cryptomator.presentation.model.mappers.CloudModelMapper;
import org.cryptomator.presentation.presenter.ChooseCloudServicePresenter; import org.cryptomator.presentation.presenter.ChooseCloudServicePresenter;
import org.cryptomator.presentation.presenter.VaultListPresenter; import org.cryptomator.presentation.presenter.VaultListPresenter;
import org.cryptomator.presentation.ui.dialog.VaultIsRootFolderOfCloudDialog;
import java.io.Serializable; import java.io.Serializable;
import java.util.Arrays; import java.util.Arrays;
@ -116,9 +117,13 @@ public class AddExistingVaultWorkflow extends Workflow<AddExistingVaultWorkflow.
@Callback @Callback
void cryptomatorFileChosen(SerializableResult<CloudFileModel> result) { void cryptomatorFileChosen(SerializableResult<CloudFileModel> result) {
CloudFileModel masterkeyFile = result.getResult(); CloudFileModel masterkeyFile = result.getResult();
state().masterkeyFile = masterkeyFile.toCloudNode(); if(!masterkeyFile.getPath().equals("/masterkey.cryptomator") && !masterkeyFile.getPath().equals("/vault.cryptomator")) {
presenter().getView().showProgress(ProgressModel.GENERIC); state().masterkeyFile = masterkeyFile.toCloudNode();
finish(); presenter().getView().showProgress(ProgressModel.GENERIC);
finish();
} else {
presenter().getView().showDialog(VaultIsRootFolderOfCloudDialog.Companion.newInstance());
}
} }
@Override @Override

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/activity_vertical_margin">
<TextView
android:id="@+id/tv_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:text="@string/dialog_vault_is_root_folder_of_cloud_hint" />
</RelativeLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/activity_vertical_margin">
<TextView
android:id="@+id/tv_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:text="@string/dialog_vaults_removed_during_migration_hint" />
</RelativeLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -9,38 +9,10 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<RelativeLayout
android:id="@+id/rv_local_default_cloud"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone">
<include
android:id="@+id/default_local_cloud"
layout="@layout/view_default_local_cloud"
android:layout_width="match_parent"
android:layout_height="72dp"
android:clickable="true"
android:clipToPadding="true"
android:focusable="true" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/default_local_cloud"
android:layout_marginStart="16dp"
android:singleLine="true"
android:text="@string/screen_cloud_local_custom_storage_title"
android:textColor="@color/colorPrimary"
android:textSize="16sp" />
</RelativeLayout>
<include <include
layout="@layout/recycler_view_layout" layout="@layout/recycler_view_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_below="@+id/rv_local_default_cloud"
android:clipToPadding="true" android:clipToPadding="true"
android:paddingBottom="88dp" /> android:paddingBottom="88dp" />

View File

@ -61,6 +61,8 @@
<string name="permission_message_upload_file">Cryptomator needs storage access to upload files</string> <string name="permission_message_upload_file">Cryptomator needs storage access to upload files</string>
<string name="permission_message_share_file">Cryptomator needs storage access to share files</string> <string name="permission_message_share_file">Cryptomator needs storage access to share files</string>
<string name="permission_revoked_re_request_permission">Cryptomator has lost permission to access this location. Please select this folder again to restore the permission.</string>
<string name="snack_bar_action_title_settings">Settings</string> <string name="snack_bar_action_title_settings">Settings</string>
<string name="snack_bar_action_title_search">Search</string> <string name="snack_bar_action_title_search">Search</string>
<string name="snack_bar_action_title_search_previous">Previous</string> <string name="snack_bar_action_title_search_previous">Previous</string>
@ -166,8 +168,6 @@
<string name="screen_cloud_settings_option_delete" translatable="false">@string/screen_vault_list_vault_action_delete</string> <string name="screen_cloud_settings_option_delete" translatable="false">@string/screen_vault_list_vault_action_delete</string>
<string name="screen_cloud_connections_no_connections">Click here to add locations</string> <string name="screen_cloud_connections_no_connections">Click here to add locations</string>
<string name="screen_cloud_error_webdav_not_supported">Server doesn\'t seem to be WebDAV compatible</string> <string name="screen_cloud_error_webdav_not_supported">Server doesn\'t seem to be WebDAV compatible</string>
<string name="screen_cloud_local_custom_storage_title">Custom locations</string>
<string name="screen_cloud_local_default_storage_title">Default storage</string>
<string name="screen_cloud_local_error_no_content_provider">No additional locations available.</string> <string name="screen_cloud_local_error_no_content_provider">No additional locations available.</string>
<!-- ## screen: webdav settings --> <!-- ## screen: webdav settings -->
@ -417,6 +417,14 @@
<string name="dialog_app_is_obscured_info_hint">Another app is displaying something on top of Cryptomator (e.g., a blue light filter or night mode app). For security reasons, Cryptomator is disabled.\n\n<a href="https://docs.cryptomator.org/en/1.5/android/settings/#block-app-when-obscured">How to enable Cryptomator</a></string> <string name="dialog_app_is_obscured_info_hint">Another app is displaying something on top of Cryptomator (e.g., a blue light filter or night mode app). For security reasons, Cryptomator is disabled.\n\n<a href="https://docs.cryptomator.org/en/1.5/android/settings/#block-app-when-obscured">How to enable Cryptomator</a></string>
<string name="dialog_app_is_obscured_info_neutral_button">Close</string> <string name="dialog_app_is_obscured_info_neutral_button">Close</string>
<string name="dialog_vaults_removed_during_migration_title">Please re-add vaults for %1s cloud</string>
<string name="dialog_vaults_removed_during_migration_hint">While migrating to this app version we need to remove the following vaults from the app:\n%2s \n\nThose vaults aren\'t removed from the cloud but only from this app. Sorry for the inconvenience and please re-add these vaults to continue working with them.</string>
<string name="dialog_vaults_removed_during_migration_neutral_button" translatable="false">@string/dialog_unable_to_share_positive_button</string>
<string name="dialog_vault_is_root_folder_of_cloud_title">Vault is root folder of the cloud connection</string>
<string name="dialog_vault_is_root_folder_of_cloud_hint">Create a new cloud connection where you select at least the parent folder of this vault folder as the root directory to add this vault.</string>
<string name="dialog_vault_is_root_folder_of_cloud_neutral_button" translatable="false">@string/dialog_unable_to_share_positive_button</string>
<string name="dialog_disable_secure_screen_disclaimer_hint">This setting is a security feature and prevents other apps from tricking users into doing things they do not wan\'t to do.\n\nBy disabling, you confirm that you are <a href="https://docs.cryptomator.org/en/1.5/android/settings/#screen-security">aware of the risks</a>.</string> <string name="dialog_disable_secure_screen_disclaimer_hint">This setting is a security feature and prevents other apps from tricking users into doing things they do not wan\'t to do.\n\nBy disabling, you confirm that you are <a href="https://docs.cryptomator.org/en/1.5/android/settings/#screen-security">aware of the risks</a>.</string>
<string name="dialog_delete_cloud_connection_with_vaults_message">Are you sure you want to remove this cloud connection?</string> <string name="dialog_delete_cloud_connection_with_vaults_message">Are you sure you want to remove this cloud connection?</string>

View File

@ -1,8 +1,10 @@
package org.cryptomator.presentation.presenter package org.cryptomator.presentation.presenter
import android.Manifest
import android.accounts.AccountManager import android.accounts.AccountManager
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.Intent.ACTION_OPEN_DOCUMENT_TREE
import android.provider.DocumentsContract
import android.widget.Toast import android.widget.Toast
import com.dropbox.core.android.Auth import com.dropbox.core.android.Auth
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential
@ -38,6 +40,7 @@ import org.cryptomator.presentation.intent.AuthenticateCloudIntent
import org.cryptomator.presentation.intent.Intents import org.cryptomator.presentation.intent.Intents
import org.cryptomator.presentation.model.CloudModel import org.cryptomator.presentation.model.CloudModel
import org.cryptomator.presentation.model.CloudTypeModel import org.cryptomator.presentation.model.CloudTypeModel
import org.cryptomator.presentation.model.LocalStorageModel
import org.cryptomator.presentation.model.ProgressModel import org.cryptomator.presentation.model.ProgressModel
import org.cryptomator.presentation.model.ProgressStateModel import org.cryptomator.presentation.model.ProgressStateModel
import org.cryptomator.presentation.model.S3CloudModel import org.cryptomator.presentation.model.S3CloudModel
@ -47,7 +50,6 @@ import org.cryptomator.presentation.ui.activity.view.AuthenticateCloudView
import org.cryptomator.presentation.workflow.ActivityResult import org.cryptomator.presentation.workflow.ActivityResult
import org.cryptomator.presentation.workflow.AddExistingVaultWorkflow import org.cryptomator.presentation.workflow.AddExistingVaultWorkflow
import org.cryptomator.presentation.workflow.CreateNewVaultWorkflow import org.cryptomator.presentation.workflow.CreateNewVaultWorkflow
import org.cryptomator.presentation.workflow.PermissionsResult
import org.cryptomator.presentation.workflow.Workflow import org.cryptomator.presentation.workflow.Workflow
import org.cryptomator.util.ExceptionUtil import org.cryptomator.util.ExceptionUtil
import org.cryptomator.util.crypto.CredentialCryptor import org.cryptomator.util.crypto.CredentialCryptor
@ -479,6 +481,7 @@ class AuthenticateCloudPresenter @Inject constructor( //
private inner class LocalStorageAuthStrategy : AuthStrategy { private inner class LocalStorageAuthStrategy : AuthStrategy {
private var authenticationStarted = false private var authenticationStarted = false
override fun supports(cloud: CloudModel): Boolean { override fun supports(cloud: CloudModel): Boolean {
return cloud.cloudType() == CloudTypeModel.LOCAL return cloud.cloudType() == CloudTypeModel.LOCAL
} }
@ -491,22 +494,41 @@ class AuthenticateCloudPresenter @Inject constructor( //
private fun startAuthentication(cloud: CloudModel) { private fun startAuthentication(cloud: CloudModel) {
authenticationStarted = true authenticationStarted = true
requestPermissions(
PermissionsResultCallbacks.onLocalStorageAuthenticated(cloud), // val uri = (cloud as LocalStorageModel).uri()
R.string.permission_snackbar_auth_local_vault, //
Manifest.permission.READ_EXTERNAL_STORAGE, // val permissions = context().contentResolver.persistedUriPermissions
Manifest.permission.WRITE_EXTERNAL_STORAGE for (permission in permissions) {
) if (permission.uri.toString() == uri) {
succeedAuthenticationWith(cloud.toCloud())
}
}
Timber.tag("AuthicateCloudPrester").e("Permission revoked, ask to re-pick location")
Toast.makeText(context(), getString(R.string.permission_revoked_re_request_permission), Toast.LENGTH_LONG).show()
val openDocumentTree = Intent(ACTION_OPEN_DOCUMENT_TREE).apply {
putExtra(DocumentsContract.EXTRA_INITIAL_URI, uri)
}
requestActivityResult(ActivityResultCallbacks.rePickedLocalStorageLocation(cloud), openDocumentTree)
} }
} }
@Callback @Callback
fun onLocalStorageAuthenticated(result: PermissionsResult, cloud: CloudModel) { fun rePickedLocalStorageLocation(result: ActivityResult, cloud: LocalStorageModel) {
if (result.granted()) { val rootTreeUriOfLocalStorage = result.intent().data
succeedAuthenticationWith(cloud.toCloud()) rootTreeUriOfLocalStorage?.let {
} else { context() //
failAuthentication(PermissionNotGrantedException(R.string.permission_snackbar_auth_local_vault)) .contentResolver //
.takePersistableUriPermission( //
it, //
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
} }
Timber.tag("AuthicateCloudPrester").e("Permission granted again")
succeedAuthenticationWith(cloud.toCloud())
} }
private fun encrypt(password: String): String { private fun encrypt(password: String): String {

View File

@ -236,6 +236,31 @@ constructor(context: Context) : SharedPreferences.OnSharedPreferenceChangeListen
return defaultSharedPreferences.getBoolean(BACKGROUND_UNLOCK_PREPARATION, true) return defaultSharedPreferences.getBoolean(BACKGROUND_UNLOCK_PREPARATION, true)
} }
fun vaultsRemovedDuringMigration(vaultsToBeRemoved: Pair<String, List<String>>?) {
vaultsToBeRemoved?.let {
val vaultsToBeRemovedString = if (it.second.isNotEmpty()) {
it.second.reduce { acc, s -> "$acc,$s" }
} else {
""
}
defaultSharedPreferences.setValue(VAULTS_REMOVED_DURING_MIGRATION_TYPE, it.first)
defaultSharedPreferences.setValue(VAULTS_REMOVED_DURING_MIGRATION, vaultsToBeRemovedString)
} ?: run {
defaultSharedPreferences.setValue(VAULTS_REMOVED_DURING_MIGRATION_TYPE, null)
defaultSharedPreferences.setValue(VAULTS_REMOVED_DURING_MIGRATION, null)
}
}
fun vaultsRemovedDuringMigration(): Pair<String, List<String>>? {
val vaultsRemovedDuringMigrationType = defaultSharedPreferences.getString(VAULTS_REMOVED_DURING_MIGRATION_TYPE, null)
val vaultsRemovedDuringMigration = defaultSharedPreferences.getString(VAULTS_REMOVED_DURING_MIGRATION, null)
return if(vaultsRemovedDuringMigrationType != null && vaultsRemovedDuringMigration != null) {
Pair(vaultsRemovedDuringMigrationType, ArrayList(vaultsRemovedDuringMigration.split(',')))
} else {
null
}
}
companion object { companion object {
private const val SCREEN_LOCK_DIALOG_SHOWN = "askForScreenLockDialogShown" private const val SCREEN_LOCK_DIALOG_SHOWN = "askForScreenLockDialogShown"
@ -248,6 +273,8 @@ constructor(context: Context) : SharedPreferences.OnSharedPreferenceChangeListen
private const val GLOB_SEARCH = "globSearch" private const val GLOB_SEARCH = "globSearch"
private const val KEEP_UNLOCKED_WHILE_EDITING = "keepUnlockedWhileEditing" private const val KEEP_UNLOCKED_WHILE_EDITING = "keepUnlockedWhileEditing"
private const val BACKGROUND_UNLOCK_PREPARATION = "backgroundUnlockPreparation" private const val BACKGROUND_UNLOCK_PREPARATION = "backgroundUnlockPreparation"
private const val VAULTS_REMOVED_DURING_MIGRATION = "vaultsRemovedDuringMigration"
private const val VAULTS_REMOVED_DURING_MIGRATION_TYPE = "vaultsRemovedDuringMigrationType"
const val DEBUG_MODE = "debugMode" const val DEBUG_MODE = "debugMode"
const val DISABLE_APP_WHEN_OBSCURED = "disableAppWhenObscured" const val DISABLE_APP_WHEN_OBSCURED = "disableAppWhenObscured"
const val SECURE_SCREEN = "secureScreen" const val SECURE_SCREEN = "secureScreen"

View File

@ -1,7 +1,6 @@
package org.cryptomator.util.crypto; package org.cryptomator.util.crypto;
import android.content.Context; import android.content.Context;
import android.os.Build;
import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties; import android.security.keystore.KeyProperties;
@ -43,13 +42,10 @@ class CryptoOperationsImpl implements CryptoOperations {
KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec // KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec //
.Builder(alias, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) // .Builder(alias, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) //
.setBlockModes(KeyProperties.BLOCK_MODE_CBC) // .setBlockModes(KeyProperties.BLOCK_MODE_CBC) //
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7); .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) //
.setUserAuthenticationRequired(requireUserAuthentication) //
.setInvalidatedByBiometricEnrollment(requireUserAuthentication);
builder.setUserAuthenticationRequired(requireUserAuthentication);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
builder.setInvalidatedByBiometricEnrollment(requireUserAuthentication);
}
generator.init(builder.build()); generator.init(builder.build());
generator.generateKey(); generator.generateKey();
}; };