Merge branch 'feature/add-to-fdroid-main-repo' into develop

This commit is contained in:
Julian Raufelder 2022-06-04 18:14:32 +02:00
commit 91148d6d3e
No known key found for this signature in database
GPG Key ID: 17EE71F6634E381D
242 changed files with 1789 additions and 1260 deletions

View File

@ -30,9 +30,9 @@ ext {
javaxAnnotationVersion = '1.0'
// support lib
androidSupportAnnotationsVersion = '1.2.0'
androidSupportAppcompatVersion = '1.3.1'
androidSupportDesignVersion = '1.4.0'
androidSupportAnnotationsVersion = '1.3.0'
androidSupportAppcompatVersion = '1.4.1'
androidMaterialDesignVersion = '1.6.0'
coreDesugaringVersion = '1.1.5'
@ -102,15 +102,16 @@ ext {
uiautomatorVersion = '2.2.0'
androidxTestJunitKtlnVersion = '1.1.3'
androidxCoreVersion = '1.6.0'
androidxFragmentVersion = '1.3.6'
androidxCoreVersion = '1.7.0'
androidxFragmentVersion = '1.4.1'
androidxViewpagerVersion = '1.0.0'
androidxSwiperefreshVersion = '1.1.0'
androidxPreferenceVersion = '1.1.1'
androidxPreferenceVersion = '1.2.0'
androidxRecyclerViewVersion = '1.2.1'
androidxDocumentfileVersion = '1.0.1'
androidxBiometricVersion = '1.1.0'
androidxTestCoreVersion = '1.4.0'
androidxSplashscreenVersion = '1.0.0-rc01'
jsonWebTokenApiVersion = '0.11.5'
@ -126,13 +127,14 @@ ext {
androidxPreference : "androidx.preference:preference:${androidxPreferenceVersion}",
documentFile : "androidx.documentfile:documentfile:${androidxDocumentfileVersion}",
recyclerView : "androidx.recyclerview:recyclerview:${androidxRecyclerViewVersion}",
androidxSplashscreen : "androidx.core:core-splashscreen:${androidxSplashscreenVersion}",
androidxTestCore : "androidx.test:core:${androidxTestCoreVersion}",
androidxTestJunitKtln : "androidx.test.ext:junit-ktx:${androidxTestJunitKtlnVersion}",
commonsCodec : "commons-codec:commons-codec:${commonsCodecVersion}",
cryptolib : "org.cryptomator:cryptolib:${cryptolibVersion}",
dagger : "com.google.dagger:dagger:${daggerVersion}",
daggerCompiler : "com.google.dagger:dagger-compiler:${daggerVersion}",
design : "com.google.android.material:material:${androidSupportDesignVersion}",
design : "com.google.android.material:material:${androidMaterialDesignVersion}",
coreDesugaring : "com.android.tools:desugar_jdk_libs:${coreDesugaringVersion}",
dropbox : "com.dropbox.core:dropbox-core-sdk:${dropboxVersion}",
espresso : "androidx.test.espresso:espresso-core:${espressoVersion}",

View File

@ -53,19 +53,27 @@ android {
fdroid {
dimension "version"
}
lite {
dimension "version"
}
}
sourceSets {
playstore {
java.srcDirs = ['src/main/java', 'src/main/java/', 'src/notFoss/java', 'src/notFoss/java/']
java.srcDirs = ['src/main/java/', 'src/apiKey/java/', 'src/apkStorePlaystore/java/']
}
apkstore {
java.srcDirs = ['src/main/java', 'src/main/java/', 'src/notFoss/java', 'src/notFoss/java/']
java.srcDirs = ['src/main/java/', 'src/apiKey/java/', 'src/apkStorePlaystore/java/']
}
fdroid {
java.srcDirs = ['src/main/java', 'src/main/java/', 'src/foss/java', 'src/foss/java/']
java.srcDirs = ['src/main/java/', 'src/apiKey/java/', 'src/fdroid/java/']
}
lite {
java.srcDirs = ['src/main/java/', 'src/lite/java/']
}
}
packagingOptions {
@ -95,7 +103,9 @@ dependencies {
implementation project(':domain')
implementation project(':util')
implementation project(':pcloud-sdk-java')
playstoreImplementation project(':pcloud-sdk-java')
apkstoreImplementation project(':pcloud-sdk-java')
fdroidImplementation project(':pcloud-sdk-java')
coreLibraryDesugaring dependencies.coreDesugaring
@ -113,9 +123,16 @@ dependencies {
implementation dependencies.jsonWebTokenJson
// cloud
implementation dependencies.dropbox
implementation dependencies.msgraphAuth
implementation dependencies.msgraph
playstoreImplementation dependencies.dropbox
apkstoreImplementation dependencies.dropbox
fdroidImplementation dependencies.dropbox
playstoreImplementation dependencies.msgraphAuth
apkstoreImplementation dependencies.msgraphAuth
fdroidImplementation dependencies.msgraphAuth
playstoreImplementation dependencies.msgraph
apkstoreImplementation dependencies.msgraph
fdroidImplementation dependencies.msgraph
implementation dependencies.stax
api dependencies.minIo

View File

@ -1,5 +1,7 @@
package org.cryptomator.data.cloud;
import static java.util.Arrays.asList;
import org.cryptomator.data.cloud.crypto.CryptoCloudContentRepositoryFactory;
import org.cryptomator.data.cloud.dropbox.DropboxCloudContentRepositoryFactory;
import org.cryptomator.data.cloud.googledrive.GoogleDriveCloudContentRepositoryFactory;
@ -16,8 +18,6 @@ import java.util.Iterator;
import javax.inject.Inject;
import javax.inject.Singleton;
import static java.util.Arrays.asList;
@Singleton
public class CloudContentRepositoryFactories implements Iterable<CloudContentRepositoryFactory> {

View File

@ -0,0 +1,40 @@
package org.cryptomator.data.cloud;
import static java.util.Arrays.asList;
import org.cryptomator.data.cloud.crypto.CryptoCloudContentRepositoryFactory;
import org.cryptomator.data.cloud.local.LocalStorageContentRepositoryFactory;
import org.cryptomator.data.cloud.s3.S3CloudContentRepositoryFactory;
import org.cryptomator.data.cloud.webdav.WebDavCloudContentRepositoryFactory;
import org.cryptomator.data.repository.CloudContentRepositoryFactory;
import org.jetbrains.annotations.NotNull;
import java.util.Iterator;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class CloudContentRepositoryFactories implements Iterable<CloudContentRepositoryFactory> {
private final Iterable<CloudContentRepositoryFactory> factories;
@Inject
public CloudContentRepositoryFactories(
S3CloudContentRepositoryFactory s3Factory, //
CryptoCloudContentRepositoryFactory cryptoFactory, //
LocalStorageContentRepositoryFactory localStorageFactory, //
WebDavCloudContentRepositoryFactory webDavFactory) {
factories = asList(s3Factory, //
cryptoFactory, //
localStorageFactory, //
webDavFactory);
}
@NotNull
@Override
public Iterator<CloudContentRepositoryFactory> iterator() {
return factories.iterator();
}
}

View File

@ -29,6 +29,7 @@ platform :android do |options|
deployToPlaystore(alpha:options[:alpha], beta:options[:beta])
deployToServer(alpha:options[:alpha], beta:options[:beta])
deployToFDroid(alpha:options[:alpha], beta:options[:beta])
testLite(alpha:options[:alpha], beta:options[:beta])
createGitHubDraftRelease(alpha:options[:alpha], beta:options[:beta])
slack(
@ -220,6 +221,29 @@ platform :android do |options|
FileUtils.cp(lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH], "release/Cryptomator-#{version}_fdroid_signed.apk")
end
desc "Deploy new lite version"
lane :deployLite do |options|
gradle(task: "clean")
gradle(
task: "assemble",
build_type: "Release",
flavor: "lite",
print_command: false,
properties: {
"android.injected.signing.store.file" => ENV["SIGNING_KEYSTORE_PATH"],
"android.injected.signing.store.password" => ENV["SIGNING_KEYSTORE_PASSWORD"],
"android.injected.signing.key.alias" => ENV["SIGNING_KEY_ALIAS"],
"android.injected.signing.key.password" => ENV["SIGNING_KEY_PASSWORD"],
}
)
checkTrackingAddedInDependencyUsingIzzyScript(alpha:options[:alpha], beta:options[:beta], flavor: 'lite')
checkTrackingAddedInDependencyUsingExodus(alpha:options[:alpha], beta:options[:beta], flavor: 'lite')
FileUtils.cp(lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH], "release/Cryptomator-#{version}_lite_signed.apk")
end
desc "Check if tracking added in some dependency using Izzy's script"
lane :checkTrackingAddedInDependencyUsingIzzyScript do |options|
flavor = options[:flavor]
@ -289,8 +313,9 @@ platform :android do |options|
website_apk_sha256 = Digest::SHA256.hexdigest File.read "release/Cryptomator-#{version}_signed.apk"
fdroid_apk_sha256 = Digest::SHA256.hexdigest File.read "release/Cryptomator-#{version}_fdroid_signed.apk"
lite_sha256 = Digest::SHA256.hexdigest File.read "release/Cryptomator-#{version}_lite_signed.apk"
release_note = "## What's New\n\n" + File.read(release_note_path_en) + "\n\n---\n\nSHA256 Signature: `#{website_apk_sha256}`\nSHA256 Signature fdroid: `#{fdroid_apk_sha256}`\n"
release_note = "## What's New\n\n" + File.read(release_note_path_en) + "\n\n---\n\nSHA256 Signature: `#{website_apk_sha256}`\nSHA256 Signature fdroid: `#{fdroid_apk_sha256}`\nSHA256 Signature lite: `#{lite_sha256}`\n"
puts release_note
@ -303,7 +328,7 @@ platform :android do |options|
commitish: target_branch,
is_draft: true,
is_prerelease: prerelease,
upload_assets: ["fastlane/release/Cryptomator-#{version}_fdroid_signed.apk", "fastlane/release/Cryptomator-#{version}_signed.apk"]
upload_assets: ["fastlane/release/Cryptomator-#{version}_fdroid_signed.apk", "fastlane/release/Cryptomator-#{version}_lite_signed.apk", "fastlane/release/Cryptomator-#{version}_signed.apk"]
)
end
@ -362,5 +387,23 @@ platform :android do |options|
checkTrackingAddedInDependencyUsingIzzyScript(alpha:options[:alpha], beta:options[:beta], flavor: 'fdroid')
checkTrackingAddedInDependencyUsingExodus(alpha:options[:alpha], beta:options[:beta], flavor: 'fdroid')
gradle(task: "clean")
gradle(
task: "assemble",
build_type: "Release",
flavor: "lite",
print_command: false,
properties: {
"android.injected.signing.store.file" => ENV["SIGNING_KEYSTORE_PATH"],
"android.injected.signing.store.password" => ENV["SIGNING_KEYSTORE_PASSWORD"],
"android.injected.signing.key.alias" => ENV["SIGNING_KEY_ALIAS"],
"android.injected.signing.key.password" => ENV["SIGNING_KEY_PASSWORD"],
}
)
checkTrackingAddedInDependencyUsingIzzyScript(alpha:options[:alpha], beta:options[:beta], flavor: 'lite')
checkTrackingAddedInDependencyUsingExodus(alpha:options[:alpha], beta:options[:beta], flavor: 'lite')
end
end

View File

@ -55,6 +55,14 @@ Deploy new version to server
Deploy new version to F-Droid
### android deployLite
```sh
[bundle exec] fastlane android deployLite
```
Deploy new lite version
### android checkTrackingAddedInDependencyUsingIzzyScript
```sh

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -50,8 +50,6 @@ android {
buildConfigField "String", "PCLOUD_CLIENT_ID", "\"" + getApiKey('PCLOUD_CLIENT_ID') + "\""
manifestPlaceholders = [DROPBOX_API_KEY: getApiKey('DROPBOX_API_KEY'), ONEDRIVE_API_KEY_DECODED: getOnedriveApiKey()]
resValue "string", "app_id", androidApplicationId
}
debug {
@ -69,8 +67,6 @@ android {
applicationIdSuffix ".debug"
versionNameSuffix '-DEBUG'
resValue "string", "app_id", androidApplicationId + applicationIdSuffix
}
}
@ -88,19 +84,30 @@ android {
fdroid {
dimension "version"
}
lite {
dimension "version"
applicationIdSuffix ".lite"
resValue "string", "app_id", androidApplicationId + applicationIdSuffix
}
}
sourceSets {
playstore {
java.srcDirs = ['src/main/java', 'src/main/java/', 'src/notFoss/java', 'src/notFoss/java/']
java.srcDirs = ['src/main/java', 'src/apiKey/java/', 'src/apkStorePlaystore/java/']
}
apkstore {
java.srcDirs = ['src/main/java', 'src/main/java/', 'src/notFoss/java', 'src/notFoss/java/']
java.srcDirs = ['src/main/java/', 'src/apiKey/java/', 'src/apkStorePlaystore/java/']
}
fdroid {
java.srcDirs = ['src/main/java', 'src/main/java/', 'src/foss/java', 'src/foss/java/']
java.srcDirs = ['src/main/java/', 'src/apiKey/java/', 'src/fdroid/java/', 'src/fdroidAndLite/java/']
}
lite {
java.srcDirs = ['src/main/java/', 'src/lite/java/', 'src/fdroidAndLite/java/']
}
}
packagingOptions {
@ -142,14 +149,22 @@ dependencies {
implementation dependencies.androidxCore
implementation dependencies.androidxFragment
implementation dependencies.androidxViewpager
implementation dependencies.androidxSplashscreen
implementation dependencies.androidxSwiperefresh
implementation dependencies.androidxPreference
implementation dependencies.androidxBiometric
// cloud
implementation dependencies.dropbox
implementation dependencies.msgraph
implementation dependencies.msgraphAuth
playstoreImplementation dependencies.dropbox
apkstoreImplementation dependencies.dropbox
fdroidImplementation dependencies.dropbox
playstoreImplementation dependencies.msgraphAuth
apkstoreImplementation dependencies.msgraphAuth
fdroidImplementation dependencies.msgraphAuth
playstoreImplementation dependencies.msgraph
apkstoreImplementation dependencies.msgraph
fdroidImplementation dependencies.msgraph
playstoreImplementation(dependencies.googleApiServicesDrive) {
exclude module: 'guava-jdk5'

View File

@ -0,0 +1,17 @@
package org.cryptomator.presentation.presenter
import android.content.Context
import com.dropbox.core.android.Auth
import org.cryptomator.presentation.BuildConfig
object DropboxAuthHelper {
fun startOAuth2Authentication(context: Context) {
Auth.startOAuth2Authentication(context, BuildConfig.DROPBOX_API_KEY)
}
fun getOAuth2Token(): String? {
return Auth.getOAuth2Token()
}
}

View File

@ -0,0 +1,139 @@
package org.cryptomator.presentation.presenter
import android.app.Activity
import android.content.Context
import com.microsoft.identity.client.AuthenticationCallback
import com.microsoft.identity.client.IAccount
import com.microsoft.identity.client.IAuthenticationResult
import com.microsoft.identity.client.IMultipleAccountPublicClientApplication
import com.microsoft.identity.client.IPublicClientApplication
import com.microsoft.identity.client.PublicClientApplication
import com.microsoft.identity.client.exception.MsalException
import com.microsoft.identity.client.exception.MsalUiRequiredException
import org.cryptomator.domain.OnedriveCloud
import org.cryptomator.domain.exception.FatalBackendException
import org.cryptomator.presentation.R
import org.cryptomator.util.crypto.CredentialCryptor
import timber.log.Timber
object OnedriveAuthentication {
fun refreshOrCheckAuth(activity: Activity, cloud: OnedriveCloud, success: (cloud: OnedriveCloud) -> Unit, failed: (e: FatalBackendException) -> Unit) {
PublicClientApplication.createMultipleAccountPublicClientApplication(
activity.applicationContext,
R.raw.auth_config_onedrive,
object : IPublicClientApplication.IMultipleAccountApplicationCreatedListener {
override fun onCreated(application: IMultipleAccountPublicClientApplication) {
application.getAccounts(object : IPublicClientApplication.LoadAccountsCallback {
override fun onTaskCompleted(accounts: List<IAccount>) {
if (accounts.isEmpty()) {
application.acquireToken(activity, AuthenticateCloudPresenter.onedriveScopes(), getAuthInteractiveCallback(activity.applicationContext, cloud, success, failed))
} else {
accounts.find { account -> account.username == cloud.username() }?.let {
application.acquireTokenSilentAsync(
AuthenticateCloudPresenter.onedriveScopes(),
it,
"https://login.microsoftonline.com/common",
getAuthSilentCallback(activity, cloud, success, failed, application)
)
} ?: application.acquireToken(activity, AuthenticateCloudPresenter.onedriveScopes(), getAuthInteractiveCallback(activity.applicationContext, cloud, success, failed))
}
}
override fun onError(e: MsalException) {
Timber.tag("AuthenticateCloudPresenter").e(e, "Error to get accounts")
failed(FatalBackendException(e))
}
})
}
override fun onError(e: MsalException) {
Timber.tag("AuthenticateCloudPresenter").i(e, "Error in configuration")
failed(FatalBackendException(e))
}
})
}
private fun getAuthSilentCallback(
activity: Activity,
cloud: OnedriveCloud,
success: (cloud: OnedriveCloud) -> Unit,
failed: (e: FatalBackendException) -> Unit,
application: IMultipleAccountPublicClientApplication
): AuthenticationCallback {
return object : AuthenticationCallback {
override fun onSuccess(authenticationResult: IAuthenticationResult) {
onTokenObtained(activity.applicationContext, cloud, authenticationResult, success)
}
override fun onError(e: MsalException) {
Timber.tag("AuthenticateCloudPresenter").e(e, "Failed to acquireToken")
when (e) {
is MsalUiRequiredException -> {
/* Tokens expired or no session, retry with interactive */
application.acquireToken(activity, AuthenticateCloudPresenter.onedriveScopes(), getAuthInteractiveCallback(activity.applicationContext, cloud, success, failed))
}
else -> failed(FatalBackendException(e))
}
}
override fun onCancel() {
Timber.tag("AuthenticateCloudPresenter").i("User cancelled login")
}
}
}
private fun onTokenObtained(context: Context, cloud: OnedriveCloud?, authenticationResult: IAuthenticationResult, success: (cloud: OnedriveCloud) -> Unit) {
Timber.tag("AuthenticateCloudPresenter").i("Successfully authenticated")
val accessToken = CredentialCryptor.getInstance(context).encrypt(authenticationResult.accessToken)
val cloudBuilder = cloud?.let { OnedriveCloud.aCopyOf(it) } ?: OnedriveCloud.aOnedriveCloud()
val onedriveSkeleton = cloudBuilder.withAccessToken(accessToken).withUsername(authenticationResult.account.username).build()
success(onedriveSkeleton)
}
fun getAuthenticatedOnedriveCloud(activity: Activity, success: (cloud: OnedriveCloud) -> Unit, failed: (e: FatalBackendException) -> Unit) {
PublicClientApplication.createMultipleAccountPublicClientApplication(
activity.applicationContext,
R.raw.auth_config_onedrive,
object : IPublicClientApplication.IMultipleAccountApplicationCreatedListener {
override fun onCreated(application: IMultipleAccountPublicClientApplication) {
application.getAccounts(object : IPublicClientApplication.LoadAccountsCallback {
override fun onTaskCompleted(accounts: List<IAccount>) {
application.acquireToken(activity, AuthenticateCloudPresenter.onedriveScopes(), getAuthInteractiveCallback(activity.applicationContext, null, success, failed))
}
override fun onError(e: MsalException) {
Timber.tag("AuthenticateCloudPresenter").e(e, "Error to get accounts")
failed(FatalBackendException(e))
}
})
}
override fun onError(e: MsalException) {
Timber.tag("AuthenticateCloudPresenter").i(e, "Error in configuration")
failed(FatalBackendException(e))
}
})
}
private fun getAuthInteractiveCallback(context: Context, cloud: OnedriveCloud?, success: (cloud: OnedriveCloud) -> Unit, failed: (e: FatalBackendException) -> Unit): AuthenticationCallback {
return object : AuthenticationCallback {
override fun onSuccess(authenticationResult: IAuthenticationResult) {
onTokenObtained(context, cloud, authenticationResult, success)
}
override fun onError(e: MsalException) {
Timber.tag("AuthenticateCloudPresenter").e(e, "Successfully authenticated")
failed(FatalBackendException(e))
}
override fun onCancel() {
Timber.tag("AuthenticateCloudPresenter").i("User cancelled login")
}
}
}
}

View File

@ -0,0 +1,13 @@
package org.cryptomator.presentation.presenter
import android.content.Context
import android.content.Intent
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential
import com.google.api.services.drive.DriveScopes
object GoogleAuthHelper {
fun getChooseAccountIntent(context: Context): Intent? {
return GoogleAccountCredential.usingOAuth2(context, setOf(DriveScopes.DRIVE)).newChooseAccountIntent()
}
}

View File

@ -5,4 +5,39 @@
<!-- Required to self update Cryptomator in the apkstore variant -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<!-- Keep in sync with apkstore, playstore and fdroid -->
<application>
<activity
android:name="com.dropbox.core.android.AuthActivity"
android:configChanges="orientation|keyboard"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<data android:scheme="db-${DROPBOX_API_KEY}" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name="com.microsoft.identity.client.BrowserTabActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="org.cryptomator"
android:path="/${ONEDRIVE_API_KEY_DECODED}"
android:scheme="msauth" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.cryptomator.presentation">
<!-- Required to self update Cryptomator in the apkstore variant -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<!-- Keep in sync with apkstore, playstore and fdroid -->
<application>
<activity
android:name="com.dropbox.core.android.AuthActivity"
android:configChanges="orientation|keyboard"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<data android:scheme="db-${DROPBOX_API_KEY}" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name="com.microsoft.identity.client.BrowserTabActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="org.cryptomator"
android:path="/${ONEDRIVE_API_KEY_DECODED}"
android:scheme="msauth" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,11 @@
package org.cryptomator.presentation.presenter
import android.content.Context
import android.content.Intent
object GoogleAuthHelper {
fun getChooseAccountIntent(context: Context): Intent? {
return null
}
}

View File

@ -1,610 +0,0 @@
package org.cryptomator.presentation.presenter
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 com.microsoft.identity.client.AuthenticationCallback
import com.microsoft.identity.client.IAccount
import com.microsoft.identity.client.IAuthenticationResult
import com.microsoft.identity.client.IMultipleAccountPublicClientApplication
import com.microsoft.identity.client.IPublicClientApplication
import com.microsoft.identity.client.PublicClientApplication
import com.microsoft.identity.client.exception.MsalClientException
import com.microsoft.identity.client.exception.MsalException
import com.microsoft.identity.client.exception.MsalServiceException
import com.microsoft.identity.client.exception.MsalUiRequiredException
import org.cryptomator.data.util.X509CertificateHelper
import org.cryptomator.domain.Cloud
import org.cryptomator.domain.CloudType
import org.cryptomator.domain.DropboxCloud
import org.cryptomator.domain.GoogleDriveCloud
import org.cryptomator.domain.OnedriveCloud
import org.cryptomator.domain.PCloud
import org.cryptomator.domain.WebDavCloud
import org.cryptomator.domain.di.PerView
import org.cryptomator.domain.exception.FatalBackendException
import org.cryptomator.domain.exception.NetworkConnectionException
import org.cryptomator.domain.exception.authentication.AuthenticationException
import org.cryptomator.domain.exception.authentication.WebDavCertificateUntrustedAuthenticationException
import org.cryptomator.domain.exception.authentication.WebDavNotSupportedException
import org.cryptomator.domain.exception.authentication.WebDavServerNotFoundException
import org.cryptomator.domain.exception.authentication.WrongCredentialsException
import org.cryptomator.domain.usecases.cloud.AddOrChangeCloudConnectionUseCase
import org.cryptomator.domain.usecases.cloud.GetCloudsUseCase
import org.cryptomator.domain.usecases.cloud.GetUsernameUseCase
import org.cryptomator.generator.Callback
import org.cryptomator.presentation.BuildConfig
import org.cryptomator.presentation.R
import org.cryptomator.presentation.exception.ExceptionHandlers
import org.cryptomator.presentation.exception.PermissionNotGrantedException
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
import org.cryptomator.presentation.model.WebDavCloudModel
import org.cryptomator.presentation.model.mappers.CloudModelMapper
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.Workflow
import org.cryptomator.util.ExceptionUtil
import org.cryptomator.util.crypto.CredentialCryptor
import java.security.cert.CertificateEncodingException
import java.security.cert.CertificateException
import java.security.cert.X509Certificate
import javax.inject.Inject
import timber.log.Timber
@PerView
class AuthenticateCloudPresenter @Inject constructor( //
exceptionHandlers: ExceptionHandlers, //
private val cloudModelMapper: CloudModelMapper, //
private val addOrChangeCloudConnectionUseCase: AddOrChangeCloudConnectionUseCase, //
private val getCloudsUseCase: GetCloudsUseCase, //
private val getUsernameUseCase: GetUsernameUseCase, //
private val addExistingVaultWorkflow: AddExistingVaultWorkflow, //
private val createNewVaultWorkflow: CreateNewVaultWorkflow
) : Presenter<AuthenticateCloudView>(exceptionHandlers) {
private val strategies = arrayOf( //
DropboxAuthStrategy(), //
OnedriveAuthStrategy(), //
PCloudAuthStrategy(), //
WebDAVAuthStrategy(), //
S3AuthStrategy(), //
LocalStorageAuthStrategy() //
)
override fun workflows(): Iterable<Workflow<*>> {
return listOf(createNewVaultWorkflow, addExistingVaultWorkflow)
}
override fun resumed() {
val cloud = view?.intent()?.cloud()
val error = view?.intent()?.error()
handleNetworkConnectionExceptionIfRequired(error)
view?.intent()?.let { cloud?.let { cloud -> authStrategyFor(cloud).resumed(it) } }
}
private fun handleNetworkConnectionExceptionIfRequired(error: AuthenticationException?) {
if (error != null && ExceptionUtil.contains(error, NetworkConnectionException::class.java)) {
view?.showMessage(R.string.error_no_network_connection)
finish()
}
}
private fun authStrategyFor(cloud: CloudModel): AuthStrategy {
strategies.forEach { strategy ->
if (strategy.supports(cloud)) {
return strategy
}
}
return FailingAuthStrategy()
}
private fun getUsernameAndSuceedAuthentication(cloud: Cloud) {
getUsernameUseCase.withCloud(cloud).run(object : DefaultResultHandler<String>() {
override fun onSuccess(username: String) {
succeedAuthenticationWith(updateUsernameOf(cloud, username))
}
override fun onError(e: Throwable) {
super.onError(e)
finish()
}
})
}
private fun updateUsernameOf(cloud: Cloud, username: String): Cloud {
when (cloud.type()) {
CloudType.DROPBOX -> return DropboxCloud.aCopyOf(cloud as DropboxCloud).withUsername(username).build()
CloudType.ONEDRIVE -> return OnedriveCloud.aCopyOf(cloud as OnedriveCloud).withUsername(username).build()
}
throw IllegalStateException("Cloud " + cloud.type() + " is not supported")
}
private fun succeedAuthenticationWith(cloud: Cloud) {
addOrChangeCloudConnectionUseCase //
.withCloud(cloud) //
.run(object : DefaultResultHandler<Void?>() {
override fun onSuccess(void: Void?) {
finishWithResult(cloudModelMapper.toModel(cloud))
}
override fun onError(e: Throwable) {
super.onError(e)
finish()
}
})
}
private fun failAuthentication(cloudName: Int) {
activity().runOnUiThread {
view?.showMessage(String.format(getString(R.string.screen_authenticate_auth_authentication_failed), getString(cloudName)))
}
finish()
}
private fun failAuthentication(error: PermissionNotGrantedException) {
finishWithResult(error)
}
private inner class DropboxAuthStrategy : AuthStrategy {
private var authenticationStarted = false
override fun supports(cloud: CloudModel): Boolean {
return cloud.cloudType() == CloudTypeModel.DROPBOX
}
override fun resumed(intent: AuthenticateCloudIntent) {
if (authenticationStarted) {
handleAuthenticationResult(intent.cloud())
} else {
startAuthentication()
}
}
private fun startAuthentication() {
showProgress(ProgressModel(ProgressStateModel.AUTHENTICATION))
authenticationStarted = true
Auth.startOAuth2Authentication(context(), BuildConfig.DROPBOX_API_KEY)
view?.skipTransition()
}
private fun handleAuthenticationResult(cloudModel: CloudModel) {
val authToken = Auth.getOAuth2Token()
if (authToken == null) {
failAuthentication(cloudModel.name())
} else {
getUsernameAndSuceedAuthentication( //
DropboxCloud.aCopyOf(cloudModel.toCloud() as DropboxCloud) //
.withAccessToken(encrypt(authToken)) //
.build()
)
}
}
}
@Callback(dispatchResultOkOnly = false)
fun onUserRecoveryFinished(result: ActivityResult, cloud: CloudModel) {
if (result.isResultOk) {
succeedAuthenticationWith(cloud.toCloud())
} else {
failAuthentication(cloud.name())
}
}
@Callback(dispatchResultOkOnly = false)
fun onGoogleDriveAuthenticated(result: ActivityResult, cloud: CloudModel) {
if (result.isResultOk) {
val accountName = result.intent()?.extras?.getString(AccountManager.KEY_ACCOUNT_NAME)
succeedAuthenticationWith(
GoogleDriveCloud.aCopyOf(cloud.toCloud() as GoogleDriveCloud) //
.withUsername(accountName) //
.withAccessToken(accountName) //
.build()
)
} else {
failAuthentication(cloud.name())
}
}
private inner class OnedriveAuthStrategy : AuthStrategy {
private var authenticationStarted = false
override fun supports(cloud: CloudModel): Boolean {
return cloud.cloudType() == CloudTypeModel.ONEDRIVE
}
override fun resumed(intent: AuthenticateCloudIntent) {
if (!authenticationStarted) {
startAuthentication(intent.cloud())
}
}
private fun startAuthentication(cloud: CloudModel) {
authenticationStarted = true
Toast.makeText(context(), R.string.notification_authenticating, Toast.LENGTH_SHORT).show()
PublicClientApplication.createMultipleAccountPublicClientApplication(
context(),
R.raw.auth_config_onedrive,
object : IPublicClientApplication.IMultipleAccountApplicationCreatedListener {
override fun onCreated(application: IMultipleAccountPublicClientApplication) {
application.getAccounts(object : IPublicClientApplication.LoadAccountsCallback {
override fun onTaskCompleted(accounts: List<IAccount>) {
if (accounts.isEmpty()) {
application.acquireToken(activity(), onedriveScopes(), getAuthInteractiveCallback(cloud))
} else {
accounts.find { account -> account.username == cloud.username() }?.let {
application.acquireTokenSilentAsync(
onedriveScopes(),
it,
"https://login.microsoftonline.com/common",
getAuthSilentCallback(cloud, application)
)
} ?: application.acquireToken(activity(), onedriveScopes(), getAuthInteractiveCallback(cloud))
}
}
override fun onError(e: MsalException) {
Timber.tag("AuthenticateCloudPresenter").e(e, "Error to get accounts")
failAuthentication(cloud.name())
}
})
}
override fun onError(e: MsalException) {
Timber.tag("AuthenticateCloudPresenter").i(e, "Error in configuration")
failAuthentication(cloud.name())
}
})
}
private fun getAuthSilentCallback(cloud: CloudModel, application: IMultipleAccountPublicClientApplication): AuthenticationCallback {
return object : AuthenticationCallback {
override fun onSuccess(authenticationResult: IAuthenticationResult) {
Timber.tag("AuthenticateCloudPresenter").i("Successfully authenticated")
handleAuthenticationResult(cloud, authenticationResult.accessToken)
}
override fun onError(e: MsalException) {
Timber.tag("AuthenticateCloudPresenter").e(e, "Failed to acquireToken")
when (e) {
is MsalClientException -> {
/* Exception inside MSAL, more info inside MsalError.java */
failAuthentication(cloud.name())
}
is MsalServiceException -> {
/* Exception when communicating with the STS, likely config issue */
failAuthentication(cloud.name())
}
is MsalUiRequiredException -> {
/* Tokens expired or no session, retry with interactive */
application.acquireToken(activity(), onedriveScopes(), getAuthInteractiveCallback(cloud))
}
}
}
override fun onCancel() {
Timber.tag("AuthenticateCloudPresenter").i("User cancelled login")
}
}
}
private fun getAuthInteractiveCallback(cloud: CloudModel): AuthenticationCallback {
return object : AuthenticationCallback {
override fun onSuccess(authenticationResult: IAuthenticationResult) {
Timber.tag("AuthenticateCloudPresenter").i("Successfully authenticated")
handleAuthenticationResult(cloud, authenticationResult.accessToken, authenticationResult.account.username)
}
override fun onError(e: MsalException) {
Timber.tag("AuthenticateCloudPresenter").e(e, "Successfully authenticated")
failAuthentication(cloud.name())
}
override fun onCancel() {
Timber.tag("AuthenticateCloudPresenter").i("User cancelled login")
}
}
}
private fun handleAuthenticationResult(cloud: CloudModel, accessToken: String) {
getUsernameAndSuceedAuthentication( //
OnedriveCloud.aCopyOf(cloud.toCloud() as OnedriveCloud) //
.withAccessToken(encrypt(accessToken)) //
.build()
)
}
private fun handleAuthenticationResult(cloud: CloudModel, accessToken: String, username: String) {
getUsernameAndSuceedAuthentication( //
OnedriveCloud.aCopyOf(cloud.toCloud() as OnedriveCloud) //
.withAccessToken(encrypt(accessToken)) //
.withUsername(username)
.build()
)
}
}
private inner class PCloudAuthStrategy : AuthStrategy {
private var authenticationStarted = false
override fun supports(cloud: CloudModel): Boolean {
return cloud.cloudType() == CloudTypeModel.PCLOUD
}
override fun resumed(intent: AuthenticateCloudIntent) {
if (authenticationStarted) {
finish()
} else {
startAuthentication(intent.cloud())
Toast.makeText(
context(),
String.format(getString(R.string.error_authentication_failed_re_authenticate), intent.cloud().username()),
Toast.LENGTH_LONG
).show()
}
}
private fun startAuthentication(cloud: CloudModel) {
authenticationStarted = true
showProgress(ProgressModel(ProgressStateModel.AUTHENTICATION))
view?.skipTransition()
requestActivityResult(
ActivityResultCallbacks.pCloudReAuthenticationFinished(cloud), //
Intents.cloudConnectionListIntent() //
.withCloudType(CloudTypeModel.PCLOUD) //
.withDialogTitle(context().getString(R.string.screen_update_pcloud_connections_title)) //
.withFinishOnCloudItemClick(false) //
)
}
}
@Callback
fun pCloudReAuthenticationFinished(activityResult: ActivityResult, cloud: CloudModel) {
val code = activityResult.intent().extras?.getString(CloudConnectionListPresenter.PCLOUD_OAUTH_AUTH_CODE, "")
val hostname = activityResult.intent().extras?.getString(CloudConnectionListPresenter.PCLOUD_HOSTNAME, "")
if (!code.isNullOrEmpty() && !hostname.isNullOrEmpty()) {
Timber.tag("CloudConnectionListPresenter").i("PCloud OAuth code successfully retrieved")
val accessToken = CredentialCryptor //
.getInstance(this.context()) //
.encrypt(code)
val pCloudSkeleton = PCloud.aPCloud() //
.withAccessToken(accessToken)
.withUrl(hostname)
.build();
getUsernameUseCase //
.withCloud(pCloudSkeleton) //
.run(object : DefaultResultHandler<String>() {
override fun onSuccess(username: String) {
Timber.tag("CloudConnectionListPresenter").i("PCloud Authentication successfully")
prepareForSavingPCloud(PCloud.aCopyOf(pCloudSkeleton).withUsername(username).build())
}
})
} else {
Timber.tag("CloudConnectionListPresenter").i("PCloud Authentication not successful")
failAuthentication(cloud.name())
}
}
fun prepareForSavingPCloud(cloud: PCloud) {
getCloudsUseCase //
.withCloudType(cloud.type()) //
.run(object : DefaultResultHandler<List<Cloud>>() {
override fun onSuccess(clouds: List<Cloud>) {
clouds.firstOrNull {
(it as PCloud).username() == cloud.username()
}?.let {
it as PCloud
succeedAuthenticationWith(
PCloud.aCopyOf(it) //
.withUrl(cloud.url())
.withAccessToken(cloud.accessToken())
.build()
)
} ?: succeedAuthenticationWith(cloud)
}
})
}
private inner class WebDAVAuthStrategy : AuthStrategy {
override fun supports(cloud: CloudModel): Boolean {
return cloud.cloudType() == CloudTypeModel.WEBDAV
}
override fun resumed(intent: AuthenticateCloudIntent) {
handleWebDavAuthenticationExceptionIfRequired(intent.cloud() as WebDavCloudModel, intent.error())
}
private fun handleWebDavAuthenticationExceptionIfRequired(cloud: WebDavCloudModel, e: AuthenticationException) {
Timber.tag("AuthicateCloudPrester").e(e)
when {
ExceptionUtil.contains(e, WrongCredentialsException::class.java) -> {
failAuthentication(cloud.name())
}
ExceptionUtil.contains(e, WebDavCertificateUntrustedAuthenticationException::class.java) -> {
handleCertificateUntrustedExceptionIfRequired(cloud, e)
}
ExceptionUtil.contains(e, WebDavServerNotFoundException::class.java) -> {
view?.showMessage(R.string.error_server_not_found)
finish()
}
ExceptionUtil.contains(e, WebDavNotSupportedException::class.java) -> {
view?.showMessage(R.string.screen_cloud_error_webdav_not_supported)
finish()
}
}
}
private fun handleCertificateUntrustedExceptionIfRequired(cloud: WebDavCloudModel, e: AuthenticationException) {
val untrustedException = ExceptionUtil.extract(e, WebDavCertificateUntrustedAuthenticationException::class.java)
try {
val certificate = X509CertificateHelper.convertFromPem(untrustedException.get().certificate)
view?.showUntrustedCertificateDialog(cloud.toCloud() as WebDavCloud, certificate)
} catch (ex: CertificateException) {
Timber.tag("AuthicateCloudPrester").e(ex)
throw FatalBackendException(ex)
}
}
}
fun onAcceptWebDavCertificateClicked(cloud: WebDavCloud, certificate: X509Certificate) {
try {
val webDavCloudWithAcceptedCert = WebDavCloud.aCopyOf(cloud) //
.withCertificate(X509CertificateHelper.convertToPem(certificate)) //
.build()
finishWithResultAndExtra(
cloudModelMapper.toModel(webDavCloudWithAcceptedCert), //
WEBDAV_ACCEPTED_UNTRUSTED_CERTIFICATE, //
true
)
} catch (e: CertificateEncodingException) {
Timber.tag("AuthicateCloudPrester").e(e)
throw FatalBackendException(e)
}
}
fun onAcceptWebDavCertificateDenied() {
finish()
}
private inner class S3AuthStrategy : AuthStrategy {
private var authenticationStarted = false
override fun supports(cloud: CloudModel): Boolean {
return cloud.cloudType() == CloudTypeModel.S3
}
override fun resumed(intent: AuthenticateCloudIntent) {
when {
ExceptionUtil.contains(intent.error(), WrongCredentialsException::class.java) -> {
if (!authenticationStarted) {
startAuthentication(intent.cloud())
Toast.makeText(
context(),
String.format(getString(R.string.error_authentication_failed), intent.cloud().username()),
Toast.LENGTH_LONG
).show()
}
}
else -> {
Timber.tag("AuthicateCloudPrester").e(intent.error())
failAuthentication(intent.cloud().name())
}
}
}
private fun startAuthentication(cloud: CloudModel) {
authenticationStarted = true
startIntent(Intents.s3AddOrChangeIntent().withS3Cloud(cloud as S3CloudModel))
}
}
private inner class LocalStorageAuthStrategy : AuthStrategy {
private var authenticationStarted = false
override fun supports(cloud: CloudModel): Boolean {
return cloud.cloudType() == CloudTypeModel.LOCAL
}
override fun resumed(intent: AuthenticateCloudIntent) {
if (!authenticationStarted) {
startAuthentication(intent.cloud())
}
}
private fun startAuthentication(cloud: CloudModel) {
authenticationStarted = true
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 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 {
return CredentialCryptor //
.getInstance(context()) //
.encrypt(password)
}
private inner class FailingAuthStrategy : AuthStrategy {
override fun supports(cloud: CloudModel): Boolean {
return false
}
override fun resumed(intent: AuthenticateCloudIntent) {
view?.showError(R.string.error_authentication_failed)
finish()
}
}
private interface AuthStrategy {
fun supports(cloud: CloudModel): Boolean
fun resumed(intent: AuthenticateCloudIntent)
}
companion object {
const val WEBDAV_ACCEPTED_UNTRUSTED_CERTIFICATE = "acceptedUntrustedCertificate"
fun onedriveScopes(): Array<String> {
return arrayOf("User.Read", "Files.ReadWrite")
}
}
init {
unsubscribeOnDestroy(addOrChangeCloudConnectionUseCase, getCloudsUseCase, getUsernameUseCase)
}
}

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.cryptomator.presentation">
<!-- Required to send intent to F-Droid app -->
<queries>
<package android:name="org.fdroid.fdroid" />
</queries>
<!-- Required to self update Cryptomator in the F-Droid main variant -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
</manifest>

View File

@ -0,0 +1,15 @@
package org.cryptomator.presentation.presenter
import android.content.Context
object DropboxAuthHelper {
fun startOAuth2Authentication(context: Context) {
// no-op
}
fun getOAuth2Token(): String? {
return null
}
}

View File

@ -0,0 +1,18 @@
package org.cryptomator.presentation.presenter
import android.app.Activity
import org.cryptomator.domain.OnedriveCloud
import org.cryptomator.domain.exception.FatalBackendException
object OnedriveAuthentication {
fun getAuthenticatedOnedriveCloud(activity: Activity, success: (cloud: OnedriveCloud) -> Unit, failed: (e: FatalBackendException) -> Unit) {
// no-op
}
fun refreshOrCheckAuth(activity: Activity, cloud: OnedriveCloud, success: (cloud: OnedriveCloud) -> Unit, failed: (e: FatalBackendException) -> Unit) {
// no-op
}
}

View File

@ -0,0 +1,23 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group android:scaleX="0.05172973"
android:scaleY="0.05172973"
android:translateX="25.29"
android:translateY="29.480108">
<path
android:pathData="m877,689.3a51.6,51.6 0,0 0,-23.7 -5.8h-40.5c27.8,-41.7 46.3,-92.3 46.3,-153.2 0.1,-34.3 -5.4,-61.2 -10.5,-78.8a83.6,83.6 0,0 1,-0.6 -45.6c14.7,-53.1 5.1,-111.8 5.1,-111.8 -185.1,-57.8 -300.1,-0.5 -300.1,-0.5s-114.8,-57.6 -300,-0.4c0,0 -9.9,58.6 4.7,111.8a83.7,83.7 0,0 1,-0.7 45.6c-5.1,17.6 -10.7,44.5 -10.8,78.8 -0.1,61.3 18.4,112.2 46.4,154.1h-41.3a51.6,51.6 0,0 0,-23.7 5.8c-19.8,10.1 -35.9,32.7 -45.3,63.7 -8.7,28.8 -11,62.1 -6.5,93.9 8.5,59.3 38.8,99.2 75.5,99.2h103.5v-0.1c4.4,-0.2 8.8,-2.2 12.9,-5.9l49.7,-43.8c10.2,-9.1 17.8,-32 20.2,-61.3a248,248 0,0 0,0.7 -25.9c61.4,33.1 113.8,46.2 113.8,46.2s52.5,-13 114.1,-46a245.8,245.8 0,0 0,0.7 25.7c2.4,29.3 10,52.2 20.2,61.3l49.7,43.8c4.2,3.7 8.5,5.6 12.9,5.9v0.1h103.5c36.7,0 67,-39.8 75.5,-99.2 4.6,-31.8 2.3,-65.2 -6.5,-93.9 -9.4,-30.9 -25.5,-53.5 -45.4,-63.7zM404.2,881.4c-0.5,0.3 -25.9,22.9 -40.4,35.7a11.2,11.2 0,0 1,-17.4 -3.3c-21,-41.1 -23.3,-124.9 -8.4,-176.5 25.2,24.3 53.1,44.2 80.1,60.3 1.5,30.4 -4.3,78.1 -14,83.7zM597.1,670.3 L552.4,683 507.7,670.2 540,534.7a57.5,57.5 0,1 1,25.3 0zM758.5,913.8a11.2,11.2 0,0 1,-17.4 3.3c-14.5,-12.8 -39.9,-35.4 -40.4,-35.7 -9.6,-5.6 -15.5,-53.1 -14,-83.5 27.1,-16.1 55,-36 80.3,-60.2 14.8,51.6 12.5,135.1 -8.5,176.1z"
android:fillColor="#49b04a"/>
<path
android:pathData="m178.9,584.1 l20.6,-40.4a49.4,49.4 0,0 0,-0.5 -46l18.1,-35.3a74.8,74.8 0,0 0,16.4 -0.6c1.4,-6.4 2.9,-12.1 4.3,-16.9a63.6,63.6 0,0 0,0.6 -34.7c-9.9,-36.1 -9.5,-73.8 -7.7,-97.3a75.1,75.1 0,0 0,-68.6 119.1l-19.3,37.7a49.3,49.3 0,0 0,-36.5 26.3l-20.6,40.4a49.3,49.3 0,0 0,-0.5 43.9,82.8 82.8,0 0,0 -74.8,122.8 20,20 0,0 0,35 -19.3,42.8 42.8,0 1,1 42.2,21.8 20,20 0,0 0,2.2 39.9,21.8 21.8,0 0,0 2.3,-0.1 82.8,82.8 0,0 0,54 -135.7,49.3 49.3,0 0,0 32.8,-25.6z"
android:fillColor="#49b04a"/>
<path
android:pathData="m1099,619.4a82.9,82.9 0,0 0,-73.6 -45.1,49.3 49.3,0 0,0 -3,-37.9l-20.6,-40.4a49.3,49.3 0,0 0,-36.5 -26.3l-19.2,-37.7a75,75 0,0 0,-70.7 -118.8c1.7,23.4 2.1,61.6 -8,97.9a63.6,63.6 0,0 0,0.5 34.7c1.3,4.5 2.7,9.7 4,15.5a75.4,75.4 0,0 0,19.2 1l18.1,35.3a49.4,49.4 0,0 0,-0.5 46l20.6,40.4a49.2,49.2 0,0 0,29 24.6,82.8 82.8,0 0,0 57.8,130.7 22.1,22.1 0,0 0,2.3 0.1,20 20,0 0,0 2.2,-39.9 42.8,42.8 0,1 1,42.2 -21.8,20 20,0 1,0 35,19.3 82.8,82.8 0,0 0,1.3 -77.7z"
android:fillColor="#49b04a"/>
<path
android:pathData="m553.1,271.7c19,-7.6 67.6,-23.5 139.3,-23.5 39.7,0 80.5,4.9 122,14.5 -9.8,-133.1 -112.4,-262.7 -261.7,-262.7 -160.5,0 -252.7,129.2 -261.7,262.1 41.3,-9.5 81.9,-14.4 121.4,-14.4 72.5,0 121.5,16.2 140.7,24zM623.1,216.9a11.5,11.5 0,1 1,11.5 -11.5,11.5 11.5,0 0,1 -11.5,11.5zM646.4,107.1a60.6,60.6 0,0 1,60.6 60.6h-121.2a60.6,60.6 0,0 1,60.6 -60.6zM577.8,193.9a11.5,11.5 0,1 1,-11.5 11.5,11.5 11.5,0 0,1 11.6,-11.5zM487.2,216.9a11.5,11.5 0,1 1,11.5 -11.5,11.5 11.5,0 0,1 -11.5,11.5zM532.5,193.9a11.5,11.5 0,1 1,-11.5 11.5,11.5 11.5,0 0,1 11.6,-11.5zM459,107.1a60.6,60.6 0,0 1,60.6 60.6h-121.3a60.6,60.6 0,0 1,60.7 -60.6z"
android:fillColor="#49b04a"/>
</group>
</vector>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#F1C40F</color>
</resources>

View File

@ -36,20 +36,17 @@
android:required="false" />
<activity
android:name=".ui.activity.SplashActivity"
android:name=".ui.activity.VaultListActivity"
android:exported="true"
android:screenOrientation="portrait"
android:theme="@style/SplashTheme">
android:theme="@style/AppTheme.Starting"
android:windowSoftInputMode="adjustPan">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ui.activity.VaultListActivity"
android:exported="false"
android:windowSoftInputMode="adjustPan" />
<activity android:name=".ui.activity.ChooseCloudServiceActivity" />
<activity
android:name=".ui.activity.CreateVaultActivity"
@ -90,22 +87,6 @@
android:exported="false"
android:theme="@style/TransparentPopUp" />
<!-- Cloud Services -->
<activity
android:name="com.dropbox.core.android.AuthActivity"
android:configChanges="orientation|keyboard"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<data android:scheme="db-${DROPBOX_API_KEY}" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<!-- Settings -->
<activity
android:name=".ui.activity.AutoUploadChooseVaultActivity"
@ -119,6 +100,9 @@
<activity
android:name=".ui.activity.CloudSettingsActivity"
android:exported="false" />
<activity
android:name=".ui.activity.CryptomatorVariantsActivity"
android:exported="false" />
<activity
android:name=".ui.activity.LicensesActivity"
android:exported="true" />
@ -188,22 +172,6 @@
</intent-filter>
</activity>
<activity
android:name="com.microsoft.identity.client.BrowserTabActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="org.cryptomator"
android:path="/${ONEDRIVE_API_KEY_DECODED}"
android:scheme="msauth" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"

View File

@ -51,6 +51,9 @@ class CryptomatorApp : MultiDexApplication(), HasComponent<ApplicationComponent>
"fdroid" -> {
"F-Droid Edition"
}
"lite" -> {
"F-Droid Main Repo Edition"
}
else -> "Google Play Edition"
}
Timber.tag("App").i(

View File

@ -13,6 +13,7 @@ import org.cryptomator.presentation.ui.activity.ChooseCloudServiceActivity;
import org.cryptomator.presentation.ui.activity.CloudConnectionListActivity;
import org.cryptomator.presentation.ui.activity.CloudSettingsActivity;
import org.cryptomator.presentation.ui.activity.CreateVaultActivity;
import org.cryptomator.presentation.ui.activity.CryptomatorVariantsActivity;
import org.cryptomator.presentation.ui.activity.ImagePreviewActivity;
import org.cryptomator.presentation.ui.activity.LicenseCheckActivity;
import org.cryptomator.presentation.ui.activity.LicensesActivity;
@ -20,7 +21,6 @@ import org.cryptomator.presentation.ui.activity.S3AddOrChangeActivity;
import org.cryptomator.presentation.ui.activity.SetPasswordActivity;
import org.cryptomator.presentation.ui.activity.SettingsActivity;
import org.cryptomator.presentation.ui.activity.SharedFilesActivity;
import org.cryptomator.presentation.ui.activity.SplashActivity;
import org.cryptomator.presentation.ui.activity.TextEditorActivity;
import org.cryptomator.presentation.ui.activity.UnlockVaultActivity;
import org.cryptomator.presentation.ui.activity.VaultListActivity;
@ -50,8 +50,6 @@ public interface ActivityComponent {
Activity activity();
void inject(SplashActivity splashActivity);
void inject(VaultListActivity vaultListActivity);
void inject(SetPasswordActivity setPasswordActivity);
@ -123,4 +121,7 @@ public interface ActivityComponent {
void inject(S3AddOrChangeActivity s3AddOrChangeActivity);
void inject(S3AddOrChangeFragment s3AddOrChangeFragment);
void inject(CryptomatorVariantsActivity cryptomatorVariantsActivity);
}

View File

@ -0,0 +1,9 @@
package org.cryptomator.presentation.intent;
import org.cryptomator.generator.Intent;
import org.cryptomator.presentation.ui.activity.AutoUploadChooseVaultActivity;
@Intent(AutoUploadChooseVaultActivity.class)
public interface AutoUploadChooseVaultIntent {
}

View File

@ -0,0 +1,9 @@
package org.cryptomator.presentation.intent;
import org.cryptomator.generator.Intent;
import org.cryptomator.presentation.ui.activity.CryptomatorVariantsActivity;
@Intent(CryptomatorVariantsActivity.class)
public interface CryptomatorVariantsIntent {
}

View File

@ -6,19 +6,6 @@ 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
import com.google.api.services.drive.DriveScopes
import com.microsoft.identity.client.AuthenticationCallback
import com.microsoft.identity.client.IAccount
import com.microsoft.identity.client.IAuthenticationResult
import com.microsoft.identity.client.IMultipleAccountPublicClientApplication
import com.microsoft.identity.client.IPublicClientApplication
import com.microsoft.identity.client.PublicClientApplication
import com.microsoft.identity.client.exception.MsalClientException
import com.microsoft.identity.client.exception.MsalException
import com.microsoft.identity.client.exception.MsalServiceException
import com.microsoft.identity.client.exception.MsalUiRequiredException
import org.cryptomator.data.util.X509CertificateHelper
import org.cryptomator.domain.Cloud
import org.cryptomator.domain.CloudType
@ -39,7 +26,6 @@ import org.cryptomator.domain.usecases.cloud.AddOrChangeCloudConnectionUseCase
import org.cryptomator.domain.usecases.cloud.GetCloudsUseCase
import org.cryptomator.domain.usecases.cloud.GetUsernameUseCase
import org.cryptomator.generator.Callback
import org.cryptomator.presentation.BuildConfig
import org.cryptomator.presentation.R
import org.cryptomator.presentation.exception.ExceptionHandlers
import org.cryptomator.presentation.intent.AuthenticateCloudIntent
@ -175,12 +161,12 @@ class AuthenticateCloudPresenter @Inject constructor( //
private fun startAuthentication() {
showProgress(ProgressModel(ProgressStateModel.AUTHENTICATION))
authenticationStarted = true
Auth.startOAuth2Authentication(context(), BuildConfig.DROPBOX_API_KEY)
DropboxAuthHelper.startOAuth2Authentication(context())
view?.skipTransition()
}
private fun handleAuthenticationResult(cloudModel: CloudModel) {
val authToken = Auth.getOAuth2Token()
val authToken = DropboxAuthHelper.getOAuth2Token()
if (authToken == null) {
failAuthentication(cloudModel.name())
} else {
@ -221,11 +207,10 @@ class AuthenticateCloudPresenter @Inject constructor( //
}
private fun chooseAccount(cloud: CloudModel) {
val chooseAccountIntent = GoogleAccountCredential.usingOAuth2(context(), setOf(DriveScopes.DRIVE)).newChooseAccountIntent()
try {
requestActivityResult( //
ActivityResultCallbacks.onGoogleDriveAuthenticated(cloud), //
chooseAccountIntent
GoogleAuthHelper.getChooseAccountIntent(context())
)
} catch (e: ActivityNotFoundException) {
view?.showMessage(R.string.error_play_services_not_available)
@ -261,6 +246,7 @@ class AuthenticateCloudPresenter @Inject constructor( //
private inner class OnedriveAuthStrategy : AuthStrategy {
private var authenticationStarted = false
override fun supports(cloud: CloudModel): Boolean {
return cloud.cloudType() == CloudTypeModel.ONEDRIVE
}
@ -276,108 +262,12 @@ class AuthenticateCloudPresenter @Inject constructor( //
Toast.makeText(context(), R.string.notification_authenticating, Toast.LENGTH_SHORT).show()
PublicClientApplication.createMultipleAccountPublicClientApplication(
context(),
R.raw.auth_config_onedrive,
object : IPublicClientApplication.IMultipleAccountApplicationCreatedListener {
override fun onCreated(application: IMultipleAccountPublicClientApplication) {
application.getAccounts(object : IPublicClientApplication.LoadAccountsCallback {
override fun onTaskCompleted(accounts: List<IAccount>) {
if (accounts.isEmpty()) {
application.acquireToken(activity(), onedriveScopes(), getAuthInteractiveCallback(cloud))
} else {
accounts.find { account -> account.username == cloud.username() }?.let {
application.acquireTokenSilentAsync(
onedriveScopes(),
it,
"https://login.microsoftonline.com/common",
getAuthSilentCallback(cloud, application)
)
} ?: application.acquireToken(activity(), onedriveScopes(), getAuthInteractiveCallback(cloud))
}
}
override fun onError(e: MsalException) {
Timber.tag("AuthenticateCloudPresenter").e(e, "Error to get accounts")
OnedriveAuthentication.refreshOrCheckAuth(activity(), cloud.toCloud() as OnedriveCloud, { authenticatedCloud ->
getUsernameAndSuceedAuthentication(authenticatedCloud)
}, {
failAuthentication(cloud.name())
}
})
}
override fun onError(e: MsalException) {
Timber.tag("AuthenticateCloudPresenter").i(e, "Error in configuration")
failAuthentication(cloud.name())
}
})
}
private fun getAuthSilentCallback(cloud: CloudModel, application: IMultipleAccountPublicClientApplication): AuthenticationCallback {
return object : AuthenticationCallback {
override fun onSuccess(authenticationResult: IAuthenticationResult) {
Timber.tag("AuthenticateCloudPresenter").i("Successfully authenticated")
handleAuthenticationResult(cloud, authenticationResult.accessToken)
}
override fun onError(e: MsalException) {
Timber.tag("AuthenticateCloudPresenter").e(e, "Failed to acquireToken")
when (e) {
is MsalClientException -> {
/* Exception inside MSAL, more info inside MsalError.java */
failAuthentication(cloud.name())
}
is MsalServiceException -> {
/* Exception when communicating with the STS, likely config issue */
failAuthentication(cloud.name())
}
is MsalUiRequiredException -> {
/* Tokens expired or no session, retry with interactive */
application.acquireToken(activity(), onedriveScopes(), getAuthInteractiveCallback(cloud))
}
}
}
override fun onCancel() {
Timber.tag("AuthenticateCloudPresenter").i("User cancelled login")
}
}
}
private fun getAuthInteractiveCallback(cloud: CloudModel): AuthenticationCallback {
return object : AuthenticationCallback {
override fun onSuccess(authenticationResult: IAuthenticationResult) {
Timber.tag("AuthenticateCloudPresenter").i("Successfully authenticated")
handleAuthenticationResult(cloud, authenticationResult.accessToken, authenticationResult.account.username)
}
override fun onError(e: MsalException) {
Timber.tag("AuthenticateCloudPresenter").e(e, "Successfully authenticated")
failAuthentication(cloud.name())
}
override fun onCancel() {
Timber.tag("AuthenticateCloudPresenter").i("User cancelled login")
}
}
}
private fun handleAuthenticationResult(cloud: CloudModel, accessToken: String) {
getUsernameAndSuceedAuthentication( //
OnedriveCloud.aCopyOf(cloud.toCloud() as OnedriveCloud) //
.withAccessToken(encrypt(accessToken)) //
.build()
)
}
private fun handleAuthenticationResult(cloud: CloudModel, accessToken: String, username: String) {
getUsernameAndSuceedAuthentication( //
OnedriveCloud.aCopyOf(cloud.toCloud() as OnedriveCloud) //
.withAccessToken(encrypt(accessToken)) //
.withUsername(username)
.build()
)
}
}
private inner class PCloudAuthStrategy : AuthStrategy {

View File

@ -1,5 +1,6 @@
package org.cryptomator.presentation.presenter
import android.view.View
import org.cryptomator.domain.Cloud
import org.cryptomator.domain.di.PerView
import org.cryptomator.domain.exception.FatalBackendException
@ -12,6 +13,7 @@ import org.cryptomator.presentation.intent.Intents
import org.cryptomator.presentation.model.CloudTypeModel
import org.cryptomator.presentation.model.mappers.CloudModelMapper
import org.cryptomator.presentation.ui.activity.view.ChooseCloudServiceView
import org.cryptomator.presentation.ui.snackbar.SnackbarAction
import org.cryptomator.presentation.workflow.ActivityResult
import org.cryptomator.presentation.workflow.AddExistingVaultWorkflow
import org.cryptomator.presentation.workflow.CreateNewVaultWorkflow
@ -37,6 +39,11 @@ class ChooseCloudServicePresenter @Inject constructor( //
if (BuildConfig.FLAVOR == "fdroid") {
cloudTypeModels.remove(CloudTypeModel.GOOGLE_DRIVE)
} else if (BuildConfig.FLAVOR == "lite") {
cloudTypeModels.remove(CloudTypeModel.GOOGLE_DRIVE)
cloudTypeModels.remove(CloudTypeModel.DROPBOX)
cloudTypeModels.remove(CloudTypeModel.ONEDRIVE)
cloudTypeModels.remove(CloudTypeModel.PCLOUD)
}
view?.render(cloudTypeModels)
@ -87,6 +94,18 @@ class ChooseCloudServicePresenter @Inject constructor( //
finishWithResult(cloudModelMapper.toModel(cloud))
}
fun showCloudMissingSnackbarHintInLiteVariant() {
if (BuildConfig.FLAVOR == "lite") {
view?.showSnackbar(R.string.snack_bar_cryptomator_variants_hint, object: SnackbarAction {
override fun onClick(v: View?) {
startIntent(Intents.cryptomatorVariantsIntent())
}
override val text: Int
get() = R.string.snack_bar_cryptomator_variants_title
})
}
}
init {
unsubscribeOnDestroy(getCloudsUseCase)
}

View File

@ -4,13 +4,6 @@ import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import com.microsoft.identity.client.AuthenticationCallback
import com.microsoft.identity.client.IAccount
import com.microsoft.identity.client.IAuthenticationResult
import com.microsoft.identity.client.IMultipleAccountPublicClientApplication
import com.microsoft.identity.client.IPublicClientApplication
import com.microsoft.identity.client.PublicClientApplication
import com.microsoft.identity.client.exception.MsalException
import org.cryptomator.domain.Cloud
import org.cryptomator.domain.LocalStorageCloud
import org.cryptomator.domain.OnedriveCloud
@ -137,51 +130,13 @@ class CloudConnectionListPresenter @Inject constructor( //
}
private fun addOnedriveCloud() {
PublicClientApplication.createMultipleAccountPublicClientApplication(
context(),
R.raw.auth_config_onedrive,
object : IPublicClientApplication.IMultipleAccountApplicationCreatedListener {
override fun onCreated(application: IMultipleAccountPublicClientApplication) {
application.getAccounts(object : IPublicClientApplication.LoadAccountsCallback {
override fun onTaskCompleted(accounts: List<IAccount>) {
application.acquireToken(activity(), AuthenticateCloudPresenter.onedriveScopes(), getAuthInteractiveCallback())
}
override fun onError(e: MsalException) {
Timber.tag("AuthenticateCloudPresenter").e(e, "Error to get accounts")
showError(e);
}
OnedriveAuthentication.getAuthenticatedOnedriveCloud(activity(), { cloud ->
saveOnedriveCloud(cloud)
}, { e ->
showError(e)
})
}
override fun onError(e: MsalException) {
Timber.tag("AuthenticateCloudPresenter").i(e, "Error in configuration")
showError(e);
}
})
}
private fun getAuthInteractiveCallback(): AuthenticationCallback {
return object : AuthenticationCallback {
override fun onSuccess(authenticationResult: IAuthenticationResult) {
Timber.tag("AuthenticateCloudPresenter").i("Successfully authenticated")
val accessToken = CredentialCryptor.getInstance(context()).encrypt(authenticationResult.accessToken)
val onedriveSkeleton = OnedriveCloud.aOnedriveCloud().withAccessToken(accessToken).withUsername(authenticationResult.account.username).build()
saveOnedriveCloud(onedriveSkeleton)
}
override fun onError(e: MsalException) {
Timber.tag("AuthenticateCloudPresenter").e(e, "Successfully authenticated")
showError(e);
}
override fun onCancel() {
Timber.tag("AuthenticateCloudPresenter").i("User cancelled login")
}
}
}
private fun saveOnedriveCloud(onedriveSkeleton: OnedriveCloud) {
getUsernameUseCase //
.withCloud(onedriveSkeleton) //

View File

@ -140,9 +140,28 @@ class CloudSettingsPresenter @Inject constructor( //
it.add(aS3Cloud())
it.add(aLocalCloud())
}
.filter { cloud -> !(BuildConfig.FLAVOR == "lite" && excludeApiCloudsInLite(cloud.cloudType())) } //
view?.render(cloudModel)
}
private fun excludeApiCloudsInLite(cloudType: CloudTypeModel): Boolean {
return when (cloudType) {
CloudTypeModel.GOOGLE_DRIVE -> {
true
}
CloudTypeModel.ONEDRIVE -> {
true
}
CloudTypeModel.DROPBOX -> {
true
}
CloudTypeModel.PCLOUD -> {
true
}
else -> false
}
}
private fun aOnedriveCloud(): OnedriveCloudModel {
return OnedriveCloudModel(OnedriveCloud.aOnedriveCloud().build())
}

View File

@ -0,0 +1,103 @@
package org.cryptomator.presentation.presenter
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import com.google.common.base.Optional
import org.cryptomator.data.util.NetworkConnectionCheck
import org.cryptomator.domain.di.PerView
import org.cryptomator.domain.usecases.DoUpdateCheckUseCase
import org.cryptomator.domain.usecases.DoUpdateUseCase
import org.cryptomator.domain.usecases.NoOpResultHandler
import org.cryptomator.domain.usecases.UpdateCheck
import org.cryptomator.presentation.R
import org.cryptomator.presentation.exception.ExceptionHandlers
import org.cryptomator.presentation.model.ProgressModel
import org.cryptomator.presentation.ui.activity.view.CryptomatorVariantsView
import org.cryptomator.presentation.util.FileUtil
import javax.inject.Inject
@PerView
class CryptomatorVariantsPresenter @Inject constructor(
//
exceptionMappings: ExceptionHandlers, //
private val updateCheckUseCase: DoUpdateCheckUseCase, //
private val updateUseCase: DoUpdateUseCase, //
private val networkConnectionCheck: NetworkConnectionCheck, //
private val fileUtil: FileUtil, //
) : Presenter<CryptomatorVariantsView>(exceptionMappings) {
private val fDroidPackageName = "org.fdroid.fdroid"
fun onInstallMainFDroidVariantClicked() {
context().packageManager.getLaunchIntentForPackage(fDroidPackageName)?.let {
it.data = Uri.parse("https://f-droid.org/packages/org.cryptomator.light")
context().startActivity(it)
} ?: Toast.makeText(context(), R.string.error_interact_with_fdroid_but_fdroid_missing, Toast.LENGTH_SHORT).show()
}
fun onAddRepoClicked() {
context().packageManager.getLaunchIntentForPackage(fDroidPackageName)?.let {
it.data = Uri.parse("https://static.cryptomator.org/android/fdroid/repo?fingerprint=F7C3EC3B0D588D3CB52983E9EB1A7421C93D4339A286398E71D7B651E8D8ECDD")
context().startActivity(it)
} ?: Toast.makeText(context(), R.string.error_interact_with_fdroid_but_fdroid_missing, Toast.LENGTH_SHORT).show()
}
fun onInstallFDroidVariantClicked() {
context().packageManager.getLaunchIntentForPackage(fDroidPackageName)?.let {
it.data = Uri.parse("https://f-droid.org/packages/org.cryptomator")
context().startActivity(it)
} ?: Toast.makeText(context(), R.string.error_interact_with_fdroid_but_fdroid_missing, Toast.LENGTH_SHORT).show()
}
fun onInstallWebsiteVariantClicked() {
if (networkConnectionCheck.isPresent) {
view?.showProgress(ProgressModel.GENERIC)
updateCheckUseCase //
.withVersion("0.0.0")
.run(object : NoOpResultHandler<Optional<UpdateCheck>>() {
override fun onSuccess(result: Optional<UpdateCheck>) {
installUpdate()
}
override fun onError(e: Throwable) {
view?.showProgress(ProgressModel.COMPLETED)
showError(e)
}
})
} else {
Toast.makeText(context(), R.string.error_update_no_internet, Toast.LENGTH_SHORT).show()
}
}
private fun installUpdate() {
val uri = fileUtil.contentUriForNewTempFile("cryptomator.apk")
val file = fileUtil.tempFile("cryptomator.apk")
updateUseCase //
.withFile(file) //
.run(object : NoOpResultHandler<Void?>() {
override fun onError(e: Throwable) {
showError(e)
}
override fun onSuccess(result: Void?) {
super.onSuccess(result)
val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(uri, "application/vnd.android.package-archive")
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
context().startActivity(intent)
}
override fun onFinished() {
view?.showProgress(ProgressModel.COMPLETED)
}
})
}
init {
unsubscribeOnDestroy(updateCheckUseCase, updateUseCase)
}
}

View File

@ -83,6 +83,9 @@ class SettingsPresenter @Inject internal constructor(
"fdroid" -> {
"F-Droid"
}
"lite" -> {
"F-Droid Main Repo Edition"
}
else -> "Google Play"
}
return StringBuilder().append("## ").append(context().getString(R.string.error_report_subject)).append("\n\n") //

View File

@ -1,16 +0,0 @@
package org.cryptomator.presentation.presenter
import org.cryptomator.domain.di.PerView
import org.cryptomator.presentation.exception.ExceptionHandlers
import org.cryptomator.presentation.intent.Intents
import org.cryptomator.presentation.ui.activity.view.SplashView
import javax.inject.Inject
@PerView
class SplashPresenter @Inject constructor(exceptionMappings: ExceptionHandlers) : Presenter<SplashView>(exceptionMappings) {
override fun resumed() {
Intents.vaultListIntent().startActivity(this)
finish()
}
}

View File

@ -402,6 +402,10 @@ class UnlockVaultPresenter @Inject constructor(
finishWithResult(null)
}
fun onChangePasswordCanceled() {
finishWithResult(null)
}
private open class PendingUnlock(private val vault: Vault?) : Serializable {
private var unlockToken: UnlockToken? = null

Some files were not shown because too many files have changed in this diff Show More