diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageHelper.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageHelper.kt index 00dbab6020..3b103e09b8 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageHelper.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageHelper.kt @@ -44,7 +44,7 @@ class MessageHelper(private val harness: SignalActivityRule, var startTime: Long init { val threadIdSlot = slot() 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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index c2e576fdba..58e130489b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -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()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActivity.java b/app/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActivity.java index 5d42970cc1..cb718802c6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActivity.java @@ -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() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt index c19f12af9d..7a3be3166a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt @@ -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, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsState.kt index 4a8f85a92a..630e4f79aa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsState.kt @@ -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, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsViewModel.kt index 8f8b1f3ce9..127cf3fbb5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsViewModel.kt @@ -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(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/GroupTable.kt index 5b52b7fccd..96ddea65d6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupTable.kt @@ -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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt index a58e6870d9..c1b8bfc8ef 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt index ed1cf5cdc3..4a568d87d5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt @@ -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()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java index 4ad6ff916c..327100cc86 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java @@ -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(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.kt index cf115037c6..dfe66a399a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.kt @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AccountConsistencyWorkerJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/AccountConsistencyWorkerJob.kt index c18d7a5e3c..9f0acffeaf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AccountConsistencyWorkerJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AccountConsistencyWorkerJob.kt @@ -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()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupV2UpdateSelfProfileKeyJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupV2UpdateSelfProfileKeyJob.java index e33d67ed31..de4fac875c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupV2UpdateSelfProfileKeyJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupV2UpdateSelfProfileKeyJob.java @@ -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); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileUploadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileUploadJob.java index c6e6e0994f..312b55efb0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileUploadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileUploadJob.java @@ -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."); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java index 236ce4c3d8..30f44f3d40 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java @@ -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(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ThreadUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/ThreadUpdateJob.java index ce32d9c34a..ca987a9eb7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ThreadUpdateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ThreadUpdateJob.java @@ -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)); } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/RegistrationValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/RegistrationValues.kt index d73c9808c7..17d173cdba 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/RegistrationValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/RegistrationValues.kt @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java index 80351f4a41..a2e6a8a5a1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java @@ -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(); } /** diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/PinsForAllSchedule.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/PinsForAllSchedule.java index 2b9a0dc2a7..dea1e1ae70 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/PinsForAllSchedule.java +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/PinsForAllSchedule.java @@ -49,6 +49,10 @@ class PinsForAllSchedule implements MegaphoneSchedule { return false; } + if (SignalStore.account().isLinkedDevice()) { + return false; + } + if (pinCreationFailedDuringRegistration()) { return true; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/SignalPinReminderSchedule.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/SignalPinReminderSchedule.java index b0e1d95cae..c5f64fc6b3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/SignalPinReminderSchedule.java +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/SignalPinReminderSchedule.java @@ -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(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/VerifyBackupKeyReminderSchedule.kt b/app/src/main/java/org/thoughtcrime/securesms/megaphone/VerifyBackupKeyReminderSchedule.kt index 722ac1d49c..36aa211e61 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/VerifyBackupKeyReminderSchedule.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/VerifyBackupKeyReminderSchedule.kt @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingMessage.kt b/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingMessage.kt index c6d8592e42..2a4897c212 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingMessage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingMessage.kt @@ -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 ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/data/RegistrationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/data/RegistrationRepository.kt index 07cc82b0ae..1796ceedf8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/data/RegistrationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/data/RegistrationRepository.kt @@ -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()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/StorageServiceRestore.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/StorageServiceRestore.kt index 8a41fc351a..9963a55209 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/StorageServiceRestore.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/StorageServiceRestore.kt @@ -51,7 +51,7 @@ object StorageServiceRestore { val isMissingProfileData = RegistrationRepository.isMissingProfileData() RegistrationUtil.maybeMarkRegistrationComplete() - if (!isMissingProfileData) { + if (!isMissingProfileData && SignalStore.account.isPrimaryDevice) { AppDependencies.jobManager.add(ProfileUploadJob()) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/util/RegistrationUtil.java b/app/src/main/java/org/thoughtcrime/securesms/registration/util/RegistrationUtil.java index 591f973623..4ab5b51700 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/util/RegistrationUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/util/RegistrationUtil.java @@ -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()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java index 1a7b0fd289..72585325d9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java @@ -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 gv2UpdateJobs = SignalDatabase.groups() .getAllGroupV2Ids() .stream() diff --git a/app/src/test/java/org/thoughtcrime/securesms/registration/util/RegistrationUtilTest.kt b/app/src/test/java/org/thoughtcrime/securesms/registration/util/RegistrationUtilTest.kt index d133f0161a..13e2b833c3 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/registration/util/RegistrationUtilTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/registration/util/RegistrationUtilTest.kt @@ -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()