Migrate local account data into SignalStore.

This commit is contained in:
Greyson Parrelli
2021-11-17 15:08:28 -05:00
committed by Cody Henthorne
parent 87f175a96b
commit 8aea20f147
87 changed files with 1063 additions and 756 deletions

View File

@@ -0,0 +1,155 @@
package org.thoughtcrime.securesms.keyvalue
import android.content.Context
import androidx.annotation.VisibleForTesting
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
internal class AccountValues internal constructor(store: KeyValueStore) : SignalStoreValues(store) {
companion object {
private val TAG = Log.tag(AccountValues::class.java)
private const val KEY_ACI = "account.aci"
private const val KEY_PNI = "account.pni"
private const val KEY_SERVICE_PASSWORD = "account.service_password"
private const val KEY_IS_REGISTERED = "account.is_registered"
private const val KEY_REGISTRATION_ID = "account.registration_id"
private const val KEY_FCM_ENABLED = "account.fcm_enabled"
private const val KEY_FCM_TOKEN = "account.fcm_token"
private const val KEY_FCM_TOKEN_VERSION = "account.fcm_token_version"
private const val KEY_FCM_TOKEN_LAST_SET_TIME = "account.fcm_token_last_set_time"
@VisibleForTesting
const val KEY_E164 = "account.e164"
}
init {
if (!store.containsKey(KEY_ACI)) {
migrateFromSharedPrefs(ApplicationDependencies.getApplication())
}
}
public override fun onFirstEverAppLaunch() = Unit
public override fun getKeysToIncludeInBackup(): List<String> {
return emptyList()
}
/** The local user's [ACI]. */
val aci: ACI?
get() = ACI.parseOrNull(getString(KEY_ACI, null))
fun setAci(aci: ACI) {
putString(KEY_ACI, aci.toString())
}
/** The local user's [PNI]. */
val pni: PNI?
get() = PNI.parseOrNull(getString(KEY_ACI, null))
fun setPni(pni: PNI) {
putString(KEY_PNI, pni.toString())
}
/** The local user's E164. */
val e164: String?
get() = getString(KEY_E164, null)
fun setE164(e164: String) {
putString(KEY_E164, e164)
}
/** The password for communicating with the Signal service. */
val servicePassword: String?
get() = getString(KEY_SERVICE_PASSWORD, null)
fun setServicePassword(servicePassword: String) {
putString(KEY_SERVICE_PASSWORD, servicePassword)
}
/** A randomly-generated value that represents this registration instance. Helps the server know if you reinstalled. */
var registrationId: Int
get() = getInteger(KEY_REGISTRATION_ID, 0)
set(value) = putInteger(KEY_REGISTRATION_ID, value)
/** Indicates whether the user has the ability to receive FCM messages. Largely coupled to whether they have Play Service. */
var fcmEnabled: Boolean
@JvmName("isFcmEnabled")
get() = getBoolean(KEY_FCM_ENABLED, false)
set(value) = putBoolean(KEY_FCM_ENABLED, value)
/** The FCM token, which allows the server to send us FCM messages. */
var fcmToken: String?
get() {
val tokenVersion: Int = getInteger(KEY_FCM_TOKEN_VERSION, 0)
return if (tokenVersion == Util.getCanonicalVersionCode()) {
getString(KEY_FCM_TOKEN, null)
} else {
null
}
}
set(value) {
store.beginWrite()
.putString(KEY_FCM_TOKEN, value)
.putInteger(KEY_FCM_TOKEN_VERSION, Util.getCanonicalVersionCode())
.putLong(KEY_FCM_TOKEN_LAST_SET_TIME, System.currentTimeMillis())
.apply()
}
/** When we last set the [fcmToken] */
val fcmTokenLastSetTime: Long
get() = getLong(KEY_FCM_TOKEN_LAST_SET_TIME, 0)
/** Whether or not the user is registered with the Signal service. */
val isRegistered: Boolean
get() = getBoolean(KEY_IS_REGISTERED, false)
fun setRegistered(registered: Boolean) {
Log.i(TAG, "Setting push registered: $registered", Throwable())
val previous = isRegistered
putBoolean(KEY_IS_REGISTERED, registered)
ApplicationDependencies.getIncomingMessageObserver().notifyRegistrationChanged()
if (previous != registered) {
Recipient.self().live().refresh()
}
if (previous && !registered) {
clearLocalCredentials(ApplicationDependencies.getApplication())
}
}
private fun clearLocalCredentials(context: Context) {
putString(KEY_SERVICE_PASSWORD, Util.getSecret(18))
val newProfileKey = ProfileKeyUtil.createNew()
val self = Recipient.self()
DatabaseFactory.getRecipientDatabase(context).setProfileKey(self.id, newProfileKey)
ApplicationDependencies.getGroupsV2Authorization().clear()
}
private fun migrateFromSharedPrefs(context: Context) {
Log.i(TAG, "Migrating account values from shared prefs.")
putString(KEY_ACI, TextSecurePreferences.getStringPreference(context, "pref_local_uuid", null))
putString(KEY_E164, TextSecurePreferences.getStringPreference(context, "pref_local_number", null))
putString(KEY_SERVICE_PASSWORD, TextSecurePreferences.getStringPreference(context, "pref_gcm_password", null))
putBoolean(KEY_IS_REGISTERED, TextSecurePreferences.getBooleanPreference(context, "pref_gcm_registered", false))
putInteger(KEY_REGISTRATION_ID, TextSecurePreferences.getIntegerPreference(context, "pref_local_registration_id", 0))
putBoolean(KEY_FCM_ENABLED, !TextSecurePreferences.getBooleanPreference(context, "pref_gcm_disabled", false))
putString(KEY_FCM_TOKEN, TextSecurePreferences.getStringPreference(context, "pref_gcm_registration_id", null))
putInteger(KEY_FCM_TOKEN_VERSION, TextSecurePreferences.getIntegerPreference(context, "pref_gcm_registration_id_version", 0))
putLong(KEY_FCM_TOKEN_LAST_SET_TIME, TextSecurePreferences.getLongPreference(context, "pref_gcm_registration_id_last_set_time", 0))
}
}

View File

@@ -7,11 +7,9 @@ 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.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.payments.currency.CurrencyUtil
import org.thoughtcrime.securesms.subscription.LevelUpdateOperation
import org.thoughtcrime.securesms.subscription.Subscriber
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.api.subscriptions.IdempotencyKey
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
import java.util.Currency
@@ -52,7 +50,7 @@ internal class DonationsValues internal constructor(store: KeyValueStore) : Sign
val currency: Currency? = if (currencyCode == null) {
val localeCurrency = CurrencyUtil.getCurrencyByLocale(Locale.getDefault())
if (localeCurrency == null) {
val e164 = TextSecurePreferences.getLocalNumber(ApplicationDependencies.getApplication())
val e164: String? = SignalStore.account().e164
if (e164 == null) {
null
} else {

View File

@@ -0,0 +1,10 @@
package org.thoughtcrime.securesms.keyvalue;
import androidx.annotation.NonNull;
import java.util.Collection;
public interface KeyValuePersistentStorage {
void writeDataSet(@NonNull KeyValueDataSet dataSet, @NonNull Collection<String> removes);
@NonNull KeyValueDataSet getDataSet();
}

View File

@@ -1,8 +1,5 @@
package org.thoughtcrime.securesms.keyvalue;
import android.app.Application;
import android.content.Context;
import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -11,8 +8,6 @@ import androidx.annotation.WorkerThread;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.KeyValueDatabase;
import org.thoughtcrime.securesms.util.SignalUncaughtExceptionHandler;
import java.util.Collection;
@@ -35,14 +30,14 @@ public final class KeyValueStore implements KeyValueReader {
private static final String TAG = Log.tag(KeyValueStore.class);
private final ExecutorService executor;
private final KeyValueDatabase database;
private final ExecutorService executor;
private final KeyValuePersistentStorage storage;
private KeyValueDataSet dataSet;
public KeyValueStore(@NonNull Application application) {
public KeyValueStore(@NonNull KeyValuePersistentStorage storage) {
this.executor = SignalExecutors.newCachedSingleThreadExecutor("signal-KeyValueStore");
this.database = KeyValueDatabase.getInstance(application);
this.storage = storage;
}
@AnyThread
@@ -150,12 +145,12 @@ public final class KeyValueStore implements KeyValueReader {
dataSet.putAll(newDataSet);
dataSet.removeAll(removes);
executor.execute(() -> database.writeDataSet(newDataSet, removes));
executor.execute(() -> storage.writeDataSet(newDataSet, removes));
}
private void initializeIfNecessary() {
if (dataSet != null) return;
this.dataSet = database.getDataSet();
this.dataSet = storage.getDataSet();
}
class Writer {

View File

@@ -1,366 +0,0 @@
package org.thoughtcrime.securesms.keyvalue;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import com.google.protobuf.InvalidProtocolBufferException;
import com.mobilecoin.lib.Mnemonics;
import com.mobilecoin.lib.exceptions.BadMnemonicException;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType;
import org.thoughtcrime.securesms.payments.Balance;
import org.thoughtcrime.securesms.payments.Entropy;
import org.thoughtcrime.securesms.payments.GeographicalRestrictions;
import org.thoughtcrime.securesms.payments.Mnemonic;
import org.thoughtcrime.securesms.payments.MobileCoinLedgerWrapper;
import org.thoughtcrime.securesms.payments.currency.CurrencyUtil;
import org.thoughtcrime.securesms.payments.proto.MobileCoinLedger;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.api.payments.Money;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Currency;
import java.util.List;
import java.util.Locale;
public final class PaymentsValues extends SignalStoreValues {
private static final String TAG = Log.tag(PaymentsValues.class);
private static final String PAYMENTS_ENTROPY = "payments_entropy";
private static final String MOB_PAYMENTS_ENABLED = "mob_payments_enabled";
private static final String MOB_LEDGER = "mob_ledger";
private static final String PAYMENTS_CURRENT_CURRENCY = "payments_current_currency";
private static final String DEFAULT_CURRENCY_CODE = "GBP";
private static final String USER_CONFIRMED_MNEMONIC = "mob_payments_user_confirmed_mnemonic";
private static final String SHOW_ABOUT_MOBILE_COIN_INFO_CARD = "mob_payments_show_about_mobile_coin_info_card";
private static final String SHOW_ADDING_TO_YOUR_WALLET_INFO_CARD = "mob_payments_show_adding_to_your_wallet_info_card";
private static final String SHOW_CASHING_OUT_INFO_CARD = "mob_payments_show_cashing_out_info_card";
private static final String SHOW_RECOVERY_PHRASE_INFO_CARD = "mob_payments_show_recovery_phrase_info_card";
private static final String SHOW_UPDATE_PIN_INFO_CARD = "mob_payments_show_update_pin_info_card";
private static final Money.MobileCoin LARGE_BALANCE_THRESHOLD = Money.mobileCoin(BigDecimal.valueOf(500));
private final MutableLiveData<Currency> liveCurrentCurrency;
private final MutableLiveData<MobileCoinLedgerWrapper> liveMobileCoinLedger;
private final LiveData<Balance> liveMobileCoinBalance;
PaymentsValues(@NonNull KeyValueStore store) {
super(store);
this.liveCurrentCurrency = new MutableLiveData<>(currentCurrency());
this.liveMobileCoinLedger = new MutableLiveData<>(mobileCoinLatestFullLedger());
this.liveMobileCoinBalance = Transformations.map(liveMobileCoinLedger, MobileCoinLedgerWrapper::getBalance);
}
@Override void onFirstEverAppLaunch() {
}
@Override
@NonNull List<String> getKeysToIncludeInBackup() {
return Arrays.asList(PAYMENTS_ENTROPY,
MOB_PAYMENTS_ENABLED,
MOB_LEDGER,
PAYMENTS_CURRENT_CURRENCY,
DEFAULT_CURRENCY_CODE,
USER_CONFIRMED_MNEMONIC,
SHOW_ABOUT_MOBILE_COIN_INFO_CARD,
SHOW_ADDING_TO_YOUR_WALLET_INFO_CARD,
SHOW_CASHING_OUT_INFO_CARD,
SHOW_RECOVERY_PHRASE_INFO_CARD,
SHOW_UPDATE_PIN_INFO_CARD);
}
public boolean userConfirmedMnemonic() {
return getStore().getBoolean(USER_CONFIRMED_MNEMONIC, false);
}
public void setUserConfirmedMnemonic(boolean userConfirmedMnemonic) {
getStore().beginWrite().putBoolean(USER_CONFIRMED_MNEMONIC, userConfirmedMnemonic).commit();
}
/**
* Consider using {@link #getPaymentsAvailability} which includes feature flag and region status.
*/
public boolean mobileCoinPaymentsEnabled() {
KeyValueReader reader = getStore().beginRead();
return reader.getBoolean(MOB_PAYMENTS_ENABLED, false);
}
/**
* Applies feature flags and region restrictions to return an enum which describes the available feature set for the user.
*/
public PaymentsAvailability getPaymentsAvailability() {
Context context = ApplicationDependencies.getApplication();
if (!TextSecurePreferences.isPushRegistered(context) ||
!GeographicalRestrictions.e164Allowed(TextSecurePreferences.getLocalNumber(context)))
{
return PaymentsAvailability.NOT_IN_REGION;
}
if (FeatureFlags.payments()) {
if (mobileCoinPaymentsEnabled()) {
return PaymentsAvailability.WITHDRAW_AND_SEND;
} else {
return PaymentsAvailability.REGISTRATION_AVAILABLE;
}
} else {
if (mobileCoinPaymentsEnabled()) {
return PaymentsAvailability.WITHDRAW_ONLY;
} else {
return PaymentsAvailability.DISABLED_REMOTELY;
}
}
}
@WorkerThread
public void setMobileCoinPaymentsEnabled(boolean isMobileCoinPaymentsEnabled) {
if (mobileCoinPaymentsEnabled() == isMobileCoinPaymentsEnabled) {
return;
}
if (isMobileCoinPaymentsEnabled) {
Entropy entropy = getPaymentsEntropy();
if (entropy == null) {
entropy = Entropy.generateNew();
Log.i(TAG, "Generated new payments entropy");
}
getStore().beginWrite()
.putBlob(PAYMENTS_ENTROPY, entropy.getBytes())
.putBoolean(MOB_PAYMENTS_ENABLED, true)
.putString(PAYMENTS_CURRENT_CURRENCY, currentCurrency().getCurrencyCode())
.commit();
} else {
getStore().beginWrite()
.putBoolean(MOB_PAYMENTS_ENABLED, false)
.putBoolean(USER_CONFIRMED_MNEMONIC, false)
.commit();
}
DatabaseFactory.getRecipientDatabase(ApplicationDependencies.getApplication()).markNeedsSync(Recipient.self().getId());
StorageSyncHelper.scheduleSyncForDataChange();
}
public @NonNull Mnemonic getPaymentsMnemonic() {
Entropy paymentsEntropy = getPaymentsEntropy();
if (paymentsEntropy == null) {
throw new IllegalStateException("Entropy has not been set");
}
return paymentsEntropy.asMnemonic();
}
/**
* True if a local entropy is set, regardless of whether payments is currently enabled.
*/
public boolean hasPaymentsEntropy() {
return getPaymentsEntropy() != null;
}
/**
* Returns the local payments entropy, regardless of whether payments is currently enabled.
* <p>
* And null if has never been set.
*/
public @Nullable Entropy getPaymentsEntropy() {
return Entropy.fromBytes(getStore().getBlob(PAYMENTS_ENTROPY, null));
}
public @NonNull Balance mobileCoinLatestBalance() {
return mobileCoinLatestFullLedger().getBalance();
}
public @NonNull LiveData<MobileCoinLedgerWrapper> liveMobileCoinLedger() {
return liveMobileCoinLedger;
}
public @NonNull LiveData<Balance> liveMobileCoinBalance() {
return liveMobileCoinBalance;
}
public void setCurrentCurrency(@NonNull Currency currentCurrency) {
getStore().beginWrite()
.putString(PAYMENTS_CURRENT_CURRENCY, currentCurrency.getCurrencyCode())
.commit();
liveCurrentCurrency.postValue(currentCurrency);
}
public @NonNull Currency currentCurrency() {
String currencyCode = getStore().getString(PAYMENTS_CURRENT_CURRENCY, null);
return currencyCode == null ? determineCurrency()
: Currency.getInstance(currencyCode);
}
public @NonNull MutableLiveData<Currency> liveCurrentCurrency() {
return liveCurrentCurrency;
}
public boolean showAboutMobileCoinInfoCard() {
return getStore().getBoolean(SHOW_ABOUT_MOBILE_COIN_INFO_CARD, true);
}
public boolean showAddingToYourWalletInfoCard() {
return getStore().getBoolean(SHOW_ADDING_TO_YOUR_WALLET_INFO_CARD, true);
}
public boolean showCashingOutInfoCard() {
return getStore().getBoolean(SHOW_CASHING_OUT_INFO_CARD, true);
}
public boolean showRecoveryPhraseInfoCard() {
if (userHasLargeBalance()) {
return getStore().getBoolean(SHOW_CASHING_OUT_INFO_CARD, true);
} else {
return false;
}
}
public boolean showUpdatePinInfoCard() {
if (userHasLargeBalance() &&
SignalStore.kbsValues().hasPin() &&
!SignalStore.kbsValues().hasOptedOut() &&
SignalStore.pinValues().getKeyboardType().equals(PinKeyboardType.NUMERIC)) {
return getStore().getBoolean(SHOW_CASHING_OUT_INFO_CARD, true);
} else {
return false;
}
}
public void dismissAboutMobileCoinInfoCard() {
getStore().beginWrite()
.putBoolean(SHOW_ABOUT_MOBILE_COIN_INFO_CARD, false)
.apply();
}
public void dismissAddingToYourWalletInfoCard() {
getStore().beginWrite()
.putBoolean(SHOW_ADDING_TO_YOUR_WALLET_INFO_CARD, false)
.apply();
}
public void dismissCashingOutInfoCard() {
getStore().beginWrite()
.putBoolean(SHOW_CASHING_OUT_INFO_CARD, false)
.apply();
}
public void dismissRecoveryPhraseInfoCard() {
getStore().beginWrite()
.putBoolean(SHOW_RECOVERY_PHRASE_INFO_CARD, false)
.apply();
}
public void dismissUpdatePinInfoCard() {
getStore().beginWrite()
.putBoolean(SHOW_UPDATE_PIN_INFO_CARD, false)
.apply();
}
public void setMobileCoinFullLedger(@NonNull MobileCoinLedgerWrapper ledger) {
getStore().beginWrite()
.putBlob(MOB_LEDGER, ledger.serialize())
.commit();
liveMobileCoinLedger.postValue(ledger);
}
public @NonNull MobileCoinLedgerWrapper mobileCoinLatestFullLedger() {
byte[] blob = getStore().getBlob(MOB_LEDGER, null);
if (blob == null) {
return new MobileCoinLedgerWrapper(MobileCoinLedger.getDefaultInstance());
}
try {
return new MobileCoinLedgerWrapper(MobileCoinLedger.parseFrom(blob));
} catch (InvalidProtocolBufferException e) {
Log.w(TAG, "Bad cached ledger, clearing", e);
setMobileCoinFullLedger(new MobileCoinLedgerWrapper(MobileCoinLedger.getDefaultInstance()));
throw new AssertionError(e);
}
}
private @NonNull Currency determineCurrency() {
String localE164 = TextSecurePreferences.getLocalNumber(ApplicationDependencies.getApplication());
if (localE164 == null) {
localE164 = "";
}
return Util.firstNonNull(CurrencyUtil.getCurrencyByE164(localE164),
CurrencyUtil.getCurrencyByLocale(Locale.getDefault()),
Currency.getInstance(DEFAULT_CURRENCY_CODE));
}
/**
* Does not trigger a storage sync.
*/
public void setEnabledAndEntropy(boolean enabled, @Nullable Entropy entropy) {
KeyValueStore.Writer writer = getStore().beginWrite();
if (entropy != null) {
writer.putBlob(PAYMENTS_ENTROPY, entropy.getBytes());
}
writer.putBoolean(MOB_PAYMENTS_ENABLED, enabled)
.commit();
}
@WorkerThread
public WalletRestoreResult restoreWallet(@NonNull String mnemonic) {
byte[] entropyFromMnemonic;
try {
entropyFromMnemonic = Mnemonics.bip39EntropyFromMnemonic(mnemonic);
} catch (BadMnemonicException e) {
return WalletRestoreResult.MNEMONIC_ERROR;
}
Entropy paymentsEntropy = getPaymentsEntropy();
if (paymentsEntropy != null) {
byte[] existingEntropy = paymentsEntropy.getBytes();
if (Arrays.equals(existingEntropy, entropyFromMnemonic)) {
setMobileCoinPaymentsEnabled(true);
setUserConfirmedMnemonic(true);
return WalletRestoreResult.ENTROPY_UNCHANGED;
}
}
getStore().beginWrite()
.putBlob(PAYMENTS_ENTROPY, entropyFromMnemonic)
.putBoolean(MOB_PAYMENTS_ENABLED, true)
.remove(MOB_LEDGER)
.putBoolean(USER_CONFIRMED_MNEMONIC, true)
.commit();
liveMobileCoinLedger.postValue(new MobileCoinLedgerWrapper(MobileCoinLedger.getDefaultInstance()));
StorageSyncHelper.scheduleSyncForDataChange();
return WalletRestoreResult.ENTROPY_CHANGED;
}
public enum WalletRestoreResult {
ENTROPY_CHANGED,
ENTROPY_UNCHANGED,
MNEMONIC_ERROR
}
private boolean userHasLargeBalance() {
return mobileCoinLatestBalance().getFullAmount().requireMobileCoin().greaterThan(LARGE_BALANCE_THRESHOLD);
}
}

View File

@@ -0,0 +1,333 @@
package org.thoughtcrime.securesms.keyvalue
import androidx.annotation.WorkerThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import com.google.protobuf.InvalidProtocolBufferException
import com.mobilecoin.lib.Mnemonics
import com.mobilecoin.lib.exceptions.BadMnemonicException
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.PaymentsValues.WalletRestoreResult
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
import org.thoughtcrime.securesms.payments.Balance
import org.thoughtcrime.securesms.payments.Entropy
import org.thoughtcrime.securesms.payments.GeographicalRestrictions
import org.thoughtcrime.securesms.payments.Mnemonic
import org.thoughtcrime.securesms.payments.MobileCoinLedgerWrapper
import org.thoughtcrime.securesms.payments.currency.CurrencyUtil
import org.thoughtcrime.securesms.payments.proto.MobileCoinLedger
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.payments.Money
import java.lang.AssertionError
import java.lang.IllegalStateException
import java.math.BigDecimal
import java.util.Arrays
import java.util.Currency
import java.util.Locale
internal class PaymentsValues internal constructor(store: KeyValueStore) : SignalStoreValues(store) {
companion object {
private val TAG = Log.tag(PaymentsValues::class.java)
private const val PAYMENTS_ENTROPY = "payments_entropy"
private const val MOB_PAYMENTS_ENABLED = "mob_payments_enabled"
private const val MOB_LEDGER = "mob_ledger"
private const val PAYMENTS_CURRENT_CURRENCY = "payments_current_currency"
private const val DEFAULT_CURRENCY_CODE = "GBP"
private const val USER_CONFIRMED_MNEMONIC = "mob_payments_user_confirmed_mnemonic"
private const val SHOW_ABOUT_MOBILE_COIN_INFO_CARD = "mob_payments_show_about_mobile_coin_info_card"
private const val SHOW_ADDING_TO_YOUR_WALLET_INFO_CARD = "mob_payments_show_adding_to_your_wallet_info_card"
private const val SHOW_CASHING_OUT_INFO_CARD = "mob_payments_show_cashing_out_info_card"
private const val SHOW_RECOVERY_PHRASE_INFO_CARD = "mob_payments_show_recovery_phrase_info_card"
private const val SHOW_UPDATE_PIN_INFO_CARD = "mob_payments_show_update_pin_info_card"
private val LARGE_BALANCE_THRESHOLD = Money.mobileCoin(BigDecimal.valueOf(500))
}
private val liveCurrentCurrency: MutableLiveData<Currency> by lazy { MutableLiveData(currentCurrency()) }
private val liveMobileCoinLedger: MutableLiveData<MobileCoinLedgerWrapper> by lazy { MutableLiveData(mobileCoinLatestFullLedger()) }
private val liveMobileCoinBalance: LiveData<Balance> by lazy { Transformations.map(liveMobileCoinLedger) { obj: MobileCoinLedgerWrapper -> obj.balance } }
public override fun onFirstEverAppLaunch() {}
public override fun getKeysToIncludeInBackup(): List<String> {
return listOf(
PAYMENTS_ENTROPY,
MOB_PAYMENTS_ENABLED,
MOB_LEDGER,
PAYMENTS_CURRENT_CURRENCY,
DEFAULT_CURRENCY_CODE,
USER_CONFIRMED_MNEMONIC,
SHOW_ABOUT_MOBILE_COIN_INFO_CARD,
SHOW_ADDING_TO_YOUR_WALLET_INFO_CARD,
SHOW_CASHING_OUT_INFO_CARD,
SHOW_RECOVERY_PHRASE_INFO_CARD,
SHOW_UPDATE_PIN_INFO_CARD
)
}
fun userConfirmedMnemonic(): Boolean {
return store.getBoolean(USER_CONFIRMED_MNEMONIC, false)
}
fun setUserConfirmedMnemonic(userConfirmedMnemonic: Boolean) {
store.beginWrite().putBoolean(USER_CONFIRMED_MNEMONIC, userConfirmedMnemonic).commit()
}
/**
* Consider using [.getPaymentsAvailability] which includes feature flag and region status.
*/
fun mobileCoinPaymentsEnabled(): Boolean {
return getBoolean(MOB_PAYMENTS_ENABLED, false)
}
/**
* Applies feature flags and region restrictions to return an enum which describes the available feature set for the user.
*/
val paymentsAvailability: PaymentsAvailability
get() {
if (!SignalStore.account().isRegistered ||
!GeographicalRestrictions.e164Allowed(Recipient.self().requireE164())
) {
return PaymentsAvailability.NOT_IN_REGION
}
return if (FeatureFlags.payments()) {
if (mobileCoinPaymentsEnabled()) {
PaymentsAvailability.WITHDRAW_AND_SEND
} else {
PaymentsAvailability.REGISTRATION_AVAILABLE
}
} else {
if (mobileCoinPaymentsEnabled()) {
PaymentsAvailability.WITHDRAW_ONLY
} else {
PaymentsAvailability.DISABLED_REMOTELY
}
}
}
@WorkerThread
fun setMobileCoinPaymentsEnabled(isMobileCoinPaymentsEnabled: Boolean) {
if (mobileCoinPaymentsEnabled() == isMobileCoinPaymentsEnabled) {
return
}
if (isMobileCoinPaymentsEnabled) {
var entropy = paymentsEntropy
if (entropy == null) {
entropy = Entropy.generateNew()
Log.i(TAG, "Generated new payments entropy")
}
store.beginWrite()
.putBlob(PAYMENTS_ENTROPY, entropy.bytes)
.putBoolean(MOB_PAYMENTS_ENABLED, true)
.putString(PAYMENTS_CURRENT_CURRENCY, currentCurrency().currencyCode)
.commit()
} else {
store.beginWrite()
.putBoolean(MOB_PAYMENTS_ENABLED, false)
.putBoolean(USER_CONFIRMED_MNEMONIC, false)
.commit()
}
DatabaseFactory.getRecipientDatabase(ApplicationDependencies.getApplication()).markNeedsSync(Recipient.self().id)
StorageSyncHelper.scheduleSyncForDataChange()
}
val paymentsMnemonic: Mnemonic
get() {
val paymentsEntropy = paymentsEntropy ?: throw IllegalStateException("Entropy has not been set")
return paymentsEntropy.asMnemonic()
}
/**
* True if a local entropy is set, regardless of whether payments is currently enabled.
*/
fun hasPaymentsEntropy(): Boolean {
return paymentsEntropy != null
}
/**
* Returns the local payments entropy, regardless of whether payments is currently enabled.
*
*
* And null if has never been set.
*/
val paymentsEntropy: Entropy?
get() = Entropy.fromBytes(store.getBlob(PAYMENTS_ENTROPY, null))
fun mobileCoinLatestBalance(): Balance {
return mobileCoinLatestFullLedger().balance
}
fun liveMobileCoinLedger(): LiveData<MobileCoinLedgerWrapper> {
return liveMobileCoinLedger
}
fun liveMobileCoinBalance(): LiveData<Balance> {
return liveMobileCoinBalance
}
fun setCurrentCurrency(currentCurrency: Currency) {
store.beginWrite()
.putString(PAYMENTS_CURRENT_CURRENCY, currentCurrency.currencyCode)
.commit()
liveCurrentCurrency.postValue(currentCurrency)
}
fun currentCurrency(): Currency {
val currencyCode = store.getString(PAYMENTS_CURRENT_CURRENCY, null)
return if (currencyCode == null) determineCurrency() else Currency.getInstance(currencyCode)
}
fun liveCurrentCurrency(): MutableLiveData<Currency> {
return liveCurrentCurrency
}
fun showAboutMobileCoinInfoCard(): Boolean {
return store.getBoolean(SHOW_ABOUT_MOBILE_COIN_INFO_CARD, true)
}
fun showAddingToYourWalletInfoCard(): Boolean {
return store.getBoolean(SHOW_ADDING_TO_YOUR_WALLET_INFO_CARD, true)
}
fun showCashingOutInfoCard(): Boolean {
return store.getBoolean(SHOW_CASHING_OUT_INFO_CARD, true)
}
fun showRecoveryPhraseInfoCard(): Boolean {
return if (userHasLargeBalance()) {
store.getBoolean(SHOW_CASHING_OUT_INFO_CARD, true)
} else {
false
}
}
fun showUpdatePinInfoCard(): Boolean {
return if (userHasLargeBalance() &&
SignalStore.kbsValues().hasPin() &&
!SignalStore.kbsValues().hasOptedOut() && SignalStore.pinValues().keyboardType == PinKeyboardType.NUMERIC
) {
store.getBoolean(SHOW_CASHING_OUT_INFO_CARD, true)
} else {
false
}
}
fun dismissAboutMobileCoinInfoCard() {
store.beginWrite()
.putBoolean(SHOW_ABOUT_MOBILE_COIN_INFO_CARD, false)
.apply()
}
fun dismissAddingToYourWalletInfoCard() {
store.beginWrite()
.putBoolean(SHOW_ADDING_TO_YOUR_WALLET_INFO_CARD, false)
.apply()
}
fun dismissCashingOutInfoCard() {
store.beginWrite()
.putBoolean(SHOW_CASHING_OUT_INFO_CARD, false)
.apply()
}
fun dismissRecoveryPhraseInfoCard() {
store.beginWrite()
.putBoolean(SHOW_RECOVERY_PHRASE_INFO_CARD, false)
.apply()
}
fun dismissUpdatePinInfoCard() {
store.beginWrite()
.putBoolean(SHOW_UPDATE_PIN_INFO_CARD, false)
.apply()
}
fun setMobileCoinFullLedger(ledger: MobileCoinLedgerWrapper) {
store.beginWrite()
.putBlob(MOB_LEDGER, ledger.serialize())
.commit()
liveMobileCoinLedger.postValue(ledger)
}
fun mobileCoinLatestFullLedger(): MobileCoinLedgerWrapper {
val blob = store.getBlob(MOB_LEDGER, null) ?: return MobileCoinLedgerWrapper(MobileCoinLedger.getDefaultInstance())
return try {
MobileCoinLedgerWrapper(MobileCoinLedger.parseFrom(blob))
} catch (e: InvalidProtocolBufferException) {
Log.w(TAG, "Bad cached ledger, clearing", e)
setMobileCoinFullLedger(MobileCoinLedgerWrapper(MobileCoinLedger.getDefaultInstance()))
throw AssertionError(e)
}
}
private fun determineCurrency(): Currency {
val localE164: String = SignalStore.account().e164 ?: ""
return Util.firstNonNull(
CurrencyUtil.getCurrencyByE164(localE164),
CurrencyUtil.getCurrencyByLocale(Locale.getDefault()),
Currency.getInstance(DEFAULT_CURRENCY_CODE)
)
}
/**
* Does not trigger a storage sync.
*/
fun setEnabledAndEntropy(enabled: Boolean, entropy: Entropy?) {
val writer = store.beginWrite()
if (entropy != null) {
writer.putBlob(PAYMENTS_ENTROPY, entropy.bytes)
}
writer.putBoolean(MOB_PAYMENTS_ENABLED, enabled).commit()
}
@WorkerThread
fun restoreWallet(mnemonic: String): WalletRestoreResult {
val entropyFromMnemonic: ByteArray = try {
Mnemonics.bip39EntropyFromMnemonic(mnemonic)
} catch (e: BadMnemonicException) {
return WalletRestoreResult.MNEMONIC_ERROR
}
val paymentsEntropy = paymentsEntropy
if (paymentsEntropy != null) {
val existingEntropy = paymentsEntropy.bytes
if (Arrays.equals(existingEntropy, entropyFromMnemonic)) {
setMobileCoinPaymentsEnabled(true)
setUserConfirmedMnemonic(true)
return WalletRestoreResult.ENTROPY_UNCHANGED
}
}
store.beginWrite()
.putBlob(PAYMENTS_ENTROPY, entropyFromMnemonic)
.putBoolean(MOB_PAYMENTS_ENABLED, true)
.remove(MOB_LEDGER)
.putBoolean(USER_CONFIRMED_MNEMONIC, true)
.commit()
liveMobileCoinLedger.postValue(MobileCoinLedgerWrapper(MobileCoinLedger.getDefaultInstance()))
StorageSyncHelper.scheduleSyncForDataChange()
return WalletRestoreResult.ENTROPY_CHANGED
}
enum class WalletRestoreResult {
ENTROPY_CHANGED, ENTROPY_UNCHANGED, MNEMONIC_ERROR
}
private fun userHasLargeBalance(): Boolean {
return mobileCoinLatestBalance().fullAmount.requireMobileCoin().greaterThan(LARGE_BALANCE_THRESHOLD)
}
}

View File

@@ -4,6 +4,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.preference.PreferenceDataStore;
import org.thoughtcrime.securesms.database.KeyValueDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.util.SignalUncaughtExceptionHandler;
@@ -15,9 +16,9 @@ import java.util.List;
*/
public final class SignalStore {
private static final SignalStore INSTANCE = new SignalStore();
private KeyValueStore store;
private final KeyValueStore store;
private final AccountValues accountValues;
private final KbsValues kbsValues;
private final RegistrationValues registrationValues;
private final PinValues pinValues;
@@ -40,8 +41,23 @@ public final class SignalStore {
private final ChatColorsValues chatColorsValues;
private final ImageEditorValues imageEditorValues;
private SignalStore() {
this.store = new KeyValueStore(ApplicationDependencies.getApplication());
private static volatile SignalStore instance;
private static @NonNull SignalStore getInstance() {
if (instance == null) {
synchronized (SignalStore.class) {
if (instance == null) {
instance = new SignalStore(new KeyValueStore(KeyValueDatabase.getInstance(ApplicationDependencies.getApplication())));
}
}
}
return instance;
}
private SignalStore(@NonNull KeyValueStore store) {
this.store = store;
this.accountValues = new AccountValues(store);
this.kbsValues = new KbsValues(store);
this.registrationValues = new RegistrationValues(store);
this.pinValues = new PinValues(store);
@@ -66,6 +82,7 @@ public final class SignalStore {
}
public static void onFirstEverAppLaunch() {
account().onFirstEverAppLaunch();
kbsValues().onFirstEverAppLaunch();
registrationValues().onFirstEverAppLaunch();
pinValues().onFirstEverAppLaunch();
@@ -91,6 +108,7 @@ public final class SignalStore {
public static List<String> getKeysToIncludeInBackup() {
List<String> keys = new ArrayList<>();
keys.addAll(account().getKeysToIncludeInBackup());
keys.addAll(kbsValues().getKeysToIncludeInBackup());
keys.addAll(registrationValues().getKeysToIncludeInBackup());
keys.addAll(pinValues().getKeysToIncludeInBackup());
@@ -121,91 +139,95 @@ public final class SignalStore {
*/
@VisibleForTesting
public static void resetCache() {
INSTANCE.store.resetCache();
getInstance().store.resetCache();
}
public static @NonNull AccountValues account() {
return getInstance().accountValues;
}
public static @NonNull KbsValues kbsValues() {
return INSTANCE.kbsValues;
return getInstance().kbsValues;
}
public static @NonNull RegistrationValues registrationValues() {
return INSTANCE.registrationValues;
return getInstance().registrationValues;
}
public static @NonNull PinValues pinValues() {
return INSTANCE.pinValues;
return getInstance().pinValues;
}
public static @NonNull RemoteConfigValues remoteConfigValues() {
return INSTANCE.remoteConfigValues;
return getInstance().remoteConfigValues;
}
public static @NonNull StorageServiceValues storageService() {
return INSTANCE.storageServiceValues;
return getInstance().storageServiceValues;
}
public static @NonNull UiHints uiHints() {
return INSTANCE.uiHints;
return getInstance().uiHints;
}
public static @NonNull TooltipValues tooltips() {
return INSTANCE.tooltipValues;
return getInstance().tooltipValues;
}
public static @NonNull MiscellaneousValues misc() {
return INSTANCE.misc;
return getInstance().misc;
}
public static @NonNull InternalValues internalValues() {
return INSTANCE.internalValues;
return getInstance().internalValues;
}
public static @NonNull EmojiValues emojiValues() {
return INSTANCE.emojiValues;
return getInstance().emojiValues;
}
public static @NonNull SettingsValues settings() {
return INSTANCE.settingsValues;
return getInstance().settingsValues;
}
public static @NonNull CertificateValues certificateValues() {
return INSTANCE.certificateValues;
return getInstance().certificateValues;
}
public static @NonNull PhoneNumberPrivacyValues phoneNumberPrivacy() {
return INSTANCE.phoneNumberPrivacyValues;
return getInstance().phoneNumberPrivacyValues;
}
public static @NonNull OnboardingValues onboarding() {
return INSTANCE.onboardingValues;
return getInstance().onboardingValues;
}
public static @NonNull WallpaperValues wallpaper() {
return INSTANCE.wallpaperValues;
return getInstance().wallpaperValues;
}
public static @NonNull PaymentsValues paymentsValues() {
return INSTANCE.paymentsValues;
return getInstance().paymentsValues;
}
public static @NonNull DonationsValues donationsValues() {
return INSTANCE.donationsValues;
return getInstance().donationsValues;
}
public static @NonNull ProxyValues proxy() {
return INSTANCE.proxyValues;
return getInstance().proxyValues;
}
public static @NonNull RateLimitValues rateLimit() {
return INSTANCE.rateLimitValues;
return getInstance().rateLimitValues;
}
public static @NonNull ChatColorsValues chatColorsValues() {
return INSTANCE.chatColorsValues;
return getInstance().chatColorsValues;
}
public static @NonNull ImageEditorValues imageEditorValues() {
return INSTANCE.imageEditorValues;
return getInstance().imageEditorValues;
}
public static @NonNull GroupsV2AuthorizationSignalStoreCache groupsV2AuthorizationCache() {
@@ -225,6 +247,14 @@ public final class SignalStore {
}
private static @NonNull KeyValueStore getStore() {
return INSTANCE.store;
return getInstance().store;
}
/**
* Allows you to set a custom KeyValueStore to read from. Only for testing!
*/
@VisibleForTesting
public static void inject(@NonNull KeyValueStore store) {
instance = new SignalStore(store);
}
}