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:
parent
3b641a22d8
commit
91d1a65ba7
@ -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'
|
||||
|
@ -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) // FIXME
|
||||
} 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)
|
||||
}
|
||||
}
|
@ -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,6 +1,5 @@
|
||||
package org.cryptomator.presentation.presenter
|
||||
|
||||
import android.Manifest
|
||||
import android.accounts.AccountManager
|
||||
import android.widget.Toast
|
||||
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.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 +44,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
|
||||
@ -445,20 +444,15 @@ 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 permissions = context().contentResolver.persistedUriPermissions
|
||||
for (permission in permissions) {
|
||||
if (permission.uri.toString() == (cloud as LocalStorageModel).uri()) {
|
||||
succeedAuthenticationWith(cloud.toCloud())
|
||||
}
|
||||
}
|
||||
|
||||
@Callback
|
||||
fun onLocalStorageAuthenticated(result: PermissionsResult, cloud: CloudModel) {
|
||||
if (result.granted()) {
|
||||
succeedAuthenticationWith(cloud.toCloud())
|
||||
} else {
|
||||
// FIXME think about how to re-request permission
|
||||
// FIXME change in the FOSS variant too
|
||||
failAuthentication(PermissionNotGrantedException(R.string.permission_snackbar_auth_local_vault))
|
||||
}
|
||||
}
|
||||
|
@ -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 //
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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" />
|
||||
|
||||
|
@ -167,7 +167,6 @@
|
||||
<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 -->
|
||||
|
@ -1,6 +1,5 @@
|
||||
package org.cryptomator.presentation.presenter
|
||||
|
||||
import android.Manifest
|
||||
import android.accounts.AccountManager
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.widget.Toast
|
||||
@ -38,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
|
||||
@ -47,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
|
||||
@ -491,20 +490,15 @@ 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 permissions = context().contentResolver.persistedUriPermissions
|
||||
for (permission in permissions) {
|
||||
if (permission.uri.toString() == (cloud as LocalStorageModel).uri()) {
|
||||
succeedAuthenticationWith(cloud.toCloud())
|
||||
}
|
||||
}
|
||||
|
||||
@Callback
|
||||
fun onLocalStorageAuthenticated(result: PermissionsResult, cloud: CloudModel) {
|
||||
if (result.granted()) {
|
||||
succeedAuthenticationWith(cloud.toCloud())
|
||||
} else {
|
||||
// FIXME think about how to re-request permission
|
||||
// FIXME change in the FOSS variant too
|
||||
failAuthentication(PermissionNotGrantedException(R.string.permission_snackbar_auth_local_vault))
|
||||
}
|
||||
}
|
||||
|
@ -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