Skip to content

Commit 67db219

Browse files
committed
Extract licensing helpers, deduplicate dialog/IAP boilerplate, simplify code
1 parent e9c996b commit 67db219

32 files changed

Lines changed: 552 additions & 698 deletions

domain/src/main/java/org/cryptomator/domain/usecases/DoLicenseCheck.java

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,12 @@ public LicenseCheck execute() throws BackendException {
6464
private String useLicenseOrRetrieveFromPreferences(String license) throws NoLicenseAvailableException {
6565
if (!license.isEmpty()) {
6666
return license;
67-
} else {
68-
license = sharedPreferencesHandler.licenseToken();
69-
if (license.isEmpty()) {
70-
throw new NoLicenseAvailableException();
71-
}
7267
}
73-
return license;
68+
String stored = sharedPreferencesHandler.licenseToken();
69+
if (stored.isEmpty()) {
70+
throw new NoLicenseAvailableException();
71+
}
72+
return stored;
7473
}
7574

7675
private ECPublicKey getPublicKey(String publicKey) throws NoSuchAlgorithmException, InvalidKeySpecException {

presentation/src/main/java/org/cryptomator/presentation/CryptomatorApp.kt

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@ import org.cryptomator.presentation.service.AutoUploadNotification
2626
import org.cryptomator.presentation.service.AutoUploadService
2727
import org.cryptomator.presentation.service.CryptorsService
2828
import org.cryptomator.presentation.service.IapBillingService
29+
import org.cryptomator.presentation.service.PendingCallbackQueue
2930
import org.cryptomator.presentation.service.ProductInfo
3031
import org.cryptomator.presentation.service.PurchaseRevokedToastObserver
3132
import org.cryptomator.presentation.service.RestoreOutcome
33+
import org.cryptomator.presentation.service.RestoreOutcomeDialogObserver
3234
import org.cryptomator.util.FlavorConfig
3335
import org.cryptomator.util.NoOpActivityLifecycleCallbacks
3436
import org.cryptomator.util.SharedPreferencesHandler
@@ -51,21 +53,18 @@ class CryptomatorApp : MultiDexApplication(), HasComponent<ApplicationComponent>
5153
@Volatile
5254
private var iapBillingServiceBinder: IapBillingService.Binder? = null
5355

54-
@Volatile
55-
var lastRestoreOutcome: RestoreOutcome? = null
56-
57-
fun consumeLastRestoreOutcome(): RestoreOutcome? {
58-
val outcome = lastRestoreOutcome
59-
lastRestoreOutcome = null
60-
return outcome
56+
fun restorePurchasesAndStore() {
57+
val handler = SharedPreferencesHandler(applicationContext())
58+
restorePurchases { outcome -> handler.setPendingRestoreOutcome(outcome.kind.name) }
6159
}
6260

63-
private val pendingProductDetailsCallbacks = mutableListOf<(List<ProductInfo>) -> Unit>()
61+
private val pendingProductDetailsCallbacks = PendingCallbackQueue<List<ProductInfo>>()
6462

6563
override fun onCreate() {
6664
super.onCreate()
6765
setupLogging()
6866
val sharedPreferencesHandler = SharedPreferencesHandler(applicationContext())
67+
6968
@Suppress("KotlinConstantConditions") //
7069
val flavor = when (BuildConfig.FLAVOR) {
7170
"apkstore" -> "APK Store Edition"
@@ -88,6 +87,7 @@ class CryptomatorApp : MultiDexApplication(), HasComponent<ApplicationComponent>
8887
registerActivityLifecycleCallbacks(serviceNotifier)
8988
if (FlavorConfig.isFreemiumFlavor) {
9089
registerActivityLifecycleCallbacks(PurchaseRevokedToastObserver(sharedPreferencesHandler))
90+
registerActivityLifecycleCallbacks(RestoreOutcomeDialogObserver(sharedPreferencesHandler))
9191
}
9292
AppCompatDelegate.setDefaultNightMode(sharedPreferencesHandler.screenStyleMode)
9393
cleanupCache()
@@ -165,25 +165,17 @@ class CryptomatorApp : MultiDexApplication(), HasComponent<ApplicationComponent>
165165
}
166166

167167
fun queryProductDetails(callback: (List<ProductInfo>) -> Unit) {
168-
if (FlavorConfig.isFreemiumFlavor) {
169-
synchronized(pendingProductDetailsCallbacks) {
170-
iapBillingServiceBinder?.queryProductDetails(callback) ?: pendingProductDetailsCallbacks.add(callback)
171-
}
172-
} else {
168+
if (!FlavorConfig.isFreemiumFlavor) {
173169
callback(emptyList())
170+
return
174171
}
172+
iapBillingServiceBinder?.queryProductDetails(callback) ?: pendingProductDetailsCallbacks.enqueue(callback)
175173
}
176174

177175
private fun drainPendingProductDetailsCallbacks() {
178-
synchronized(pendingProductDetailsCallbacks) {
179-
if (pendingProductDetailsCallbacks.isEmpty()) {
180-
return
181-
}
182-
val callbacks = ArrayList(pendingProductDetailsCallbacks)
183-
pendingProductDetailsCallbacks.clear()
184-
iapBillingServiceBinder?.queryProductDetails { products ->
185-
callbacks.forEach { it(products) }
186-
}
176+
val snapshot = pendingProductDetailsCallbacks.drainSnapshot() ?: return
177+
iapBillingServiceBinder?.queryProductDetails { products ->
178+
snapshot.forEach { it(products) }
187179
}
188180
}
189181

presentation/src/main/java/org/cryptomator/presentation/licensing/LicenseEnforcer.kt

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
package org.cryptomator.presentation.licensing
22

33
import android.app.Activity
4-
import android.content.Context
54
import android.content.Intent
65
import android.widget.Toast
76
import androidx.annotation.StringRes
8-
import org.cryptomator.domain.di.PerView
97
import org.cryptomator.presentation.R
108
import org.cryptomator.presentation.intent.Intents
119
import org.cryptomator.presentation.model.VaultModel
@@ -17,7 +15,6 @@ import java.util.Date
1715
import java.util.concurrent.TimeUnit
1816
import javax.inject.Inject
1917

20-
@PerView
2118
class LicenseEnforcer @Inject constructor(private val sharedPreferencesHandler: SharedPreferencesHandler) {
2219

2320
enum class LockedAction(
@@ -56,19 +53,22 @@ class LicenseEnforcer @Inject constructor(private val sharedPreferencesHandler:
5653
sharedPreferencesHandler.setTrialExpirationDate(trialExpiration)
5754
}
5855

59-
fun hasActiveTrial(): Boolean {
60-
return evaluateTrialState().isActive
61-
}
56+
fun hasActiveTrial(): Boolean = evaluateTrialState().isActive
6257

6358
fun evaluateTrialState(): TrialState {
59+
val state = readTrialState()
60+
if (state.isExpired && !sharedPreferencesHandler.isTrialExpired()) {
61+
sharedPreferencesHandler.setTrialExpired(true)
62+
}
63+
return state
64+
}
65+
66+
private fun readTrialState(): TrialState {
6467
val trialExpiration = sharedPreferencesHandler.trialExpirationDate()
6568
val now = System.currentTimeMillis()
6669
val sticky = sharedPreferencesHandler.isTrialExpired()
6770
val active = trialExpiration > 0 && trialExpiration > now && !sticky
6871
val expired = trialExpiration > 0 && (trialExpiration <= now || sticky)
69-
if (expired && !sticky) {
70-
sharedPreferencesHandler.setTrialExpired(true)
71-
}
7272
val formattedDate = if (active || expired) {
7373
DateFormat.getDateInstance().format(Date(trialExpiration))
7474
} else null
@@ -82,25 +82,18 @@ class LicenseEnforcer @Inject constructor(private val sharedPreferencesHandler:
8282
val hasPaidLicense: Boolean,
8383
val hasLifetimeLicense: Boolean,
8484
val hasRunningSubscription: Boolean,
85-
val trialState: TrialState,
86-
val trialExpirationText: String?
85+
val trialState: TrialState
8786
)
8887

89-
fun evaluateUiState(context: Context): LicenseUiState {
88+
fun evaluateUiState(): LicenseUiState {
9089
val trialState = evaluateTrialState()
9190
val paidLicense = hasPaidLicense()
92-
val lifetimeLicense = sharedPreferencesHandler.licenseToken().isNotEmpty()
93-
val runningSubscription = sharedPreferencesHandler.hasRunningSubscription()
94-
val expirationText = if (trialState.isActive || trialState.isExpired) {
95-
context.getString(R.string.screen_license_check_trial_expiration, trialState.formattedExpirationDate)
96-
} else null
9791
return LicenseUiState(
9892
hasWriteAccess = paidLicense || trialState.isActive,
9993
hasPaidLicense = paidLicense,
100-
hasLifetimeLicense = lifetimeLicense,
101-
hasRunningSubscription = runningSubscription,
102-
trialState = trialState,
103-
trialExpirationText = expirationText
94+
hasLifetimeLicense = sharedPreferencesHandler.licenseToken().isNotEmpty(),
95+
hasRunningSubscription = sharedPreferencesHandler.hasRunningSubscription(),
96+
trialState = trialState
10497
)
10598
}
10699

@@ -109,10 +102,6 @@ class LicenseEnforcer @Inject constructor(private val sharedPreferencesHandler:
109102
return true
110103
}
111104

112-
if (FlavorConfig.isPremiumFlavor) {
113-
return false
114-
}
115-
116105
val intent = Intents.licenseCheckIntent()
117106
.withLockedAction(action.name)
118107
.build(activity as ContextHolder)
Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,31 @@
11
package org.cryptomator.presentation.licensing
22

3-
import android.content.Context
43
import org.cryptomator.util.FlavorConfig
54
import org.cryptomator.util.SharedPreferencesHandler
65
import java.util.function.Consumer
76

87
class LicenseStateOrchestrator(
98
private val sharedPreferencesHandler: SharedPreferencesHandler,
109
private val licenseEnforcer: LicenseEnforcer,
11-
private val contextProvider: () -> Context,
12-
private val onStateChanged: (LicenseEnforcer.LicenseUiState) -> Unit,
10+
private val callback: Callback,
1311
private val priceLoader: (() -> Unit)? = null
1412
) {
1513

14+
interface Callback {
15+
fun onLicenseStateChanged(uiState: LicenseEnforcer.LicenseUiState)
16+
fun onSubscriptionActivatedFirstTime() {}
17+
fun onSubscriptionUpgradedToLifetime() {}
18+
}
19+
1620
private val licenseChangeListener = Consumer<String> { _ -> updateState() }
21+
private var wasSubscriptionOnly = false
1722

1823
fun onResume() {
24+
wasSubscriptionOnly = isSubscriptionOnly(
25+
hasRunningSubscription = sharedPreferencesHandler.hasRunningSubscription(),
26+
hasLifetimeLicense = sharedPreferencesHandler.licenseToken().isNotEmpty()
27+
)
1928
sharedPreferencesHandler.addLicenseChangedListeners(licenseChangeListener)
20-
updateState()
2129
if (FlavorConfig.isFreemiumFlavor) {
2230
priceLoader?.invoke()
2331
}
@@ -28,6 +36,20 @@ class LicenseStateOrchestrator(
2836
}
2937

3038
fun updateState() {
31-
onStateChanged(licenseEnforcer.evaluateUiState(contextProvider()))
39+
val uiState = licenseEnforcer.evaluateUiState()
40+
val nowSubOnly = isSubscriptionOnly(uiState.hasRunningSubscription, uiState.hasLifetimeLicense)
41+
val wasSubOnly = wasSubscriptionOnly
42+
wasSubscriptionOnly = nowSubOnly
43+
44+
if (wasSubOnly && uiState.hasLifetimeLicense && uiState.hasRunningSubscription) {
45+
callback.onSubscriptionUpgradedToLifetime()
46+
}
47+
if (!wasSubOnly && nowSubOnly) {
48+
callback.onSubscriptionActivatedFirstTime()
49+
}
50+
callback.onLicenseStateChanged(uiState)
3251
}
52+
53+
private fun isSubscriptionOnly(hasRunningSubscription: Boolean, hasLifetimeLicense: Boolean): Boolean =
54+
hasRunningSubscription && !hasLifetimeLicense
3355
}

presentation/src/main/java/org/cryptomator/presentation/presenter/LicenseCheckPresenter.kt

Lines changed: 7 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ package org.cryptomator.presentation.presenter
22

33
import android.net.Uri
44
import org.cryptomator.domain.usecases.DoLicenseCheckUseCase
5-
import org.cryptomator.domain.usecases.LicenseCheck
6-
import org.cryptomator.domain.usecases.NoOpResultHandler
75
import org.cryptomator.presentation.exception.ExceptionHandlers
86
import org.cryptomator.presentation.ui.activity.view.LicenseView
97
import org.cryptomator.presentation.ui.dialog.AppIsObscuredInfoDialog
@@ -12,43 +10,15 @@ import javax.inject.Inject
1210

1311
class LicenseCheckPresenter @Inject internal constructor(
1412
exceptionHandlers: ExceptionHandlers,
15-
private val doLicenseCheckUseCase: DoLicenseCheckUseCase,
16-
private val sharedPreferencesHandler: SharedPreferencesHandler
13+
doLicenseCheckUseCase: DoLicenseCheckUseCase,
14+
sharedPreferencesHandler: SharedPreferencesHandler
1715
) : Presenter<LicenseView>(exceptionHandlers) {
1816

19-
fun validate(data: Uri?) {
20-
data?.let {
21-
val license = it.fragment ?: it.lastPathSegment
22-
if (license.isNullOrEmpty()) {
23-
return
24-
}
25-
doLicenseCheckUseCase.withLicense(license).run(CheckLicenseStatusSubscriber())
26-
}
27-
}
17+
private val validator = LicenseKeyValidator(doLicenseCheckUseCase, sharedPreferencesHandler, { view }, ::showError)
2818

29-
fun validateDialogAware(license: String?) {
30-
doLicenseCheckUseCase.withLicense(license).run(CheckLicenseStatusSubscriber())
31-
}
19+
fun validate(data: Uri?) = validator.validate(data)
20+
fun validateDialogAware(license: String?) = validator.validateDialogAware(license)
21+
fun onFilteredTouchEventForSecurity() = view?.showDialog(AppIsObscuredInfoDialog.newInstance())
3222

33-
fun onFilteredTouchEventForSecurity() {
34-
view?.showDialog(AppIsObscuredInfoDialog.newInstance())
35-
}
36-
37-
private inner class CheckLicenseStatusSubscriber : NoOpResultHandler<LicenseCheck>() {
38-
override fun onSuccess(licenseCheck: LicenseCheck) {
39-
super.onSuccess(licenseCheck)
40-
view?.closeDialog()
41-
sharedPreferencesHandler.setMail(licenseCheck.mail())
42-
view?.showConfirmationDialog(licenseCheck.mail())
43-
}
44-
45-
override fun onError(t: Throwable) {
46-
super.onError(t)
47-
showError(t)
48-
}
49-
}
50-
51-
init {
52-
unsubscribeOnDestroy(doLicenseCheckUseCase)
53-
}
23+
init { unsubscribeOnDestroy(doLicenseCheckUseCase) }
5424
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package org.cryptomator.presentation.presenter
2+
3+
import android.net.Uri
4+
import org.cryptomator.domain.usecases.DoLicenseCheckUseCase
5+
import org.cryptomator.domain.usecases.LicenseCheck
6+
import org.cryptomator.domain.usecases.NoOpResultHandler
7+
import org.cryptomator.presentation.ui.activity.view.LicenseView
8+
import org.cryptomator.util.SharedPreferencesHandler
9+
10+
internal class LicenseKeyValidator(
11+
private val doLicenseCheckUseCase: DoLicenseCheckUseCase,
12+
private val sharedPreferencesHandler: SharedPreferencesHandler,
13+
private val getView: () -> LicenseView?
14+
) {
15+
fun validate(data: Uri?, onLicenseExtracted: ((String) -> Unit)? = null) {
16+
data?.let {
17+
val license = it.fragment ?: it.lastPathSegment
18+
if (license.isNullOrEmpty()) {
19+
return
20+
}
21+
onLicenseExtracted?.invoke(license)
22+
doLicenseCheckUseCase.withLicense(license).run(subscriber())
23+
}
24+
}
25+
26+
fun validateDialogAware(license: String?) {
27+
doLicenseCheckUseCase.withLicense(license).run(subscriber())
28+
}
29+
30+
private fun subscriber(): NoOpResultHandler<LicenseCheck> = object : NoOpResultHandler<LicenseCheck>() {
31+
override fun onSuccess(licenseCheck: LicenseCheck) {
32+
super.onSuccess(licenseCheck)
33+
getView()?.closeDialog()
34+
sharedPreferencesHandler.setMail(licenseCheck.mail())
35+
getView()?.showConfirmationDialog(licenseCheck.mail())
36+
}
37+
38+
override fun onError(t: Throwable) {
39+
super.onError(t)
40+
onError(t)
41+
}
42+
}
43+
}

0 commit comments

Comments
 (0)