Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ class CryptomatorApp : Application(), HasComponent<ApplicationComponent> {

override fun onServiceDisconnected(name: ComponentName) {
Timber.tag("App").i("IAP Billing service disconnected")
pendingProductDetailsCallbacks.markNotReady()
iapBillingServiceBinder = null
}
}, BIND_AUTO_CREATE)
Expand All @@ -162,11 +163,13 @@ class CryptomatorApp : Application(), HasComponent<ApplicationComponent> {
callback(emptyList())
return
}
iapBillingServiceBinder?.queryProductDetails(callback) ?: pendingProductDetailsCallbacks.enqueue(callback)
if (!pendingProductDetailsCallbacks.enqueueUnlessReady(callback)) {
iapBillingServiceBinder?.queryProductDetails(callback)
}
}

private fun drainPendingProductDetailsCallbacks() {
val snapshot = pendingProductDetailsCallbacks.drainSnapshot() ?: return
val snapshot = pendingProductDetailsCallbacks.markReadyAndDrain() ?: return
iapBillingServiceBinder?.queryProductDetails { products ->
snapshot.forEach { it(products) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.cryptomator.domain.repository.VaultRepository;
import org.cryptomator.presentation.di.module.ApplicationModule;
import org.cryptomator.presentation.di.module.ThreadModule;
import org.cryptomator.presentation.service.ProductPricesCache;
import org.cryptomator.presentation.util.ContentResolverUtil;
import org.cryptomator.presentation.util.FileUtil;

Expand Down Expand Up @@ -48,4 +49,6 @@ public interface ApplicationComponent {

NetworkConnectionCheck networkConnectionCheck();

ProductPricesCache productPricesCache();

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
import android.content.Context;

import org.cryptomator.presentation.CryptomatorApp;
import org.cryptomator.presentation.service.ProductPricesCache;

import javax.inject.Singleton;

import dagger.Module;
import dagger.Provides;
import kotlin.Unit;

@Module
public class ApplicationModule {
Expand All @@ -23,4 +25,13 @@ public ApplicationModule(CryptomatorApp application) {
Context provideApplicationContext() {
return application;
}

@Provides
@Singleton
ProductPricesCache provideProductPricesCache() {
return new ProductPricesCache(callback -> {
application.queryProductDetails(callback);
return Unit.INSTANCE;
}, System::currentTimeMillis);
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
package org.cryptomator.presentation.licensing

import org.cryptomator.util.FlavorConfig
import org.cryptomator.util.SharedPreferencesHandler
import java.util.function.Consumer

class LicenseStateOrchestrator(
private val sharedPreferencesHandler: SharedPreferencesHandler,
private val licenseEnforcer: LicenseEnforcer,
private val callback: Callback,
private val priceLoader: (() -> Unit)? = null
private val callback: Callback
) {

interface Callback {
Expand All @@ -25,9 +23,6 @@ class LicenseStateOrchestrator(
hasLifetimeLicense = sharedPreferencesHandler.licenseToken().isNotEmpty()
)
sharedPreferencesHandler.addLicenseChangedListeners(licenseChangeListener)
if (FlavorConfig.isFreemiumFlavor) {
priceLoader?.invoke()
}
}

fun onPause() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package org.cryptomator.presentation.presenter
import android.net.Uri
import org.cryptomator.domain.usecases.DoLicenseCheckUseCase
import org.cryptomator.presentation.exception.ExceptionHandlers
import org.cryptomator.presentation.service.ProductPricesCache
import org.cryptomator.presentation.ui.activity.view.LicenseView
import org.cryptomator.presentation.ui.dialog.AppIsObscuredInfoDialog
import org.cryptomator.util.SharedPreferencesHandler
Expand All @@ -11,11 +12,16 @@ import javax.inject.Inject
class LicenseCheckPresenter @Inject internal constructor(
exceptionHandlers: ExceptionHandlers,
doLicenseCheckUseCase: DoLicenseCheckUseCase,
sharedPreferencesHandler: SharedPreferencesHandler
sharedPreferencesHandler: SharedPreferencesHandler,
private val productPricesCache: ProductPricesCache
) : Presenter<LicenseView>(exceptionHandlers) {

private val validator = LicenseKeyValidator(doLicenseCheckUseCase, sharedPreferencesHandler, { view }, ::showError)

override fun resumed() {
loadProductPrices(productPricesCache)
}

fun validate(data: Uri?) = validator.validate(data)
fun validateDialogAware(license: String?) = validator.validateDialogAware(license)
fun onFilteredTouchEventForSecurity() = view?.showDialog(AppIsObscuredInfoDialog.newInstance())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.cryptomator.presentation.presenter

import org.cryptomator.presentation.service.ProductPricesCache
import org.cryptomator.presentation.ui.activity.view.LicenseView
import org.cryptomator.util.FlavorConfig

internal fun Presenter<out LicenseView>.loadProductPrices(productPricesCache: ProductPricesCache) {
if (!FlavorConfig.isFreemiumFlavor) {
return
}
productPricesCache.refreshProductPrices { prices ->
activity().runOnUiThread {
if (!isPaused) {
view?.bindProductPrices(prices)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ import org.cryptomator.presentation.model.CloudTypeModel
import org.cryptomator.presentation.model.ProgressModel
import org.cryptomator.presentation.model.VaultModel
import org.cryptomator.presentation.model.mappers.CloudFolderModelMapper
import org.cryptomator.presentation.service.ProductPrices
import org.cryptomator.presentation.service.ProductPricesCache
import org.cryptomator.presentation.service.SalePromo
import org.cryptomator.presentation.service.resolveSalePromo
import org.cryptomator.presentation.ui.activity.WelcomeActivity
import org.cryptomator.presentation.ui.activity.view.VaultListView
import org.cryptomator.presentation.ui.dialog.AppIsObscuredInfoDialog
Expand All @@ -66,6 +70,7 @@ import org.cryptomator.presentation.workflow.Workflow
import org.cryptomator.util.FlavorConfig
import org.cryptomator.util.SharedPreferencesHandler
import org.cryptomator.util.crypto.CryptoMode
import java.util.function.Consumer
import javax.inject.Inject
import timber.log.Timber

Expand Down Expand Up @@ -93,10 +98,16 @@ class VaultListPresenter @Inject constructor( //
private val cloudFolderModelMapper: CloudFolderModelMapper, //
private val licenseEnforcer: LicenseEnforcer, //
private val sharedPreferencesHandler: SharedPreferencesHandler, //
private val productPricesCache: ProductPricesCache, //
exceptionMappings: ExceptionHandlers
) : Presenter<VaultListView>(exceptionMappings) {

private var vaultAction: VaultAction? = null
private val licenseChangeListener = Consumer<String> { _ ->
if (!isPaused) {
updateSalePromo()
}
}

override fun workflows(): Iterable<Workflow<*>> {
return listOf(addExistingVaultWorkflow, createNewVaultWorkflow)
Expand All @@ -113,6 +124,8 @@ class VaultListPresenter @Inject constructor( //
if (!alreadyKnownExpired && trialState.isExpired && !licenseEnforcer.hasPaidLicense()) {
view?.showDialog(TrialExpiredDialog.newInstance())
}
// Invokes the listener right away, so this also evaluates the sale promo on resume.
sharedPreferencesHandler.addLicenseChangedListeners(licenseChangeListener)
}
}

Expand All @@ -127,6 +140,42 @@ class VaultListPresenter @Inject constructor( //
return false
}

private fun updateSalePromo() {
if (licenseEnforcer.hasPaidLicense()) {
view?.hideSalePromoBanner()
} else {
loadSalePromo()
}
}

private fun loadSalePromo() {
productPricesCache.queryProductPrices { prices ->
activity().runOnUiThread {
if (!isPaused) {
showOrHideSalePromoBanner(prices)
}
}
}
}

private fun showOrHideSalePromoBanner(prices: ProductPrices) {
val salePromo = prices.resolveSalePromo(sharedPreferencesHandler.salePromoDismissedUntil(), System.currentTimeMillis())
if (salePromo != null && !licenseEnforcer.hasPaidLicense()) {
view?.showSalePromoBanner(salePromo)
Comment thread
SailReal marked this conversation as resolved.
} else {
view?.hideSalePromoBanner()
}
}

fun onUnlockFullVersionClicked() {
Intents.licenseCheckIntent().startActivity(this)
}

fun onSalePromoBannerDismissed(salePromo: SalePromo) {
sharedPreferencesHandler.setSalePromoDismissedUntil(salePromo.endTimeMillis)
view?.hideSalePromoBanner()
}

fun onWindowFocusChanged(hasFocus: Boolean) {
if (hasFocus) {
loadVaultList()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import org.cryptomator.domain.usecases.DoLicenseCheckUseCase
import org.cryptomator.generator.Callback
import org.cryptomator.presentation.R
import org.cryptomator.presentation.exception.ExceptionHandlers
import org.cryptomator.presentation.service.ProductPricesCache
import org.cryptomator.presentation.ui.activity.view.WelcomeView
import org.cryptomator.presentation.ui.dialog.AppIsObscuredInfoDialog
import org.cryptomator.presentation.workflow.PermissionsResult
Expand All @@ -22,14 +23,25 @@ import javax.inject.Inject
class WelcomePresenter @Inject internal constructor(
exceptionHandlers: ExceptionHandlers,
doLicenseCheckUseCase: DoLicenseCheckUseCase,
sharedPreferencesHandler: SharedPreferencesHandler
sharedPreferencesHandler: SharedPreferencesHandler,
private val productPricesCache: ProductPricesCache
) : Presenter<WelcomeView>(exceptionHandlers) {

private val validator = LicenseKeyValidator(doLicenseCheckUseCase, sharedPreferencesHandler, { view }, ::showError)

fun validate(data: Uri?) = validator.validate(data) { view?.showOrUpdateLicenseEntry(it) }
fun validateDialogAware(license: String?) = validator.validateDialogAware(license)

override fun resumed() {
if (view?.hasLicenseView() == true) {
loadProductPrices(productPricesCache)
}
}

fun onLicenseViewReady() {
loadProductPrices(productPricesCache)
}

fun onFilteredTouchEventForSecurity() {
view?.showDialog(AppIsObscuredInfoDialog.newInstance())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,32 @@ class PendingCallbackQueue<T> {

private val pending = mutableListOf<(T) -> Unit>()

@Volatile
var isReady = false
private set

@Synchronized
fun enqueue(callback: (T) -> Unit) {
fun enqueueUnlessReady(callback: (T) -> Unit): Boolean {
if (isReady) {
return false
}
pending.add(callback)
return true
}

@Synchronized
fun drainSnapshot(): List<(T) -> Unit>? {
fun markReadyAndDrain(): List<(T) -> Unit>? {
isReady = true
if (pending.isEmpty()) {
return null
}
val snapshot = ArrayList(pending)
pending.clear()
return snapshot
}

@Synchronized
fun markNotReady() {
isReady = false
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package org.cryptomator.presentation.service

import java.text.DateFormat
import java.time.ZoneOffset
import java.util.Date
import java.util.TimeZone

data class ProductInfo(
val productId: String,
val price: String,
Expand All @@ -21,6 +26,11 @@ data class ProductPrices(
val lifetimeDiscountEndTimeMillis: Long?
)

data class SalePromo(
val discountPercent: Int,
val endTimeMillis: Long
)

fun List<ProductInfo>.resolveProductPrices(): ProductPrices {
val subscription = find { it.productId == ProductInfo.PRODUCT_YEARLY_SUBSCRIPTION }
val lifetime = find { it.productId == ProductInfo.PRODUCT_FULL_VERSION }
Expand All @@ -32,3 +42,31 @@ fun List<ProductInfo>.resolveProductPrices(): ProductPrices {
lifetimeDiscountEndTimeMillis = lifetime?.discountEndTimeMillis
)
}

fun ProductPrices.resolveSalePromo(dismissedUntilMillis: Long, nowMillis: Long): SalePromo? {
val discountPercent = lifetimeDiscountPercent
val endTimeMillis = lifetimeDiscountEndTimeMillis
if (discountPercent == null || endTimeMillis == null) {
return null
}
if (nowMillis >= endTimeMillis || nowMillis < dismissedUntilMillis) {
return null
}
return SalePromo(discountPercent, endTimeMillis)
}

fun calculateDiscountPercent(priceMicros: Long, discountPriceMicros: Long): Int? {
if (priceMicros <= 0 || discountPriceMicros >= priceMicros) {
return null
}
val discountPercent = ((priceMicros - discountPriceMicros) * 100 / priceMicros).toInt()
return if (discountPercent >= 1) discountPercent else null
}

// The end time is exclusive, so the date of the last millisecond before it is shown.
// Formatting in UTC gives every time zone the same date.
fun formatDiscountEndDate(endTimeMillis: Long): String {
val dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM)
dateFormat.timeZone = TimeZone.getTimeZone(ZoneOffset.UTC)
return dateFormat.format(Date(endTimeMillis - 1))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.cryptomator.presentation.service

import java.util.concurrent.TimeUnit

class ProductPricesCache(
private val queryProductDetails: ((List<ProductInfo>) -> Unit) -> Unit,
private val currentTimeMillis: () -> Long
) {

@Volatile
private var cachedEntry: Entry? = null

fun queryProductPrices(callback: (ProductPrices) -> Unit) {
val entry = cachedEntry
if (entry != null && currentTimeMillis() - entry.loadedAtMillis < CACHE_DURATION_MILLIS) {
callback(entry.prices)
return
}
refreshProductPrices { prices -> callback(cachedEntry?.prices ?: prices) }
}

fun refreshProductPrices(callback: (ProductPrices) -> Unit) {
queryProductDetails { products ->
val prices = products.resolveProductPrices()
if (prices.lifetimePrice != null) {
cachedEntry = Entry(prices, currentTimeMillis())
}
callback(prices)
}
}

private class Entry(val prices: ProductPrices, val loadedAtMillis: Long)

companion object {

private val CACHE_DURATION_MILLIS = TimeUnit.HOURS.toMillis(1)
}
}
Comment thread
SailReal marked this conversation as resolved.
Loading
Loading