From 588663b3c2c5fb9c81eef7e3bafdb8f01c59d4ce Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Mon, 16 May 2022 16:19:26 -0300 Subject: [PATCH] Add better handling for unexpected cancellations. --- .../securesms/ApplicationContext.java | 5 +- .../securesms/badges/models/Badge.kt | 2 + .../ExpiredBadgeBottomSheetDialogFragment.kt | 4 +- .../app/internal/InternalSettingsFragment.kt | 12 ++++ .../subscription/DonationPaymentRepository.kt | 3 +- .../subscribe/SubscribeViewModel.kt | 14 +---- .../securesms/jobs/RefreshOwnProfileJob.java | 17 ++++++ .../jobs/SubscriptionKeepAliveJob.java | 16 ++++- .../securesms/keyvalue/DonationsValues.kt | 61 +++++++++++++++++++ .../securesms/storage/StorageSyncHelper.java | 6 +- app/src/main/res/values/strings.xml | 1 + 11 files changed, 115 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 93b4f70a9f..b103e7434b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -18,7 +18,6 @@ package org.thoughtcrime.securesms; import android.content.Context; import android.os.Build; -import android.os.StrictMode; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -87,7 +86,6 @@ import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.util.AppForegroundObserver; import org.thoughtcrime.securesms.util.AppStartup; import org.thoughtcrime.securesms.util.DynamicTheme; -import org.thoughtcrime.securesms.util.Environment; import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.SignalLocalMetrics; import org.thoughtcrime.securesms.util.SignalUncaughtExceptionHandler; @@ -99,7 +97,6 @@ import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWra import java.net.SocketException; import java.net.SocketTimeoutException; import java.security.Security; -import java.util.Objects; import java.util.concurrent.TimeUnit; import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException; @@ -221,7 +218,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr ApplicationDependencies.getFrameRateTracker().start(); ApplicationDependencies.getMegaphoneRepository().onAppForegrounded(); ApplicationDependencies.getDeadlockDetector().start(); - SubscriptionKeepAliveJob.launchSubscriberIdKeepAliveJobIfNecessary(); + SubscriptionKeepAliveJob.enqueueAndTrackTimeIfNecessary(); SignalExecutors.BOUNDED.execute(() -> { FeatureFlags.refreshIfNecessary(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/models/Badge.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/models/Badge.kt index 413481ff2c..5baa020083 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/models/Badge.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/models/Badge.kt @@ -40,6 +40,8 @@ data class Badge( fun isExpired(): Boolean = expirationTimestamp < System.currentTimeMillis() && expirationTimestamp > 0 fun isBoost(): Boolean = id == BOOST_BADGE_ID + fun isGift(): Boolean = id == GIFT_BADGE_ID + fun isSubscription(): Boolean = !isBoost() && !isGift() override fun updateDiskCacheKey(messageDigest: MessageDigest) { messageDigest.update(id.toByteArray(Key.CHARSET)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/self/expired/ExpiredBadgeBottomSheetDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/self/expired/ExpiredBadgeBottomSheetDialogFragment.kt index 06c5205240..2ff5eeffc6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/self/expired/ExpiredBadgeBottomSheetDialogFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/self/expired/ExpiredBadgeBottomSheetDialogFragment.kt @@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.errors.Un import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.util.BottomSheetUtil +import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription /** * Bottom sheet displaying a fading badge with a notice and action for becoming a subscriber again. @@ -30,7 +31,8 @@ class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment( private fun getConfiguration(): DSLConfiguration { val args = ExpiredBadgeBottomSheetDialogFragmentArgs.fromBundle(requireArguments()) val badge: Badge = args.badge - val cancellationReason: UnexpectedSubscriptionCancellation? = UnexpectedSubscriptionCancellation.fromStatus(args.cancelationReason) + val cancellationReason = UnexpectedSubscriptionCancellation.fromStatus(args.cancelationReason) + val chargeFailure: ActiveSubscription.ChargeFailure? = SignalStore.donationsValues().getUnexpectedSubscriptionCancelationChargeFailure() val isLikelyASustainer = SignalStore.donationsValues().isLikelyASustainer() val inactive = cancellationReason == UnexpectedSubscriptionCancellation.INACTIVE diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt index 8ad93202b1..e1797baa96 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt @@ -31,6 +31,7 @@ import org.thoughtcrime.securesms.jobs.RemoteConfigRefreshJob import org.thoughtcrime.securesms.jobs.RetrieveRemoteAnnouncementsJob import org.thoughtcrime.securesms.jobs.RotateProfileKeyJob import org.thoughtcrime.securesms.jobs.StorageForcePushJob +import org.thoughtcrime.securesms.jobs.SubscriptionKeepAliveJob import org.thoughtcrime.securesms.jobs.SubscriptionReceiptRequestResponseJob import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.payments.DataExportUtil @@ -401,6 +402,13 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter enqueueSubscriptionRedemption() } ) + + clickPref( + title = DSLSettingsText.from(R.string.preferences__internal_badges_enqueue_keep_alive), + onClick = { + enqueueSubscriptionKeepAlive() + } + ) } dividerPref() @@ -573,6 +581,10 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain().enqueue() } + private fun enqueueSubscriptionKeepAlive() { + SubscriptionKeepAliveJob.enqueueAndTrackTime(System.currentTimeMillis()) + } + private fun clearCdsHistory() { SignalDatabase.cds.clearAll() SignalStore.misc().cdsToken = null diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationPaymentRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationPaymentRepository.kt index 5a8e192f50..d81e10dade 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationPaymentRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationPaymentRepository.kt @@ -277,9 +277,8 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet ).flatMapCompletable { if (it.status == 200 || it.status == 204) { Log.d(TAG, "Successfully set user subscription to level $subscriptionLevel with response code ${it.status}", true) - SignalStore.donationsValues().clearUserManuallyCancelled() + SignalStore.donationsValues().updateLocalStateForLocalSubscribe() scheduleSyncForAccountRecordChange() - SignalStore.donationsValues().clearLevelOperations() LevelUpdate.updateProcessingState(false) Completable.complete() } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/subscribe/SubscribeViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/subscribe/SubscribeViewModel.kt index 3e8fa9159d..0c2f370aa2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/subscribe/SubscribeViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/subscribe/SubscribeViewModel.kt @@ -164,12 +164,7 @@ class SubscribeViewModel( return Single.just(SignalStore.donationsValues().shouldCancelSubscriptionBeforeNextSubscribeAttempt).flatMapCompletable { if (it) { donationPaymentRepository.cancelActiveSubscription().doOnComplete { - SignalStore.donationsValues().setLastEndOfPeriod(0L) - SignalStore.donationsValues().clearLevelOperations() - SignalStore.donationsValues().shouldCancelSubscriptionBeforeNextSubscribeAttempt = false - SignalStore.donationsValues().setUnexpectedSubscriptionCancelationChargeFailure(null) - SignalStore.donationsValues().unexpectedSubscriptionCancelationReason = null - SignalStore.donationsValues().unexpectedSubscriptionCancelationTimestamp = 0L + SignalStore.donationsValues().updateLocalStateForManualCancellation() MultiDeviceSubscriptionSyncRequestJob.enqueue() } } else { @@ -183,12 +178,7 @@ class SubscribeViewModel( disposables += donationPaymentRepository.cancelActiveSubscription().subscribeBy( onComplete = { eventPublisher.onNext(DonationEvent.SubscriptionCancelled) - SignalStore.donationsValues().setLastEndOfPeriod(0L) - SignalStore.donationsValues().clearLevelOperations() - SignalStore.donationsValues().markUserManuallyCancelled() - SignalStore.donationsValues().setUnexpectedSubscriptionCancelationChargeFailure(null) - SignalStore.donationsValues().unexpectedSubscriptionCancelationReason = null - SignalStore.donationsValues().unexpectedSubscriptionCancelationTimestamp = 0L + SignalStore.donationsValues().updateLocalStateForManualCancellation() refreshActiveSubscription() MultiDeviceSubscriptionSyncRequestJob.enqueue() donationPaymentRepository.scheduleSyncForAccountRecordChange() diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java index 90933e0a35..b99c1c05b3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java @@ -299,6 +299,10 @@ public class RefreshOwnProfileJob extends BaseJob { Log.d(TAG, "Unexpected expiry due to payment failure.", true); isDueToPaymentFailure = true; } + + if (activeSubscription.getChargeFailure() != null) { + Log.d(TAG, "Active payment contains a charge failure: " + activeSubscription.getChargeFailure().getCode(), true); + } } } @@ -320,6 +324,16 @@ public class RefreshOwnProfileJob extends BaseJob { Log.d(TAG, "Marking boost badge as expired, should notify next time the conversation list is open.", true); SignalStore.donationsValues().setExpiredBadge(mostRecentExpiration); + } else { + Badge badge = SignalStore.donationsValues().getExpiredBadge(); + + if (badge != null && badge.isSubscription() && remoteHasSubscriptionBadges) { + Log.d(TAG, "Remote has subscription badges. Clearing local expired subscription badge.", true); + SignalStore.donationsValues().setExpiredBadge(null); + } else if (badge != null && badge.isBoost() && remoteHasBoostBadges) { + Log.d(TAG, "Remote has boost badges. Clearing local expired boost badge.", true); + SignalStore.donationsValues().setExpiredBadge(null); + } } if (!remoteHasGiftBadges && localHasGiftBadges) { @@ -333,6 +347,9 @@ public class RefreshOwnProfileJob extends BaseJob { Log.d(TAG, "Marking gift badge as expired, should notify next time the manage donations screen is open.", true); SignalStore.donationsValues().setExpiredGiftBadge(mostRecentExpiration); + } else if (remoteHasGiftBadges) { + Log.d(TAG, "We have remote gift badges. Clearing local expired gift badge.", true); + SignalStore.donationsValues().setExpiredGiftBadge(null); } boolean userHasVisibleBadges = badges.stream().anyMatch(SignalServiceProfile.Badge::isVisible); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionKeepAliveJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionKeepAliveJob.java index 809c68dee9..59f3a493b3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionKeepAliveJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionKeepAliveJob.java @@ -29,16 +29,20 @@ public class SubscriptionKeepAliveJob extends BaseJob { private static final String TAG = Log.tag(SubscriptionKeepAliveJob.class); private static final long JOB_TIMEOUT = TimeUnit.DAYS.toMillis(3); - public static void launchSubscriberIdKeepAliveJobIfNecessary() { + public static void enqueueAndTrackTimeIfNecessary() { long nextLaunchTime = SignalStore.donationsValues().getLastKeepAliveLaunchTime() + TimeUnit.DAYS.toMillis(3); long now = System.currentTimeMillis(); if (nextLaunchTime <= now) { - ApplicationDependencies.getJobManager().add(new SubscriptionKeepAliveJob()); - SignalStore.donationsValues().setLastKeepAliveLaunchTime(now); + enqueueAndTrackTime(now); } } + public static void enqueueAndTrackTime(long now) { + ApplicationDependencies.getJobManager().add(new SubscriptionKeepAliveJob()); + SignalStore.donationsValues().setLastKeepAliveLaunchTime(now); + } + private SubscriptionKeepAliveJob() { this(new Parameters.Builder() .setQueue(KEY) @@ -70,6 +74,12 @@ public class SubscriptionKeepAliveJob extends BaseJob { @Override protected void onRun() throws Exception { + synchronized (SubscriptionReceiptRequestResponseJob.MUTEX) { + doRun(); + } + } + + private void doRun() throws Exception { Subscriber subscriber = SignalStore.donationsValues().getSubscriber(); if (subscriber == null) { return; diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/DonationsValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/DonationsValues.kt index 7ce1bce866..daf2a2d56e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/DonationsValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/DonationsValues.kt @@ -1,5 +1,6 @@ package org.thoughtcrime.securesms.keyvalue +import androidx.annotation.WorkerThread import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.subjects.BehaviorSubject import io.reactivex.rxjava3.subjects.Subject @@ -8,6 +9,7 @@ import org.signal.donations.StripeApi import org.thoughtcrime.securesms.badges.Badges import org.thoughtcrime.securesms.badges.models.Badge import org.thoughtcrime.securesms.database.model.databaseprotos.BadgeList +import org.thoughtcrime.securesms.jobs.SubscriptionReceiptRequestResponseJob import org.thoughtcrime.securesms.payments.currency.CurrencyUtil import org.thoughtcrime.securesms.subscription.LevelUpdateOperation import org.thoughtcrime.securesms.subscription.Subscriber @@ -287,4 +289,63 @@ internal class DonationsValues internal constructor(store: KeyValueStore) : Sign var shouldCancelSubscriptionBeforeNextSubscribeAttempt: Boolean get() = getBoolean(SHOULD_CANCEL_SUBSCRIPTION_BEFORE_NEXT_SUBSCRIBE_ATTEMPT, false) set(value) = putBoolean(SHOULD_CANCEL_SUBSCRIPTION_BEFORE_NEXT_SUBSCRIBE_ATTEMPT, value) + + /** + * Consolidates a bunch of data clears that should occur whenever a user manually cancels their + * subscription: + * + * 1. Clears keep-alive flag + * 1. Clears level operation + * 1. Marks the user as manually cancelled + * 1. Clears out unexpected cancelation state + * 1. Clears expired badge if it is for a subscription + */ + @WorkerThread + fun updateLocalStateForManualCancellation() { + synchronized(SubscriptionReceiptRequestResponseJob.MUTEX) { + Log.d(TAG, "[updateLocalStateForManualCancellation] Clearing donation values.") + + setLastEndOfPeriod(0L) + clearLevelOperations() + markUserManuallyCancelled() + shouldCancelSubscriptionBeforeNextSubscribeAttempt = false + setUnexpectedSubscriptionCancelationChargeFailure(null) + unexpectedSubscriptionCancelationReason = null + unexpectedSubscriptionCancelationTimestamp = 0L + + val expiredBadge = getExpiredBadge() + if (expiredBadge != null && expiredBadge.isSubscription()) { + Log.d(TAG, "[updateLocalStateForManualCancellation] Clearing expired badge.") + setExpiredBadge(null) + } + } + } + + /** + * Consolidates a bunch of data clears that should occur whenever a user begins a new subscription: + * + * 1. Manual cancellation marker + * 1. Any set level operations + * 1. Unexpected cancelation flags + * 1. Expired badge, if it is of a subscription + */ + @WorkerThread + fun updateLocalStateForLocalSubscribe() { + synchronized(SubscriptionReceiptRequestResponseJob.MUTEX) { + Log.d(TAG, "[updateLocalStateForLocalSubscribe] Clearing donation values.") + + clearUserManuallyCancelled() + clearLevelOperations() + shouldCancelSubscriptionBeforeNextSubscribeAttempt = false + setUnexpectedSubscriptionCancelationChargeFailure(null) + unexpectedSubscriptionCancelationReason = null + unexpectedSubscriptionCancelationTimestamp = 0L + + val expiredBadge = getExpiredBadge() + if (expiredBadge != null && expiredBadge.isSubscription()) { + Log.d(TAG, "[updateLocalStateForLocalSubscribe] Clearing expired badge.") + setExpiredBadge(null) + } + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.java b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.java index 8369e2cf84..9dd0130dd5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.java @@ -9,6 +9,7 @@ import androidx.annotation.VisibleForTesting; import com.annimon.stream.Collectors; import com.annimon.stream.Stream; +import org.signal.core.util.SetUtil; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.SignalDatabase; @@ -22,7 +23,6 @@ import org.thoughtcrime.securesms.payments.Entropy; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.subscription.Subscriber; import org.thoughtcrime.securesms.util.Base64; -import org.signal.core.util.SetUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.signalservice.api.storage.SignalAccountRecord; @@ -160,9 +160,7 @@ public final class StorageSyncHelper { SignalStore.donationsValues().setDisplayBadgesOnProfile(update.getNew().isDisplayBadgesOnProfile()); if (update.getNew().isSubscriptionManuallyCancelled()) { - SignalStore.donationsValues().markUserManuallyCancelled(); - SignalStore.donationsValues().setUnexpectedSubscriptionCancelationReason(null); - SignalStore.donationsValues().setUnexpectedSubscriptionCancelationTimestamp(0L); + SignalStore.donationsValues().updateLocalStateForManualCancellation(); } else { SignalStore.donationsValues().clearUserManuallyCancelled(); } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e3229cef7f..78be0a8ca4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2738,6 +2738,7 @@ Disable Telecom integration Badges Enqueue redemption. + Enqueue keep-alive. Release channel Fetch release channel Set last version seen back 10 versions