Go multi-module: app-common, app-phone, app-wear

This commit is contained in:
darken
2022-09-14 18:00:18 +02:00
committed by Matthias Urhahn
parent 753d311593
commit 688748da36
631 changed files with 1595 additions and 49 deletions
@@ -1,17 +0,0 @@
package eu.darken.capod.common.upgrade
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import eu.darken.capod.common.upgrade.core.UpgradeControlFoss
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module
abstract class UpgradeModule {
@Binds
@Singleton
abstract fun control(foss: UpgradeControlFoss): UpgradeRepo
}
@@ -1,24 +0,0 @@
package eu.darken.capod.common.upgrade.core
import android.content.Context
import com.squareup.moshi.Moshi
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.common.preferences.createFlowPreference
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class FossCache @Inject constructor(
@ApplicationContext context: Context,
moshi: Moshi
) {
private val preferences = context.getSharedPreferences("settings_foss", Context.MODE_PRIVATE)
val upgrade = preferences.createFlowPreference<FossUpgrade?>(
key = "foss.upgrade",
moshi = moshi,
defaultValue = null,
)
}
@@ -1,18 +0,0 @@
package eu.darken.capod.common.upgrade.core
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.time.Instant
@JsonClass(generateAdapter = true)
data class FossUpgrade(
val upgradedAt: Instant,
val reason: Reason
) {
@JsonClass(generateAdapter = false)
enum class Reason {
@Json(name = "foss.upgrade.reason.donated") DONATED,
@Json(name = "foss.upgrade.reason.alreadydonated") ALREADY_DONATED,
@Json(name = "foss.upgrade.reason.nomoney") NO_MONEY;
}
}
@@ -1,71 +0,0 @@
package eu.darken.capod.common.upgrade.core
import android.app.Activity
import android.widget.Toast
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.darken.capod.R
import eu.darken.capod.common.WebpageTool
import eu.darken.capod.common.upgrade.UpgradeRepo
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import java.time.Instant
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class UpgradeControlFoss @Inject constructor(
private val fossCache: FossCache,
private val webpageTool: WebpageTool,
) : UpgradeRepo {
override val upgradeInfo: Flow<UpgradeRepo.Info> = fossCache.upgrade.flow.map { data ->
if (data == null) {
Info()
} else {
Info(
isPro = true,
upgradedAt = data.upgradedAt,
upgradeReason = data.reason
)
}
}
override fun launchBillingFlow(activity: Activity) {
MaterialAlertDialogBuilder(activity).apply {
setIcon(R.drawable.ic_heart)
setTitle(R.string.upgrade_capod_label)
setMessage(R.string.upgrade_capod_description)
setPositiveButton(R.string.foss_upgrade_donate_label) { _, _ ->
fossCache.upgrade.value = FossUpgrade(
upgradedAt = Instant.now(),
reason = FossUpgrade.Reason.DONATED
)
webpageTool.open("https://github.com/d4rken-org/capod#support-the-project")
Toast.makeText(activity, R.string.general_thank_you_label, Toast.LENGTH_SHORT).show()
}
setNegativeButton(R.string.foss_upgrade_alreadydonated_label) { _, _ ->
fossCache.upgrade.value = FossUpgrade(
upgradedAt = Instant.now(),
reason = FossUpgrade.Reason.ALREADY_DONATED
)
Toast.makeText(activity, R.string.general_thank_you_label, Toast.LENGTH_SHORT).show()
}
setNeutralButton(R.string.foss_upgrade_no_money_label) { _, _ ->
fossCache.upgrade.value = FossUpgrade(
upgradedAt = Instant.now(),
reason = FossUpgrade.Reason.NO_MONEY
)
Toast.makeText(activity, "¯\\_(ツ)_/¯", Toast.LENGTH_SHORT).show()
}
}.show()
}
data class Info(
override val isPro: Boolean = false,
override val upgradedAt: Instant? = null,
val upgradeReason: FossUpgrade.Reason? = null,
) : UpgradeRepo.Info {
override val type: UpgradeRepo.Type = UpgradeRepo.Type.FOSS
}
}
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
-2
View File
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
-2
View File
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">İanə ver</string>
<string name="foss_upgrade_alreadydonated_label">Artıq ianə vermişəm</string>
<string name="foss_upgrade_no_money_label">Bütün pulumu AirPods-lara xərcləyirəm</string>
</resources>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">Ахвяраваць</string>
<string name="foss_upgrade_alreadydonated_label">Я ўжо ахвяраваў</string>
<string name="foss_upgrade_no_money_label" comment="Can't be too long otherwise the dialog ellipsizes it. foss_upgrade_no_money_label" maxLength="35">Я патраціў усе грошы на AirPods</string>
</resources>
-2
View File
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">Donacions</string>
<string name="foss_upgrade_alreadydonated_label">Ja he donat</string>
<string name="foss_upgrade_no_money_label">Gasto tots els meus diners en AirPods</string>
</resources>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">Přispět</string>
<string name="foss_upgrade_alreadydonated_label">Již jsem přispěl/a</string>
<string name="foss_upgrade_no_money_label">Všechny peníze jsem utratil/a za AirPods</string>
</resources>
-2
View File
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">Spenden</string>
<string name="foss_upgrade_alreadydonated_label">Ich habe schon gespendet</string>
<string name="foss_upgrade_no_money_label" comment="Can't be too long otherwise the dialog ellipsizes it. foss_upgrade_no_money_label" maxLength="35">Hab alles für AirPods ausgegeben</string>
</resources>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">Δωρεά</string>
<string name="foss_upgrade_alreadydonated_label">Ήδη δώρισα</string>
<string name="foss_upgrade_no_money_label">Ξόδεψα όλα τα χρήματά μου στα AirPods</string>
</resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">Donar</string>
<string name="foss_upgrade_alreadydonated_label">Ya doné</string>
<string name="foss_upgrade_no_money_label">Gasto todo mi dinero en AirPods</string>
</resources>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">Donar</string>
<string name="foss_upgrade_alreadydonated_label">Ya he donado</string>
<string name="foss_upgrade_no_money_label">Gasto todo mi dinero en AirPods</string>
</resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
-2
View File
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
-2
View File
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
-2
View File
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">Faire un don</string>
<string name="foss_upgrade_alreadydonated_label">Jai déjà fait un don</string>
<string name="foss_upgrade_no_money_label">Jai dépensé tout mon argent sur les AirPods</string>
</resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">दान करना</string>
<string name="foss_upgrade_alreadydonated_label">मैंने पहले ही दान कर दिया है</string>
<string name="foss_upgrade_no_money_label">मैं अपना सारा पैसा AirPods पर खर्च करता हूं</string>
</resources>
-2
View File
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">Adomány</string>
<string name="foss_upgrade_alreadydonated_label">Már adományoztam</string>
<string name="foss_upgrade_no_money_label">Minden pénzemet AirPodokra költöm</string>
</resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">Menyumbang</string>
<string name="foss_upgrade_alreadydonated_label">Saya telah berdonasi</string>
<string name="foss_upgrade_no_money_label">Saya menghabiskan semua uang saya untuk AirPods</string>
</resources>
-2
View File
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">Dona</string>
<string name="foss_upgrade_alreadydonated_label">Ho già donato</string>
<string name="foss_upgrade_no_money_label">Spendo tutti i miei soldi per gli AirPods</string>
</resources>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">תרומה</string>
<string name="foss_upgrade_alreadydonated_label">כבר תרמתי</string>
<string name="foss_upgrade_no_money_label">מיטב כספי מושקע ב-AirPods</string>
</resources>
-2
View File
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">후원하기</string>
<string name="foss_upgrade_alreadydonated_label">이미 후원했어요</string>
<string name="foss_upgrade_no_money_label">AirPods에 돈을 다 썼어요</string>
</resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
-2
View File
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
-2
View File
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">Derma</string>
<string name="foss_upgrade_alreadydonated_label">Saya sudah menderma</string>
<string name="foss_upgrade_no_money_label" comment="Can't be too long otherwise the dialog ellipsizes it. foss_upgrade_no_money_label" maxLength="35">Saya belanjakan semua wang saya untuk AirPods</string>
</resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">Doneren</string>
<string name="foss_upgrade_alreadydonated_label">Ik heb al gedoneerd</string>
<string name="foss_upgrade_no_money_label">Ik besteed al mijn geld aan AirPods</string>
</resources>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">Doner</string>
<string name="foss_upgrade_alreadydonated_label">Jeg har allerede donert</string>
<string name="foss_upgrade_no_money_label">Jeg bruker alle pengene mine på AirPods</string>
</resources>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">Dotacja</string>
<string name="foss_upgrade_alreadydonated_label">Już się zrzuciłem</string>
<string name="foss_upgrade_no_money_label">Wydałem wszystkie pieniądze na AirPods</string>
</resources>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">Doar</string>
<string name="foss_upgrade_alreadydonated_label">Eu já doei</string>
<string name="foss_upgrade_no_money_label">Eu gasto todo o meu dinheiro em AirPods</string>
</resources>
-2
View File
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
-2
View File
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
-2
View File
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">Пожертвовать</string>
<string name="foss_upgrade_alreadydonated_label">Я уже пожертвовал</string>
<string name="foss_upgrade_no_money_label">Я потратил все свои деньги на AirPods</string>
</resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">පරිත්‍යාග</string>
</resources>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">Prispieť</string>
<string name="foss_upgrade_alreadydonated_label">Už som prispel/a</string>
<string name="foss_upgrade_no_money_label">Všetky peniaze som minul na AirPods</string>
</resources>
-2
View File
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
-2
View File
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
-2
View File
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
-2
View File
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
-2
View File
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">Bağış</string>
<string name="foss_upgrade_alreadydonated_label">Ben zaten bağışladım</string>
<string name="foss_upgrade_no_money_label">Tüm paramı AirPod\'lara harcıyorum</string>
</resources>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">Пожертва</string>
<string name="foss_upgrade_alreadydonated_label">Вже пожертвував</string>
<string name="foss_upgrade_no_money_label">Витратив усі гроші на AirPods</string>
</resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
-2
View File
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">Quyên tặng</string>
<string name="foss_upgrade_alreadydonated_label">Tôi đã quyên góp</string>
<string name="foss_upgrade_no_money_label" comment="Can't be too long otherwise the dialog ellipsizes it. foss_upgrade_no_money_label" maxLength="35">Tôi tiêu hết tiền cho AirPods</string>
</resources>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">捐赠</string>
<string name="foss_upgrade_alreadydonated_label">已捐赠过了</string>
<string name="foss_upgrade_no_money_label">钱都用来买 AirPods 了</string>
</resources>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">抖內</string>
<string name="foss_upgrade_alreadydonated_label">我已經抖內了</string>
<string name="foss_upgrade_no_money_label" comment="Can't be too long otherwise the dialog ellipsizes it. foss_upgrade_no_money_label" maxLength="35">我已經為 AirPods 傾家蕩產了</string>
</resources>
-2
View File
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">Donate</string>
<string name="foss_upgrade_alreadydonated_label">I already donated</string>
<string name="foss_upgrade_no_money_label">I spend all my money on AirPods</string>
</resources>
@@ -1,17 +0,0 @@
package eu.darken.capod.common.upgrade
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import eu.darken.capod.common.upgrade.core.UpgradeRepoGplay
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module
abstract class UpgradeModule {
@Binds
@Singleton
abstract fun control(gplay: UpgradeRepoGplay): UpgradeRepo
}
@@ -1,20 +0,0 @@
package eu.darken.capod.common.upgrade.core
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.common.preferences.createFlowPreference
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class BillingCache @Inject constructor(
@ApplicationContext private val context: Context,
) {
private val preferences = context.getSharedPreferences("settings_gplay", Context.MODE_PRIVATE)
val lastProStateAt = preferences.createFlowPreference(
"gplay.cache.lastProAt",
0L
)
}
@@ -1,13 +0,0 @@
package eu.darken.capod.common.upgrade.core
import com.android.billingclient.api.Purchase
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.upgrade.core.data.PurchasedSku
import eu.darken.capod.common.upgrade.core.data.Sku
fun Purchase.toPurchasedSku(): Collection<PurchasedSku> = skus.map {
PurchasedSku(Sku(it), this)
}
private val TAG: String = logTag("Upgrade", "Gplay", "Billing", "Extensions")
@@ -1,9 +0,0 @@
package eu.darken.capod.common.upgrade.core
import eu.darken.capod.common.BuildConfigWrap
import eu.darken.capod.common.upgrade.core.data.AvailableSku
import eu.darken.capod.common.upgrade.core.data.Sku
enum class CapodSku constructor(override val sku: Sku) : AvailableSku {
PRO_UPGRADE(Sku("${BuildConfigWrap.APPLICATION_ID}.iap.upgrade.pro"))
}
@@ -1,141 +0,0 @@
package eu.darken.capod.common.upgrade.core
import android.app.Activity
import android.widget.Toast
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.darken.capod.R
import eu.darken.capod.common.coroutine.AppScope
import eu.darken.capod.common.coroutine.DispatcherProvider
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.capod.common.debug.logging.asLog
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.error.asErrorDialogBuilder
import eu.darken.capod.common.flow.replayingShare
import eu.darken.capod.common.upgrade.UpgradeRepo
import eu.darken.capod.common.upgrade.core.data.BillingData
import eu.darken.capod.common.upgrade.core.data.BillingDataRepo
import eu.darken.capod.common.upgrade.core.data.PurchasedSku
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.time.Instant
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class UpgradeRepoGplay @Inject constructor(
@AppScope private val scope: CoroutineScope,
private val dispatcherProvider: DispatcherProvider,
private val billingDataRepo: BillingDataRepo,
private val billingCache: BillingCache,
) : UpgradeRepo {
private var lastProStateAt: Long
get() = billingCache.lastProStateAt.value
set(value) = billingCache.lastProStateAt.update { value }
override val upgradeInfo: Flow<UpgradeRepo.Info> = billingDataRepo.billingData
.map { data -> // Only relinquish pro state if we haven't had it for a while
val now = System.currentTimeMillis()
val proSku = data.getProSku()
log(TAG) { "now=$now, lastProStateAt=$lastProStateAt, data=${data}" }
when {
proSku != null -> {
// If we are pro refresh timestamp
lastProStateAt = now
Info(billingData = data)
}
(now - lastProStateAt) < 6 * 60 * 1000L -> { // 6 hours
log(TAG, VERBOSE) { "We are not pro, but were recently, did GPlay try annoy us again?" }
Info(gracePeriod = true, billingData = null)
}
else -> {
Info(billingData = data)
}
}
}
.catch {
// Ignore Google Play errors if the last pro state was recent
val now = System.currentTimeMillis()
log(TAG) { "now=$now, lastProStateAt=$lastProStateAt, error=$it" }
if ((now - lastProStateAt) < 6 * 60 * 60 * 1000L) { // 6 hours
log(TAG, VERBOSE) { "We are not pro, but were recently, and just and an error, what is GPlay doing???" }
emit(Info(gracePeriod = true, billingData = null))
} else {
throw it
}
}
.replayingShare(scope)
override fun launchBillingFlow(activity: Activity) {
MaterialAlertDialogBuilder(activity).apply {
setIcon(R.drawable.ic_heart)
setTitle(R.string.upgrade_capod_label)
setMessage(R.string.upgrade_capod_description)
setPositiveButton(R.string.general_upgrade_action) { _, _ ->
scope.launch {
try {
billingDataRepo.startIapFlow(activity, CapodSku.PRO_UPGRADE.sku)
} catch (e: Exception) {
log(TAG) { "startIapFlow failed:${e.asLog()}" }
withContext(dispatcherProvider.Main) {
e.asErrorDialogBuilder(activity).show()
}
}
}
}
setNeutralButton(R.string.general_check_action) { dialog, _ ->
log(TAG) { "recheck()" }
scope.launch {
try {
val data = billingDataRepo.getIapData()
log(TAG) { "Recheck successful: $data" }
withContext(dispatcherProvider.Main) {
if (data.purchases.isEmpty()) {
Toast.makeText(
activity,
R.string.upgrades_no_purchases_found_check_account,
Toast.LENGTH_LONG
).show()
}
}
} catch (e: Exception) {
log(TAG) { "Recheck failed:${e.asLog()}" }
withContext(dispatcherProvider.Main) {
e.asErrorDialogBuilder(activity).show()
}
}
}
}
}.show()
}
data class Info(
private val gracePeriod: Boolean = false,
private val billingData: BillingData?,
) : UpgradeRepo.Info {
override val type: UpgradeRepo.Type
get() = UpgradeRepo.Type.GPLAY
override val isPro: Boolean
get() = billingData?.getProSku() != null
override val upgradedAt: Instant?
get() = billingData
?.getProSku()
?.purchase?.purchaseTime
?.let { Instant.ofEpochMilli(it) }
}
companion object {
private fun BillingData.getProSku(): PurchasedSku? = purchasedSkus
.firstOrNull { it.sku == CapodSku.PRO_UPGRADE.sku }
val TAG: String = logTag("Upgrade", "Gplay", "Control")
}
}
@@ -1,109 +0,0 @@
package eu.darken.capod.common.upgrade.core.client
import android.app.Activity
import com.android.billingclient.api.*
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.flow.setupCommonEventHandlers
import eu.darken.capod.common.upgrade.core.data.Sku
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
data class BillingClientConnection(
private val client: BillingClient,
private val purchasesGlobal: Flow<Collection<Purchase>>,
) {
private val purchasesLocal = MutableStateFlow<Collection<Purchase>>(emptySet())
val purchases: Flow<Collection<Purchase>> = combine(purchasesGlobal, purchasesLocal) { global, local ->
val combined = mutableMapOf<String, Purchase>()
global.plus(local).toSet().sortedByDescending { it.purchaseTime }.forEach { purchase ->
combined[purchase.orderId] = purchase
}
combined.values
}
.setupCommonEventHandlers(TAG) { "purchases" }
suspend fun queryPurchases(): Collection<Purchase> {
val (result: BillingResult, purchases) = suspendCoroutine<Pair<BillingResult, Collection<Purchase>?>> { continuation ->
client.queryPurchasesAsync(BillingClient.SkuType.INAPP) { result, purchases ->
continuation.resume(result to purchases)
}
}
log(TAG) { "queryPurchases(): code=${result.responseCode}, message=${result.debugMessage}, purchases=$purchases" }
if (!result.isSuccess) {
log(TAG, WARN) { "queryPurchases() failed" }
throw BillingClientException(result)
} else {
requireNotNull(purchases)
}
purchasesLocal.value = purchases
return purchases
}
suspend fun acknowledgePurchase(purchase: Purchase): BillingResult {
val ack = AcknowledgePurchaseParams.newBuilder().apply {
setPurchaseToken(purchase.purchaseToken)
}.build()
val ackResult = suspendCoroutine<BillingResult> { continuation ->
client.acknowledgePurchase(ack) { continuation.resume(it) }
}
log(TAG) {
"acknowledgePurchase(purchase=$purchase): code=${ackResult.responseCode}, message=${ackResult.debugMessage})"
}
if (!ackResult.isSuccess) {
throw BillingClientException(ackResult)
}
return ackResult
}
suspend fun querySku(sku: Sku): Sku.Details {
val skuParams = SkuDetailsParams.newBuilder().apply {
setType(BillingClient.SkuType.INAPP)
setSkusList(listOf(sku.id))
}.build()
val (result, details) = suspendCoroutine<Pair<BillingResult, Collection<SkuDetails>?>> { continuation ->
client.querySkuDetailsAsync(skuParams) { skuResult, skuDetails ->
continuation.resume(skuResult to skuDetails)
}
}
log(TAG) {
"querySku(sku=$sku): code=${result.responseCode}, debug=${result.debugMessage}), skuDetails=$details"
}
if (!result.isSuccess) throw BillingClientException(result)
if (details.isNullOrEmpty()) throw IllegalStateException("Unknown SKU, no details available.")
return Sku.Details(sku, details)
}
suspend fun launchBillingFlow(activity: Activity, sku: Sku): BillingResult {
log(TAG) { "launchBillingFlow(activity=$activity, sku=$sku)" }
val skuDetails = querySku(sku)
return launchBillingFlow(activity, skuDetails)
}
suspend fun launchBillingFlow(activity: Activity, skuDetails: Sku.Details): BillingResult {
log(TAG) { "launchBillingFlow(activity=$activity, skuDetails=$skuDetails)" }
return client.launchBillingFlow(
activity,
BillingFlowParams.newBuilder().setSkuDetails(skuDetails.details.single()).build()
)
}
companion object {
val TAG: String = logTag("Upgrade", "Gplay", "Billing", "ClientConnection")
}
}
@@ -1,116 +0,0 @@
package eu.darken.capod.common.upgrade.core.client
import android.content.Context
import com.android.billingclient.api.BillingClient.*
import com.android.billingclient.api.BillingClientStateListener
import com.android.billingclient.api.BillingResult
import com.android.billingclient.api.Purchase
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.common.debug.logging.Logging.Priority.*
import eu.darken.capod.common.debug.logging.asLog
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.flow.setupCommonEventHandlers
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@Singleton
class BillingClientConnectionProvider @Inject constructor(
@ApplicationContext private val context: Context,
) {
private val connectionProvider: Flow<BillingClientConnection> = callbackFlow {
val purchasePublisher = MutableStateFlow<Collection<Purchase>>(emptySet())
val client = newBuilder(context).apply {
enablePendingPurchases()
setListener { result, purchases ->
if (result.isSuccess) {
log(TAG) {
"onPurchasesUpdated(code=${result.responseCode}, message=${result.debugMessage}, purchases=$purchases)"
}
purchasePublisher.value = purchases.orEmpty()
} else {
log(TAG, WARN) {
"error: onPurchasesUpdated(code=${result.responseCode}, message=${result.debugMessage}, purchases=$purchases)"
}
}
}
}.build()
val connectionResult = suspendCoroutine<BillingResult> { continuation ->
log(TAG, VERBOSE) { "startConnection(...)" }
client.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(result: BillingResult) {
log(TAG, VERBOSE) {
"onBillingSetupFinished(code=${result.responseCode}, message=${result.debugMessage})"
}
continuation.resume(result)
}
override fun onBillingServiceDisconnected() {
log(TAG, VERBOSE) { "onBillingServiceDisconnected() " }
close(CancellationException("Billing service disconnected"))
}
})
}
val billingClientConnection = when (connectionResult.responseCode) {
BillingResponseCode.OK -> BillingClientConnection(client, purchasePublisher)
else -> throw BillingClientException(connectionResult)
}
try {
purchasePublisher.value = billingClientConnection.queryPurchases()
log(TAG) { "Initial IAP query successful." }
} catch (e: Exception) {
log(TAG, ERROR) { "Initial IAP query failed:\n${e.asLog()}" }
}
send(billingClientConnection)
log(TAG) { "Awaiting close." }
awaitClose {
log(TAG) { "Stopping billing client connection" }
client.endConnection()
}
}
val connection: Flow<BillingClientConnection> = connectionProvider
.setupCommonEventHandlers(TAG) { "connection" }
.retryWhen { cause, attempt ->
if (cause is CancellationException) {
log(TAG) { "BillingClient connection cancelled." }
return@retryWhen false
}
if (attempt > 5) {
log(TAG, WARN) { "Reached attempt limit: $attempt due to $cause" }
return@retryWhen false
}
if (cause !is BillingClientException) {
log(TAG, WARN) { "Unknown BillingClient exception type: $cause" }
return@retryWhen false
} else {
log(TAG) { "BillingClient exception: $cause; ${cause.result}" }
}
if (cause.result.responseCode == BillingResponseCode.BILLING_UNAVAILABLE) {
log(TAG) { "Got BILLING_UNAVAILABLE while trying to connect client." }
return@retryWhen false
}
log(TAG) { "Will retry BillingClient connection... *sigh*" }
delay(3000 * attempt)
true
}
companion object {
val TAG: String = logTag("Upgrade", "Gplay", "Billing", "Client", "ConnectionProvider")
}
}
@@ -1,11 +0,0 @@
package eu.darken.capod.common.upgrade.core.client
import com.android.billingclient.api.BillingResult
class BillingClientException(val result: BillingResult) : Exception() {
override val message: String?
get() = result.debugMessage
override fun toString(): String =
"BillingClientException(code=${result.responseCode}, message=${result.debugMessage})"
}
@@ -1,7 +0,0 @@
package eu.darken.capod.common.upgrade.core.client
import com.android.billingclient.api.BillingClient
import com.android.billingclient.api.BillingResult
internal val BillingResult.isSuccess: Boolean
get() = responseCode == BillingClient.BillingResponseCode.OK
@@ -1,19 +0,0 @@
package eu.darken.capod.common.upgrade.core.client
import android.content.Context
import eu.darken.capod.R
import eu.darken.capod.common.error.HasLocalizedError
import eu.darken.capod.common.error.LocalizedError
class GplayServiceUnavailableException(cause: Throwable) : Exception("Google Play services are unavailable.", cause),
HasLocalizedError {
override fun getLocalizedError(context: Context): LocalizedError {
return LocalizedError(
throwable = this,
label = "Google Play Services Unavailable",
description = context.getString(R.string.upgrades_gplay_unavailable_error)
)
}
}
@@ -1,5 +0,0 @@
package eu.darken.capod.common.upgrade.core.data
interface AvailableSku {
val sku: Sku
}
@@ -1,11 +0,0 @@
package eu.darken.capod.common.upgrade.core.data
import com.android.billingclient.api.Purchase
import eu.darken.capod.common.upgrade.core.toPurchasedSku
data class BillingData(
val purchases: Collection<Purchase>
) {
val purchasedSkus: Collection<PurchasedSku>
get() = purchases.map { it.toPurchasedSku() }.flatten()
}
@@ -1,135 +0,0 @@
package eu.darken.capod.common.upgrade.core.data
import android.app.Activity
import com.android.billingclient.api.*
import com.android.billingclient.api.BillingClient.*
import eu.darken.capod.common.coroutine.AppScope
import eu.darken.capod.common.debug.Bugs
import eu.darken.capod.common.debug.logging.Logging.Priority.*
import eu.darken.capod.common.debug.logging.asLog
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.flow.replayingShare
import eu.darken.capod.common.flow.setupCommonEventHandlers
import eu.darken.capod.common.upgrade.core.client.BillingClientConnectionProvider
import eu.darken.capod.common.upgrade.core.client.BillingClientException
import eu.darken.capod.common.upgrade.core.client.GplayServiceUnavailableException
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class BillingDataRepo @Inject constructor(
billingClientConnectionProvider: BillingClientConnectionProvider,
@AppScope private val scope: CoroutineScope,
) {
private val connectionProvider = billingClientConnectionProvider.connection
.catch { log(TAG, ERROR) { "Unable to provide client connection:\n${it.asLog()}" } }
.replayingShare(scope)
val billingData: Flow<BillingData> = connectionProvider
.flatMapLatest { it.purchases }
.map { BillingData(purchases = it) }
.setupCommonEventHandlers(TAG) { "iapData" }
.replayingShare(scope)
init {
connectionProvider
.flatMapLatest { client ->
client.purchases.map { client to it }
}
.onEach { (client, purchases) ->
purchases
.filter {
val needsAck = !it.isAcknowledged
if (needsAck) log(TAG, INFO) { "Needs ACK: $it" }
else log(TAG) { "Already ACK'ed: $it" }
needsAck
}
.forEach {
log(TAG, INFO) { "Acknowledging purchase: $it" }
try {
client.acknowledgePurchase(it)
} catch (e: Exception) {
log(TAG, ERROR) { "Failed to ancknowledge purchase: $it\n${e.asLog()}" }
}
}
}
.setupCommonEventHandlers(TAG) { "connection-acks" }
.retryWhen { cause, attempt ->
if (cause is CancellationException) {
log(TAG) { "Ack was cancelled (appScope?) cancelled." }
return@retryWhen false
}
if (attempt > 5) {
log(TAG, WARN) { "Reached attempt limit: $attempt due to $cause" }
return@retryWhen false
}
if (cause !is BillingClientException) {
log(TAG, WARN) { "Unknown BillingClient exception type: $cause" }
return@retryWhen false
} else {
log(TAG) { "BillingClient exception: $cause; ${cause.result}" }
}
if (cause.result.responseCode == BillingResponseCode.BILLING_UNAVAILABLE) {
log(TAG) { "Got BILLING_UNAVAILABLE while trying to ACK purchase." }
return@retryWhen false
}
log(TAG) { "Will retry ACK" }
delay(3000 * attempt)
true
}
.launchIn(scope)
}
suspend fun getIapData(): BillingData = try {
val clientConnection = connectionProvider.first()
val iaps = clientConnection.queryPurchases()
BillingData(
purchases = iaps
)
} catch (e: Exception) {
throw e.tryMapUserFriendly()
}
suspend fun startIapFlow(activity: Activity, sku: Sku) {
try {
val clientConnection = connectionProvider.first()
clientConnection.launchBillingFlow(activity, sku)
} catch (e: Exception) {
log(TAG, WARN) { "Failed to start IAP flow:\n${e.asLog()}" }
val ignoredCodes = listOf(3, 6)
if (e !is BillingClientException || !e.result.responseCode.let { ignoredCodes.contains(it) }) {
Bugs.report(TAG, "IAP flow failed for $sku", e)
}
throw e.tryMapUserFriendly()
}
}
companion object {
val TAG: String = logTag("Upgrade", "Gplay", "Billing", "DataRepo")
internal fun Throwable.tryMapUserFriendly(): Throwable {
if (this !is BillingClientException) return this
return when (result.responseCode) {
BillingResponseCode.BILLING_UNAVAILABLE,
BillingResponseCode.SERVICE_UNAVAILABLE,
BillingResponseCode.SERVICE_DISCONNECTED,
BillingResponseCode.SERVICE_TIMEOUT -> GplayServiceUnavailableException(this)
else -> this
}
}
}
}
@@ -1,8 +0,0 @@
package eu.darken.capod.common.upgrade.core.data
import com.android.billingclient.api.Purchase
import eu.darken.capod.common.upgrade.core.data.Sku
data class PurchasedSku(val sku: Sku, val purchase: Purchase) {
override fun toString(): String = "IAP(sku=$sku, purchase=${purchase.skus})"
}
@@ -1,12 +0,0 @@
package eu.darken.capod.common.upgrade.core.data
import com.android.billingclient.api.SkuDetails
data class Sku(
val id: String
) {
data class Details(
val sku: Sku,
val details: Collection<SkuDetails>,
)
}
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
-2
View File
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
-2
View File
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
-5
View File
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="upgrades_gplay_unavailable_error">Google Play xidmətləri əlçatmazdır.</string>
<string name="upgrades_no_purchases_found_check_account">Heç bir satın alma tapılmadı. Doğru hesabı istifadə edirsiniz?</string>
</resources>
-5
View File
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="upgrades_gplay_unavailable_error">Службы Google Play недаступныя.</string>
<string name="upgrades_no_purchases_found_check_account">Пакупак не знойдзена. Вы выкарыстоўваеце правільны ўліковы запіс?</string>
</resources>

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