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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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.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 org.cryptomator.domain.CloudNode

View File

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

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.provider.DocumentsContract

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -76,9 +76,6 @@ class CloudRepositoryImpl implements CloudRepository {
@Override
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");
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,5 @@
package org.cryptomator.presentation.ui.fragment
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()
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -9,38 +9,10 @@
android:layout_width="match_parent"
android:layout_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" />

View File

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

View File

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

View File

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

View File

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