Update target API to 30 (Android 11)

Write external storage got removed due to forced scoped storage. Local storage cloud is now only available using the storage access framework implementation. Migration still needs to be implemented as well as how to recover when the authentications fails in the LocalStorageAuthStrategy.

Fixes #251
This commit is contained in:
Julian Raufelder 2021-10-22 15:52:02 +02:00
parent 3b641a22d8
commit 91d1a65ba7
No known key found for this signature in database
GPG Key ID: 17EE71F6634E381D
45 changed files with 90 additions and 661 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

@ -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) // FIXME
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

@ -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,6 +1,5 @@
package org.cryptomator.presentation.presenter package org.cryptomator.presentation.presenter
import android.Manifest
import android.accounts.AccountManager import android.accounts.AccountManager
import android.widget.Toast import android.widget.Toast
import com.dropbox.core.android.Auth import com.dropbox.core.android.Auth
@ -35,6 +34,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 +44,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
@ -445,20 +444,15 @@ class AuthenticateCloudPresenter @Inject constructor( //
private fun startAuthentication(cloud: CloudModel) { private fun startAuthentication(cloud: CloudModel) {
authenticationStarted = true authenticationStarted = true
requestPermissions( val permissions = context().contentResolver.persistedUriPermissions
PermissionsResultCallbacks.onLocalStorageAuthenticated(cloud), // for (permission in permissions) {
R.string.permission_snackbar_auth_local_vault, // if (permission.uri.toString() == (cloud as LocalStorageModel).uri()) {
Manifest.permission.READ_EXTERNAL_STORAGE, // succeedAuthenticationWith(cloud.toCloud())
Manifest.permission.WRITE_EXTERNAL_STORAGE }
) }
}
}
@Callback // FIXME think about how to re-request permission
fun onLocalStorageAuthenticated(result: PermissionsResult, cloud: CloudModel) { // FIXME change in the FOSS variant too
if (result.granted()) {
succeedAuthenticationWith(cloud.toCloud())
} else {
failAuthentication(PermissionNotGrantedException(R.string.permission_snackbar_auth_local_vault)) failAuthentication(PermissionNotGrantedException(R.string.permission_snackbar_auth_local_vault))
} }
} }

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 //

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

@ -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

@ -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

@ -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

@ -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

@ -167,7 +167,6 @@
<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_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 -->

View File

@ -1,6 +1,5 @@
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.widget.Toast import android.widget.Toast
@ -38,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
@ -47,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
@ -491,20 +490,15 @@ class AuthenticateCloudPresenter @Inject constructor( //
private fun startAuthentication(cloud: CloudModel) { private fun startAuthentication(cloud: CloudModel) {
authenticationStarted = true authenticationStarted = true
requestPermissions( val permissions = context().contentResolver.persistedUriPermissions
PermissionsResultCallbacks.onLocalStorageAuthenticated(cloud), // for (permission in permissions) {
R.string.permission_snackbar_auth_local_vault, // if (permission.uri.toString() == (cloud as LocalStorageModel).uri()) {
Manifest.permission.READ_EXTERNAL_STORAGE, // succeedAuthenticationWith(cloud.toCloud())
Manifest.permission.WRITE_EXTERNAL_STORAGE }
) }
}
}
@Callback // FIXME think about how to re-request permission
fun onLocalStorageAuthenticated(result: PermissionsResult, cloud: CloudModel) { // FIXME change in the FOSS variant too
if (result.granted()) {
succeedAuthenticationWith(cloud.toCloud())
} else {
failAuthentication(PermissionNotGrantedException(R.string.permission_snackbar_auth_local_vault)) failAuthentication(PermissionNotGrantedException(R.string.permission_snackbar_auth_local_vault))
} }
} }

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();
}; };