mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-20 17:57:29 +00:00
Refactor RefreshOwnProfileJob to Kotlin.
This commit is contained in:
@@ -1,507 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.core.util.Base64;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.usernames.BaseUsernameException;
|
||||
import org.signal.libsignal.usernames.Username;
|
||||
import org.signal.libsignal.zkgroup.VerificationFailedException;
|
||||
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
import org.thoughtcrime.securesms.badges.BadgeRepository;
|
||||
import org.thoughtcrime.securesms.badges.Badges;
|
||||
import org.thoughtcrime.securesms.badges.models.Badge;
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.RecipientTable;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.net.SignalNetwork;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.ProfileUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.signal.core.util.Util;
|
||||
import org.whispersystems.signalservice.api.NetworkResultUtil;
|
||||
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
|
||||
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
|
||||
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||
import org.whispersystems.signalservice.api.push.UsernameLinkComponents;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription;
|
||||
import org.whispersystems.signalservice.api.util.ExpiringProfileCredentialUtil;
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||
import org.whispersystems.signalservice.internal.push.WhoAmIResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
/**
|
||||
* Refreshes the profile of the local user. Different from {@link RetrieveProfileJob} in that we
|
||||
* have to sometimes look at/set different data stores, and we will *always* do the fetch regardless
|
||||
* of caching.
|
||||
*/
|
||||
public class RefreshOwnProfileJob extends BaseJob {
|
||||
|
||||
public static final String KEY = "RefreshOwnProfileJob";
|
||||
|
||||
private static final String TAG = Log.tag(RefreshOwnProfileJob.class);
|
||||
|
||||
private static final String SUBSCRIPTION_QUEUE = ProfileUploadJob.QUEUE + "_Subscription";
|
||||
private static final String BOOST_QUEUE = ProfileUploadJob.QUEUE + "_Boost";
|
||||
|
||||
public RefreshOwnProfileJob() {
|
||||
this(ProfileUploadJob.QUEUE);
|
||||
}
|
||||
|
||||
private RefreshOwnProfileJob(@NonNull String queue) {
|
||||
this(new Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setQueue(queue)
|
||||
.setMaxInstancesForFactory(1)
|
||||
.setMaxAttempts(10)
|
||||
.build());
|
||||
}
|
||||
|
||||
public static @NonNull RefreshOwnProfileJob forSubscription() {
|
||||
return new RefreshOwnProfileJob(SUBSCRIPTION_QUEUE);
|
||||
}
|
||||
|
||||
public static @NonNull RefreshOwnProfileJob forBoost() {
|
||||
return new RefreshOwnProfileJob(BOOST_QUEUE);
|
||||
}
|
||||
|
||||
|
||||
private RefreshOwnProfileJob(@NonNull Parameters parameters) {
|
||||
super(parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable byte[] serialize() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRun() throws Exception {
|
||||
if (!SignalStore.account().isRegistered() || TextUtils.isEmpty(SignalStore.account().getE164())) {
|
||||
Log.w(TAG, "Not yet registered!");
|
||||
return;
|
||||
}
|
||||
|
||||
if ((SignalStore.svr().hasPin() || SignalStore.account().restoredAccountEntropyPool()) && !SignalStore.svr().hasOptedOut() && SignalStore.storageService().getLastSyncTime() == 0) {
|
||||
Log.i(TAG, "Registered with PIN or AEP but haven't completed storage sync yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!SignalStore.registration().hasUploadedProfile() && SignalStore.account().isPrimaryDevice()) {
|
||||
Log.i(TAG, "Registered but haven't uploaded profile yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
Recipient self = Recipient.self();
|
||||
|
||||
ProfileAndCredential profileAndCredential;
|
||||
try {
|
||||
profileAndCredential = ProfileUtil.retrieveProfileSync(context, self, getRequestType(self), false);
|
||||
} catch (IllegalStateException e) {
|
||||
Log.w(TAG, "Unexpected exception result from profile fetch. Skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
SignalServiceProfile profile = profileAndCredential.getProfile();
|
||||
|
||||
if (Util.isEmpty(profile.getName()) &&
|
||||
Util.isEmpty(profile.getAvatar()) &&
|
||||
Util.isEmpty(profile.getAbout()) &&
|
||||
Util.isEmpty(profile.getAboutEmoji()))
|
||||
{
|
||||
Log.w(TAG, "The profile we retrieved was empty! Ignoring it.");
|
||||
|
||||
if (!self.getProfileName().isEmpty()) {
|
||||
Log.w(TAG, "We have a name locally. Scheduling a profile upload.");
|
||||
AppDependencies.getJobManager().add(new ProfileUploadJob());
|
||||
} else {
|
||||
Log.w(TAG, "We don't have a name locally, either!");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setProfileName(profile.getName());
|
||||
setProfileAbout(profile.getAbout(), profile.getAboutEmoji());
|
||||
setProfileAvatar(profile.getAvatar());
|
||||
setProfileCapabilities(profile.getCapabilities());
|
||||
setProfileBadges(profile.getBadges());
|
||||
ensureUnidentifiedAccessCorrect(profile.getUnidentifiedAccess(), profile.isUnrestrictedUnidentifiedAccess());
|
||||
ensurePhoneNumberSharingIsCorrect(profile.getPhoneNumberSharing());
|
||||
|
||||
profileAndCredential.getExpiringProfileKeyCredential()
|
||||
.ifPresent(expiringProfileKeyCredential -> setExpiringProfileKeyCredential(self, ProfileKeyUtil.getSelfProfileKey(), expiringProfileKeyCredential));
|
||||
|
||||
SignalStore.registration().setHasDownloadedProfile(true);
|
||||
|
||||
StoryOnboardingDownloadJob.Companion.enqueueIfNeeded();
|
||||
|
||||
checkUsernameIsInSync();
|
||||
}
|
||||
|
||||
private void setExpiringProfileKeyCredential(@NonNull Recipient recipient,
|
||||
@NonNull ProfileKey recipientProfileKey,
|
||||
@NonNull ExpiringProfileKeyCredential credential)
|
||||
{
|
||||
RecipientTable recipientTable = SignalDatabase.recipients();
|
||||
recipientTable.setProfileKeyCredential(recipient.getId(), recipientProfileKey, credential);
|
||||
}
|
||||
|
||||
private static SignalServiceProfile.RequestType getRequestType(@NonNull Recipient recipient) {
|
||||
return ExpiringProfileCredentialUtil.isValid(recipient.getExpiringProfileKeyCredential()) ? SignalServiceProfile.RequestType.PROFILE
|
||||
: SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onShouldRetry(@NonNull Exception e) {
|
||||
return e instanceof PushNetworkException;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure() { }
|
||||
|
||||
private void setProfileName(@Nullable String encryptedName) {
|
||||
try {
|
||||
ProfileKey profileKey = ProfileKeyUtil.getSelfProfileKey();
|
||||
String plaintextName = ProfileUtil.decryptString(profileKey, encryptedName);
|
||||
ProfileName profileName = ProfileName.fromSerialized(plaintextName);
|
||||
|
||||
if (!profileName.isEmpty()) {
|
||||
Log.d(TAG, "Saving non-empty name.");
|
||||
SignalDatabase.recipients().setProfileName(Recipient.self().getId(), profileName);
|
||||
} else {
|
||||
Log.w(TAG, "Ignoring empty name.");
|
||||
}
|
||||
|
||||
} catch (InvalidCiphertextException | IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void setProfileAbout(@Nullable String encryptedAbout, @Nullable String encryptedEmoji) {
|
||||
try {
|
||||
ProfileKey profileKey = ProfileKeyUtil.getSelfProfileKey();
|
||||
String plaintextAbout = ProfileUtil.decryptString(profileKey, encryptedAbout);
|
||||
String plaintextEmoji = ProfileUtil.decryptString(profileKey, encryptedEmoji);
|
||||
|
||||
Log.d(TAG, "Saving " + (!Util.isEmpty(plaintextAbout) ? "non-" : "") + "empty about.");
|
||||
Log.d(TAG, "Saving " + (!Util.isEmpty(plaintextEmoji) ? "non-" : "") + "empty emoji.");
|
||||
|
||||
SignalDatabase.recipients().setAbout(Recipient.self().getId(), plaintextAbout, plaintextEmoji);
|
||||
} catch (InvalidCiphertextException | IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void setProfileAvatar(@Nullable String avatar) {
|
||||
Log.d(TAG, "Saving " + (!Util.isEmpty(avatar) ? "non-" : "") + "empty avatar.");
|
||||
AppDependencies.getJobManager().add(new RetrieveProfileAvatarJob(Recipient.self(), avatar));
|
||||
}
|
||||
|
||||
private void setProfileCapabilities(@Nullable SignalServiceProfile.Capabilities capabilities) {
|
||||
if (capabilities == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Recipient selfSnapshot = Recipient.self();
|
||||
|
||||
SignalDatabase.recipients().setCapabilities(Recipient.self().getId(), capabilities);
|
||||
}
|
||||
|
||||
private void ensureUnidentifiedAccessCorrect(@Nullable String unidentifiedAccessVerifier, boolean universalUnidentifiedAccess) {
|
||||
if (unidentifiedAccessVerifier == null) {
|
||||
Log.w(TAG, "No unidentified access is set remotely! Refreshing attributes.");
|
||||
AppDependencies.getJobManager().add(new RefreshAttributesJob());
|
||||
return;
|
||||
}
|
||||
|
||||
if (TextSecurePreferences.isUniversalUnidentifiedAccess(context) != universalUnidentifiedAccess) {
|
||||
Log.w(TAG, "The universal access flag doesn't match our local value (local: " + TextSecurePreferences.isUniversalUnidentifiedAccess(context) + ", remote: " + universalUnidentifiedAccess + ")! Refreshing attributes.");
|
||||
AppDependencies.getJobManager().add(new RefreshAttributesJob());
|
||||
return;
|
||||
}
|
||||
|
||||
ProfileKey profileKey = ProfileKeyUtil.getSelfProfileKey();
|
||||
ProfileCipher cipher = new ProfileCipher(profileKey);
|
||||
|
||||
boolean verified;
|
||||
try {
|
||||
verified = cipher.verifyUnidentifiedAccess(Base64.decode(unidentifiedAccessVerifier));
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to decode unidentified access!", e);
|
||||
verified = false;
|
||||
}
|
||||
|
||||
if (!verified) {
|
||||
Log.w(TAG, "Unidentified access failed to verify! Refreshing attributes.");
|
||||
AppDependencies.getJobManager().add(new RefreshAttributesJob());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to make sure that our phone number sharing setting matches what's on our profile. If there's a mismatch, we first sync with storage service
|
||||
* (to limit race conditions between devices) and then upload our profile.
|
||||
*/
|
||||
private void ensurePhoneNumberSharingIsCorrect(@Nullable String phoneNumberSharingCiphertext) {
|
||||
if (phoneNumberSharingCiphertext == null) {
|
||||
Log.w(TAG, "No phone number sharing is set remotely! Syncing with storage service, then uploading our profile.");
|
||||
syncWithStorageServiceThenUploadProfile();
|
||||
return;
|
||||
}
|
||||
|
||||
ProfileKey profileKey = ProfileKeyUtil.getSelfProfileKey();
|
||||
ProfileCipher cipher = new ProfileCipher(profileKey);
|
||||
|
||||
try {
|
||||
RecipientTable.PhoneNumberSharingState remotePhoneNumberSharing = cipher.decryptBoolean(Base64.decode(phoneNumberSharingCiphertext))
|
||||
.map(value -> value ? RecipientTable.PhoneNumberSharingState.ENABLED : RecipientTable.PhoneNumberSharingState.DISABLED)
|
||||
.orElse(RecipientTable.PhoneNumberSharingState.UNKNOWN);
|
||||
|
||||
if (remotePhoneNumberSharing == RecipientTable.PhoneNumberSharingState.UNKNOWN || remotePhoneNumberSharing.getEnabled() != SignalStore.phoneNumberPrivacy().isPhoneNumberSharingEnabled()) {
|
||||
Log.w(TAG, "Phone number sharing setting did not match! Syncing with storage service, then uploading our profile.");
|
||||
syncWithStorageServiceThenUploadProfile();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to decode phone number sharing! Syncing with storage service, then uploading our profile.", e);
|
||||
syncWithStorageServiceThenUploadProfile();
|
||||
} catch (InvalidCiphertextException e) {
|
||||
Log.w(TAG, "Failed to decrypt phone number sharing! Syncing with storage service, then uploading our profile.", e);
|
||||
syncWithStorageServiceThenUploadProfile();
|
||||
}
|
||||
}
|
||||
|
||||
private void syncWithStorageServiceThenUploadProfile() {
|
||||
AppDependencies.getJobManager()
|
||||
.startChain(StorageSyncJob.forRemoteChange())
|
||||
.then(new ProfileUploadJob())
|
||||
.enqueue();
|
||||
}
|
||||
|
||||
private static void checkUsernameIsInSync() {
|
||||
boolean validated = false;
|
||||
|
||||
try {
|
||||
String localUsername = SignalStore.account().getUsername();
|
||||
|
||||
WhoAmIResponse whoAmIResponse = AppDependencies.getSignalServiceAccountManager().getWhoAmI();
|
||||
String remoteUsernameHash = whoAmIResponse.getUsernameHash();
|
||||
String localUsernameHash = localUsername != null ? Base64.encodeUrlSafeWithoutPadding(new Username(localUsername).getHash()) : null;
|
||||
|
||||
if (TextUtils.isEmpty(localUsernameHash) && TextUtils.isEmpty(remoteUsernameHash)) {
|
||||
Log.d(TAG, "Local and remote username hash are both empty. Considering validated.");
|
||||
UsernameRepository.onUsernameConsistencyValidated();
|
||||
} else if (!Objects.equals(localUsernameHash, remoteUsernameHash)) {
|
||||
Log.w(TAG, "Local username hash does not match server username hash. Local hash: " + (TextUtils.isEmpty(localUsername) ? "empty" : "present") + ", Remote hash: " + (TextUtils.isEmpty(remoteUsernameHash) ? "empty" : "present"));
|
||||
UsernameRepository.onUsernameMismatchDetected();
|
||||
return;
|
||||
} else {
|
||||
Log.d(TAG, "Username validated.");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed perform synchronization check during username phase.", e);
|
||||
} catch (BaseUsernameException e) {
|
||||
Log.w(TAG, "Our local username data is invalid!", e);
|
||||
UsernameRepository.onUsernameMismatchDetected();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
UsernameLinkComponents localUsernameLink = SignalStore.account().getUsernameLink();
|
||||
|
||||
if (localUsernameLink != null) {
|
||||
byte[] remoteEncryptedUsername = NetworkResultUtil.toBasicLegacy(SignalNetwork.username().getEncryptedUsernameFromLinkServerId(localUsernameLink.getServerId()));
|
||||
Username.UsernameLink combinedLink = new Username.UsernameLink(localUsernameLink.getEntropy(), remoteEncryptedUsername);
|
||||
Username remoteUsername = Username.fromLink(combinedLink);
|
||||
|
||||
if (!remoteUsername.getUsername().equals(SignalStore.account().getUsername())) {
|
||||
Log.w(TAG, "The remote username decrypted ok, but the decrypted username did not match our local username!");
|
||||
UsernameRepository.onUsernameLinkMismatchDetected();
|
||||
} else {
|
||||
Log.d(TAG, "Username link validated.");
|
||||
}
|
||||
|
||||
validated = true;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed perform synchronization check during the username link phase.", e);
|
||||
} catch (BaseUsernameException e) {
|
||||
Log.w(TAG, "Failed to decrypt username link using the remote encrypted username and our local entropy!", e);
|
||||
UsernameRepository.onUsernameLinkMismatchDetected();
|
||||
}
|
||||
|
||||
if (validated) {
|
||||
UsernameRepository.onUsernameConsistencyValidated();
|
||||
}
|
||||
}
|
||||
|
||||
private void setProfileBadges(@Nullable List<SignalServiceProfile.Badge> badges) throws IOException {
|
||||
if (badges == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Set<String> localDonorBadgeIds = Recipient.self()
|
||||
.getBadges()
|
||||
.stream()
|
||||
.filter(badge -> badge.getCategory() == Badge.Category.Donor)
|
||||
.map(Badge::getId)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
Set<String> remoteDonorBadgeIds = badges.stream()
|
||||
.filter(badge -> Objects.equals(badge.getCategory(), Badge.Category.Donor.getCode()))
|
||||
.map(SignalServiceProfile.Badge::getId)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
boolean remoteHasSubscriptionBadges = remoteDonorBadgeIds.stream().anyMatch(RefreshOwnProfileJob::isSubscription);
|
||||
boolean localHasSubscriptionBadges = localDonorBadgeIds.stream().anyMatch(RefreshOwnProfileJob::isSubscription);
|
||||
boolean remoteHasBoostBadges = remoteDonorBadgeIds.stream().anyMatch(RefreshOwnProfileJob::isBoost);
|
||||
boolean localHasBoostBadges = localDonorBadgeIds.stream().anyMatch(RefreshOwnProfileJob::isBoost);
|
||||
boolean remoteHasGiftBadges = remoteDonorBadgeIds.stream().anyMatch(RefreshOwnProfileJob::isGift);
|
||||
boolean localHasGiftBadges = localDonorBadgeIds.stream().anyMatch(RefreshOwnProfileJob::isGift);
|
||||
|
||||
if (!remoteHasSubscriptionBadges && localHasSubscriptionBadges) {
|
||||
Badge mostRecentExpiration = Recipient.self()
|
||||
.getBadges()
|
||||
.stream()
|
||||
.filter(badge -> badge.getCategory() == Badge.Category.Donor)
|
||||
.filter(badge -> isSubscription(badge.getId()))
|
||||
.max(Comparator.comparingLong(Badge::getExpirationTimestamp))
|
||||
.get();
|
||||
|
||||
Log.d(TAG, "Marking subscription badge as expired, should notify next time the conversation list is open.", true);
|
||||
SignalStore.inAppPayments().setExpiredBadge(mostRecentExpiration);
|
||||
|
||||
if (!InAppPaymentsRepository.isUserManuallyCancelled(InAppPaymentSubscriberRecord.Type.DONATION)) {
|
||||
Log.d(TAG, "Detected an unexpected subscription expiry.", true);
|
||||
InAppPaymentSubscriberRecord subscriber = InAppPaymentsRepository.getSubscriber(InAppPaymentSubscriberRecord.Type.DONATION);
|
||||
|
||||
boolean isDueToPaymentFailure = false;
|
||||
if (subscriber != null) {
|
||||
ServiceResponse<ActiveSubscription> response = AppDependencies.getDonationsService()
|
||||
.getSubscription(subscriber.getSubscriberId());
|
||||
|
||||
if (response.getResult().isPresent()) {
|
||||
ActiveSubscription activeSubscription = response.getResult().get();
|
||||
if (activeSubscription.isFailedPayment()) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
InAppPaymentsRepository.setShouldCancelSubscriptionBeforeNextSubscribeAttempt(subscriber, true);
|
||||
}
|
||||
|
||||
if (!isDueToPaymentFailure) {
|
||||
Log.d(TAG, "Unexpected expiry due to inactivity.", true);
|
||||
}
|
||||
|
||||
MultiDeviceSubscriptionSyncRequestJob.enqueue();
|
||||
}
|
||||
} else if (!remoteHasBoostBadges && localHasBoostBadges) {
|
||||
Badge mostRecentExpiration = Recipient.self()
|
||||
.getBadges()
|
||||
.stream()
|
||||
.filter(badge -> badge.getCategory() == Badge.Category.Donor)
|
||||
.filter(badge -> isBoost(badge.getId()))
|
||||
.max(Comparator.comparingLong(Badge::getExpirationTimestamp))
|
||||
.get();
|
||||
|
||||
Log.d(TAG, "Marking boost badge as expired, should notify next time the conversation list is open.", true);
|
||||
SignalStore.inAppPayments().setExpiredBadge(mostRecentExpiration);
|
||||
} else {
|
||||
Badge badge = SignalStore.inAppPayments().getExpiredBadge();
|
||||
|
||||
if (badge != null && badge.isSubscription() && remoteHasSubscriptionBadges) {
|
||||
Log.d(TAG, "Remote has subscription badges. Clearing local expired subscription badge.", true);
|
||||
SignalStore.inAppPayments().setExpiredBadge(null);
|
||||
} else if (badge != null && badge.isBoost() && remoteHasBoostBadges) {
|
||||
Log.d(TAG, "Remote has boost badges. Clearing local expired boost badge.", true);
|
||||
SignalStore.inAppPayments().setExpiredBadge(null);
|
||||
}
|
||||
}
|
||||
|
||||
if (!remoteHasGiftBadges && localHasGiftBadges) {
|
||||
Badge mostRecentExpiration = Recipient.self()
|
||||
.getBadges()
|
||||
.stream()
|
||||
.filter(badge -> badge.getCategory() == Badge.Category.Donor)
|
||||
.filter(badge -> isGift(badge.getId()))
|
||||
.max(Comparator.comparingLong(Badge::getExpirationTimestamp))
|
||||
.get();
|
||||
|
||||
Log.d(TAG, "Marking gift badge as expired, should notify next time the manage donations screen is open.", true);
|
||||
SignalStore.inAppPayments().setExpiredGiftBadge(mostRecentExpiration);
|
||||
} else if (remoteHasGiftBadges) {
|
||||
Log.d(TAG, "We have remote gift badges. Clearing local expired gift badge.", true);
|
||||
SignalStore.inAppPayments().setExpiredGiftBadge(null);
|
||||
}
|
||||
|
||||
boolean userHasVisibleBadges = badges.stream().anyMatch(SignalServiceProfile.Badge::isVisible);
|
||||
boolean userHasInvisibleBadges = badges.stream().anyMatch(b -> !b.isVisible());
|
||||
|
||||
List<Badge> appBadges = badges.stream().map(Badges::fromServiceBadge).collect(Collectors.toList());
|
||||
|
||||
if (userHasVisibleBadges && userHasInvisibleBadges) {
|
||||
boolean displayBadgesOnProfile = SignalStore.inAppPayments().getDisplayBadgesOnProfile();
|
||||
Log.d(TAG, "Detected mixed visibility of badges. Telling the server to mark them all " +
|
||||
(displayBadgesOnProfile ? "" : "not") +
|
||||
" visible.", true);
|
||||
|
||||
BadgeRepository badgeRepository = new BadgeRepository(context);
|
||||
List<Badge> updatedBadges = badgeRepository.setVisibilityForAllBadgesSync(displayBadgesOnProfile, appBadges);
|
||||
SignalDatabase.recipients().setBadges(Recipient.self().getId(), updatedBadges);
|
||||
} else {
|
||||
SignalDatabase.recipients().setBadges(Recipient.self().getId(), appBadges);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isSubscription(String badgeId) {
|
||||
return !isBoost(badgeId) && !isGift(badgeId);
|
||||
}
|
||||
|
||||
private static boolean isBoost(String badgeId) {
|
||||
return Objects.equals(badgeId, Badge.BOOST_BADGE_ID);
|
||||
}
|
||||
|
||||
private static boolean isGift(String badgeId) {
|
||||
return Objects.equals(badgeId, Badge.GIFT_BADGE_ID);
|
||||
}
|
||||
|
||||
public static final class Factory implements Job.Factory<RefreshOwnProfileJob> {
|
||||
|
||||
@Override
|
||||
public @NonNull RefreshOwnProfileJob create(@NonNull Parameters parameters, @Nullable byte[] serializedData) {
|
||||
return new RefreshOwnProfileJob(parameters);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import android.text.TextUtils
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.Util
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.usernames.BaseUsernameException
|
||||
import org.signal.libsignal.usernames.Username
|
||||
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.badges.BadgeRepository
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
||||
import org.thoughtcrime.securesms.database.RecipientTable.PhoneNumberSharingState
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.net.SignalNetwork
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.ProfileUtil
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.api.NetworkResultUtil
|
||||
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException
|
||||
import org.whispersystems.signalservice.api.crypto.ProfileCipher
|
||||
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException
|
||||
import org.whispersystems.signalservice.api.util.ExpiringProfileCredentialUtil
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* Refreshes the profile of the local user. Different from [RetrieveProfileJob] in that we
|
||||
* have to sometimes look at/set different data stores, and we will *always* do the fetch regardless
|
||||
* of caching.
|
||||
*/
|
||||
class RefreshOwnProfileJob private constructor(parameters: Parameters) : BaseJob(parameters) {
|
||||
|
||||
companion object {
|
||||
const val KEY: String = "RefreshOwnProfileJob"
|
||||
|
||||
private val TAG = Log.tag(RefreshOwnProfileJob::class.java)
|
||||
|
||||
private val SUBSCRIPTION_QUEUE = ProfileUploadJob.QUEUE + "_Subscription"
|
||||
private val BOOST_QUEUE = ProfileUploadJob.QUEUE + "_Boost"
|
||||
|
||||
fun forSubscription(): RefreshOwnProfileJob {
|
||||
return RefreshOwnProfileJob(SUBSCRIPTION_QUEUE)
|
||||
}
|
||||
|
||||
fun forBoost(): RefreshOwnProfileJob {
|
||||
return RefreshOwnProfileJob(BOOST_QUEUE)
|
||||
}
|
||||
}
|
||||
|
||||
constructor() : this(ProfileUploadJob.QUEUE)
|
||||
|
||||
private constructor(queue: String) : this(
|
||||
Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setQueue(queue)
|
||||
.setMaxInstancesForFactory(1)
|
||||
.setMaxAttempts(10)
|
||||
.build()
|
||||
)
|
||||
|
||||
override fun serialize(): ByteArray? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getFactoryKey(): String {
|
||||
return KEY
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun onRun() {
|
||||
if (!SignalStore.account.isRegistered || SignalStore.account.e164.isNullOrEmpty()) {
|
||||
Log.w(TAG, "Not yet registered!")
|
||||
return
|
||||
}
|
||||
|
||||
if ((SignalStore.svr.hasPin() || SignalStore.account.restoredAccountEntropyPool) && !SignalStore.svr.hasOptedOut() && SignalStore.storageService.lastSyncTime == 0L) {
|
||||
Log.i(TAG, "Registered with PIN or AEP but haven't completed storage sync yet.")
|
||||
return
|
||||
}
|
||||
|
||||
if (!SignalStore.registration.hasUploadedProfile && SignalStore.account.isPrimaryDevice) {
|
||||
Log.i(TAG, "Registered but haven't uploaded profile yet.")
|
||||
return
|
||||
}
|
||||
|
||||
val self = Recipient.self()
|
||||
|
||||
val profileAndCredential: ProfileAndCredential
|
||||
try {
|
||||
profileAndCredential = ProfileUtil.retrieveProfileSync(context, self, getRequestType(self), false)
|
||||
} catch (e: IllegalStateException) {
|
||||
Log.w(TAG, "Unexpected exception result from profile fetch. Skipping.")
|
||||
return
|
||||
}
|
||||
|
||||
val profile = profileAndCredential.getProfile()
|
||||
|
||||
if (Util.isEmpty(profile.getName()) &&
|
||||
Util.isEmpty(profile.getAvatar()) &&
|
||||
Util.isEmpty(profile.getAbout()) &&
|
||||
Util.isEmpty(profile.getAboutEmoji())
|
||||
) {
|
||||
Log.w(TAG, "The profile we retrieved was empty! Ignoring it.")
|
||||
|
||||
if (!self.profileName.isEmpty()) {
|
||||
Log.w(TAG, "We have a name locally. Scheduling a profile upload.")
|
||||
AppDependencies.jobManager.add(ProfileUploadJob())
|
||||
} else {
|
||||
Log.w(TAG, "We don't have a name locally, either!")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
setProfileName(profile.getName())
|
||||
setProfileAbout(profile.getAbout(), profile.getAboutEmoji())
|
||||
setProfileAvatar(profile.getAvatar())
|
||||
setProfileCapabilities(profile.getCapabilities())
|
||||
setProfileBadges(profile.getBadges())
|
||||
ensureUnidentifiedAccessCorrect(profile.getUnidentifiedAccess(), profile.isUnrestrictedUnidentifiedAccess())
|
||||
ensurePhoneNumberSharingIsCorrect(profile.getPhoneNumberSharing())
|
||||
|
||||
profileAndCredential.getExpiringProfileKeyCredential()
|
||||
.ifPresent { setExpiringProfileKeyCredential(self, ProfileKeyUtil.getSelfProfileKey(), it) }
|
||||
|
||||
SignalStore.registration.hasDownloadedProfile = true
|
||||
|
||||
StoryOnboardingDownloadJob.enqueueIfNeeded()
|
||||
|
||||
checkUsernameIsInSync()
|
||||
}
|
||||
|
||||
override fun onShouldRetry(e: Exception): Boolean {
|
||||
return e is PushNetworkException
|
||||
}
|
||||
|
||||
override fun onFailure() = Unit
|
||||
|
||||
private fun setExpiringProfileKeyCredential(
|
||||
recipient: Recipient,
|
||||
recipientProfileKey: ProfileKey,
|
||||
credential: ExpiringProfileKeyCredential
|
||||
) {
|
||||
SignalDatabase.recipients.setProfileKeyCredential(recipient.id, recipientProfileKey, credential)
|
||||
}
|
||||
|
||||
private fun setProfileName(encryptedName: String?) {
|
||||
try {
|
||||
val profileKey = ProfileKeyUtil.getSelfProfileKey()
|
||||
val plaintextName = ProfileUtil.decryptString(profileKey, encryptedName)
|
||||
val profileName = ProfileName.fromSerialized(plaintextName)
|
||||
|
||||
if (!profileName.isEmpty()) {
|
||||
Log.d(TAG, "Saving non-empty name.")
|
||||
SignalDatabase.recipients.setProfileName(Recipient.self().id, profileName)
|
||||
} else {
|
||||
Log.w(TAG, "Ignoring empty name.")
|
||||
}
|
||||
} catch (e: InvalidCiphertextException) {
|
||||
Log.w(TAG, e)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setProfileAbout(encryptedAbout: String?, encryptedEmoji: String?) {
|
||||
try {
|
||||
val profileKey = ProfileKeyUtil.getSelfProfileKey()
|
||||
val plaintextAbout = ProfileUtil.decryptString(profileKey, encryptedAbout)
|
||||
val plaintextEmoji = ProfileUtil.decryptString(profileKey, encryptedEmoji)
|
||||
|
||||
Log.d(TAG, "Saving " + (if (!Util.isEmpty(plaintextAbout)) "non-" else "") + "empty about.")
|
||||
Log.d(TAG, "Saving " + (if (!Util.isEmpty(plaintextEmoji)) "non-" else "") + "empty emoji.")
|
||||
|
||||
SignalDatabase.recipients.setAbout(Recipient.self().id, plaintextAbout, plaintextEmoji)
|
||||
} catch (e: InvalidCiphertextException) {
|
||||
Log.w(TAG, e)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setProfileAvatar(avatar: String?) {
|
||||
Log.d(TAG, "Saving " + (if (!Util.isEmpty(avatar)) "non-" else "") + "empty avatar.")
|
||||
AppDependencies.jobManager.add(RetrieveProfileAvatarJob(Recipient.self(), avatar))
|
||||
}
|
||||
|
||||
private fun setProfileCapabilities(capabilities: SignalServiceProfile.Capabilities?) {
|
||||
if (capabilities == null) {
|
||||
return
|
||||
}
|
||||
|
||||
SignalDatabase.recipients.setCapabilities(Recipient.self().id, capabilities)
|
||||
}
|
||||
|
||||
private fun ensureUnidentifiedAccessCorrect(unidentifiedAccessVerifier: String?, universalUnidentifiedAccess: Boolean) {
|
||||
if (unidentifiedAccessVerifier == null) {
|
||||
Log.w(TAG, "No unidentified access is set remotely! Refreshing attributes.")
|
||||
AppDependencies.jobManager.add(RefreshAttributesJob())
|
||||
return
|
||||
}
|
||||
|
||||
if (TextSecurePreferences.isUniversalUnidentifiedAccess(context) != universalUnidentifiedAccess) {
|
||||
Log.w(TAG, "The universal access flag doesn't match our local value (local: " + TextSecurePreferences.isUniversalUnidentifiedAccess(context) + ", remote: " + universalUnidentifiedAccess + ")! Refreshing attributes.")
|
||||
AppDependencies.jobManager.add(RefreshAttributesJob())
|
||||
return
|
||||
}
|
||||
|
||||
val profileKey = ProfileKeyUtil.getSelfProfileKey()
|
||||
val cipher = ProfileCipher(profileKey)
|
||||
|
||||
val verified = try {
|
||||
cipher.verifyUnidentifiedAccess(Base64.decode(unidentifiedAccessVerifier))
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Failed to decode unidentified access!", e)
|
||||
false
|
||||
}
|
||||
|
||||
if (!verified) {
|
||||
Log.w(TAG, "Unidentified access failed to verify! Refreshing attributes.")
|
||||
AppDependencies.jobManager.add(RefreshAttributesJob())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to make sure that our phone number sharing setting matches what's on our profile. If there's a mismatch, we first sync with storage service
|
||||
* (to limit race conditions between devices) and then upload our profile.
|
||||
*/
|
||||
private fun ensurePhoneNumberSharingIsCorrect(phoneNumberSharingCiphertext: String?) {
|
||||
if (phoneNumberSharingCiphertext == null) {
|
||||
Log.w(TAG, "No phone number sharing is set remotely! Syncing with storage service, then uploading our profile.")
|
||||
syncWithStorageServiceThenUploadProfile()
|
||||
return
|
||||
}
|
||||
|
||||
val profileKey = ProfileKeyUtil.getSelfProfileKey()
|
||||
val cipher = ProfileCipher(profileKey)
|
||||
|
||||
try {
|
||||
val remotePhoneNumberSharing = cipher.decryptBoolean(Base64.decode(phoneNumberSharingCiphertext))
|
||||
.map { value -> if (value) PhoneNumberSharingState.ENABLED else PhoneNumberSharingState.DISABLED }
|
||||
.orElse(PhoneNumberSharingState.UNKNOWN)
|
||||
|
||||
if (remotePhoneNumberSharing == PhoneNumberSharingState.UNKNOWN || remotePhoneNumberSharing.enabled != SignalStore.phoneNumberPrivacy.isPhoneNumberSharingEnabled()) {
|
||||
Log.w(TAG, "Phone number sharing setting did not match! Syncing with storage service, then uploading our profile.")
|
||||
syncWithStorageServiceThenUploadProfile()
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Failed to decode phone number sharing! Syncing with storage service, then uploading our profile.", e)
|
||||
syncWithStorageServiceThenUploadProfile()
|
||||
} catch (e: InvalidCiphertextException) {
|
||||
Log.w(TAG, "Failed to decrypt phone number sharing! Syncing with storage service, then uploading our profile.", e)
|
||||
syncWithStorageServiceThenUploadProfile()
|
||||
}
|
||||
}
|
||||
|
||||
private fun syncWithStorageServiceThenUploadProfile() {
|
||||
AppDependencies.jobManager
|
||||
.startChain(StorageSyncJob.forRemoteChange())
|
||||
.then(ProfileUploadJob())
|
||||
.enqueue()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun setProfileBadges(badges: List<SignalServiceProfile.Badge>?) {
|
||||
if (badges == null) {
|
||||
return
|
||||
}
|
||||
|
||||
val localDonorBadgeIds = Recipient.self()
|
||||
.badges
|
||||
.filter { it.category == Badge.Category.Donor }
|
||||
.map { it.id }
|
||||
.toSet()
|
||||
|
||||
val remoteDonorBadgeIds = badges
|
||||
.filter { it.getCategory() == Badge.Category.Donor.code }
|
||||
.map { it.getId() }
|
||||
.toSet()
|
||||
|
||||
val remoteHasSubscriptionBadges = remoteDonorBadgeIds.any { isSubscription(it) }
|
||||
val localHasSubscriptionBadges = localDonorBadgeIds.any { isSubscription(it) }
|
||||
val remoteHasBoostBadges = remoteDonorBadgeIds.any { isBoost(it) }
|
||||
val localHasBoostBadges = localDonorBadgeIds.any { isBoost(it) }
|
||||
val remoteHasGiftBadges = remoteDonorBadgeIds.any { isGift(it) }
|
||||
val localHasGiftBadges = localDonorBadgeIds.any { isGift(it) }
|
||||
|
||||
if (!remoteHasSubscriptionBadges && localHasSubscriptionBadges) {
|
||||
val mostRecentExpiration = Recipient.self()
|
||||
.badges
|
||||
.filter { it.category == Badge.Category.Donor }
|
||||
.filter { isSubscription(it.id) }
|
||||
.maxByOrNull { it.expirationTimestamp }
|
||||
?: throw NoSuchElementException("No value present")
|
||||
|
||||
Log.d(TAG, "Marking subscription badge as expired, should notify next time the conversation list is open.", true)
|
||||
SignalStore.inAppPayments.setExpiredBadge(mostRecentExpiration)
|
||||
|
||||
if (!InAppPaymentsRepository.isUserManuallyCancelled(InAppPaymentSubscriberRecord.Type.DONATION)) {
|
||||
Log.d(TAG, "Detected an unexpected subscription expiry.", true)
|
||||
val subscriber = InAppPaymentsRepository.getSubscriber(InAppPaymentSubscriberRecord.Type.DONATION)
|
||||
|
||||
var isDueToPaymentFailure = false
|
||||
if (subscriber != null) {
|
||||
val response = AppDependencies.donationsService
|
||||
.getSubscription(subscriber.subscriberId)
|
||||
|
||||
if (response.getResult().isPresent()) {
|
||||
val activeSubscription = response.getResult().get()
|
||||
if (activeSubscription.isFailedPayment()) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
InAppPaymentsRepository.setShouldCancelSubscriptionBeforeNextSubscribeAttempt(subscriber, true)
|
||||
}
|
||||
|
||||
if (!isDueToPaymentFailure) {
|
||||
Log.d(TAG, "Unexpected expiry due to inactivity.", true)
|
||||
}
|
||||
|
||||
MultiDeviceSubscriptionSyncRequestJob.enqueue()
|
||||
}
|
||||
} else if (!remoteHasBoostBadges && localHasBoostBadges) {
|
||||
val mostRecentExpiration = Recipient.self()
|
||||
.badges
|
||||
.filter { it.category == Badge.Category.Donor }
|
||||
.filter { isBoost(it.id) }
|
||||
.maxByOrNull { it.expirationTimestamp }
|
||||
?: throw NoSuchElementException("No value present")
|
||||
|
||||
Log.d(TAG, "Marking boost badge as expired, should notify next time the conversation list is open.", true)
|
||||
SignalStore.inAppPayments.setExpiredBadge(mostRecentExpiration)
|
||||
} else {
|
||||
val badge = SignalStore.inAppPayments.getExpiredBadge()
|
||||
|
||||
if (badge != null && badge.isSubscription() && remoteHasSubscriptionBadges) {
|
||||
Log.d(TAG, "Remote has subscription badges. Clearing local expired subscription badge.", true)
|
||||
SignalStore.inAppPayments.setExpiredBadge(null)
|
||||
} else if (badge != null && badge.isBoost() && remoteHasBoostBadges) {
|
||||
Log.d(TAG, "Remote has boost badges. Clearing local expired boost badge.", true)
|
||||
SignalStore.inAppPayments.setExpiredBadge(null)
|
||||
}
|
||||
}
|
||||
|
||||
if (!remoteHasGiftBadges && localHasGiftBadges) {
|
||||
val mostRecentExpiration = Recipient.self()
|
||||
.badges
|
||||
.filter { it.category == Badge.Category.Donor }
|
||||
.filter { isGift(it.id) }
|
||||
.maxByOrNull { it.expirationTimestamp }
|
||||
?: throw NoSuchElementException("No value present")
|
||||
|
||||
Log.d(TAG, "Marking gift badge as expired, should notify next time the manage donations screen is open.", true)
|
||||
SignalStore.inAppPayments.setExpiredGiftBadge(mostRecentExpiration)
|
||||
} else if (remoteHasGiftBadges) {
|
||||
Log.d(TAG, "We have remote gift badges. Clearing local expired gift badge.", true)
|
||||
SignalStore.inAppPayments.setExpiredGiftBadge(null)
|
||||
}
|
||||
|
||||
val userHasVisibleBadges = badges.any { it.isVisible() }
|
||||
val userHasInvisibleBadges = badges.any { !it.isVisible() }
|
||||
|
||||
val appBadges = badges.map { Badges.fromServiceBadge(it) }
|
||||
|
||||
if (userHasVisibleBadges && userHasInvisibleBadges) {
|
||||
val displayBadgesOnProfile = SignalStore.inAppPayments.getDisplayBadgesOnProfile()
|
||||
Log.d(
|
||||
TAG, "Detected mixed visibility of badges. Telling the server to mark them all " +
|
||||
(if (displayBadgesOnProfile) "" else "not") +
|
||||
" visible.", true
|
||||
)
|
||||
|
||||
val badgeRepository = BadgeRepository(context)
|
||||
val updatedBadges = badgeRepository.setVisibilityForAllBadgesSync(displayBadgesOnProfile, appBadges)
|
||||
SignalDatabase.recipients.setBadges(Recipient.self().id, updatedBadges)
|
||||
} else {
|
||||
SignalDatabase.recipients.setBadges(Recipient.self().id, appBadges)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkUsernameIsInSync() {
|
||||
var validated = false
|
||||
|
||||
try {
|
||||
val localUsername = SignalStore.account.username
|
||||
|
||||
val whoAmIResponse = AppDependencies.signalServiceAccountManager.getWhoAmI()
|
||||
val remoteUsernameHash = whoAmIResponse.usernameHash
|
||||
val localUsernameHash = if (localUsername != null) Base64.encodeUrlSafeWithoutPadding(Username(localUsername).getHash()) else null
|
||||
|
||||
if (TextUtils.isEmpty(localUsernameHash) && TextUtils.isEmpty(remoteUsernameHash)) {
|
||||
Log.d(TAG, "Local and remote username hash are both empty. Considering validated.")
|
||||
UsernameRepository.onUsernameConsistencyValidated()
|
||||
} else if (localUsernameHash != remoteUsernameHash) {
|
||||
Log.w(
|
||||
TAG,
|
||||
"Local username hash does not match server username hash. Local hash: " + (if (TextUtils.isEmpty(localUsername)) "empty" else "present") + ", Remote hash: " + (if (TextUtils.isEmpty(remoteUsernameHash)) "empty" else "present")
|
||||
)
|
||||
UsernameRepository.onUsernameMismatchDetected()
|
||||
return
|
||||
} else {
|
||||
Log.d(TAG, "Username validated.")
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Failed perform synchronization check during username phase.", e)
|
||||
} catch (e: BaseUsernameException) {
|
||||
Log.w(TAG, "Our local username data is invalid!", e)
|
||||
UsernameRepository.onUsernameMismatchDetected()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val localUsernameLink = SignalStore.account.usernameLink
|
||||
|
||||
if (localUsernameLink != null) {
|
||||
val remoteEncryptedUsername = NetworkResultUtil.toBasicLegacy<ByteArray>(SignalNetwork.username.getEncryptedUsernameFromLinkServerId(localUsernameLink.serverId))
|
||||
val combinedLink = Username.UsernameLink(localUsernameLink.entropy, remoteEncryptedUsername)
|
||||
val remoteUsername = Username.fromLink(combinedLink)
|
||||
|
||||
if (remoteUsername.getUsername() != SignalStore.account.username) {
|
||||
Log.w(TAG, "The remote username decrypted ok, but the decrypted username did not match our local username!")
|
||||
UsernameRepository.onUsernameLinkMismatchDetected()
|
||||
} else {
|
||||
Log.d(TAG, "Username link validated.")
|
||||
}
|
||||
|
||||
validated = true
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Failed perform synchronization check during the username link phase.", e)
|
||||
} catch (e: BaseUsernameException) {
|
||||
Log.w(TAG, "Failed to decrypt username link using the remote encrypted username and our local entropy!", e)
|
||||
UsernameRepository.onUsernameLinkMismatchDetected()
|
||||
}
|
||||
|
||||
if (validated) {
|
||||
UsernameRepository.onUsernameConsistencyValidated()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getRequestType(recipient: Recipient): SignalServiceProfile.RequestType {
|
||||
return if (ExpiringProfileCredentialUtil.isValid(recipient.expiringProfileKeyCredential))
|
||||
SignalServiceProfile.RequestType.PROFILE
|
||||
else
|
||||
SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL
|
||||
}
|
||||
|
||||
private fun isSubscription(badgeId: String?): Boolean {
|
||||
return !isBoost(badgeId) && !isGift(badgeId)
|
||||
}
|
||||
|
||||
private fun isBoost(badgeId: String?): Boolean {
|
||||
return badgeId == Badge.BOOST_BADGE_ID
|
||||
}
|
||||
|
||||
private fun isGift(badgeId: String?): Boolean {
|
||||
return badgeId == Badge.GIFT_BADGE_ID
|
||||
}
|
||||
|
||||
class Factory : Job.Factory<RefreshOwnProfileJob> {
|
||||
override fun create(parameters: Parameters, serializedData: ByteArray?): RefreshOwnProfileJob {
|
||||
return RefreshOwnProfileJob(parameters)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user