Fix blocking bugs for internal link and sync testing.

This commit is contained in:
Cody Henthorne
2025-10-08 16:09:01 -04:00
parent 33a7f55fa3
commit f8eaa96412
27 changed files with 212 additions and 124 deletions

View File

@@ -44,7 +44,7 @@ class MessageHelper(private val harness: SignalActivityRule, var startTime: Long
init {
val threadIdSlot = slot<Long>()
mockkStatic(ThreadUpdateJob::class)
every { ThreadUpdateJob.enqueue(capture(threadIdSlot)) } answers {
every { ThreadUpdateJob.enqueue(capture(threadIdSlot), any()) } answers {
SignalDatabase.threads.update(threadIdSlot.captured, false)
}
}
@@ -148,7 +148,7 @@ class MessageHelper(private val harness: SignalActivityRule, var startTime: Long
.groupChangeUpdate(GroupsV2UpdateMessageConverter.translateDecryptedChange(SignalStore.account.getServiceIds(), decryptedGroupV2Context))
.build()
val outgoingMessage = OutgoingMessage.groupUpdateMessage(groupRecipient, updateDescription, startTime)
val outgoingMessage = OutgoingMessage.groupUpdateMessage(groupRecipient, updateDescription, startTime, false)
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(groupRecipient)
val messageId = SignalDatabase.messages.insertMessageOutbox(outgoingMessage, threadId, false, null).messageId

View File

@@ -490,7 +490,7 @@ public class ApplicationContext extends Application implements AppForegroundObse
}
private void ensureProfileUploaded() {
if (SignalStore.account().isRegistered() && !SignalStore.registration().hasUploadedProfile() && !Recipient.self().getProfileName().isEmpty()) {
if (SignalStore.account().isRegistered() && !SignalStore.registration().hasUploadedProfile() && !Recipient.self().getProfileName().isEmpty() && SignalStore.account().isPrimaryDevice()) {
Log.w(TAG, "User has a profile, but has not uploaded one. Uploading now.");
AppDependencies.getJobManager().add(new ProfileUploadJob());
}

View File

@@ -195,7 +195,8 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
return !SignalStore.registration().isRegistrationComplete() &&
!SignalStore.svr().hasPin() &&
!SignalStore.svr().lastPinCreateFailed() &&
!SignalStore.svr().hasOptedOut();
!SignalStore.svr().hasOptedOut() &&
SignalStore.account().isPrimaryDevice();
}
private boolean userMustSetProfileName() {

View File

@@ -290,68 +290,70 @@ private fun AppSettingsContent(
BackupFailureState.NONE -> Unit
}
item {
Rows.TextRow(
text = stringResource(R.string.AccountSettingsFragment__account),
icon = painterResource(R.drawable.symbol_person_circle_24),
onClick = {
callbacks.navigate(AppSettingsRoute.AccountRoute.Account)
}
)
}
if (state.isPrimaryDevice) {
item {
Rows.TextRow(
text = stringResource(R.string.AccountSettingsFragment__account),
icon = painterResource(R.drawable.symbol_person_circle_24),
onClick = {
callbacks.navigate(AppSettingsRoute.AccountRoute.Account)
}
)
}
item {
Rows.TextRow(
text = stringResource(R.string.preferences__linked_devices),
icon = painterResource(R.drawable.symbol_devices_24),
onClick = {
callbacks.navigate(AppSettingsRoute.LinkDeviceRoute.LinkDevice)
},
enabled = isRegisteredAndUpToDate
)
}
item {
Rows.TextRow(
text = stringResource(R.string.preferences__linked_devices),
icon = painterResource(R.drawable.symbol_devices_24),
onClick = {
callbacks.navigate(AppSettingsRoute.LinkDeviceRoute.LinkDevice)
},
enabled = isRegisteredAndUpToDate
)
}
item {
val context = LocalContext.current
val donateUrl = stringResource(R.string.donate_url)
item {
val context = LocalContext.current
val donateUrl = stringResource(R.string.donate_url)
Rows.TextRow(
text = {
Text(
text = stringResource(R.string.preferences__donate_to_signal),
modifier = Modifier.weight(1f)
)
if (state.hasExpiredGiftBadge) {
Icon(
painter = painterResource(R.drawable.symbol_info_fill_24),
tint = colorResource(R.color.signal_accent_primary),
contentDescription = null
Rows.TextRow(
text = {
Text(
text = stringResource(R.string.preferences__donate_to_signal),
modifier = Modifier.weight(1f)
)
}
},
icon = {
Icon(
painter = painterResource(R.drawable.symbol_heart_24),
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface
)
},
onClick = {
if (state.allowUserToGoToDonationManagementScreen) {
callbacks.navigate(AppSettingsRoute.DonationsRoute.Donations())
} else {
CommunicationActions.openBrowserLink(context, donateUrl)
}
},
onLongClick = {
callbacks.copyDonorBadgeSubscriberIdToClipboard()
}
)
}
item {
Dividers.Default()
if (state.hasExpiredGiftBadge) {
Icon(
painter = painterResource(R.drawable.symbol_info_fill_24),
tint = colorResource(R.color.signal_accent_primary),
contentDescription = null
)
}
},
icon = {
Icon(
painter = painterResource(R.drawable.symbol_heart_24),
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface
)
},
onClick = {
if (state.allowUserToGoToDonationManagementScreen) {
callbacks.navigate(AppSettingsRoute.DonationsRoute.Donations())
} else {
CommunicationActions.openBrowserLink(context, donateUrl)
}
},
onLongClick = {
callbacks.copyDonorBadgeSubscriberIdToClipboard()
}
)
}
item {
Dividers.Default()
}
}
item {
@@ -408,29 +410,31 @@ private fun AppSettingsContent(
)
}
item {
Rows.TextRow(
text = {
TextWithBetaLabel(
text = stringResource(R.string.preferences_chats__backups),
textStyle = MaterialTheme.typography.bodyLarge
)
},
icon = {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.symbol_backup_24),
contentDescription = stringResource(R.string.preferences_chats__backups),
tint = MaterialTheme.colorScheme.onSurface
)
},
onClick = {
callbacks.navigate(AppSettingsRoute.BackupsRoute.Backups)
},
onLongClick = {
callbacks.copyRemoteBackupsSubscriberIdToClipboard()
},
enabled = isRegisteredAndUpToDate
)
if (state.isPrimaryDevice) {
item {
Rows.TextRow(
text = {
TextWithBetaLabel(
text = stringResource(R.string.preferences_chats__backups),
textStyle = MaterialTheme.typography.bodyLarge
)
},
icon = {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.symbol_backup_24),
contentDescription = stringResource(R.string.preferences_chats__backups),
tint = MaterialTheme.colorScheme.onSurface
)
},
onClick = {
callbacks.navigate(AppSettingsRoute.BackupsRoute.Backups)
},
onLongClick = {
callbacks.copyRemoteBackupsSubscriberIdToClipboard()
},
enabled = isRegisteredAndUpToDate
)
}
}
item {
@@ -455,7 +459,7 @@ private fun AppSettingsContent(
}
}
if (state.showPayments) {
if (state.isPrimaryDevice && state.showPayments) {
item {
Dividers.Default()
}
@@ -692,6 +696,7 @@ private fun AppSettingsContentPreview() {
)
),
state = AppSettingsState(
isPrimaryDevice = true,
unreadPaymentsCount = 5,
hasExpiredGiftBadge = true,
allowUserToGoToDonationManagementScreen = true,

View File

@@ -7,6 +7,7 @@ import org.thoughtcrime.securesms.util.RemoteConfig
@Immutable
data class AppSettingsState(
val isPrimaryDevice: Boolean,
val unreadPaymentsCount: Int,
val hasExpiredGiftBadge: Boolean,
val allowUserToGoToDonationManagementScreen: Boolean,

View File

@@ -21,6 +21,7 @@ class AppSettingsViewModel : ViewModel() {
private val store = Store(
AppSettingsState(
isPrimaryDevice = SignalStore.account.isPrimaryDevice,
unreadPaymentsCount = 0,
hasExpiredGiftBadge = SignalStore.inAppPayments.getExpiredGiftBadge() != null,
allowUserToGoToDonationManagementScreen = SignalStore.inAppPayments.isLikelyASustainer() || InAppDonations.hasAtLeastOnePaymentMethodAvailable(),

View File

@@ -880,6 +880,8 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) :
val groupRecipient = recipients.getOrInsertFromGroupId(groupId)
Recipient.live(groupRecipient).refresh()
notifyConversationListListeners()
}
fun remove(groupId: GroupId, source: RecipientId) {

View File

@@ -2889,7 +2889,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
) {
val incrementUnreadMentions = retrieved.mentions.isNotEmpty() && retrieved.mentions.any { it.recipientId == Recipient.self().id }
threads.incrementUnread(threadId, 1, if (incrementUnreadMentions) 1 else 0)
ThreadUpdateJob.enqueue(threadId)
ThreadUpdateJob.enqueue(threadId, true)
}
if (notifyObservers) {
@@ -3362,7 +3362,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
}
if (!message.isIdentityVerified && !message.isIdentityDefault) {
ThreadUpdateJob.enqueue(threadId)
ThreadUpdateJob.enqueue(threadId, !message.isSelfGroupAdd)
}
TrimThreadJob.enqueueAsync(threadId)

View File

@@ -1582,20 +1582,20 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
}
fun applyStorageSyncUpdate(recipientId: RecipientId, record: SignalContactRecord) {
applyStorageSyncUpdate(recipientId, record.proto.archived, record.proto.markedUnread)
applyStorageSyncUpdate(recipientId, record.proto.archived, record.proto.markedUnread, isGroup = false)
}
fun applyStorageSyncUpdate(recipientId: RecipientId, record: SignalGroupV1Record) {
applyStorageSyncUpdate(recipientId, record.proto.archived, record.proto.markedUnread)
applyStorageSyncUpdate(recipientId, record.proto.archived, record.proto.markedUnread, isGroup = true)
}
fun applyStorageSyncUpdate(recipientId: RecipientId, record: SignalGroupV2Record) {
applyStorageSyncUpdate(recipientId, record.proto.archived, record.proto.markedUnread)
applyStorageSyncUpdate(recipientId, record.proto.archived, record.proto.markedUnread, isGroup = true)
}
fun applyStorageSyncUpdate(recipientId: RecipientId, record: SignalAccountRecord) {
writableDatabase.withinTransaction { db ->
applyStorageSyncUpdate(recipientId, record.proto.noteToSelfArchived, record.proto.noteToSelfMarkedUnread)
applyStorageSyncUpdate(recipientId, record.proto.noteToSelfArchived, record.proto.noteToSelfMarkedUnread, isGroup = false)
db.updateAll(TABLE_NAME)
.values(PINNED_ORDER to null)
@@ -1631,6 +1631,8 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
}
if (pinnedRecipient != null) {
getOrCreateThreadIdFor(pinnedRecipient)
db.update(TABLE_NAME)
.values(PINNED_ORDER to pinnedPosition, ACTIVE to 1)
.where("$RECIPIENT_ID = ?", pinnedRecipient.id)
@@ -1644,11 +1646,11 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
notifyConversationListListeners()
}
private fun applyStorageSyncUpdate(recipientId: RecipientId, archived: Boolean, forcedUnread: Boolean) {
private fun applyStorageSyncUpdate(recipientId: RecipientId, archived: Boolean, forcedUnread: Boolean, isGroup: Boolean) {
val values = ContentValues()
values.put(ARCHIVED, if (archived) 1 else 0)
val threadId: Long? = getThreadIdFor(recipientId)
val threadId: Long? = if (archived) getOrCreateThreadIdFor(recipientId, isGroup) else getThreadIdFor(recipientId)
if (forcedUnread) {
values.put(READ, ReadStatus.FORCED_UNREAD.serialize())

View File

@@ -1272,7 +1272,7 @@ final class GroupManagerV2 {
GroupId.V2 groupId = GroupId.v2(masterKey);
Recipient groupRecipient = Recipient.externalGroupExact(groupId);
GV2UpdateDescription updateDescription = GroupProtoUtil.createOutgoingGroupV2UpdateDescription(masterKey, groupMutation, signedGroupChange);
OutgoingMessage outgoingMessage = OutgoingMessage.groupUpdateMessage(groupRecipient, updateDescription, System.currentTimeMillis());
OutgoingMessage outgoingMessage = OutgoingMessage.groupUpdateMessage(groupRecipient, updateDescription, System.currentTimeMillis(), false);
DecryptedGroupChange plainGroupChange = groupMutation.getGroupChange();

View File

@@ -672,7 +672,7 @@ class GroupsV2StateProcessor private constructor(
)
val updateDescription = GroupProtoUtil.createOutgoingGroupV2UpdateDescription(masterKey, GroupMutation(decryptedGroup, simulatedGroupChange, simulatedGroupState), null)
val leaveMessage = OutgoingMessage.groupUpdateMessage(groupRecipient, updateDescription, System.currentTimeMillis())
val leaveMessage = OutgoingMessage.groupUpdateMessage(groupRecipient, updateDescription, System.currentTimeMillis(), isSelfGroupAdd = false)
try {
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(groupRecipient)
@@ -728,10 +728,17 @@ class GroupsV2StateProcessor private constructor(
)
if (outgoing) {
val isSelfGroupAdd = updateDescription
.groupChangeUpdate!!
.updates
.asSequence()
.mapNotNull { it.groupMemberJoinedUpdate }
.any { serviceIds.matches(it.newMemberAci) }
try {
val recipientId = SignalDatabase.recipients.getOrInsertFromGroupId(groupId)
val recipient = Recipient.resolved(recipientId)
val outgoingMessage = OutgoingMessage.groupUpdateMessage(recipient, updateDescription, timestamp)
val outgoingMessage = OutgoingMessage.groupUpdateMessage(recipient, updateDescription, timestamp, isSelfGroupAdd)
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
val messageId = SignalDatabase.messages.insertMessageOutbox(outgoingMessage, threadId, false, null).messageId

View File

@@ -24,7 +24,7 @@ class AccountConsistencyWorkerJob private constructor(parameters: Parameters) :
@JvmStatic
fun enqueueIfNecessary() {
if (System.currentTimeMillis() - SignalStore.misc.lastConsistencyCheckTime > 3.days.inWholeMilliseconds) {
if (SignalStore.account.isPrimaryDevice && System.currentTimeMillis() - SignalStore.misc.lastConsistencyCheckTime > 3.days.inWholeMilliseconds) {
AppDependencies.jobManager.add(AccountConsistencyWorkerJob())
}
}
@@ -56,6 +56,11 @@ class AccountConsistencyWorkerJob private constructor(parameters: Parameters) :
return
}
if (SignalStore.account.isLinkedDevice) {
Log.i(TAG, "Linked device, skipping.")
return
}
val aciProfile: SignalServiceProfile = ProfileUtil.retrieveProfileSync(context, Recipient.self(), SignalServiceProfile.RequestType.PROFILE, false).profile
val encodedAciPublicKey = Base64.encodeWithPadding(SignalStore.account.aciIdentityKey.publicKey.serialize())

View File

@@ -88,6 +88,11 @@ public final class GroupV2UpdateSelfProfileKeyJob extends BaseJob {
return;
}
if (SignalStore.account().isLinkedDevice()) {
Log.i(TAG, "Linked device, skipping");
return;
}
byte[] rawProfileKey = Recipient.self().getProfileKey();
if (rawProfileKey == null) {
@@ -160,6 +165,11 @@ public final class GroupV2UpdateSelfProfileKeyJob extends BaseJob {
public void onRun()
throws IOException, GroupNotAMemberException, GroupChangeFailedException, GroupInsufficientRightsException, GroupChangeBusyException
{
if (SignalStore.account().isLinkedDevice()) {
Log.i(TAG, "Linked device, skipping");
return;
}
Log.i(TAG, "Ensuring profile key up to date on group " + groupId);
GroupManager.updateSelfProfileKeyInGroup(context, groupId);
}

View File

@@ -5,6 +5,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log;
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;
@@ -41,6 +42,16 @@ public final class ProfileUploadJob extends BaseJob {
return;
}
if (SignalStore.account().isLinkedDevice() && !SignalStore.registration().hasDownloadedProfile()) {
Log.w(TAG, "Attempting to upload profile before downloading, forcing download first");
AppDependencies.getJobManager()
.startChain(new RefreshOwnProfileJob())
.then(new ProfileUploadJob())
.enqueue();
return;
}
ProfileUtil.uploadProfile(context);
Log.i(TAG, "Profile uploaded.");
}

View File

@@ -159,6 +159,8 @@ public class RefreshOwnProfileJob extends BaseJob {
profileAndCredential.getExpiringProfileKeyCredential()
.ifPresent(expiringProfileKeyCredential -> setExpiringProfileKeyCredential(self, ProfileKeyUtil.getSelfProfileKey(), expiringProfileKeyCredential));
SignalStore.registration().setHasDownloadedProfile(true);
StoryOnboardingDownloadJob.Companion.enqueueIfNeeded();
checkUsernameIsInSync();

View File

@@ -6,8 +6,8 @@ import androidx.annotation.Nullable;
import org.signal.core.util.ThreadUtil;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
/**
* A job that effectively debounces thread updates through a combination of having a max instance count
@@ -18,34 +18,39 @@ public final class ThreadUpdateJob extends BaseJob {
public static final String KEY = "ThreadUpdateJob";
private static final String KEY_THREAD_ID = "thread_id";
private static final String KEY_UNARCHIVE = "unarchive";
private static final long DEBOUNCE_INTERVAL = 500;
private static final long DEBOUNCE_INTERVAL_WITH_BACKLOG = 3000;
private final long threadId;
private final long threadId;
private final boolean unarchive;
private ThreadUpdateJob(long threadId) {
private ThreadUpdateJob(long threadId, boolean unarchive) {
this(new Parameters.Builder()
.setQueue("ThreadUpdateJob_" + threadId)
.setMaxInstancesForQueue(2)
.build(),
threadId);
threadId,
unarchive);
}
private ThreadUpdateJob(@NonNull Parameters parameters, long threadId) {
private ThreadUpdateJob(@NonNull Parameters parameters, long threadId, boolean unarchive) {
super(parameters);
this.threadId = threadId;
this.threadId = threadId;
this.unarchive = unarchive;
}
public static void enqueue(long threadId) {
public static void enqueue(long threadId, boolean unarchive) {
SignalDatabase.runPostSuccessfulTransaction(KEY + threadId, () -> {
AppDependencies.getJobManager().add(new ThreadUpdateJob(threadId));
AppDependencies.getJobManager().add(new ThreadUpdateJob(threadId, unarchive));
});
}
@Override
public @Nullable byte[] serialize() {
return new JsonJobData.Builder().putLong(KEY_THREAD_ID, threadId).serialize();
return new JsonJobData.Builder().putLong(KEY_THREAD_ID, threadId)
.putBoolean(KEY_UNARCHIVE, unarchive)
.serialize();
}
@Override
@@ -55,7 +60,7 @@ public final class ThreadUpdateJob extends BaseJob {
@Override
protected void onRun() throws Exception {
SignalDatabase.threads().update(threadId, true, true);
SignalDatabase.threads().update(threadId, unarchive, true);
if (!AppDependencies.getIncomingMessageObserver().getDecryptionDrained()) {
ThreadUtil.sleep(DEBOUNCE_INTERVAL_WITH_BACKLOG);
}
@@ -74,7 +79,7 @@ public final class ThreadUpdateJob extends BaseJob {
@Override
public @NonNull ThreadUpdateJob create(@NonNull Parameters parameters, @Nullable byte[] serializedData) {
JsonJobData data = JsonJobData.deserialize(serializedData);
return new ThreadUpdateJob(parameters, data.getLong(KEY_THREAD_ID));
return new ThreadUpdateJob(parameters, data.getLong(KEY_THREAD_ID), data.getBooleanOrDefault(KEY_UNARCHIVE, true));
}
}
}

View File

@@ -15,6 +15,7 @@ class RegistrationValues internal constructor(store: KeyValueStore) : SignalStor
private const val REGISTRATION_COMPLETE = "registration.complete"
private const val PIN_REQUIRED = "registration.pin_required"
private const val HAS_UPLOADED_PROFILE = "registration.has_uploaded_profile"
private const val HAS_DOWNLOADED_PROFILE = "registration.has_downloaded_profile"
private const val SESSION_E164 = "registration.session_e164"
private const val SESSION_ID = "registration.session_id"
private const val LOCAL_REGISTRATION_DATA = "registration.local_registration_data"
@@ -32,6 +33,7 @@ class RegistrationValues internal constructor(store: KeyValueStore) : SignalStor
store
.beginWrite()
.putBoolean(HAS_UPLOADED_PROFILE, false)
.putBoolean(HAS_DOWNLOADED_PROFILE, false)
.putBoolean(REGISTRATION_COMPLETE, false)
.putBoolean(PIN_REQUIRED, true)
.putBlob(RESTORE_DECISION_STATE, RestoreDecisionState.Start.encode())
@@ -67,6 +69,10 @@ class RegistrationValues internal constructor(store: KeyValueStore) : SignalStor
@get:JvmName("hasUploadedProfile")
var hasUploadedProfile: Boolean by booleanValue(HAS_UPLOADED_PROFILE, true)
@get:JvmName("hasDownloadedProfile")
var hasDownloadedProfile: Boolean by booleanValue(HAS_DOWNLOADED_PROFILE, true)
var sessionId: String? by stringValue(SESSION_ID, null)
var sessionE164: String? by stringValue(SESSION_E164, null)

View File

@@ -135,6 +135,10 @@ public final class Megaphones {
}
private static boolean shouldShowLinkedDeviceInactiveMegaphone() {
if (SignalStore.account().isLinkedDevice()) {
return false;
}
LeastActiveLinkedDevice device = SignalStore.misc().getLeastActiveLinkedDevice();
if (device == null) {
return false;
@@ -487,11 +491,11 @@ public final class Megaphones {
}
private static boolean shouldShowOnboardingMegaphone(@NonNull Context context) {
return SignalStore.onboarding().hasOnboarding(context);
return SignalStore.account().isPrimaryDevice() && SignalStore.onboarding().hasOnboarding(context);
}
private static boolean shouldShowNewLinkedDeviceMegaphone() {
return SignalStore.misc().getNewLinkedDeviceId() > 0 && !NotificationChannels.getInstance().areNotificationsEnabled();
return SignalStore.account().isPrimaryDevice() && SignalStore.misc().getNewLinkedDeviceId() > 0 && !NotificationChannels.getInstance().areNotificationsEnabled();
}
private static boolean shouldShowTurnOffCircumventionMegaphone() {
@@ -542,7 +546,8 @@ public final class Megaphones {
long phoneNumberDiscoveryDisabledAt = SignalStore.phoneNumberPrivacy().getPhoneNumberDiscoverabilityModeTimestamp();
PhoneNumberDiscoverabilityMode listingMode = SignalStore.phoneNumberPrivacy().getPhoneNumberDiscoverabilityMode();
return !hasUsername &&
return SignalStore.account().isPrimaryDevice() &&
!hasUsername &&
listingMode == PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE &&
!hasCompleted &&
phoneNumberDiscoveryDisabledAt > 0 &&
@@ -550,7 +555,7 @@ public final class Megaphones {
}
private static boolean shouldShowPnpLaunchMegaphone() {
return TextUtils.isEmpty(SignalStore.account().getUsername()) && !SignalStore.uiHints().hasCompletedUsernameOnboarding();
return SignalStore.account().isPrimaryDevice() && TextUtils.isEmpty(SignalStore.account().getUsername()) && !SignalStore.uiHints().hasCompletedUsernameOnboarding();
}
private static boolean shouldShowTurnOnBackupsMegaphone(@NonNull Context context) {
@@ -562,7 +567,7 @@ public final class Megaphones {
return false;
}
if (!SignalStore.account().isRegistered() || TextSecurePreferences.isUnauthorizedReceived(context)) {
if (!SignalStore.account().isRegistered() || TextSecurePreferences.isUnauthorizedReceived(context) || SignalStore.account().isLinkedDevice()) {
return false;
}
@@ -580,7 +585,7 @@ public final class Megaphones {
}
private static boolean shouldShowBackupSchedulePermissionMegaphone(@NonNull Context context) {
return Build.VERSION.SDK_INT >= 31 && SignalStore.settings().isBackupEnabled() && !ServiceUtil.getAlarmManager(context).canScheduleExactAlarms();
return SignalStore.account().isPrimaryDevice() && Build.VERSION.SDK_INT >= 31 && SignalStore.settings().isBackupEnabled() && !ServiceUtil.getAlarmManager(context).canScheduleExactAlarms();
}
/**

View File

@@ -49,6 +49,10 @@ class PinsForAllSchedule implements MegaphoneSchedule {
return false;
}
if (SignalStore.account().isLinkedDevice()) {
return false;
}
if (pinCreationFailedDuringRegistration()) {
return true;
}

View File

@@ -14,6 +14,10 @@ final class SignalPinReminderSchedule implements MegaphoneSchedule {
return false;
}
if (SignalStore.account().isLinkedDevice()) {
return false;
}
if (!SignalStore.pin().arePinRemindersEnabled()) {
return false;
}
@@ -22,6 +26,10 @@ final class SignalPinReminderSchedule implements MegaphoneSchedule {
return false;
}
if (SignalStore.account().isLinkedDevice()) {
return false;
}
long lastReminderTime = SignalStore.pin().getLastReminderTime();
long interval = SignalStore.pin().getCurrentInterval();

View File

@@ -16,6 +16,10 @@ class VerifyBackupKeyReminderSchedule : MegaphoneSchedule {
return false
}
if (SignalStore.account.isLinkedDevice) {
return false
}
val lastVerifiedTime = SignalStore.backup.lastVerifyKeyTime
val previouslySnoozed = SignalStore.backup.hasSnoozedVerified
val isFirstReminder = !SignalStore.backup.hasVerifiedBefore

View File

@@ -60,7 +60,8 @@ data class OutgoingMessage(
val isBlocked: Boolean = false,
val isUnblocked: Boolean = false,
val poll: Poll? = null,
val messageExtras: MessageExtras? = null
val messageExtras: MessageExtras? = null,
val isSelfGroupAdd: Boolean = false
) {
val isV2Group: Boolean = messageGroupContext != null && GroupV2UpdateMessageUtil.isGroupV2(messageGroupContext)
@@ -240,7 +241,7 @@ data class OutgoingMessage(
* Helper for creating a group update message when a state change occurs and needs to be sent to others.
*/
@JvmStatic
fun groupUpdateMessage(threadRecipient: Recipient, update: GV2UpdateDescription, sentTimeMillis: Long): OutgoingMessage {
fun groupUpdateMessage(threadRecipient: Recipient, update: GV2UpdateDescription, sentTimeMillis: Long, isSelfGroupAdd: Boolean): OutgoingMessage {
val messageExtras = MessageExtras(gv2UpdateDescription = update)
val groupContext = MessageGroupContext(update.gv2ChangeDescription!!)
@@ -251,7 +252,8 @@ data class OutgoingMessage(
isGroup = true,
isGroupUpdate = true,
isSecure = true,
messageExtras = messageExtras
messageExtras = messageExtras,
isSelfGroupAdd = isSelfGroupAdd
)
}

View File

@@ -171,6 +171,11 @@ object RegistrationRepository {
suspend fun registerAccountLocally(context: Context, data: LocalRegistrationMetadata) =
withContext(Dispatchers.IO) {
Log.v(TAG, "registerAccountLocally()")
if (data.linkedDeviceInfo != null) {
SignalStore.account.deviceId = data.linkedDeviceInfo.deviceId
SignalStore.account.deviceName = data.linkedDeviceInfo.deviceName
}
val aciIdentityKeyPair = data.getAciIdentityKeyPair()
val pniIdentityKeyPair = data.getPniIdentityKeyPair()
SignalStore.account.restoreAciIdentityKeyFromBackup(aciIdentityKeyPair.publicKey.serialize(), aciIdentityKeyPair.privateKey.serialize())
@@ -219,9 +224,6 @@ object RegistrationRepository {
saveOwnIdentityKey(selfId, pni, pniProtocolStore, now)
if (data.linkedDeviceInfo != null) {
SignalStore.account.deviceId = data.linkedDeviceInfo.deviceId
SignalStore.account.deviceName = data.linkedDeviceInfo.deviceName
if (data.linkedDeviceInfo.accountEntropyPool != null) {
SignalStore.account.setAccountEntropyPoolFromPrimaryDevice(AccountEntropyPool(data.linkedDeviceInfo.accountEntropyPool))
}
@@ -254,7 +256,6 @@ object RegistrationRepository {
RotateSignedPreKeyListener.schedule(context)
} else {
SignalStore.account.isMultiDevice = true
SignalStore.registration.hasUploadedProfile = true
jobManager.runJobBlocking(RefreshOwnProfileJob(), 30.seconds)
jobManager.add(RotateCertificateJob())

View File

@@ -51,7 +51,7 @@ object StorageServiceRestore {
val isMissingProfileData = RegistrationRepository.isMissingProfileData()
RegistrationUtil.maybeMarkRegistrationComplete()
if (!isMissingProfileData) {
if (!isMissingProfileData && SignalStore.account.isPrimaryDevice) {
AppDependencies.jobManager.add(ProfileUploadJob())
}
}

View File

@@ -36,7 +36,7 @@ public final class RegistrationUtil {
if (!SignalStore.registration().isRegistrationComplete() &&
SignalStore.account().isRegistered() &&
!Recipient.self().getProfileName().isEmpty() &&
(SignalStore.svr().hasPin() || SignalStore.svr().hasOptedOut()) &&
(SignalStore.svr().hasPin() || SignalStore.svr().hasOptedOut() || SignalStore.account().isLinkedDevice()) &&
RestoreDecisionStateUtil.isTerminal(SignalStore.registration().getRestoreDecisionState()))
{
Log.i(TAG, "Marking registration completed.", new Throwable());

View File

@@ -74,6 +74,11 @@ public final class ProfileUtil {
*/
@WorkerThread
public static void handleSelfProfileKeyChange() {
if (SignalStore.account().isLinkedDevice()) {
Log.i(TAG, "Linked devices shouldn't rotate self profile key after initial link");
return;
}
List<Job> gv2UpdateJobs = SignalDatabase.groups()
.getAllGroupV2Ids()
.stream()

View File

@@ -101,6 +101,7 @@ class RegistrationUtilTest {
every { Recipient.self() } returns Recipient(profileName = ProfileName.fromParts("Dark", "Helmet"))
every { signalStore.svr.hasPin() } returns false
every { signalStore.svr.hasOptedOut() } returns false
every { signalStore.account.isLinkedDevice } returns false
RegistrationUtil.maybeMarkRegistrationComplete()