Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
ffa55f5
Merge branch 'develop' into feature/freemium
SailReal Oct 22, 2025
f92b090
WIP IAP
SailReal Nov 19, 2025
16c73d0
WIP IAP
SailReal Mar 23, 2026
d9c1a85
Fix onboarding: auto-advance license, screen lock step, playstore fre…
tobihagemann Mar 24, 2026
967e880
Fix onboarding fragment crash, text editor license gating, and double…
tobihagemann Mar 24, 2026
954c354
Add three-option IAP purchase screen with trial, subscription, and li…
tobihagemann Mar 24, 2026
a38ca96
Fix crash: split queryProductDetails into separate INAPP and SUBS que…
tobihagemann Mar 24, 2026
78b75ed
Show purchase options during active trial
tobihagemann Mar 24, 2026
6bb4942
Fix prices not loading: defer product details query until billing con…
tobihagemann Mar 24, 2026
53c28b8
Fix thread safety in IapBillingService, extract trial state, remove d…
tobihagemann Mar 24, 2026
6c4e4b9
Load product prices when navigating to License page in onboarding
tobihagemann Mar 24, 2026
a25c485
Extract shared resolveProductPrices() from LicenseCheckActivity and W…
tobihagemann Mar 24, 2026
e66ff2f
Align IAP screen with iOS: labels, inline trial status, logo size, sp…
tobihagemann Mar 25, 2026
98961eb
Use dedicated tight-viewport drawable for IAP logo, match iOS 64dp si…
tobihagemann Mar 25, 2026
830f426
Queue product detail callbacks when billing service not yet bound
tobihagemann Mar 25, 2026
58b97d1
Support concurrent pending callbacks in IapBillingService
tobihagemann Mar 26, 2026
3c60580
Fix vertical centering of price labels in IAP pill buttons
tobihagemann Mar 26, 2026
9ceda13
Align IAP and settings screens with iOS: remove unlocked text, add tr…
tobihagemann Mar 26, 2026
7de9bb7
Remove redundant onboarding license header, add settings preference t…
tobihagemann Mar 26, 2026
ae83567
Show trial info text only when expired, add info circle icon
tobihagemann Mar 26, 2026
af96962
Hide expired trial info text on Welcome screen after purchase
tobihagemann Mar 26, 2026
118d467
Extract shared LicenseContentViewBinder for purchase and trial state …
tobihagemann Mar 26, 2026
fcd5ca7
Extract IAP layout setup, legal links, and product prices into Licens…
tobihagemann Mar 26, 2026
61cc843
Extract license-entry layout setup into LicenseContentViewBinder
tobihagemann Mar 26, 2026
196cc47
Fix billing state corruption, thread safety, and error handling in Ia…
tobihagemann Mar 27, 2026
45e8c75
Harden LicenseEnforcer: trial guard, isIapFlavor, LicenseUiState, rem…
tobihagemann Mar 27, 2026
1c819f5
Fix WelcomePresenter listener bugs, fragment lifecycle, and adopt Lic…
tobihagemann Mar 27, 2026
fad4f00
Consolidate IAP logic into LicenseContentViewBinder, wire up lockedAc…
tobihagemann Mar 27, 2026
8c7b3f2
Fix migration welcome flow, add billing refresh on resume, add night …
tobihagemann Mar 27, 2026
a15528f
Add Settings subscription management, trial-expired dialog, fix subsc…
tobihagemann Mar 27, 2026
198a5e3
Extract PurchaseManager, fix TOCTOU race, add isReady guard, preload …
tobihagemann Mar 28, 2026
f128b0d
Guard rename/move/delete behind write access, fix Hub default and Set…
tobihagemann Mar 28, 2026
998b8fd
Revert Hub subscription default to INACTIVE for Community Edition safety
tobihagemann Mar 28, 2026
cb61aee
Consolidate flavor checks, deduplicate license orchestration, defer S…
tobihagemann Mar 28, 2026
386640c
Fix store-before-verify bug, restore LicensesActivity, consolidate Hu…
tobihagemann Mar 30, 2026
79a07e6
Add license security tests, vault write-access tests, deduplicate fla…
tobihagemann Mar 30, 2026
c983539
Clean up test FQN imports, deduplicate Gradle exclude blocks
tobihagemann Mar 30, 2026
ddf97bb
Merge develop into feature/freemium, resolve conflicts
tobihagemann Mar 31, 2026
0b8605c
Remove write-access gate from onVaultSelected, remove finish() from o…
tobihagemann Mar 31, 2026
b6d6867
Address review comments: create intent interfaces, fix auto-advance r…
tobihagemann Mar 31, 2026
2a0ae21
Fix VaultListPresenterFreemiumTest NPE: use mockito-kotlin isA for no…
tobihagemann Mar 31, 2026
0751f3c
Remove unused DoLicenseCheckUseCase from VaultListPresenter
tobihagemann Mar 31, 2026
22d1854
Address remaining review comments: intent builders, braces, welcome f…
tobihagemann Apr 1, 2026
8728e06
Inline trial duration constant at usage site
tobihagemann Apr 1, 2026
f2f31ee
Add braces to single-line if block in WelcomeActivity
tobihagemann Apr 1, 2026
897194e
Remove EXTRA_LOCKED_ACTION constant, refresh @InjectIntent reader in …
tobihagemann Apr 1, 2026
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
5 changes: 4 additions & 1 deletion buildsystem/dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ ext {

recyclerViewFastScrollVersion = '2.0.1'

billingclientVersion = '8.2.1'

// testing dependencies

jUnitVersion = '5.11.4'
Expand Down Expand Up @@ -202,7 +204,8 @@ ext {
zxcvbn : "com.nulab-inc:zxcvbn:${zxcvbnVersion}",
scaleImageView : "com.github.cryptomator:subsampling-scale-image-view:${scaleImageViewVersion}",
lruFileCache : "com.github.solkin:disk-lru-cache:${lruFileCacheVersion}",
jsonWebToken : "com.auth0:java-jwt:${jsonWebTokenVersion}"
jsonWebToken : "com.auth0:java-jwt:${jsonWebTokenVersion}",
billing : "com.android.billingclient:billing:${billingclientVersion}"
]

}
86 changes: 33 additions & 53 deletions data/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ android {
dimension "version"
}

playstoreiap {
dimension "version"
}

apkstore {
dimension "version"
}
Expand All @@ -71,6 +75,10 @@ android {
java.srcDirs = ['src/main/java/', 'src/apiKey/java/', 'src/apkStorePlaystore/java/']
}

playstoreiap {
java.srcDirs = ['src/main/java/', 'src/apiKey/java/', 'src/apkStorePlaystore/java/']
}

apkstore {
java.srcDirs = ['src/main/java/', 'src/apiKey/java/', 'src/apkStorePlaystore/java/']
}
Expand All @@ -89,7 +97,7 @@ android {
}
packagingOptions {
resources {
excludes += ['META-INF/DEPENDENCIES', 'META-INF/NOTICE.md', 'META-INF/INDEX.LIST']
excludes += ['META-INF/DEPENDENCIES', 'META-INF/NOTICE.md', 'META-INF/INDEX.LIST', 'META-INF/versions/9/OSGI-INF/MANIFEST.MF']
}
}

Expand All @@ -102,7 +110,7 @@ android {
}

greendao {
schemaVersion 13
schemaVersion 14
}

configurations.all {
Expand All @@ -112,13 +120,20 @@ configurations.all {

dependencies {
def dependencies = rootProject.ext.dependencies
def cloudFlavors = ['playstore', 'playstoreiap', 'apkstore', 'fdroid', 'accrescent']
def googleFlavors = ['playstore', 'playstoreiap', 'apkstore']
def addToFlavors = { flavors, dep, Closure config = null ->
flavors.each { flavor ->
if (config) {
add("${flavor}Implementation", dep, config)
} else {
add("${flavor}Implementation", dep)
}
}
}

implementation project(':domain')
implementation project(':util')
playstoreImplementation dependencies.pcloud
apkstoreImplementation dependencies.pcloud
fdroidImplementation dependencies.pcloud
accrescentImplementation dependencies.pcloud

coreLibraryDesugaring dependencies.coreDesugaring

Expand All @@ -134,63 +149,30 @@ dependencies {
implementation dependencies.jsonWebToken

// cloud
playstoreImplementation dependencies.dropboxCore
playstoreImplementation dependencies.dropboxAndroid
apkstoreImplementation dependencies.dropboxCore
apkstoreImplementation dependencies.dropboxAndroid
fdroidImplementation dependencies.dropboxCore
fdroidImplementation dependencies.dropboxAndroid
accrescentImplementation dependencies.dropboxCore
accrescentImplementation dependencies.dropboxAndroid


playstoreImplementation dependencies.msgraphAuth
apkstoreImplementation dependencies.msgraphAuth
fdroidImplementation dependencies.msgraphAuth
accrescentImplementation dependencies.msgraphAuth
playstoreImplementation dependencies.msgraph
apkstoreImplementation dependencies.msgraph
fdroidImplementation dependencies.msgraph
accrescentImplementation dependencies.msgraph
addToFlavors(cloudFlavors, dependencies.dropboxCore)
addToFlavors(cloudFlavors, dependencies.dropboxAndroid)

addToFlavors(cloudFlavors, dependencies.msgraphAuth)
addToFlavors(cloudFlavors, dependencies.msgraph)

addToFlavors(cloudFlavors, dependencies.pcloud)

implementation dependencies.stax
api dependencies.minIo

playstoreImplementation(dependencies.googlePlayServicesAuth) {
addToFlavors(googleFlavors, dependencies.googlePlayServicesAuth) {
exclude module: 'guava-jdk5'
exclude module: 'httpclient'
exclude module: 'googlehttpclient'
exclude group: "com.google.http-client", module: "google-http-client"
}
apkstoreImplementation(dependencies.googlePlayServicesAuth) {
exclude module: 'guava-jdk5'
exclude module: 'httpclient'
exclude module: "google-http-client"
exclude group: "com.google.http-client", module: "google-http-client"
}

playstoreImplementation(dependencies.googleApiServicesDrive) {
addToFlavors(googleFlavors, dependencies.googleApiServicesDrive) {
exclude module: 'guava-jdk5'
exclude module: 'httpclient'
exclude module: 'googlehttpclient'
exclude group: "com.google.http-client", module: "google-http-client"
}
apkstoreImplementation(dependencies.googleApiServicesDrive) {
exclude module: 'guava-jdk5'
exclude module: 'httpclient'
exclude module: "google-http-client"
exclude group: "com.google.http-client", module: "google-http-client"
}

playstoreImplementation(dependencies.googleApiClientAndroid) {
exclude module: 'guava-jdk5'
exclude module: 'httpclient'
exclude module: "google-http-client"
exclude module: "jetified-google-http-client"
exclude group: "com.google.http-client", module: "google-http-client"
exclude group: "com.google.http-client", module: "jetified-google-http-client"
}
apkstoreImplementation(dependencies.googleApiClientAndroid) {
addToFlavors(googleFlavors + ['accrescent'], dependencies.googleApiClientAndroid) {
exclude module: 'guava-jdk5'
exclude module: 'httpclient'
exclude module: "google-http-client"
Expand All @@ -199,10 +181,8 @@ dependencies {
exclude group: "com.google.http-client", module: "jetified-google-http-client"
}

playstoreImplementation dependencies.trackingFreeGoogleCLient
apkstoreImplementation dependencies.trackingFreeGoogleCLient
playstoreImplementation dependencies.trackingFreeGoogleAndroidCLient
apkstoreImplementation dependencies.trackingFreeGoogleAndroidCLient
addToFlavors(googleFlavors, dependencies.trackingFreeGoogleCLient)
addToFlavors(googleFlavors + ['accrescent'], dependencies.trackingFreeGoogleAndroidCLient)

// rest
implementation dependencies.rxJava
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.base.Optional
import org.cryptomator.data.BuildConfig
import org.cryptomator.data.db.entities.CloudEntityDao
import org.cryptomator.data.db.entities.UpdateCheckEntityDao
import org.cryptomator.data.db.entities.VaultEntityDao
import org.cryptomator.domain.CloudType
import org.cryptomator.util.FlavorConfig
import org.cryptomator.util.SharedPreferencesHandler
import org.cryptomator.util.crypto.CredentialCryptor
import org.cryptomator.util.crypto.CryptoMode
Expand All @@ -28,17 +30,19 @@ import org.junit.runner.RunWith
class UpgradeDatabaseTest {

private val context = InstrumentationRegistry.getInstrumentation().context
private val sharedPreferencesHandler = SharedPreferencesHandler(context)
private lateinit var db: Database
private lateinit var sharedPreferencesHandler: SharedPreferencesHandler

@Before
fun setup() {
db = StandardDatabase(SQLiteDatabase.create(null))
sharedPreferencesHandler = SharedPreferencesHandler(context)
}

@After
fun tearDown() {
db.close()
sharedPreferencesHandler.removeAllEntries()
}

@Test
Expand All @@ -56,6 +60,7 @@ class UpgradeDatabaseTest {
Upgrade10To11().applyTo(db, 10)
Upgrade11To12(sharedPreferencesHandler).applyTo(db, 11)
Upgrade12To13(context).applyTo(db, 12)
Upgrade13To14(sharedPreferencesHandler).applyTo(db, 13)

CloudEntityDao(DaoConfig(db, CloudEntityDao::class.java)).loadAll()
VaultEntityDao(DaoConfig(db, VaultEntityDao::class.java)).loadAll()
Expand Down Expand Up @@ -839,7 +844,6 @@ class UpgradeDatabaseTest {
}
}


@Test
fun upgrade12To13OneDrive() {
Upgrade0To1().applyTo(db, 0)
Expand Down Expand Up @@ -951,4 +955,99 @@ class UpgradeDatabaseTest {
Assert.assertThat(it.getString(it.getColumnIndex("ACCESS_TOKEN_CRYPTO_MODE")), CoreMatchers.`is`(CryptoMode.GCM.name))
}
}

@Test
fun upgrade13To14ExistingLicense() {
Upgrade0To1().applyTo(db, 0)
Upgrade1To2().applyTo(db, 1)
Upgrade2To3(context).applyTo(db, 2)
Upgrade3To4().applyTo(db, 3)
Upgrade4To5().applyTo(db, 4)
Upgrade5To6().applyTo(db, 5)
Upgrade6To7().applyTo(db, 6)
Upgrade7To8().applyTo(db, 7)
Upgrade8To9(sharedPreferencesHandler).applyTo(db, 8)
Upgrade9To10(sharedPreferencesHandler).applyTo(db, 9)
Upgrade10To11().applyTo(db, 10)
Upgrade11To12(sharedPreferencesHandler).applyTo(db, 11)
Upgrade12To13(context).applyTo(db, 12)

val licenseToken = "licenseToken"
val releaseNote = "releaseNote"
val version = "version"
val urlApk = "urlApk"
val apkSha256 = "apkSha256"
val urlReleaseNote = "urlReleaseNote"

Sql.update("UPDATE_CHECK_ENTITY")
.set("LICENSE_TOKEN", Sql.toString(licenseToken))
.set("RELEASE_NOTE", Sql.toString(releaseNote))
.set("VERSION", Sql.toString(version))
.set("URL_TO_APK", Sql.toString(urlApk))
.set("APK_SHA256", Sql.toString(apkSha256))
.set("URL_TO_RELEASE_NOTE", Sql.toString(urlReleaseNote))
.executeOn(db)

Upgrade13To14(sharedPreferencesHandler).applyTo(db, 13)

Assert.assertThat(sharedPreferencesHandler.hasCompletedWelcomeFlow(), CoreMatchers.`is`(true))
if (!FlavorConfig.isPremiumFlavor) {
Assert.assertThat(sharedPreferencesHandler.licenseToken(), CoreMatchers.`is`(licenseToken))
}

Sql.query("UPDATE_CHECK_ENTITY").executeOn(db).use {
it.moveToFirst()
Assert.assertThat(it.getString(it.getColumnIndex("RELEASE_NOTE")), CoreMatchers.`is`(releaseNote))
Assert.assertThat(it.getString(it.getColumnIndex("VERSION")), CoreMatchers.`is`(version))
Assert.assertThat(it.getString(it.getColumnIndex("URL_TO_APK")), CoreMatchers.`is`(urlApk))
Assert.assertThat(it.getString(it.getColumnIndex("APK_SHA256")), CoreMatchers.`is`(apkSha256))
Assert.assertThat(it.getString(it.getColumnIndex("URL_TO_RELEASE_NOTE")), CoreMatchers.`is`(urlReleaseNote))
}
}


@Test
fun upgrade13To14NoLicense() {
Upgrade0To1().applyTo(db, 0)
Upgrade1To2().applyTo(db, 1)
Upgrade2To3(context).applyTo(db, 2)
Upgrade3To4().applyTo(db, 3)
Upgrade4To5().applyTo(db, 4)
Upgrade5To6().applyTo(db, 5)
Upgrade6To7().applyTo(db, 6)
Upgrade7To8().applyTo(db, 7)
Upgrade8To9(sharedPreferencesHandler).applyTo(db, 8)
Upgrade9To10(sharedPreferencesHandler).applyTo(db, 9)
Upgrade10To11().applyTo(db, 10)
Upgrade11To12(sharedPreferencesHandler).applyTo(db, 11)
Upgrade12To13(context).applyTo(db, 12)

val releaseNote = "releaseNote"
val version = "version"
val urlApk = "urlApk"
val apkSha256 = "apkSha256"
val urlReleaseNote = "urlReleaseNote"

Sql.update("UPDATE_CHECK_ENTITY")
.set("RELEASE_NOTE", Sql.toString(releaseNote))
.set("VERSION", Sql.toString(version))
.set("URL_TO_APK", Sql.toString(urlApk))
.set("APK_SHA256", Sql.toString(apkSha256))
.set("URL_TO_RELEASE_NOTE", Sql.toString(urlReleaseNote))
.executeOn(db)

Upgrade13To14(sharedPreferencesHandler).applyTo(db, 13)

Assert.assertThat(sharedPreferencesHandler.hasCompletedWelcomeFlow(), CoreMatchers.`is`(true))
Assert.assertThat(sharedPreferencesHandler.licenseToken(), CoreMatchers.`is`(""))

Sql.query("UPDATE_CHECK_ENTITY").executeOn(db).use {
it.moveToFirst()
Assert.assertThat(it.getString(it.getColumnIndex("RELEASE_NOTE")), CoreMatchers.`is`(releaseNote))
Assert.assertThat(it.getString(it.getColumnIndex("VERSION")), CoreMatchers.`is`(version))
Assert.assertThat(it.getString(it.getColumnIndex("URL_TO_APK")), CoreMatchers.`is`(urlApk))
Assert.assertThat(it.getString(it.getColumnIndex("APK_SHA256")), CoreMatchers.`is`(apkSha256))
Assert.assertThat(it.getString(it.getColumnIndex("URL_TO_RELEASE_NOTE")), CoreMatchers.`is`(urlReleaseNote))
}
}
}
10 changes: 6 additions & 4 deletions data/src/main/java/org/cryptomator/data/db/DatabaseUpgrades.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package org.cryptomator.data.db;

import static java.lang.String.format;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
Expand All @@ -12,6 +10,8 @@
import javax.inject.Inject;
import javax.inject.Singleton;

import static java.lang.String.format;

@Singleton
class DatabaseUpgrades {

Expand All @@ -31,7 +31,8 @@ public DatabaseUpgrades( //
Upgrade9To10 upgrade9To10, //
Upgrade10To11 upgrade10To11, //
Upgrade11To12 upgrade11To12, //
Upgrade12To13 upgrade12To13
Upgrade12To13 upgrade12To13, //
Upgrade13To14 upgrade13To14
) {

availableUpgrades = defineUpgrades( //
Expand All @@ -47,7 +48,8 @@ public DatabaseUpgrades( //
upgrade9To10, //
upgrade10To11, //
upgrade11To12, //
upgrade12To13);
upgrade12To13, //
upgrade13To14);
}

private Map<Integer, List<DatabaseUpgrade>> defineUpgrades(DatabaseUpgrade... upgrades) {
Expand Down
Loading
Loading