Merge branch 'feature/251-update-to-API-level-30' into develop
This commit is contained in:
commit
c18632106e
@ -8,8 +8,8 @@ allprojects {
|
||||
ext {
|
||||
androidBuildToolsVersion = "30.0.2"
|
||||
androidMinSdkVersion = 24
|
||||
androidTargetSdkVersion = 29
|
||||
androidCompileSdkVersion = 29
|
||||
androidTargetSdkVersion = 30
|
||||
androidCompileSdkVersion = 30
|
||||
|
||||
// android and java libs
|
||||
androidVersion = '4.1.1.4'
|
||||
|
@ -82,7 +82,7 @@ android {
|
||||
}
|
||||
|
||||
greendao {
|
||||
schemaVersion 9
|
||||
schemaVersion 10
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
|
@ -50,6 +50,7 @@ class UpgradeDatabaseTest {
|
||||
Upgrade6To7().applyTo(db, 6)
|
||||
Upgrade7To8().applyTo(db, 7)
|
||||
Upgrade8To9(sharedPreferencesHandler).applyTo(db, 8)
|
||||
Upgrade9To10(sharedPreferencesHandler).applyTo(db, 9)
|
||||
|
||||
CloudEntityDao(DaoConfig(db, CloudEntityDao::class.java)).loadAll()
|
||||
VaultEntityDao(DaoConfig(db, VaultEntityDao::class.java)).loadAll()
|
||||
@ -407,4 +408,66 @@ class UpgradeDatabaseTest {
|
||||
|
||||
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"))))
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -31,11 +31,6 @@ public class CryptoCloud implements Cloud {
|
||||
return vault.equals(cloud.vault);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean predefined() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean persistent() {
|
||||
return false;
|
||||
|
@ -1,4 +1,4 @@
|
||||
package org.cryptomator.data.cloud.local.storageaccessframework
|
||||
package org.cryptomator.data.cloud.local
|
||||
|
||||
import android.util.LruCache
|
||||
import org.cryptomator.domain.CloudFolder
|
@ -1,4 +1,4 @@
|
||||
package org.cryptomator.data.cloud.local.storageaccessframework
|
||||
package org.cryptomator.data.cloud.local
|
||||
|
||||
import android.net.Uri
|
||||
import org.cryptomator.domain.Cloud
|
@ -1,4 +1,4 @@
|
||||
package org.cryptomator.data.cloud.local.storageaccessframework
|
||||
package org.cryptomator.data.cloud.local
|
||||
|
||||
import android.net.Uri
|
||||
import org.cryptomator.domain.Cloud
|
@ -1,4 +1,4 @@
|
||||
package org.cryptomator.data.cloud.local.storageaccessframework
|
||||
package org.cryptomator.data.cloud.local
|
||||
|
||||
import android.content.Context
|
||||
import org.cryptomator.domain.LocalStorageCloud
|
@ -1,4 +1,4 @@
|
||||
package org.cryptomator.data.cloud.local.storageaccessframework
|
||||
package org.cryptomator.data.cloud.local
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
@ -7,10 +7,10 @@ import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.DocumentsContract
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import org.cryptomator.data.cloud.local.storageaccessframework.LocalStorageAccessFrameworkNodeFactory.file
|
||||
import org.cryptomator.data.cloud.local.storageaccessframework.LocalStorageAccessFrameworkNodeFactory.folder
|
||||
import org.cryptomator.data.cloud.local.storageaccessframework.LocalStorageAccessFrameworkNodeFactory.from
|
||||
import org.cryptomator.data.cloud.local.storageaccessframework.LocalStorageAccessFrameworkNodeFactory.getNodePath
|
||||
import org.cryptomator.data.cloud.local.LocalStorageAccessFrameworkNodeFactory.file
|
||||
import org.cryptomator.data.cloud.local.LocalStorageAccessFrameworkNodeFactory.folder
|
||||
import org.cryptomator.data.cloud.local.LocalStorageAccessFrameworkNodeFactory.from
|
||||
import org.cryptomator.data.cloud.local.LocalStorageAccessFrameworkNodeFactory.getNodePath
|
||||
import org.cryptomator.data.util.CopyStream
|
||||
import org.cryptomator.data.util.TransferredBytesAwareInputStream
|
||||
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 {
|
||||
source.parent?.let { parent ->
|
||||
var newUri = try {
|
||||
DocumentsContract.renameDocument(contentResolver(), source.uri, name)
|
||||
requireNotNull(source.uri)
|
||||
DocumentsContract.renameDocument(contentResolver(), source.uri!!, name)
|
||||
} catch (e: FileNotFoundException) {
|
||||
/* Bug in Android 9 see #460 TLDR; In this renameDocument-method, Android 9 throws
|
||||
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?> {
|
||||
return Supplier {
|
||||
val mimeType = if (mimeTypes.fromFilename(file.name) == null) MimeType.APPLICATION_OCTET_STREAM else mimeTypes.fromFilename(file.name)
|
||||
try {
|
||||
DocumentsContract.createDocument(contentResolver(), file.parent.uri, mimeType.toString(), file.name) // FIXME
|
||||
} catch (e: FileNotFoundException) {
|
||||
null
|
||||
file.parent.uri?.let {
|
||||
val mimeType = if (mimeTypes.fromFilename(file.name) == null) MimeType.APPLICATION_OCTET_STREAM else mimeTypes.fromFilename(file.name)
|
||||
try {
|
||||
DocumentsContract.createDocument(contentResolver(), it, mimeType.toString(), file.name)
|
||||
} catch (e: FileNotFoundException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -372,7 +375,7 @@ internal class LocalStorageAccessFrameworkImpl(context: Context, private val mim
|
||||
fun delete(node: LocalStorageAccessNode) {
|
||||
requireNotNull(node.uri)
|
||||
try {
|
||||
DocumentsContract.deleteDocument(contentResolver(), node.uri)
|
||||
DocumentsContract.deleteDocument(contentResolver(), node.uri!!)
|
||||
} catch (e: FileNotFoundException) {
|
||||
throw NoSuchCloudFileException(node.name)
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package org.cryptomator.data.cloud.local.storageaccessframework
|
||||
package org.cryptomator.data.cloud.local
|
||||
|
||||
import android.database.Cursor
|
||||
import android.provider.DocumentsContract
|
@ -1,4 +1,4 @@
|
||||
package org.cryptomator.data.cloud.local.storageaccessframework
|
||||
package org.cryptomator.data.cloud.local
|
||||
|
||||
import android.net.Uri
|
||||
import org.cryptomator.domain.CloudNode
|
@ -1,9 +1,10 @@
|
||||
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.domain.Cloud;
|
||||
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.util.file.MimeTypes;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
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
|
||||
public class LocalStorageContentRepositoryFactory implements CloudContentRepositoryFactory {
|
||||
|
||||
@ -39,23 +36,14 @@ public class LocalStorageContentRepositoryFactory implements CloudContentReposit
|
||||
|
||||
@Override
|
||||
public CloudContentRepository cloudContentRepositoryFor(Cloud cloud) {
|
||||
if (!hasPermissions(WRITE_EXTERNAL_STORAGE, READ_EXTERNAL_STORAGE)) {
|
||||
throw new NoAuthenticationProvidedException(cloud);
|
||||
}
|
||||
if (((LocalStorageCloud) cloud).rootUri() != null) {
|
||||
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;
|
||||
List<UriPermission> permissions = context.getContentResolver().getPersistedUriPermissions();
|
||||
for (UriPermission permission : permissions) {
|
||||
if(permission.getUri().toString().equals(((LocalStorageCloud) cloud).rootUri())) {
|
||||
return new LocalStorageAccessFrameworkContentRepository(context, mimeTypes, (LocalStorageCloud) cloud);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
throw new NoAuthenticationProvidedException(cloud);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
package org.cryptomator.data.cloud.local.storageaccessframework
|
||||
package org.cryptomator.data.cloud.local
|
||||
|
||||
import android.net.Uri
|
||||
import android.provider.DocumentsContract
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
package org.cryptomator.data.cloud.local.file
|
||||
|
||||
import org.cryptomator.domain.CloudNode
|
||||
|
||||
interface LocalNode : CloudNode {
|
||||
|
||||
override val parent: LocalFolder?
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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)))
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -27,7 +27,8 @@ class DatabaseUpgrades {
|
||||
Upgrade5To6 upgrade5To6, //
|
||||
Upgrade6To7 upgrade6To7, //
|
||||
Upgrade7To8 upgrade7To8, //
|
||||
Upgrade8To9 upgrade8To9) {
|
||||
Upgrade8To9 upgrade8To9, //
|
||||
Upgrade9To10 upgrade9To10) {
|
||||
|
||||
availableUpgrades = defineUpgrades( //
|
||||
upgrade0To1, //
|
||||
@ -38,7 +39,8 @@ class DatabaseUpgrades {
|
||||
upgrade5To6, //
|
||||
upgrade6To7, //
|
||||
upgrade7To8, //
|
||||
upgrade8To9);
|
||||
upgrade8To9, //
|
||||
upgrade9To10);
|
||||
}
|
||||
|
||||
private static Comparator<DatabaseUpgrade> reverseOrder() {
|
||||
|
48
data/src/main/java/org/cryptomator/data/db/Upgrade9To10.kt
Normal file
48
data/src/main/java/org/cryptomator/data/db/Upgrade9To10.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
@ -76,9 +76,6 @@ class CloudRepositoryImpl implements CloudRepository {
|
||||
|
||||
@Override
|
||||
public void delete(Cloud cloud) {
|
||||
if (cloud.predefined()) {
|
||||
throw new IllegalArgumentException("Can not delete predefined cloud");
|
||||
}
|
||||
if (!cloud.persistent()) {
|
||||
throw new IllegalArgumentException("Can not delete non persistent cloud");
|
||||
}
|
||||
|
@ -28,6 +28,6 @@ class NetworkConnectionCheck @Inject internal constructor(private val context: C
|
||||
fun checkWifiOnAndConnected(): Boolean {
|
||||
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
val activeNetwork = connectivityManager.activeNetwork
|
||||
return connectivityManager.getNetworkCapabilities(activeNetwork).hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
return connectivityManager.getNetworkCapabilities(activeNetwork)?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ interface Cloud : Serializable {
|
||||
fun id(): Long?
|
||||
fun type(): CloudType?
|
||||
fun configurationMatches(cloud: Cloud?): Boolean
|
||||
fun predefined(): Boolean
|
||||
fun persistent(): Boolean
|
||||
fun requiresNetwork(): Boolean
|
||||
}
|
||||
|
@ -48,11 +48,6 @@ public class DropboxCloud implements Cloud {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean predefined() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean persistent() {
|
||||
return true;
|
||||
|
@ -48,11 +48,6 @@ public class GoogleDriveCloud implements Cloud {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean predefined() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean persistent() {
|
||||
return true;
|
||||
|
@ -1,6 +1,5 @@
|
||||
package org.cryptomator.domain;
|
||||
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
|
||||
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
|
||||
public boolean persistent() {
|
||||
return true;
|
||||
|
@ -43,11 +43,6 @@ public class OnedriveCloud implements Cloud {
|
||||
return CloudType.ONEDRIVE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean predefined() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean persistent() {
|
||||
return true;
|
||||
|
@ -59,12 +59,6 @@ public class PCloud implements Cloud {
|
||||
return username.equals(cloud.username);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean predefined() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean persistent() {
|
||||
return true;
|
||||
|
@ -80,12 +80,6 @@ public class S3Cloud implements Cloud {
|
||||
return s3Bucket.equals(cloud.s3Bucket) && s3Endpoint.equals(cloud.s3Endpoint) && s3Region.equals(cloud.s3Region);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean predefined() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean persistent() {
|
||||
return true;
|
||||
|
@ -66,11 +66,6 @@ public class WebDavCloud implements Cloud {
|
||||
return certificate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean predefined() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean persistent() {
|
||||
return true;
|
||||
|
@ -1,7 +1,9 @@
|
||||
package org.cryptomator.presentation.presenter
|
||||
|
||||
import android.Manifest
|
||||
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 com.dropbox.core.android.Auth
|
||||
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.model.CloudModel
|
||||
import org.cryptomator.presentation.model.CloudTypeModel
|
||||
import org.cryptomator.presentation.model.LocalStorageModel
|
||||
import org.cryptomator.presentation.model.ProgressModel
|
||||
import org.cryptomator.presentation.model.ProgressStateModel
|
||||
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.AddExistingVaultWorkflow
|
||||
import org.cryptomator.presentation.workflow.CreateNewVaultWorkflow
|
||||
import org.cryptomator.presentation.workflow.PermissionsResult
|
||||
import org.cryptomator.presentation.workflow.Workflow
|
||||
import org.cryptomator.util.ExceptionUtil
|
||||
import org.cryptomator.util.crypto.CredentialCryptor
|
||||
@ -433,6 +435,7 @@ class AuthenticateCloudPresenter @Inject constructor( //
|
||||
private inner class LocalStorageAuthStrategy : AuthStrategy {
|
||||
|
||||
private var authenticationStarted = false
|
||||
|
||||
override fun supports(cloud: CloudModel): Boolean {
|
||||
return cloud.cloudType() == CloudTypeModel.LOCAL
|
||||
}
|
||||
@ -445,22 +448,41 @@ class AuthenticateCloudPresenter @Inject constructor( //
|
||||
|
||||
private fun startAuthentication(cloud: CloudModel) {
|
||||
authenticationStarted = true
|
||||
requestPermissions(
|
||||
PermissionsResultCallbacks.onLocalStorageAuthenticated(cloud), //
|
||||
R.string.permission_snackbar_auth_local_vault, //
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE, //
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
)
|
||||
|
||||
val uri = (cloud as LocalStorageModel).uri()
|
||||
|
||||
val permissions = context().contentResolver.persistedUriPermissions
|
||||
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
|
||||
fun onLocalStorageAuthenticated(result: PermissionsResult, cloud: CloudModel) {
|
||||
if (result.granted()) {
|
||||
succeedAuthenticationWith(cloud.toCloud())
|
||||
} else {
|
||||
failAuthentication(PermissionNotGrantedException(R.string.permission_snackbar_auth_local_vault))
|
||||
fun rePickedLocalStorageLocation(result: ActivityResult, cloud: LocalStorageModel) {
|
||||
val rootTreeUriOfLocalStorage = result.intent().data
|
||||
rootTreeUriOfLocalStorage?.let {
|
||||
context() //
|
||||
.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 {
|
||||
|
@ -7,7 +7,6 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_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.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
@ -28,7 +27,6 @@
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true">
|
||||
|
@ -3,7 +3,6 @@ package org.cryptomator.presentation
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import org.cryptomator.presentation.service.CryptorsService
|
||||
import org.cryptomator.presentation.service.PhotoContentJob
|
||||
import org.cryptomator.util.SharedPreferencesHandler
|
||||
@ -18,7 +17,7 @@ class BootAwareReceiver : BroadcastReceiver() {
|
||||
context.stopService(CryptorsService.lockAllIntent(context))
|
||||
}
|
||||
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")
|
||||
PhotoContentJob.scheduleJob(context)
|
||||
}
|
||||
|
@ -4,11 +4,8 @@ import android.Manifest
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.DocumentsContract
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import org.cryptomator.domain.CloudFile
|
||||
import org.cryptomator.domain.CloudFolder
|
||||
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.MimeType
|
||||
import org.cryptomator.util.file.MimeTypes
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.FileOutputStream
|
||||
import java.io.Serializable
|
||||
import java.security.DigestInputStream
|
||||
import java.security.MessageDigest
|
||||
@ -738,30 +733,6 @@ class BrowseFilesPresenter @Inject constructor( //
|
||||
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) {
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
@ -789,7 +760,6 @@ class BrowseFilesPresenter @Inject constructor( //
|
||||
}
|
||||
|
||||
@Callback
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
fun pickedLocalStorageLocation(
|
||||
result: ActivityResult, //
|
||||
nodesToExport: ArrayList<CloudNodeModel<*>>, //
|
||||
@ -809,7 +779,6 @@ class BrowseFilesPresenter @Inject constructor( //
|
||||
disableSelectionMode()
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
private fun collectNodesToExport(
|
||||
parentUri: Uri, //
|
||||
exportOperation: ExportOperation, //
|
||||
@ -827,7 +796,6 @@ class BrowseFilesPresenter @Inject constructor( //
|
||||
collectFolderContentForExport(parentUri, exportOperation, foldersForRecursiveDirListing, filesToExport)
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
private fun collectFolderContentForExport(
|
||||
parentUri: Uri, exportOperation: ExportOperation, folders: List<CloudFolderModel>, //
|
||||
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) {
|
||||
downloadFiles = ArrayList()
|
||||
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> {
|
||||
return filesToExport.mapTo(ArrayList()) { createDownloadFile(it, parentUri) }
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
private fun prepareFolderContentForExport(cloudFolderRecursiveListing: CloudFolderRecursiveListing, parentUri: Uri) {
|
||||
createFolder(parentUri, cloudFolderRecursiveListing.parent.name)?.let {
|
||||
downloadFiles.addAll(prepareFilesForExport(cloudFolderRecursiveListing.files, it))
|
||||
@ -877,7 +842,6 @@ class BrowseFilesPresenter @Inject constructor( //
|
||||
} ?: throw FatalBackendException("Failed to create parent folder for export")
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
private fun createFolder(parentUri: Uri, folderName: String): Uri? {
|
||||
return try {
|
||||
DocumentsContract.createDocument( //
|
||||
@ -892,7 +856,6 @@ class BrowseFilesPresenter @Inject constructor( //
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
private fun createDownloadFile(file: CloudFile, documentUri: Uri): DownloadFile {
|
||||
return try {
|
||||
DownloadFile.Builder() //
|
||||
@ -918,7 +881,6 @@ class BrowseFilesPresenter @Inject constructor( //
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
@Throws(IllegalFileNameException::class, NoSuchCloudFileException::class)
|
||||
private fun createNewDocumentUri(parentUri: Uri, fileName: String): Uri {
|
||||
val mimeType = mimeTypes.fromFilename(fileName) ?: MimeType.APPLICATION_OCTET_STREAM
|
||||
|
@ -3,9 +3,7 @@ package org.cryptomator.presentation.presenter
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import org.cryptomator.domain.Cloud
|
||||
import org.cryptomator.domain.LocalStorageCloud
|
||||
import org.cryptomator.domain.PCloud
|
||||
@ -251,7 +249,6 @@ class CloudConnectionListPresenter @Inject constructor( //
|
||||
}
|
||||
|
||||
@Callback
|
||||
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
|
||||
fun pickedLocalStorageLocation(result: ActivityResult) {
|
||||
val rootTreeUriOfLocalStorage = result.intent().data
|
||||
persistUriPermission(rootTreeUriOfLocalStorage)
|
||||
@ -266,7 +263,6 @@ class CloudConnectionListPresenter @Inject constructor( //
|
||||
})
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
|
||||
private fun persistUriPermission(rootTreeUriOfLocalStorage: Uri?) {
|
||||
rootTreeUriOfLocalStorage?.let {
|
||||
context() //
|
||||
@ -278,7 +274,6 @@ class CloudConnectionListPresenter @Inject constructor( //
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
|
||||
private fun releaseUriPermission(uri: String) {
|
||||
context() //
|
||||
.contentResolver //
|
||||
@ -294,10 +289,6 @@ class CloudConnectionListPresenter @Inject constructor( //
|
||||
}
|
||||
}
|
||||
|
||||
fun onDefaultLocalCloudConnectionClicked() {
|
||||
finishWithResult(SELECTED_CLOUD, defaultLocalStorageCloud)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val SELECTED_CLOUD = "selectedCloudConnection"
|
||||
|
@ -3,8 +3,6 @@ package org.cryptomator.presentation.presenter
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import org.cryptomator.domain.CloudFile
|
||||
import org.cryptomator.domain.CloudNode
|
||||
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.PermissionsResult
|
||||
import org.cryptomator.util.ExceptionUtil
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
@ -58,37 +54,33 @@ class ImagePreviewPresenter @Inject constructor( //
|
||||
@InstanceState
|
||||
lateinit var pageIndexes: ArrayList<Int>
|
||||
|
||||
fun onExportImageClicked(uri: Uri) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
|
||||
copyFileToDownloadDirectory(uri)
|
||||
} else {
|
||||
copyFileToUserSelectedLocation(uri)
|
||||
}
|
||||
fun exportImageToUserSelectedLocation(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.exportImageToUserSelectedLocation(uri.toString()), intent)
|
||||
}
|
||||
|
||||
private fun copyFileToDownloadDirectory(uri: Uri) {
|
||||
@Callback
|
||||
fun exportImageToUserSelectedLocation(result: ActivityResult, sourceUri: String?) {
|
||||
requestPermissions(
|
||||
PermissionsResultCallbacks.copyFileToDownloadDirectory(uri.toString()), //
|
||||
R.string.permission_message_export_file, Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
PermissionsResultCallbacks.exportImageToUserSelectedLocation(result.intent()?.dataString, sourceUri), //
|
||||
R.string.permission_message_export_file, //
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
)
|
||||
}
|
||||
|
||||
@Callback
|
||||
fun copyFileToDownloadDirectory(result: PermissionsResult, uriString: String?) {
|
||||
fun exportImageToUserSelectedLocation(result: PermissionsResult, targetUri: String?, sourceUri: String?) {
|
||||
if (result.granted()) {
|
||||
val uriFile = Uri.parse(uriString)
|
||||
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, contentResolverUtil.fileName(uriFile))
|
||||
try {
|
||||
copyFile(contentResolverUtil.openInputStream(uriFile), FileOutputStream(target))
|
||||
} catch (e: FileNotFoundException) {
|
||||
showError(e)
|
||||
}
|
||||
} else {
|
||||
view?.showError(R.string.screen_file_browser_msg_creating_download_dir_failed)
|
||||
try {
|
||||
copyFile(
|
||||
contentResolverUtil.openInputStream(Uri.parse(sourceUri)), //
|
||||
contentResolverUtil.openOutputStream(Uri.parse(targetUri))
|
||||
)
|
||||
} catch (e: FileNotFoundException) {
|
||||
showError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
shareFileHelper.shareFile(this, uri)
|
||||
}
|
||||
|
@ -90,17 +90,14 @@ class SettingsPresenter @Inject internal constructor(
|
||||
requestPermissions(
|
||||
PermissionsResultCallbacks.onLocalStoragePermissionGranted(), //
|
||||
R.string.permission_snackbar_auth_auto_upload, //
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE, //
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
)
|
||||
}
|
||||
|
||||
@Callback
|
||||
fun onLocalStoragePermissionGranted(result: PermissionsResult) {
|
||||
if (result.granted()) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
scheduleJob(context())
|
||||
}
|
||||
scheduleJob(context())
|
||||
} else {
|
||||
view?.disableAutoUpload()
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ class UnlockVaultPresenter @Inject constructor(
|
||||
super.destroyed()
|
||||
if (retryUnlockHandler != null) {
|
||||
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) {
|
||||
if (hasFocus) {
|
||||
if (retryUnlockHandler != null) {
|
||||
running = false
|
||||
retryUnlockHandler?.removeCallbacks(null)
|
||||
retryUnlockHandler?.removeCallbacksAndMessages(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import org.cryptomator.data.cloud.crypto.CryptoCloud
|
||||
import org.cryptomator.data.util.NetworkConnectionCheck
|
||||
import org.cryptomator.domain.Cloud
|
||||
import org.cryptomator.domain.CloudFolder
|
||||
import org.cryptomator.domain.CloudType
|
||||
import org.cryptomator.domain.Vault
|
||||
import org.cryptomator.domain.di.PerView
|
||||
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.dialog.AppIsObscuredInfoDialog
|
||||
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.UpdateAppAvailableDialog
|
||||
import org.cryptomator.presentation.ui.dialog.UpdateAppDialog
|
||||
import org.cryptomator.presentation.ui.dialog.VaultsRemovedDuringMigrationDialog
|
||||
import org.cryptomator.presentation.util.FileUtil
|
||||
import org.cryptomator.presentation.workflow.ActivityResult
|
||||
import org.cryptomator.presentation.workflow.AddExistingVaultWorkflow
|
||||
@ -104,6 +105,12 @@ class VaultListPresenter @Inject constructor( //
|
||||
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()
|
||||
}
|
||||
|
||||
@ -119,9 +126,10 @@ class VaultListPresenter @Inject constructor( //
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
var license: String? = ""
|
||||
if (e is LicenseNotValidException) {
|
||||
license = e.license
|
||||
val license = if (e is LicenseNotValidException) {
|
||||
e.license
|
||||
} else {
|
||||
""
|
||||
}
|
||||
val intent = Intent(context(), LicenseCheckActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
@ -272,8 +280,8 @@ class VaultListPresenter @Inject constructor( //
|
||||
view?.showVaultCreationHint()
|
||||
} else {
|
||||
view?.hideVaultCreationHint()
|
||||
view?.renderVaultList(vaultModels)
|
||||
}
|
||||
view?.renderVaultList(vaultModels)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ class ImagePreviewActivity : BaseActivity(), ImagePreviewView, ConfirmDeleteClou
|
||||
presenter.onDeleteImageClicked(imagePreviewFiles[imagePreviewSliderAdapter.getIndex(viewPager.currentItem)])
|
||||
}
|
||||
exportImage.setOnClickListener {
|
||||
currentImageUri?.let { presenter.onExportImageClicked(it) }
|
||||
currentImageUri?.let { presenter.exportImageToUserSelectedLocation(it) }
|
||||
}
|
||||
shareImage.setOnClickListener {
|
||||
currentImageUri?.let { presenter.onShareImageClicked(it) }
|
||||
|
@ -1,6 +1,5 @@
|
||||
package org.cryptomator.presentation.ui.bottomsheet
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import org.cryptomator.generator.BottomSheet
|
||||
@ -46,12 +45,10 @@ class FolderSettingsBottomSheet : BaseBottomSheet<FolderSettingsBottomSheet.Call
|
||||
callback?.onMoveFolderClicked(cloudFolderModel)
|
||||
dismiss()
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
export_folder.visibility = View.VISIBLE
|
||||
export_folder.setOnClickListener {
|
||||
callback?.onExportFolderClicked(cloudFolderModel)
|
||||
dismiss()
|
||||
}
|
||||
export_folder.visibility = View.VISIBLE
|
||||
export_folder.setOnClickListener {
|
||||
callback?.onExportFolderClicked(cloudFolderModel)
|
||||
dismiss()
|
||||
}
|
||||
delete_folder.setOnClickListener {
|
||||
callback?.onDeleteNodeClicked(cloudFolderModel)
|
||||
|
@ -13,10 +13,10 @@ import kotlinx.android.synthetic.main.dialog_app_is_obscured_info.tv_app_is_obsc
|
||||
class AppIsObscuredInfoDialog : BaseDialog<Activity>() {
|
||||
|
||||
public override fun setupDialog(builder: AlertDialog.Builder): android.app.Dialog {
|
||||
builder //
|
||||
return builder //
|
||||
.setTitle(R.string.dialog_app_is_obscured_info_title) //
|
||||
.setNeutralButton(R.string.dialog_app_is_obscured_info_neutral_button) { dialog: DialogInterface, _: Int -> dialog.dismiss() }
|
||||
return builder.create()
|
||||
.setNeutralButton(R.string.dialog_app_is_obscured_info_neutral_button) { dialog: DialogInterface, _: Int -> dialog.dismiss() } //
|
||||
.create()
|
||||
}
|
||||
|
||||
override fun disableDialogWhenObscured(): Boolean {
|
||||
|
@ -1,7 +1,6 @@
|
||||
package org.cryptomator.presentation.ui.dialog
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.Html
|
||||
import android.view.View
|
||||
@ -32,11 +31,7 @@ class UpdateAppAvailableDialog : BaseProgressErrorDialog<UpdateAppAvailableDialo
|
||||
|
||||
public override fun setupView() {
|
||||
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)
|
||||
} else {
|
||||
tv_message.text = Html.fromHtml(message)
|
||||
}
|
||||
tv_message.text = Html.fromHtml(message, Html.FROM_HTML_MODE_COMPACT)
|
||||
}
|
||||
|
||||
override fun enableViewAfterError(): View {
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
package org.cryptomator.presentation.ui.fragment
|
||||
|
||||
import android.os.Environment
|
||||
import android.util.TypedValue
|
||||
import android.view.View.GONE
|
||||
import android.view.View.VISIBLE
|
||||
@ -13,10 +12,7 @@ import org.cryptomator.presentation.presenter.CloudConnectionListPresenter
|
||||
import org.cryptomator.presentation.ui.adapter.CloudConnectionListAdapter
|
||||
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.rv_local_default_cloud
|
||||
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
|
||||
|
||||
@Fragment(R.layout.fragment_browse_cloud_connections)
|
||||
@ -42,7 +38,6 @@ class CloudConnectionListFragment : BaseFragment() {
|
||||
|
||||
override fun setupView() {
|
||||
setupRecyclerView()
|
||||
rv_local_default_cloud.setOnClickListener { cloudConnectionListPresenter.onDefaultLocalCloudConnectionClicked() }
|
||||
floating_action_button.setOnClickListener { cloudConnectionListPresenter.onAddConnectionClicked() }
|
||||
}
|
||||
|
||||
@ -71,11 +66,5 @@ class CloudConnectionListFragment : BaseFragment() {
|
||||
|
||||
fun setSelectedCloudType(selectedCloudType: CloudTypeModel) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
package org.cryptomator.presentation.ui.fragment
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableString
|
||||
import android.text.style.ForegroundColorSpan
|
||||
@ -259,9 +258,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||
if (enabled) {
|
||||
activity().grantLocalStoragePermissionForAutoUpload()
|
||||
} 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
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import org.cryptomator.presentation.model.ProgressModel;
|
||||
import org.cryptomator.presentation.model.mappers.CloudModelMapper;
|
||||
import org.cryptomator.presentation.presenter.ChooseCloudServicePresenter;
|
||||
import org.cryptomator.presentation.presenter.VaultListPresenter;
|
||||
import org.cryptomator.presentation.ui.dialog.VaultIsRootFolderOfCloudDialog;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Arrays;
|
||||
@ -116,9 +117,13 @@ public class AddExistingVaultWorkflow extends Workflow<AddExistingVaultWorkflow.
|
||||
@Callback
|
||||
void cryptomatorFileChosen(SerializableResult<CloudFileModel> result) {
|
||||
CloudFileModel masterkeyFile = result.getResult();
|
||||
state().masterkeyFile = masterkeyFile.toCloudNode();
|
||||
presenter().getView().showProgress(ProgressModel.GENERIC);
|
||||
finish();
|
||||
if(!masterkeyFile.getPath().equals("/masterkey.cryptomator") && !masterkeyFile.getPath().equals("/vault.cryptomator")) {
|
||||
state().masterkeyFile = masterkeyFile.toCloudNode();
|
||||
presenter().getView().showProgress(ProgressModel.GENERIC);
|
||||
finish();
|
||||
} else {
|
||||
presenter().getView().showDialog(VaultIsRootFolderOfCloudDialog.Companion.newInstance());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -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>
|
@ -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>
|
@ -9,38 +9,10 @@
|
||||
android:layout_width="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
|
||||
layout="@layout/recycler_view_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_below="@+id/rv_local_default_cloud"
|
||||
android:clipToPadding="true"
|
||||
android:paddingBottom="88dp" />
|
||||
|
||||
|
@ -61,6 +61,8 @@
|
||||
<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_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_search">Search</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_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_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>
|
||||
|
||||
<!-- ## 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_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_delete_cloud_connection_with_vaults_message">Are you sure you want to remove this cloud connection?</string>
|
||||
|
@ -1,8 +1,10 @@
|
||||
package org.cryptomator.presentation.presenter
|
||||
|
||||
import android.Manifest
|
||||
import android.accounts.AccountManager
|
||||
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 com.dropbox.core.android.Auth
|
||||
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.model.CloudModel
|
||||
import org.cryptomator.presentation.model.CloudTypeModel
|
||||
import org.cryptomator.presentation.model.LocalStorageModel
|
||||
import org.cryptomator.presentation.model.ProgressModel
|
||||
import org.cryptomator.presentation.model.ProgressStateModel
|
||||
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.AddExistingVaultWorkflow
|
||||
import org.cryptomator.presentation.workflow.CreateNewVaultWorkflow
|
||||
import org.cryptomator.presentation.workflow.PermissionsResult
|
||||
import org.cryptomator.presentation.workflow.Workflow
|
||||
import org.cryptomator.util.ExceptionUtil
|
||||
import org.cryptomator.util.crypto.CredentialCryptor
|
||||
@ -479,6 +481,7 @@ class AuthenticateCloudPresenter @Inject constructor( //
|
||||
private inner class LocalStorageAuthStrategy : AuthStrategy {
|
||||
|
||||
private var authenticationStarted = false
|
||||
|
||||
override fun supports(cloud: CloudModel): Boolean {
|
||||
return cloud.cloudType() == CloudTypeModel.LOCAL
|
||||
}
|
||||
@ -491,22 +494,41 @@ class AuthenticateCloudPresenter @Inject constructor( //
|
||||
|
||||
private fun startAuthentication(cloud: CloudModel) {
|
||||
authenticationStarted = true
|
||||
requestPermissions(
|
||||
PermissionsResultCallbacks.onLocalStorageAuthenticated(cloud), //
|
||||
R.string.permission_snackbar_auth_local_vault, //
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE, //
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
)
|
||||
|
||||
val uri = (cloud as LocalStorageModel).uri()
|
||||
|
||||
val permissions = context().contentResolver.persistedUriPermissions
|
||||
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
|
||||
fun onLocalStorageAuthenticated(result: PermissionsResult, cloud: CloudModel) {
|
||||
if (result.granted()) {
|
||||
succeedAuthenticationWith(cloud.toCloud())
|
||||
} else {
|
||||
failAuthentication(PermissionNotGrantedException(R.string.permission_snackbar_auth_local_vault))
|
||||
fun rePickedLocalStorageLocation(result: ActivityResult, cloud: LocalStorageModel) {
|
||||
val rootTreeUriOfLocalStorage = result.intent().data
|
||||
rootTreeUriOfLocalStorage?.let {
|
||||
context() //
|
||||
.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 {
|
||||
|
@ -236,6 +236,31 @@ constructor(context: Context) : SharedPreferences.OnSharedPreferenceChangeListen
|
||||
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 {
|
||||
|
||||
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 KEEP_UNLOCKED_WHILE_EDITING = "keepUnlockedWhileEditing"
|
||||
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 DISABLE_APP_WHEN_OBSCURED = "disableAppWhenObscured"
|
||||
const val SECURE_SCREEN = "secureScreen"
|
||||
|
@ -1,7 +1,6 @@
|
||||
package org.cryptomator.util.crypto;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.security.keystore.KeyGenParameterSpec;
|
||||
import android.security.keystore.KeyProperties;
|
||||
|
||||
@ -43,13 +42,10 @@ class CryptoOperationsImpl implements CryptoOperations {
|
||||
KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec //
|
||||
.Builder(alias, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) //
|
||||
.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.generateKey();
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user