package co.smartreceipts.android.purchases.wallet; import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import com.google.common.base.Preconditions; import org.json.JSONException; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Locale; import java.util.Map; import java.util.Set; import javax.inject.Inject; import co.smartreceipts.android.purchases.model.InAppPurchase; import co.smartreceipts.android.purchases.model.ManagedProduct; import co.smartreceipts.android.purchases.model.ManagedProductFactory; import co.smartreceipts.android.utils.log.Logger; public class DefaultPurchaseWallet implements PurchaseWallet { private static final String KEY_SKU_SET = "key_sku_set"; private static final String FORMAT_KEY_PURCHASE_DATA = "%s_purchaseData"; private static final String FORMAT_KEY_IN_APP_DATA_SIGNATURE = "%s_inAppDataSignature"; private final SharedPreferences sharedPreferences; private final Map<InAppPurchase, ManagedProduct> ownedInAppPurchasesMap; @Inject public DefaultPurchaseWallet(Context context) { this(PreferenceManager.getDefaultSharedPreferences(context)); } @VisibleForTesting protected DefaultPurchaseWallet(@NonNull SharedPreferences preferences) { this.sharedPreferences = Preconditions.checkNotNull(preferences); ownedInAppPurchasesMap = restoreWallet(); } @NonNull @Override public Set<ManagedProduct> getActivePurchases() { return new HashSet<>(ownedInAppPurchasesMap.values()); } @Override public synchronized boolean hasActivePurchase(@NonNull InAppPurchase inAppPurchase) { return ownedInAppPurchasesMap.containsKey(inAppPurchase); } @Nullable @Override public synchronized ManagedProduct getManagedProduct(@NonNull InAppPurchase inAppPurchase) { return ownedInAppPurchasesMap.get(inAppPurchase); } @Override public synchronized void updatePurchasesInWallet(@NonNull Set<ManagedProduct> managedProducts) { final Map<InAppPurchase, ManagedProduct> actualInAppPurchasesMap = new HashMap<>(); for (final ManagedProduct managedProduct : managedProducts) { actualInAppPurchasesMap.put(managedProduct.getInAppPurchase(), managedProduct); } if (!actualInAppPurchasesMap.equals(ownedInAppPurchasesMap)) { // Only update if we actually added something to the underlying set ownedInAppPurchasesMap.clear(); ownedInAppPurchasesMap.putAll(actualInAppPurchasesMap); persistWallet(); } } @Override public synchronized void removePurchaseFromWallet(@NonNull InAppPurchase inAppPurchase) { final ManagedProduct managedProduct = ownedInAppPurchasesMap.remove(inAppPurchase); if (managedProduct != null) { final SharedPreferences.Editor editor = sharedPreferences.edit(); editor.remove(getKeyForPurchaseData(inAppPurchase)); editor.remove(getKeyForInAppDataSignature(inAppPurchase)); editor.apply(); persistWallet(); // And persist our sku set } } @Override public synchronized void addPurchaseToWallet(@NonNull ManagedProduct managedProduct) { if (!ownedInAppPurchasesMap.containsKey(managedProduct.getInAppPurchase())) { ownedInAppPurchasesMap.put(managedProduct.getInAppPurchase(), managedProduct); persistWallet(); } } @NonNull private Map<InAppPurchase, ManagedProduct> restoreWallet() { final Set<String> skusSet = sharedPreferences.getStringSet(KEY_SKU_SET, Collections.<String>emptySet()); final Map<InAppPurchase, ManagedProduct> inAppPurchasesMap = new HashMap<>(); for (final String sku : skusSet) { final InAppPurchase inAppPurchase = InAppPurchase.from(sku); if (inAppPurchase != null) { final String purchaseData = sharedPreferences.getString(getKeyForPurchaseData(inAppPurchase), ""); final String inAppDataSignature = sharedPreferences.getString(getKeyForInAppDataSignature(inAppPurchase), ""); try { final ManagedProduct managedProduct = new ManagedProductFactory(inAppPurchase, purchaseData, inAppDataSignature).get(); inAppPurchasesMap.put(inAppPurchase, managedProduct); } catch (JSONException e) { Logger.error(this, "Failed to parse the purchase data for " + inAppPurchase, e); } } } return inAppPurchasesMap; } private void persistWallet() { final Set<InAppPurchase> ownedInAppPurchases = new HashSet<>(ownedInAppPurchasesMap.keySet()); final Set<String> skusSet = new HashSet<>(); final SharedPreferences.Editor editor = sharedPreferences.edit(); // Note per: https://developer.android.com/reference/android/content/SharedPreferences.Editor.html#remove(java.lang.String) // All removals are done first, regardless of whether you called remove before or after put methods on this editor. for (final InAppPurchase inAppPurchase : InAppPurchase.values()) { editor.remove(getKeyForPurchaseData(inAppPurchase)); editor.remove(getKeyForInAppDataSignature(inAppPurchase)); } for (final InAppPurchase inAppPurchase : ownedInAppPurchases) { final ManagedProduct managedProduct = ownedInAppPurchasesMap.get(inAppPurchase); skusSet.add(inAppPurchase.getSku()); editor.putString(getKeyForPurchaseData(inAppPurchase), managedProduct.getPurchaseData()); editor.putString(getKeyForInAppDataSignature(inAppPurchase), managedProduct.getInAppDataSignature()); } editor.putStringSet(KEY_SKU_SET, skusSet); editor.apply(); } @NonNull private String getKeyForPurchaseData(@NonNull InAppPurchase inAppPurchase) { return String.format(Locale.US, FORMAT_KEY_PURCHASE_DATA, inAppPurchase.getSku()); } @NonNull private String getKeyForInAppDataSignature(@NonNull InAppPurchase inAppPurchase) { return String.format(Locale.US, FORMAT_KEY_IN_APP_DATA_SIGNATURE, inAppPurchase.getSku()); } }