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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,10 @@ dependencies {
// odrcore's JNI bindings, java and native in one AAR (see settings.gradle)
implementation libs.odr.core.android

// Only Lite links the ad and consent SDKs; Foss links no Play SDKs.
// Only Lite links the ad, consent, and billing SDKs; Foss links no Play SDKs.
liteImplementation libs.play.services.ads
liteImplementation libs.user.messaging.platform
liteImplementation libs.play.billing
liteImplementation libs.play.review
proImplementation libs.play.review

Expand Down
90 changes: 90 additions & 0 deletions app/src/ads/java/app/opendocument/droid/nonfree/OwnedPurchases.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package app.opendocument.droid.nonfree

import android.content.Context
import android.os.Handler
import android.os.Looper
import com.android.billingclient.api.BillingClient
import com.android.billingclient.api.BillingClientStateListener
import com.android.billingclient.api.BillingResult
import com.android.billingclient.api.PendingPurchasesParams
import com.android.billingclient.api.Purchase
import com.android.billingclient.api.QueryPurchasesParams

/**
* Asks play whether the account owns the ad removal that lite sold until 2023: a one-time product,
* and before 2022 a subscription. Nothing sells it any more, but a buyer who reinstalls or changes
* device has only this to restore it from.
*/
object OwnedPurchases {

private val PRODUCT_TYPES =
listOf(BillingClient.ProductType.INAPP, BillingClient.ProductType.SUBS)

/** Calls [onOwned] on the main thread if any purchase is owned; says nothing otherwise. */
fun query(context: Context, crashManager: CrashManager, onOwned: () -> Unit) {
val mainThread = Handler(Looper.getMainLooper())

val client =
BillingClient.newBuilder(context.applicationContext)
.setListener { _, _ -> }
.enablePendingPurchases(
PendingPurchasesParams.newBuilder().enableOneTimeProducts().build()
)
.build()

client.startConnection(
object : BillingClientStateListener {
override fun onBillingSetupFinished(result: BillingResult) {
if (result.responseCode != BillingClient.BillingResponseCode.OK) {
crashManager.log("billing setup failed: " + result.responseCode)

client.endConnection()

return
}

var pending = PRODUCT_TYPES.size
var owned = false

for (type in PRODUCT_TYPES) {
val params = QueryPurchasesParams.newBuilder().setProductType(type).build()

client.queryPurchasesAsync(params) { queryResult, purchases ->
mainThread.post {
if (
queryResult.responseCode != BillingClient.BillingResponseCode.OK
) {
crashManager.log(
"billing query failed: " +
type +
"/" +
queryResult.responseCode
)
}

owned =
owned ||
purchases.any {
it.purchaseState == Purchase.PurchaseState.PURCHASED
}

pending--
if (pending == 0) {
client.endConnection()

if (owned) {
onOwned()
}
}
}
}
}
}

override fun onBillingServiceDisconnected() {
// the next launch asks again
}
}
)
}
}
35 changes: 29 additions & 6 deletions app/src/main/java/app/opendocument/droid/nonfree/BillingManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@ class BillingManager {
private var adManager: AdManager? = null
private var billingPreferences: BillingPreferences? = null

fun initialize(context: Context, adManager: AdManager) {
private var onPurchaseRestored: (() -> Unit)? = null

/** Fires once play reports a purchase that the preferences did not know about. */
fun setRestoreListener(listener: () -> Unit) {
onPurchaseRestored = listener
}

fun initialize(context: Context, adManager: AdManager, crashManager: CrashManager) {
this.adManager = adManager

val preferences = BillingPreferences(context)
Expand All @@ -27,16 +34,32 @@ class BillingManager {
}

if (hasPurchased()) {
adManager.removeAds()

// no banner to gate, but withdrawal has to survive the purchase, and only an
// update tells us whether the sdk wants a way back out
adManager.updateConsentInfo()
removeAds(adManager)
} else {
adManager.showGoogleAds()

// the flag lives only on this device, so a reinstall or a new device loses it. the
// ads show until play answers; a purchase is never revoked, as before
OwnedPurchases.query(context, crashManager) {
crashManager.log("purchase restored")

preferences.setPurchased(true)

removeAds(adManager)

onPurchaseRestored?.invoke()
}
}
}

private fun removeAds(adManager: AdManager) {
adManager.removeAds()

// no banner to gate, but withdrawal has to survive the purchase, and only an
// update tells us whether the sdk wants a way back out
adManager.updateConsentInfo()
}

fun hasPurchased(): Boolean {
if (!enabled) {
return true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,9 @@ class MainActivity : AppCompatActivity() {

billingManager = BillingManager()
billingManager.setEnabled(adsAvailable)
billingManager.initialize(this, adManager)
// the remove ads row goes once play has restored a purchase
billingManager.setRestoreListener { landingFragment?.refresh() }
billingManager.initialize(this, adManager, crashManager)
}

override fun onNewIntent(intent: Intent) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package app.opendocument.droid.nonfree

import android.content.Context

/** Purchases, in a build that sells none and has no ads to remove. */
object OwnedPurchases {

fun query(context: Context, crashManager: CrashManager, onOwned: () -> Unit) {}
}
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ androidxRecyclerview = "1.4.0"
androidxLifecycle = "2.9.4"
material = "1.14.0"

playBilling = "9.1.0"
playServicesAds = "25.5.0"
playReview = "2.0.2"
userMessagingPlatform = "4.0.0"
Expand All @@ -42,6 +43,7 @@ androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmode
androidx-lifecycle-livedata = { module = "androidx.lifecycle:lifecycle-livedata", version.ref = "androidxLifecycle" }
material = { module = "com.google.android.material:material", version.ref = "material" }

play-billing = { module = "com.android.billingclient:billing", version.ref = "playBilling" }
play-services-ads = { module = "com.google.android.gms:play-services-ads", version.ref = "playServicesAds" }
play-review = { module = "com.google.android.play:review", version.ref = "playReview" }
user-messaging-platform = { module = "com.google.android.ump:user-messaging-platform", version.ref = "userMessagingPlatform" }
Expand Down
Loading