mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
Go multi-module: app-common, app-phone, app-wear
This commit is contained in:
@@ -1 +0,0 @@
|
||||
/build
|
||||
@@ -1,295 +0,0 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'kotlin-android'
|
||||
id 'kotlin-kapt'
|
||||
id 'kotlin-parcelize'
|
||||
id 'androidx.navigation.safeargs.kotlin'
|
||||
id 'com.bugsnag.android.gradle'
|
||||
id 'dagger.hilt.android.plugin'
|
||||
}
|
||||
|
||||
def gitSha = 'git rev-parse --short HEAD'.execute([], project.rootDir).text.trim()
|
||||
def buildTime = new Date().format("yyyy-MM-dd'T'HH:mm:ss'Z'", TimeZone.getTimeZone("GMT+1"))
|
||||
|
||||
android {
|
||||
def packageName = "eu.darken.capod"
|
||||
|
||||
compileSdkVersion buildConfig.compileSdk
|
||||
|
||||
defaultConfig {
|
||||
applicationId "${packageName}"
|
||||
|
||||
minSdkVersion buildConfig.minSdk
|
||||
targetSdkVersion buildConfig.targetSdk
|
||||
|
||||
versionCode buildConfig.version.code
|
||||
versionName buildConfig.version.name
|
||||
|
||||
testInstrumentationRunner "eu.darken.capod.HiltTestRunner"
|
||||
|
||||
buildConfigField "String", "GITSHA", "\"${gitSha}\""
|
||||
buildConfigField "String", "BUILDTIME", "\"${buildTime}\""
|
||||
|
||||
manifestPlaceholders = [bugsnagApiKey: "fake"]
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
releaseFoss {}
|
||||
releaseGplay {}
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
releaseFoss {
|
||||
def signingFossPropFile = new File(System.properties['user.home'], ".appconfig/${packageName}/signing-foss.properties")
|
||||
Properties signingPropsFoss = new Properties()
|
||||
if (signingFossPropFile.canRead()) signingPropsFoss.load(new FileInputStream(signingFossPropFile))
|
||||
String keyStorePathFoss = System.getenv("STORE_PATH") ?: signingPropsFoss["release.storePath"]
|
||||
File keyStoreFoss = keyStorePathFoss ? new File(keyStorePathFoss) : null
|
||||
if (keyStoreFoss?.canRead()) {
|
||||
storeFile keyStoreFoss
|
||||
storePassword System.getenv("STORE_PASSWORD") ?: signingPropsFoss['release.storePassword']
|
||||
keyAlias System.getenv("KEY_ALIAS") ?: signingPropsFoss['release.keyAlias']
|
||||
keyPassword System.getenv("KEY_PASSWORD") ?: signingPropsFoss['release.keyPassword']
|
||||
}
|
||||
}
|
||||
|
||||
releaseGplay {
|
||||
def signingGplayPropFile = new File(System.properties['user.home'], ".appconfig/${packageName}/signing-gplay.properties")
|
||||
Properties signingPropsGplay = new Properties()
|
||||
if (signingGplayPropFile.canRead()) signingPropsGplay.load(new FileInputStream(signingGplayPropFile))
|
||||
String keyStorePathGplay = System.getenv("STORE_PATH") ?: signingPropsGplay["release.storePath"]
|
||||
File keyStoreGplay = keyStorePathGplay ? new File(keyStorePathGplay) : null
|
||||
if (keyStoreGplay?.canRead()) {
|
||||
storeFile keyStoreGplay
|
||||
storePassword System.getenv("STORE_PASSWORD") ?: signingPropsGplay['release.storePassword']
|
||||
keyAlias System.getenv("KEY_ALIAS") ?: signingPropsGplay['release.keyAlias']
|
||||
keyPassword System.getenv("KEY_PASSWORD") ?: signingPropsGplay['release.keyPassword']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions "version"
|
||||
productFlavors {
|
||||
foss {
|
||||
signingConfig signingConfigs.releaseFoss
|
||||
}
|
||||
gplay {
|
||||
signingConfig signingConfigs.releaseGplay
|
||||
}
|
||||
}
|
||||
|
||||
Properties bugsnagProps = new Properties()
|
||||
def bugsnagPropsFile = new File(System.properties['user.home'], ".appconfig/${packageName}/bugsnag.properties")
|
||||
if (bugsnagPropsFile.canRead()) bugsnagProps.load(new FileInputStream(bugsnagPropsFile))
|
||||
String bugSnagApiKey = System.getenv("BUGSNAG_API_KEY") ?: bugsnagProps.getProperty("bugsnag.apikey", "")
|
||||
|
||||
buildTypes {
|
||||
def proguardRulesRelease = fileTree(dir: "../proguard", include: ["*.pro"]).asList().toArray()
|
||||
debug {
|
||||
minifyEnabled false
|
||||
shrinkResources false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
|
||||
proguardFiles proguardRulesRelease
|
||||
proguardFiles 'proguard-rules-debug.pro'
|
||||
manifestPlaceholders = [bugsnagApiKey: bugSnagApiKey]
|
||||
}
|
||||
beta {
|
||||
lintOptions {
|
||||
abortOnError true
|
||||
fatal 'StopShip'
|
||||
}
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
|
||||
proguardFiles proguardRulesRelease
|
||||
manifestPlaceholders = [bugsnagApiKey: bugSnagApiKey]
|
||||
}
|
||||
release {
|
||||
lintOptions {
|
||||
abortOnError true
|
||||
fatal 'StopShip'
|
||||
}
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
|
||||
proguardFiles proguardRulesRelease
|
||||
manifestPlaceholders = [bugsnagApiKey: bugSnagApiKey]
|
||||
}
|
||||
}
|
||||
|
||||
applicationVariants.all { variant ->
|
||||
def flavor = variant.productFlavors[0].name.toUpperCase()
|
||||
def buildType = variant.buildType.name.toUpperCase()
|
||||
def versionCode = defaultConfig.versionCode
|
||||
def versionName = defaultConfig.versionName
|
||||
|
||||
if (variant.buildType.name == "debug") {
|
||||
variant.mergedFlavor.resourceConfigurations.clear()
|
||||
variant.mergedFlavor.resourceConfigurations.add("en")
|
||||
variant.mergedFlavor.resourceConfigurations.add("de")
|
||||
} else if (variant.buildType.name != "debug") {
|
||||
variant.outputs.each { output ->
|
||||
output.outputFileName = "${packageName}-v${versionName}(${versionCode})-${gitSha}-${flavor}-${buildType}.apk"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
|
||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
|
||||
freeCompilerArgs += [
|
||||
"-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-Xuse-experimental=kotlinx.coroutines.FlowPreview",
|
||||
"-Xuse-experimental=kotlin.time.ExperimentalTime",
|
||||
"-Xuse-experimental=kotlin.ExperimentalUnsignedTypes",
|
||||
"-Xuse-experimental=kotlin.contracts.ExperimentalContracts",
|
||||
"-Xopt-in=kotlin.RequiresOptIn"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests.all {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
unitTests {
|
||||
includeAndroidResources = true
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
test {
|
||||
java.srcDirs += "$projectDir/src/testShared/java"
|
||||
}
|
||||
androidTest {
|
||||
java.srcDirs += "$projectDir/src/testShared/java"
|
||||
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Kotlin
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlin.core}"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.kotlin.coroutines}"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${versions.kotlin.coroutines}"
|
||||
|
||||
testImplementation "org.jetbrains.kotlin:kotlin-reflect:${versions.kotlin.core}"
|
||||
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:${versions.kotlin.coroutines}"
|
||||
androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:${versions.kotlin.coroutines}") {
|
||||
// conflicts with mockito due to direct inclusion of byte buddy
|
||||
exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-debug"
|
||||
}
|
||||
|
||||
// Debugging
|
||||
implementation('com.bugsnag:bugsnag-android:5.9.2')
|
||||
implementation 'com.getkeepsafe.relinker:relinker:1.4.3'
|
||||
|
||||
implementation("com.squareup.moshi:moshi:1.13.0")
|
||||
kapt("com.squareup.moshi:moshi-kotlin-codegen:1.13.0")
|
||||
|
||||
// DI
|
||||
implementation "com.google.dagger:dagger:${versions.dagger.core}"
|
||||
implementation "com.google.dagger:dagger-android:${versions.dagger.core}"
|
||||
|
||||
kapt "com.google.dagger:dagger-compiler:${versions.dagger.core}"
|
||||
kapt "com.google.dagger:dagger-android-processor:${versions.dagger.core}"
|
||||
|
||||
implementation "com.google.dagger:hilt-android:${versions.dagger.core}"
|
||||
kapt "com.google.dagger:hilt-android-compiler:${versions.dagger.core}"
|
||||
|
||||
testImplementation "com.google.dagger:hilt-android-testing:${versions.dagger.core}"
|
||||
kaptTest "com.google.dagger:hilt-android-compiler:${versions.dagger.core}"
|
||||
|
||||
androidTestImplementation "com.google.dagger:hilt-android-testing:${versions.dagger.core}"
|
||||
kaptAndroidTest "com.google.dagger:hilt-android-compiler:${versions.dagger.core}"
|
||||
|
||||
kapt "androidx.hilt:hilt-compiler:1.0.0"
|
||||
implementation 'androidx.hilt:hilt-common:1.0.0'
|
||||
kaptTest "androidx.hilt:hilt-compiler:1.0.0"
|
||||
testImplementation 'androidx.hilt:hilt-common:1.0.0'
|
||||
|
||||
// Support libs
|
||||
implementation 'androidx.core:core-ktx:1.7.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.4.0'
|
||||
implementation 'androidx.annotation:annotation:1.3.0'
|
||||
|
||||
implementation 'androidx.activity:activity-ktx:1.4.0'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.4.0'
|
||||
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.4.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.4.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-process:2.4.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.0'
|
||||
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:2.3.5"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:2.3.5"
|
||||
|
||||
implementation 'androidx.preference:preference-ktx:1.1.1'
|
||||
|
||||
implementation 'androidx.core:core-splashscreen:1.0.0-alpha02'
|
||||
|
||||
|
||||
def work_version = "2.7.1"
|
||||
implementation "androidx.work:work-runtime:${work_version}"
|
||||
testImplementation "androidx.work:work-testing:${work_version}"
|
||||
implementation "androidx.work:work-runtime-ktx:${work_version}"
|
||||
implementation 'androidx.hilt:hilt-work:1.0.0'
|
||||
|
||||
// IAP
|
||||
gplayImplementation 'com.android.billingclient:billing:4.0.0'
|
||||
|
||||
// UI
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
|
||||
implementation 'com.google.android.material:material:1.6.0-alpha01'
|
||||
|
||||
// Testing
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation "org.junit.vintage:junit-vintage-engine:5.7.1"
|
||||
testImplementation "androidx.test:core-ktx:1.4.0"
|
||||
|
||||
testImplementation "io.mockk:mockk:1.12.1"
|
||||
androidTestImplementation "io.mockk:mockk-android:1.11.0"
|
||||
|
||||
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.7.1"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api:5.7.1"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-params:5.7.1"
|
||||
|
||||
androidTestImplementation "androidx.navigation:navigation-testing:2.3.5"
|
||||
|
||||
testImplementation "io.kotest:kotest-runner-junit5:4.6.2"
|
||||
testImplementation "io.kotest:kotest-assertions-core-jvm:4.6.2"
|
||||
testImplementation "io.kotest:kotest-property-jvm:4.6.2"
|
||||
androidTestImplementation "io.kotest:kotest-assertions-core-jvm:4.6.2"
|
||||
androidTestImplementation "io.kotest:kotest-property-jvm:4.6.2"
|
||||
|
||||
testImplementation 'android.arch.core:core-testing:1.1.1'
|
||||
androidTestImplementation 'android.arch.core:core-testing:1.1.1'
|
||||
debugImplementation 'androidx.test:core-ktx:1.4.0'
|
||||
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
|
||||
androidTestImplementation 'androidx.test:runner:1.4.0'
|
||||
androidTestImplementation 'androidx.test:rules:1.4.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.4.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0'
|
||||
androidTestImplementation 'androidx.test.espresso.idling:idling-concurrent:3.4.0'
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
-dontobfuscate
|
||||
@@ -1,3 +0,0 @@
|
||||
-keepclassmembernames @com.squareup.moshi.JsonClass class * extends java.lang.Enum {
|
||||
<fields>;
|
||||
}
|
||||
Vendored
-23
@@ -1,23 +0,0 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
-dontobfuscate
|
||||
@@ -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>
|
||||
@@ -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,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>
|
||||
@@ -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>
|
||||
@@ -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,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>
|
||||
@@ -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>
|
||||
@@ -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">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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,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">J’ai déjà fait un don</string>
|
||||
<string name="foss_upgrade_no_money_label">J’ai 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>
|
||||
@@ -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">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>
|
||||
@@ -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>
|
||||
@@ -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">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>
|
||||
@@ -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>
|
||||
@@ -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,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>
|
||||
@@ -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>
|
||||
@@ -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">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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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">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")
|
||||
}
|
||||
}
|
||||
-109
@@ -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")
|
||||
}
|
||||
}
|
||||
-116
@@ -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")
|
||||
}
|
||||
}
|
||||
-11
@@ -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})"
|
||||
}
|
||||
-7
@@ -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
|
||||
-19
@@ -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>,
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user