mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-27 05:03:28 +00:00
Fix InAppPayments database inconsistency.
This commit is contained in:
committed by
Cody Henthorne
parent
8d53c1b384
commit
14f99bba24
@@ -5,7 +5,6 @@
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui.subscription
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
@@ -251,25 +250,19 @@ class MessageBackupsFlowViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures we have a SubscriberId created and available for use. This is considered safe because
|
||||
* the screen this is called in is assumed to only be accessible if the user does not currently have
|
||||
* a subscription.
|
||||
*/
|
||||
@WorkerThread
|
||||
private fun ensureSubscriberIdForBackups(purchaseToken: IAPSubscriptionId.GooglePlayBillingPurchaseToken) {
|
||||
RecurringInAppPaymentRepository.ensureSubscriberId(InAppPaymentSubscriberRecord.Type.BACKUP, iapSubscriptionId = purchaseToken).blockingAwait()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a successful BillingPurchaseResult. Updates the in app payment, enqueues the appropriate job chain,
|
||||
* and handles any resulting error. Like donations, we will wait up to 10s for the completion of the job chain.
|
||||
*
|
||||
* This will always rotate the subscriber-id.
|
||||
*/
|
||||
@OptIn(FlowPreview::class)
|
||||
private suspend fun handleSuccess(result: BillingPurchaseResult.Success, inAppPaymentId: InAppPaymentTable.InAppPaymentId) {
|
||||
withContext(SignalDispatchers.IO) {
|
||||
Log.d(TAG, "Setting purchase token data on InAppPayment and InAppPaymentSubscriber.")
|
||||
ensureSubscriberIdForBackups(IAPSubscriptionId.GooglePlayBillingPurchaseToken(result.purchaseToken))
|
||||
|
||||
val iapSubscriptionId = IAPSubscriptionId.GooglePlayBillingPurchaseToken(result.purchaseToken)
|
||||
RecurringInAppPaymentRepository.ensureSubscriberId(InAppPaymentSubscriberRecord.Type.BACKUP, iapSubscriptionId = iapSubscriptionId, isRotation = true).blockingAwait()
|
||||
|
||||
val inAppPayment = SignalDatabase.inAppPayments.getById(inAppPaymentId)!!
|
||||
SignalDatabase.inAppPayments.update(
|
||||
|
||||
@@ -32,6 +32,7 @@ import org.signal.core.util.withinTransaction
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.InAppPaymentKeepAliveJob
|
||||
import org.thoughtcrime.securesms.util.parcelers.MillisecondDurationParceler
|
||||
import org.thoughtcrime.securesms.util.parcelers.NullableSubscriberIdParceler
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||
@@ -134,6 +135,9 @@ class InAppPaymentTable(context: Context, databaseHelper: SignalDatabase) : Data
|
||||
inAppPaymentData: InAppPaymentData
|
||||
): InAppPaymentId {
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
validateInAppPayment(state, inAppPaymentData)
|
||||
|
||||
return writableDatabase.insertInto(TABLE_NAME)
|
||||
.values(
|
||||
TYPE to type.code,
|
||||
@@ -153,6 +157,9 @@ class InAppPaymentTable(context: Context, databaseHelper: SignalDatabase) : Data
|
||||
inAppPayment: InAppPayment
|
||||
) {
|
||||
val updated = inAppPayment.copy(updatedAt = System.currentTimeMillis().milliseconds)
|
||||
|
||||
validateInAppPayment(updated.state, updated.data)
|
||||
|
||||
writableDatabase.update(TABLE_NAME)
|
||||
.values(InAppPayment.serialize(updated))
|
||||
.where(ID_WHERE, inAppPayment.id)
|
||||
@@ -305,6 +312,20 @@ class InAppPaymentTable(context: Context, databaseHelper: SignalDatabase) : Data
|
||||
.readToSingleObject(InAppPayment.Companion)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given InAppPayment properties and throws an exception if they're invalid.
|
||||
*/
|
||||
private fun validateInAppPayment(
|
||||
state: State,
|
||||
inAppPaymentData: InAppPaymentData
|
||||
) {
|
||||
if (inAppPaymentData.error?.data_ == InAppPaymentKeepAliveJob.KEEP_ALIVE) {
|
||||
check(state == State.PENDING) { "Data has keep-alive error: Expected PENDING state but was $state." }
|
||||
} else if (inAppPaymentData.error != null) {
|
||||
check(state == State.END) { "Data has error: Expected END state but was $state" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a database row. Nicer than returning a raw value.
|
||||
*/
|
||||
|
||||
@@ -122,6 +122,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V264_FixGroupAddMem
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V265_FixFtsTriggers
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V266_UniqueThreadPinOrder
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V267_FixGroupInvitationDeclinedUpdate
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V268_FixInAppPaymentsErrorStateConsistency
|
||||
import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSqliteDatabase
|
||||
|
||||
/**
|
||||
@@ -249,7 +250,8 @@ object SignalDatabaseMigrations {
|
||||
264 to V264_FixGroupAddMemberUpdate,
|
||||
265 to V265_FixFtsTriggers,
|
||||
266 to V266_UniqueThreadPinOrder,
|
||||
267 to V267_FixGroupInvitationDeclinedUpdate
|
||||
267 to V267_FixGroupInvitationDeclinedUpdate,
|
||||
268 to V268_FixInAppPaymentsErrorStateConsistency
|
||||
)
|
||||
|
||||
const val DATABASE_VERSION = 268
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.database.helpers.migration
|
||||
|
||||
import android.app.Application
|
||||
import androidx.core.content.contentValuesOf
|
||||
import org.signal.core.util.forEach
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.requireNonNullBlob
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SQLiteDatabase
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
|
||||
/**
|
||||
* Ensure consistent [InAppPaymentTable.State] and [InAppPaymentData.Error] state across the database.
|
||||
*/
|
||||
@Suppress("ClassName")
|
||||
object V268_FixInAppPaymentsErrorStateConsistency : SignalDatabaseMigration {
|
||||
private const val KEEP_ALIVE = "keep-alive"
|
||||
private const val STATE_PENDING = 2L
|
||||
private const val STATE_END = 3L
|
||||
|
||||
private val TAG = Log.tag(V268_FixInAppPaymentsErrorStateConsistency::class)
|
||||
|
||||
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
db.query("SELECT _id, state, data FROM in_app_payment").forEach {
|
||||
val id = it.requireLong("_id")
|
||||
val state = it.requireLong("state")
|
||||
val data = InAppPaymentData.ADAPTER.decode(it.requireNonNullBlob("data"))
|
||||
|
||||
if (data.error?.data_ == KEEP_ALIVE && state != STATE_PENDING) {
|
||||
Log.d(TAG, "Detected a data inconsistency. Expected PENDING state but was State:$state")
|
||||
val newData = data.newBuilder().error(
|
||||
data.error.newBuilder().data_(null).build()
|
||||
).build()
|
||||
|
||||
updateInAppPayment(db, id, newData)
|
||||
} else if (data.error != null && state != STATE_END) {
|
||||
Log.d(TAG, "Detected a data inconsistency. Expected END state but was State:$state")
|
||||
updateInAppPayment(db, id, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateInAppPayment(db: SQLiteDatabase, id: Long, data: InAppPaymentData) {
|
||||
db.update(
|
||||
"in_app_payment",
|
||||
contentValuesOf(
|
||||
"state" to STATE_END,
|
||||
"data" to data.encode()
|
||||
),
|
||||
"_id = ?",
|
||||
arrayOf(id.toString())
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user