Compare commits

..

1 Commits

Author SHA1 Message Date
Alex Hart ae8e050891 Bump version to 8.13.2 2026-06-03 15:35:01 -03:00
337 changed files with 3288 additions and 11443 deletions
+3 -16
View File
@@ -5,7 +5,7 @@ on:
push:
branches:
- 'main'
- '8.**'
- '7.**'
permissions:
contents: read # to fetch code (actions/checkout)
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest-8-cores
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
# gh api repos/actions/checkout/commits/v6 --jq '.sha'
with:
submodules: true
@@ -27,26 +27,13 @@ jobs:
with:
distribution: temurin
java-version: 17
cache: gradle
- name: Validate Gradle Wrapper
uses: gradle/actions/wrapper-validation@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6
# gh api repos/gradle/actions/commits/v6 --jq '.sha'
- name: Set up Gradle
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6
# gh api repos/gradle/actions/commits/v6 --jq '.sha'
with:
# Only 8.** branch builds write to the cache; everything else (PRs, etc.) reads only.
cache-read-only: ${{ !startsWith(github.ref, 'refs/heads/8.') }}
# Required to persist the Gradle configuration cache across runs.
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
- name: Build with Gradle
env:
SIGNAL_BUILD_CACHE_URL: ${{ secrets.SIGNAL_BUILD_CACHE_URL }}
SIGNAL_BUILD_CACHE_USER: ${{ secrets.SIGNAL_BUILD_CACHE_USER }}
SIGNAL_BUILD_CACHE_PASSWORD: ${{ secrets.SIGNAL_BUILD_CACHE_PASSWORD }}
SIGNAL_BUILD_CACHE_PUSH: ${{ startsWith(github.ref, 'refs/heads/8.') }}
run: ./gradlew qa
- name: Archive reports for failed build
+3 -11
View File
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest-8-cores
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
# gh api repos/actions/checkout/commits/v6 --jq '.sha'
with:
submodules: true
@@ -28,15 +28,7 @@ jobs:
with:
distribution: temurin
java-version: 17
- name: Set up Gradle
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6
# gh api repos/gradle/actions/commits/v6 --jq '.sha'
with:
# PR-only workflow: always read from the cache, never write.
cache-read-only: true
# Required to read the Gradle configuration cache persisted by 8.** builds.
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
cache: gradle
- name: Install NDK
run: echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "ndk;${{ env.NDK_VERSION }}"
@@ -61,7 +53,7 @@ jobs:
if: steps.cache-base.outputs.cache-hit != 'true'
run: mv app/build/outputs/apk/playProd/release/*arm64*.apk diffuse-base.apk
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
# gh api repos/actions/checkout/commits/v6 --jq '.sha'
with:
submodules: true
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
# gh api repos/actions/checkout/commits/v6 --jq '.sha'
- name: Build image
run: |
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
actions: write
steps:
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10
# gh api repos/actions/stale/commits/v10 --jq '.sha'
with:
days-before-stale: 60
+3 -3
View File
@@ -27,9 +27,9 @@ plugins {
val staticIps = Properties().apply { file("static-ips.properties").reader().use { load(it) } }
staticIps.stringPropertyNames().forEach { rootProject.extra[it] = staticIps.getProperty(it) }
val canonicalVersionCode = 1702
val canonicalVersionName = "8.14.2"
val currentHotfixVersion = 0
val canonicalVersionCode = 1699
val canonicalVersionName = "8.13.2"
val currentHotfixVersion = 1
val maxHotfixVersions = 100
// We don't want versions to ever end in 0 so that they don't conflict with nightly versions
@@ -138,7 +138,7 @@ class ConversationItemPreviewer {
private fun attachment(): SignalServiceAttachmentPointer {
return SignalServiceAttachmentPointer(
Cdn.CDN_3.cdnNumber,
SignalServiceAttachmentRemoteId.from("", Cdn.CDN_3.cdnNumber),
SignalServiceAttachmentRemoteId.from(""),
"image/webp",
null,
Optional.empty(),
@@ -139,7 +139,7 @@ class SignalActivityRule(private val othersCount: Int = 4, private val createGro
val recipientId = RecipientId.from(SignalServiceAddress(aci, "+15555551%03d".format(i)))
SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i"))
SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew())
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true))
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true))
SignalDatabase.recipients.setProfileSharing(recipientId, true)
SignalDatabase.recipients.markRegistered(recipientId, aci)
val otherIdentity = IdentityKeyPair.generate()
@@ -146,7 +146,7 @@ object TestMessages {
private fun imageAttachment(): SignalServiceAttachmentPointer {
return SignalServiceAttachmentPointer(
Cdn.S3.cdnNumber,
SignalServiceAttachmentRemoteId.from("", Cdn.S3.cdnNumber),
SignalServiceAttachmentRemoteId.from(""),
"image/webp",
null,
Optional.empty(),
@@ -170,7 +170,7 @@ object TestMessages {
private fun voiceAttachment(): SignalServiceAttachmentPointer {
return SignalServiceAttachmentPointer(
Cdn.S3.cdnNumber,
SignalServiceAttachmentRemoteId.from("", Cdn.S3.cdnNumber),
SignalServiceAttachmentRemoteId.from(""),
"audio/aac",
null,
Optional.empty(),
@@ -133,7 +133,7 @@ object TestUsers {
val recipientId = RecipientId.from(SignalServiceAddress(aci, "+15555551%03d".format(i)))
SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i"))
SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew())
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true))
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true))
SignalDatabase.recipients.setProfileSharing(recipientId, true)
SignalDatabase.recipients.markRegistered(recipientId, aci)
val otherIdentity = IdentityKeyPair.generate()
@@ -157,7 +157,7 @@ object TestUsers {
val recipientId = RecipientId.from(SignalServiceAddress(otherClient.serviceId, otherClient.e164))
SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i"))
SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, otherClient.profileKey)
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true))
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true))
SignalDatabase.recipients.setProfileSharing(recipientId, true)
SignalDatabase.recipients.markRegistered(recipientId, otherClient.serviceId)
AppDependencies.protocolStore.aci().saveIdentity(SignalProtocolAddress(otherClient.serviceId.toString(), 1), otherClient.identityKeyPair.publicKey)
@@ -13,8 +13,7 @@ object AppCapabilities {
storage = storageCapable,
versionedExpirationTimer = true,
attachmentBackfill = true,
spqr = true,
usernameChangeSyncMessage = false // TODO(michelle): Turn on once all clients support it and add a migration
spqr = true
)
}
}
@@ -33,10 +33,8 @@ import net.zetetic.database.Logger;
import org.conscrypt.ConscryptSignal;
import org.greenrobot.eventbus.EventBus;
import org.signal.aesgcmprovider.AesGcmProvider;
import org.signal.core.util.AppForegroundObserver;
import org.signal.core.util.DiskUtil;
import org.signal.core.util.MemoryTracker;
import org.signal.core.util.Util;
import org.signal.core.util.concurrent.AnrDetector;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.AndroidLogger;
@@ -53,7 +51,6 @@ import org.thoughtcrime.securesms.backup.v2.BackupRepository;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider;
import org.thoughtcrime.securesms.database.LogDatabase;
import org.thoughtcrime.securesms.database.SQLiteDatabase;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.SqlCipherLibraryLoader;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
@@ -62,7 +59,6 @@ import org.thoughtcrime.securesms.emoji.EmojiSource;
import org.thoughtcrime.securesms.emoji.JumboEmoji;
import org.thoughtcrime.securesms.gcm.FcmFetchManager;
import org.thoughtcrime.securesms.glide.SignalGlideComponents;
import org.thoughtcrime.securesms.jobmanager.impl.SealedSenderConstraint;
import org.thoughtcrime.securesms.jobs.AccountConsistencyWorkerJob;
import org.thoughtcrime.securesms.jobs.BackupRefreshJob;
import org.thoughtcrime.securesms.jobs.BackupSubscriptionCheckJob;
@@ -79,7 +75,6 @@ import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob;
import org.thoughtcrime.securesms.jobs.InAppPaymentAuthCheckJob;
import org.thoughtcrime.securesms.jobs.InAppPaymentKeepAliveJob;
import org.thoughtcrime.securesms.jobs.LinkedDeviceInactiveCheckJob;
import org.thoughtcrime.securesms.jobs.MessageSendLogCleanupJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.jobs.PreKeysSyncJob;
import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
@@ -88,6 +83,7 @@ import org.thoughtcrime.securesms.jobs.RefreshSvrCredentialsJob;
import org.thoughtcrime.securesms.jobs.RestoreOptimizedMediaJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.jobs.RetrieveRemoteAnnouncementsJob;
import org.thoughtcrime.securesms.jobmanager.impl.SealedSenderConstraint;
import org.thoughtcrime.securesms.jobs.StoryOnboardingDownloadJob;
import org.thoughtcrime.securesms.keyvalue.KeepMessagesDuration;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
@@ -111,8 +107,10 @@ import org.thoughtcrime.securesms.service.MessageBackupListener;
import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
import org.thoughtcrime.securesms.service.webrtc.ActiveCallManager;
import org.thoughtcrime.securesms.service.webrtc.CallingAssets;
import org.thoughtcrime.securesms.service.webrtc.AndroidTelecomUtil;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.signal.core.util.AppForegroundObserver;
import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.DeviceProperties;
import org.thoughtcrime.securesms.util.DynamicTheme;
@@ -123,6 +121,7 @@ import org.thoughtcrime.securesms.util.SignalLocalMetrics;
import org.thoughtcrime.securesms.util.SignalUncaughtExceptionHandler;
import org.thoughtcrime.securesms.util.SqlCipherLogTarget;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.signal.core.util.Util;
import org.thoughtcrime.securesms.util.VersionTracker;
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
@@ -230,7 +229,7 @@ public class ApplicationContext extends Application implements AppForegroundObse
.addPostRender(RefreshSvrCredentialsJob::enqueueIfNecessary)
.addPostRender(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this))
.addPostRender(EmojiSearchIndexDownloadJob::scheduleIfNecessary)
.addPostRender(MessageSendLogCleanupJob::enqueue)
.addPostRender(() -> SignalDatabase.messageLog().trimOldMessages(System.currentTimeMillis(), RemoteConfig.retryRespondMaxAge()))
.addPostRender(() -> JumboEmoji.updateCurrentVersion(this))
.addPostRender(RetrieveRemoteAnnouncementsJob::enqueue)
.addPostRender(AndroidTelecomUtil::registerPhoneAccount)
@@ -421,7 +420,6 @@ public class ApplicationContext extends Application implements AppForegroundObse
new org.signal.registration.RegistrationDependencies(
new org.thoughtcrime.securesms.registration.v2.AppRegistrationNetworkController(this, AppDependencies.getPushServiceSocket()),
new org.thoughtcrime.securesms.registration.v2.AppRegistrationStorageController(this),
Environment.IS_LINK_AND_SYNC_AVAILABLE,
null,
context -> {
context.startActivity(new Intent(context, SubmitDebugLogActivity.class));
@@ -512,8 +510,6 @@ public class ApplicationContext extends Application implements AppForegroundObse
if (RemoteConfig.internalUser()) {
Tracer.getInstance().setMaxBufferSize(35_000);
}
SQLiteDatabase.setSlowWriteLoggingEnabled(RemoteConfig.slowDatabaseNotifications());
}
private void initializePeriodicTasks() {
@@ -31,7 +31,7 @@ fun Attachment.toAttachmentPointer(context: Context): AttachmentPointer? {
}
try {
val remoteId = SignalServiceAttachmentRemoteId.from(attachment.remoteLocation!!, attachment.cdn.cdnNumber)
val remoteId = SignalServiceAttachmentRemoteId.from(attachment.remoteLocation!!)
var attachmentWidth = attachment.width
var attachmentHeight = attachment.height
@@ -38,8 +38,8 @@ object AvatarPickerStorage {
.getAllAvatars()
.filterIsInstance<Avatar.Photo>()
val inDatabaseFileNames = photoAvatars.mapTo(mutableSetOf()) { PartAuthority.getAvatarPickerFilename(it.uri) }
val onDiskFileNames = avatarFiles.mapTo(mutableSetOf()) { it.name }
val inDatabaseFileNames = photoAvatars.map { PartAuthority.getAvatarPickerFilename(it.uri) }
val onDiskFileNames = avatarFiles.map { it.name }
val inDatabaseButNotOnDisk = inDatabaseFileNames - onDiskFileNames
val onDiskButNotInDatabase = onDiskFileNames - inDatabaseFileNames
@@ -6,18 +6,14 @@
package org.thoughtcrime.securesms.backup
import androidx.annotation.WorkerThread
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.withContext
import org.signal.core.util.bytes
import org.signal.core.util.logging.Log
@@ -50,8 +46,6 @@ object ArchiveUploadProgress {
private val TAG = Log.tag(ArchiveUploadProgress::class)
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val _progress: MutableSharedFlow<Unit> = MutableSharedFlow(replay = 1)
private var uploadProgress: ArchiveUploadProgressState = SignalStore.backup.archiveUploadState ?: ArchiveUploadProgressState(
@@ -67,7 +61,7 @@ object ArchiveUploadProgress {
/**
* Observe this to get updates on the current upload progress.
*/
val progress: SharedFlow<ArchiveUploadProgressState> = _progress
val progress: Flow<ArchiveUploadProgressState> = _progress
.throttleLatest(500.milliseconds) {
uploadProgress.state == ArchiveUploadProgressState.State.None ||
(uploadProgress.state == ArchiveUploadProgressState.State.UploadBackupFile && uploadProgress.backupFileUploadedBytes == 0L) ||
@@ -120,11 +114,6 @@ object ArchiveUploadProgress {
}
.onStart { emit(uploadProgress) }
.flowOn(Dispatchers.IO)
.shareIn(scope, SharingStarted.Eagerly, replay = 1)
init {
_progress.tryEmit(Unit)
}
val inProgress
get() = uploadProgress.state != ArchiveUploadProgressState.State.None && uploadProgress.state != ArchiveUploadProgressState.State.UserCanceled
@@ -31,9 +31,6 @@ import org.thoughtcrime.securesms.jobmanager.impl.BatteryNotLowConstraint
import org.thoughtcrime.securesms.jobmanager.impl.DiskSpaceNotLowConstraint
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.jobmanager.impl.WifiConstraint
import org.thoughtcrime.securesms.jobs.CheckRestoreMediaLeftJob
import org.thoughtcrime.securesms.jobs.RestoreAttachmentJob
import org.thoughtcrime.securesms.jobs.RestoreLocalAttachmentJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger
@@ -160,22 +157,6 @@ object ArchiveRestoreProgress {
update()
}
/**
* Self-heal hook for restores that appear active (banner showing, media still remaining) but have no jobs left actually working on them.
*/
fun checkForStalledRestore() {
SignalExecutors.BOUNDED.execute {
val stalled = SignalStore.backup.restoreState.isMediaRestoreOperation &&
SignalDatabase.attachments.getRemainingRestorableAttachmentSize() > 0L &&
AppDependencies.jobManager.areFactoriesEmpty(setOf(RestoreAttachmentJob.KEY, RestoreLocalAttachmentJob.KEY, CheckRestoreMediaLeftJob.KEY))
if (stalled) {
Log.w(TAG, "Detected a stalled media restore with no active jobs. Enqueueing a check job to recover.")
CheckRestoreMediaLeftJob.enqueueStalledRecoveryCheck()
}
}
}
fun clearLocalRestoreDirectoryError() {
SignalStore.backup.localRestoreDirectoryError = false
update()
@@ -1465,7 +1465,6 @@ object BackupRepository {
}
SignalDatabase.remappedRecords.clearCache()
SignalDatabase.remappedRecords.trimStaleMappings()
AppDependencies.recipientCache.clear()
AppDependencies.recipientCache.warmUp()
SignalDatabase.threads.clearCache()
@@ -123,7 +123,7 @@ fun DatabaseAttachment.createArchiveAttachmentPointer(useArchiveCdn: Boolean): S
throw InvalidAttachmentException("empty content id")
}
SignalServiceAttachmentRemoteId.from(remoteLocation, cdn.cdnNumber) to cdn.cdnNumber
SignalServiceAttachmentRemoteId.from(remoteLocation) to cdn.cdnNumber
}
val key = Base64.decode(remoteKey)
@@ -87,7 +87,7 @@ fun FilePointer?.toLocalAttachment(
AttachmentType.TRANSIT -> {
val signalAttachmentPointer = SignalServiceAttachmentPointer(
cdnNumber = locatorInfo.transitCdnNumber ?: Cdn.CDN_0.cdnNumber,
remoteId = SignalServiceAttachmentRemoteId.from(locatorInfo.transitCdnKey!!, locatorInfo.transitCdnNumber ?: Cdn.CDN_0.cdnNumber),
remoteId = SignalServiceAttachmentRemoteId.from(locatorInfo.transitCdnKey!!),
contentType = contentType,
key = locatorInfo.key.toByteArray(),
size = Optional.ofNullable(locatorInfo.size),
@@ -10,7 +10,6 @@ import androidx.compose.runtime.Composable
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.onStart
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgress
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgressState
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgressState.RestoreStatus
@@ -26,7 +25,6 @@ class ArchiveRestoreStatusBanner(private val listener: RestoreProgressBannerList
override val dataFlow: Flow<ArchiveRestoreProgressState> by lazy {
ArchiveRestoreProgress
.stateFlow
.onStart { ArchiveRestoreProgress.checkForStalledRestore() }
.filter {
it.restoreStatus != RestoreStatus.NONE && (it.restoreState.isMediaRestoreOperation || it.restoreStatus == RestoreStatus.FINISHED)
}
@@ -38,12 +38,10 @@ import com.bumptech.glide.RequestManager;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.signal.core.ui.view.Stub;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.concurrent.ListenableFuture;
import org.signal.core.util.concurrent.SettableFuture;
import org.signal.core.util.logging.Log;
import org.signal.glide.decryptableuri.DecryptableUri;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
import org.thoughtcrime.securesms.animation.AnimationStartListener;
@@ -65,6 +63,7 @@ import org.thoughtcrime.securesms.database.model.StickerRecord;
import org.thoughtcrime.securesms.keyboard.KeyboardPage;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
import org.signal.glide.decryptableuri.DecryptableUri;
import org.thoughtcrime.securesms.mms.QuoteModel;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
@@ -92,33 +91,32 @@ public class InputPanel extends ConstraintLayout
private static final long QUOTE_REVEAL_DURATION_MILLIS = 150;
private static final int FADE_TIME = 150;
private RecyclerView stickerSuggestion;
private Stub<QuoteView> quoteViewStub;
private Stub<LinkPreviewView> linkPreviewStub;
private EmojiToggle mediaKeyboard;
private ComposeText composeText;
private ImageButton quickCameraToggle;
private ImageButton quickAudioToggle;
private AnimatingToggle buttonToggle;
private SendButton sendButton;
private View recordingContainer;
private View recordLockCancel;
private View composeContainer;
private View editMessageCancel;
private ImageView editMessageThumbnail;
private View editMessageTitle;
private FrameLayout composeTextContainer;
private RecyclerView stickerSuggestion;
private QuoteView quoteView;
private LinkPreviewView linkPreview;
private EmojiToggle mediaKeyboard;
private ComposeText composeText;
private ImageButton quickCameraToggle;
private ImageButton quickAudioToggle;
private AnimatingToggle buttonToggle;
private SendButton sendButton;
private View recordingContainer;
private View recordLockCancel;
private View composeContainer;
private View editMessageCancel;
private ImageView editMessageThumbnail;
private View editMessageTitle;
private FrameLayout composeTextContainer;
private MicrophoneRecorderView microphoneRecorderView;
private SlideToCancel slideToCancel;
private RecordTime recordTime;
private ValueAnimator quoteAnimator;
private ValueAnimator editMessageAnimator;
private Stub<VoiceNoteDraftView> voiceNoteDraftViewStub;
private MicrophoneRecorderView microphoneRecorderView;
private SlideToCancel slideToCancel;
private RecordTime recordTime;
private ValueAnimator quoteAnimator;
private ValueAnimator editMessageAnimator;
private VoiceNoteDraftView voiceNoteDraftView;
private @Nullable Listener listener;
private boolean emojiVisible;
private boolean wallpaperEnabled;
private boolean hideForMessageRequestState;
private boolean hideForGroupState;
@@ -129,12 +127,6 @@ public class InputPanel extends ConstraintLayout
private ConversationStickerSuggestionAdapter stickerSuggestionAdapter;
private MessageRecord messageToEdit;
private final Observer<VoiceNotePlaybackState> playbackStateObserverProxy = state -> {
if (voiceNoteDraftViewStub.resolved()) {
voiceNoteDraftViewStub.get().getPlaybackStateObserver().onChanged(state);
}
};
public InputPanel(Context context) {
super(context);
}
@@ -151,10 +143,12 @@ public class InputPanel extends ConstraintLayout
public void onFinishInflate() {
super.onFinishInflate();
View quoteDismiss = findViewById(R.id.quote_dismiss_stub);
this.composeContainer = findViewById(R.id.compose_bubble);
this.stickerSuggestion = findViewById(R.id.input_panel_sticker_suggestion);
this.quoteViewStub = new Stub<>(findViewById(R.id.quote_view));
this.linkPreviewStub = new Stub<>(findViewById(R.id.link_preview));
this.quoteView = findViewById(R.id.quote_view);
this.linkPreview = findViewById(R.id.link_preview);
this.mediaKeyboard = findViewById(R.id.emoji_toggle);
this.composeText = findViewById(R.id.embedded_text_editor);
this.composeTextContainer = findViewById(R.id.embedded_text_editor_container);
@@ -164,7 +158,7 @@ public class InputPanel extends ConstraintLayout
this.sendButton = findViewById(R.id.send_button);
this.recordingContainer = findViewById(R.id.recording_container);
this.recordLockCancel = findViewById(R.id.record_cancel);
this.voiceNoteDraftViewStub = new Stub<>(findViewById(R.id.voice_note_draft_view_stub));
this.voiceNoteDraftView = findViewById(R.id.voice_note_draft_view);
this.slideToCancel = new SlideToCancel(findViewById(R.id.slide_to_cancel));
this.microphoneRecorderView = findViewById(R.id.recorder_view);
this.microphoneRecorderView.setHandler(this);
@@ -181,6 +175,14 @@ public class InputPanel extends ConstraintLayout
mediaKeyboard.setVisibility(View.VISIBLE);
emojiVisible = true;
quoteDismiss.setOnClickListener(v -> clearQuote());
linkPreview.setCloseClickedListener(() -> {
if (listener != null) {
listener.onLinkPreviewCanceled();
}
});
stickerSuggestionAdapter = new ConversationStickerSuggestionAdapter(Glide.with(this), this);
stickerSuggestion.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false));
@@ -195,6 +197,7 @@ public class InputPanel extends ConstraintLayout
this.listener = listener;
mediaKeyboard.setOnClickListener(v -> listener.onEmojiToggle());
voiceNoteDraftView.setListener(listener);
if (Camera.getNumberOfCameras() > 0) {
quickCameraToggle.setOnClickListener(v -> listener.onQuickCameraToggleClicked());
@@ -211,35 +214,34 @@ public class InputPanel extends ConstraintLayout
@NonNull SlideDeck attachments,
@NonNull QuoteModel.Type quoteType)
{
QuoteView quoteView = requireQuoteView();
quoteView.setQuote(requestManager, id, author, body, false, attachments, null, quoteType, true, null);
this.quoteView.setQuote(requestManager, id, author, body, false, attachments, null, quoteType, true, null);
if (listener != null) {
quoteView.setOnClickListener(v -> listener.onQuoteClicked(id, author.getId()));
this.quoteView.setOnClickListener(v -> listener.onQuoteClicked(id, author.getId()));
}
int originalHeight = quoteView.getVisibility() == VISIBLE ? quoteView.getMeasuredHeight() : 0;
int originalHeight = this.quoteView.getVisibility() == VISIBLE ? this.quoteView.getMeasuredHeight()
: 0;
quoteView.setVisibility(VISIBLE);
this.quoteView.setVisibility(VISIBLE);
int maxWidth = composeContainer.getWidth();
if (quoteView.getLayoutParams() instanceof MarginLayoutParams) {
MarginLayoutParams layoutParams = (MarginLayoutParams) quoteView.getLayoutParams();
maxWidth -= layoutParams.leftMargin + layoutParams.rightMargin;
}
quoteView.measure(MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST), 0);
this.quoteView.measure(MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST), 0);
if (quoteAnimator != null) {
quoteAnimator.cancel();
}
quoteAnimator = createHeightAnimator(quoteView, originalHeight, quoteView.getMeasuredHeight(), null);
quoteAnimator = createHeightAnimator(quoteView, originalHeight, this.quoteView.getMeasuredHeight(), null);
quoteAnimator.start();
if (linkPreviewStub.getVisibility() == View.VISIBLE) {
if (this.linkPreview.getVisibility() == View.VISIBLE) {
int cornerRadius = readDimen(R.dimen.message_corner_collapse_radius);
linkPreviewStub.get().setCorners(cornerRadius, cornerRadius);
this.linkPreview.setCorners(cornerRadius, cornerRadius);
}
if (listener != null) {
@@ -248,12 +250,6 @@ public class InputPanel extends ConstraintLayout
}
public void clearQuote() {
if (!quoteViewStub.resolved()) {
return;
}
QuoteView quoteView = quoteViewStub.get();
if (quoteAnimator != null) {
quoteAnimator.cancel();
}
@@ -263,9 +259,9 @@ public class InputPanel extends ConstraintLayout
public void onAnimationEnd(Animator animation) {
quoteView.dismiss();
if (linkPreviewStub.getVisibility() == View.VISIBLE) {
if (linkPreview.getVisibility() == View.VISIBLE) {
int cornerRadius = readDimen(R.dimen.message_corner_radius);
linkPreviewStub.get().setCorners(cornerRadius, cornerRadius);
linkPreview.setCorners(cornerRadius, cornerRadius);
}
}
});
@@ -277,20 +273,6 @@ public class InputPanel extends ConstraintLayout
}
}
private @NonNull QuoteView requireQuoteView() {
boolean wasResolved = quoteViewStub.resolved();
QuoteView quoteView = quoteViewStub.get();
if (!wasResolved) {
quoteView.setWallpaperEnabled(wallpaperEnabled);
View quoteDismiss = quoteView.findViewById(R.id.quote_dismiss_stub);
if (quoteDismiss != null) {
quoteDismiss.setOnClickListener(v -> clearQuote());
}
}
return quoteView;
}
private static ValueAnimator createHeightAnimator(@NonNull View view,
int originalHeight,
int finalHeight,
@@ -312,12 +294,11 @@ public class InputPanel extends ConstraintLayout
return animator;
}
public Optional<QuoteModel> getQuote() {
if (!quoteViewStub.resolved()) {
return Optional.empty();
}
public boolean hasSaveableContent() {
return getQuote().isPresent() || voiceNoteDraftView.getDraft() != null;
}
QuoteView quoteView = quoteViewStub.get();
public Optional<QuoteModel> getQuote() {
if (quoteView.getQuoteId() > 0 && quoteView.getVisibility() == View.VISIBLE) {
return Optional.of(new QuoteModel(quoteView.getQuoteId(),
quoteView.getAuthor().getId(),
@@ -333,53 +314,41 @@ public class InputPanel extends ConstraintLayout
}
public boolean hasLinkPreview() {
return linkPreviewStub.getVisibility() == View.VISIBLE;
return linkPreview.getVisibility() == View.VISIBLE;
}
public void setLinkPreviewLoading() {
LinkPreviewView linkPreview = requireLinkPreview();
linkPreview.setVisibility(View.VISIBLE);
linkPreview.setLoading();
this.linkPreview.setVisibility(View.VISIBLE);
this.linkPreview.setLoading();
}
public void setLinkPreviewNoPreview(@Nullable LinkPreviewRepository.Error customError) {
LinkPreviewView linkPreview = requireLinkPreview();
linkPreview.setVisibility(View.VISIBLE);
linkPreview.setNoPreview(customError);
this.linkPreview.setVisibility(View.VISIBLE);
this.linkPreview.setNoPreview(customError);
}
public void setLinkPreview(@NonNull RequestManager requestManager, @NonNull Optional<LinkPreview> preview) {
if (preview.isPresent()) {
LinkPreviewView linkPreview = requireLinkPreview();
linkPreview.setVisibility(View.VISIBLE);
linkPreview.setLinkPreview(requestManager, preview.get(), true);
this.linkPreview.setVisibility(View.VISIBLE);
this.linkPreview.setLinkPreview(requestManager, preview.get(), true);
} else {
linkPreviewStub.setVisibility(View.GONE);
this.linkPreview.setVisibility(View.GONE);
}
if (linkPreviewStub.resolved()) {
int cornerRadius = quoteViewStub.getVisibility() == VISIBLE ? readDimen(R.dimen.message_corner_collapse_radius) : readDimen(R.dimen.message_corner_radius);
linkPreviewStub.get().setCorners(cornerRadius, cornerRadius);
}
}
int cornerRadius = quoteView.getVisibility() == VISIBLE ? readDimen(R.dimen.message_corner_collapse_radius)
: readDimen(R.dimen.message_corner_radius);
private @NonNull LinkPreviewView requireLinkPreview() {
boolean wasResolved = linkPreviewStub.resolved();
LinkPreviewView view = linkPreviewStub.get();
if (!wasResolved) {
view.setCloseClickedListener(() -> {
if (listener != null) listener.onLinkPreviewCanceled();
});
}
return view;
this.linkPreview.setCorners(cornerRadius, cornerRadius);
}
public void clickOnComposeInput() {
composeText.performClick();
}
public void setMediaKeyboard(@NonNull MediaKeyboard mediaKeyboard) {
this.mediaKeyboard.attach(mediaKeyboard);
}
public void setStickerSuggestions(@NonNull List<StickerRecord> stickers) {
stickerSuggestion.setVisibility(stickers.isEmpty() ? View.GONE : View.VISIBLE);
stickerSuggestionAdapter.setStickers(stickers);
@@ -434,10 +403,7 @@ public class InputPanel extends ConstraintLayout
quickCameraToggle.setColorFilter(iconTint);
composeText.setTextColor(textColor);
composeText.setHintTextColor(textHintColor);
wallpaperEnabled = enabled;
if (quoteViewStub.resolved()) {
quoteViewStub.get().setWallpaperEnabled(enabled);
}
quoteView.setWallpaperEnabled(enabled);
}
public void enterEditModeIfPossible(@NonNull RequestManager requestManager, @NonNull ConversationMessage conversationMessageToEdit, boolean fromDraft, boolean clearQuote) {
@@ -527,9 +493,7 @@ public class InputPanel extends ConstraintLayout
if (messageToEdit != null) {
composeText.setText("");
messageToEdit = null;
if (quoteViewStub.resolved()) {
quoteViewStub.get().setMessageType(QuoteView.MessageType.PREVIEW);
}
quoteView.setMessageType(QuoteView.MessageType.PREVIEW);
clearQuote();
}
updateEditModeUi();
@@ -683,7 +647,7 @@ public class InputPanel extends ConstraintLayout
}
public @NonNull Observer<VoiceNotePlaybackState> getPlaybackStateObserver() {
return playbackStateObserverProxy;
return voiceNoteDraftView.getPlaybackStateObserver();
}
public void setEnabled(boolean enabled) {
@@ -702,7 +666,7 @@ public class InputPanel extends ConstraintLayout
future.addListener(new AssertedSuccessListener<Void>() {
@Override
public void onSuccess(Void result) {
if (!voiceNoteDraftViewStub.resolved() || voiceNoteDraftViewStub.get().getDraft() == null) {
if (voiceNoteDraftView.getDraft() == null) {
fadeInNormalComposeViews();
}
}
@@ -716,6 +680,10 @@ public class InputPanel extends ConstraintLayout
mediaKeyboard.setToMedia();
}
public void setToIme() {
mediaKeyboard.setToIme();
}
@Override
public void onKeyEvent(KeyEvent keyEvent) {
composeText.dispatchKeyEvent(keyEvent);
@@ -747,35 +715,20 @@ public class InputPanel extends ConstraintLayout
public void setVoiceNoteDraft(@Nullable DraftTable.Draft voiceNoteDraft) {
if (voiceNoteDraft != null) {
VoiceNoteDraftView voiceNoteDraftView = requireVoiceNoteDraft();
voiceNoteDraftView.setDraft(voiceNoteDraft);
voiceNoteDraftView.setVisibility(VISIBLE);
hideNormalComposeViews();
fadeIn(buttonToggle);
buttonToggle.displayQuick(sendButton);
} else {
if (voiceNoteDraftViewStub.resolved()) {
VoiceNoteDraftView voiceNoteDraftView = voiceNoteDraftViewStub.get();
voiceNoteDraftView.clearDraft();
ViewUtil.fadeOut(voiceNoteDraftView, FADE_TIME);
}
voiceNoteDraftView.clearDraft();
ViewUtil.fadeOut(voiceNoteDraftView, FADE_TIME);
fadeInNormalComposeViews();
}
}
public @Nullable DraftTable.Draft getVoiceNoteDraft() {
if (!voiceNoteDraftViewStub.resolved()) return null;
return voiceNoteDraftViewStub.get().getDraft();
}
private @NonNull VoiceNoteDraftView requireVoiceNoteDraft() {
boolean wasResolved = voiceNoteDraftViewStub.resolved();
VoiceNoteDraftView voiceNoteDraftView = voiceNoteDraftViewStub.get();
if (!wasResolved) {
voiceNoteDraftView.setListener(listener);
}
return voiceNoteDraftView;
return voiceNoteDraftView.getDraft();
}
private void hideNormalComposeViews() {
@@ -434,7 +434,6 @@ public class QuoteView extends ConstraintLayout implements RecipientForeverObser
if (TextUtils.isEmpty(quoteTargetContentType)) {
thumbnailView.setVisibility(GONE);
attachmentVideoOVerlayStub.setVisibility(GONE);
attachmentNameViewStub.setVisibility(GONE);
if (dismissStub.resolved()) {
@@ -81,7 +81,6 @@ import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.SignalE164Util
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.signal.core.ui.R as CoreUiR
class AppSettingsFragment : ComposeFragment(), Callbacks {
@@ -296,7 +295,7 @@ private fun AppSettingsContent(
item {
Rows.TextRow(
text = stringResource(R.string.AccountSettingsFragment__account),
icon = painterResource(CoreUiR.drawable.symbol_person_circle_24),
icon = painterResource(R.drawable.symbol_person_circle_24),
onClick = {
callbacks.navigate(AppSettingsRoute.AccountRoute.Account)
}
@@ -306,7 +305,7 @@ private fun AppSettingsContent(
item {
Rows.TextRow(
text = stringResource(R.string.preferences__linked_devices),
icon = painterResource(CoreUiR.drawable.symbol_devices_24),
icon = painterResource(R.drawable.symbol_devices_24),
onClick = {
callbacks.navigate(AppSettingsRoute.LinkDeviceRoute.LinkDevice)
},
@@ -242,7 +242,7 @@ private fun BackupsSettingsContent(
item {
Rows.TextRow(
text = stringResource(R.string.RemoteBackupsSettingsFragment__on_device_backups),
icon = ImageVector.vectorResource(CoreUiR.drawable.symbol_device_phone_24),
icon = ImageVector.vectorResource(R.drawable.symbol_device_phone_24),
label = stringResource(R.string.RemoteBackupsSettingsFragment__save_your_backups_to),
onClick = onOnDeviceBackupsRowClick
)
@@ -6,12 +6,9 @@ import android.content.Context
import android.content.DialogInterface
import android.os.Build
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.setFragmentResultListener
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
@@ -77,10 +74,8 @@ import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.registration.data.QuickstartCredentialExporter
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.util.ConversationUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.thoughtcrime.securesms.util.setIncognitoKeyboardEnabled
import org.whispersystems.signalservice.api.push.UsernameLinkComponents
import java.util.Optional
import java.util.UUID
@@ -89,14 +84,13 @@ import kotlin.math.max
import kotlin.random.Random
import kotlin.time.Duration.Companion.milliseconds
class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__internal_preferences, R.menu.internal_settings) {
class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__internal_preferences) {
companion object {
private val TAG = Log.tag(InternalSettingsFragment::class.java)
}
private lateinit var viewModel: InternalSettingsViewModel
private var searchMenuItem: MenuItem? = null
private var scrollToPosition: Int = 0
private val layoutManager: LinearLayoutManager?
@@ -113,7 +107,6 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
scrollToPosition = SignalStore.internal.lastScrollPosition
initializeSearch(view)
setFragmentResultListener(CallQualityBottomSheetFragment.REQUEST_KEY) { _, bundle ->
if (bundle.getBoolean(CallQualityBottomSheetFragment.REQUEST_KEY, false)) {
@@ -132,11 +125,8 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
viewModel = ViewModelProvider(this, factory)[InternalSettingsViewModel::class.java]
viewModel.state.observe(viewLifecycleOwner) {
val mappingModelList = getConfiguration(it).toMappingModelList()
val filteredList = viewModel.filterPreferences(requireContext(), mappingModelList, it.searchQuery)
adapter.submitList(filteredList) {
if (scrollToPosition != 0 && it.searchQuery.isBlank()) {
adapter.submitList(getConfiguration(it).toMappingModelList()) {
if (scrollToPosition != 0) {
layoutManager?.scrollToPositionWithOffset(scrollToPosition, 0)
scrollToPosition = 0
}
@@ -144,56 +134,6 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
}
}
override fun onToolbarNavigationClicked() {
if (searchMenuItem?.isActionViewExpanded == true) {
searchMenuItem?.collapseActionView()
} else {
super.onToolbarNavigationClicked()
}
}
private fun initializeSearch(view: View) {
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
searchMenuItem = toolbar.menu.findItem(R.id.menu_search)
val searchView: SearchView = searchMenuItem?.actionView as? SearchView ?: return
val queryListener = object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
searchView.clearFocus()
viewModel.setSearchQuery(query.orEmpty())
return true
}
override fun onQueryTextChange(newText: String?): Boolean {
viewModel.setSearchQuery(newText.orEmpty())
return true
}
}
searchView.maxWidth = Integer.MAX_VALUE
searchView.queryHint = getString(R.string.CameraContacts__menu_search)
searchMenuItem?.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
searchView.setIncognitoKeyboardEnabled(TextSecurePreferences.isIncognitoKeyboardEnabled(requireContext()))
searchView.setOnQueryTextListener(queryListener)
return true
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
searchView.setOnQueryTextListener(null)
searchView.setQuery("", false)
viewModel.setSearchQuery("")
return true
}
})
val currentQuery = viewModel.state.value?.searchQuery.orEmpty()
if (currentQuery.isNotBlank() && searchMenuItem?.expandActionView() == true) {
searchView.setQuery(currentQuery, false)
}
}
private fun getConfiguration(state: InternalSettingsState): DSLConfiguration {
return configure {
sectionHeaderPref(DSLSettingsText.from("Account"))
@@ -32,6 +32,5 @@ data class InternalSettingsState(
val forceSplitPane: Boolean,
val forceSinglePane: Boolean,
val useNewMediaActivity: Boolean,
val disableInternalUser: Boolean,
val searchQuery: String = ""
val disableInternalUser: Boolean
)
@@ -1,14 +1,10 @@
package org.thoughtcrime.securesms.components.settings.app.internal
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.core.Observable
import org.signal.ringrtc.CallManager
import org.thoughtcrime.securesms.components.settings.DividerPreference
import org.thoughtcrime.securesms.components.settings.PreferenceModel
import org.thoughtcrime.securesms.components.settings.SectionHeaderPreference
import org.thoughtcrime.securesms.database.model.RemoteMegaphoneRecord
import org.thoughtcrime.securesms.jobs.StoryOnboardingDownloadJob
import org.thoughtcrime.securesms.keyvalue.InternalValues
@@ -16,10 +12,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModelList
import org.thoughtcrime.securesms.util.livedata.Store
import java.util.Locale
class InternalSettingsViewModel(private val repository: InternalSettingsRepository) : ViewModel() {
private val preferenceDataStore = SignalStore.getPreferenceDataStore()
@@ -174,47 +167,7 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
}
fun refresh() {
store.update { getState().copy(emojiVersion = it.emojiVersion, searchQuery = it.searchQuery) }
}
fun setSearchQuery(query: String) {
store.update {
if (it.searchQuery == query) {
it
} else {
it.copy(searchQuery = query)
}
}
}
fun filterPreferences(context: Context, items: MappingModelList, query: String): MappingModelList {
val normalizedQuery = query.trim().lowercase(Locale.getDefault())
if (normalizedQuery.isBlank()) {
return items
}
val groups = buildSearchGroups(items)
val filtered = MappingModelList()
groups.forEach { group ->
val headerMatches = group.header?.searchableText(context)?.contains(normalizedQuery) == true
val matchingItems = if (headerMatches) {
group.items
} else {
group.items.filter { it.searchableText(context)?.contains(normalizedQuery) == true }
}
if (headerMatches || matchingItems.isNotEmpty()) {
if (filtered.isNotEmpty() && group.divider != null) {
filtered.add(group.divider)
}
group.header?.let { filtered.add(it) }
filtered.addAll(matchingItems)
}
}
return filtered
store.update { getState().copy(emojiVersion = it.emojiVersion) }
}
private fun getState() = InternalSettingsState(
@@ -272,57 +225,6 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
refresh()
}
private fun buildSearchGroups(items: MappingModelList): List<SearchGroup> {
val groups = mutableListOf<SearchGroup>()
var divider: DividerPreference? = null
var header: SectionHeaderPreference? = null
var groupItems = mutableListOf<MappingModel<*>>()
fun flush() {
if (header != null || groupItems.isNotEmpty()) {
groups.add(SearchGroup(divider, header, groupItems))
}
divider = null
header = null
groupItems = mutableListOf()
}
items.forEach { item ->
when (item) {
is DividerPreference -> {
flush()
divider = item
}
is SectionHeaderPreference -> {
flush()
header = item
}
else -> groupItems.add(item)
}
}
flush()
return groups
}
private fun MappingModel<*>.searchableText(context: Context): String? {
return if (this is PreferenceModel<*>) {
listOfNotNull(title, summary)
.joinToString(separator = " ") { it.resolve(context).toString() }
.lowercase(Locale.getDefault())
} else {
null
}
}
private data class SearchGroup(
val divider: DividerPreference?,
val header: SectionHeaderPreference?,
val items: List<MappingModel<*>>
)
class Factory(private val repository: InternalSettingsRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return requireNotNull(modelClass.cast(InternalSettingsViewModel(repository)))
@@ -162,18 +162,11 @@ class ManageDonationsViewModel : ViewModel() {
private fun deriveRedemptionState(status: DonationRedemptionJobStatus, latestPayment: InAppPaymentTable.InAppPayment?): ManageDonationsState.RedemptionState {
return when (status) {
DonationRedemptionJobStatus.None -> ManageDonationsState.RedemptionState.NONE
DonationRedemptionJobStatus.PendingKeepAlive -> ManageDonationsState.RedemptionState.SUBSCRIPTION_REFRESH
DonationRedemptionJobStatus.FailedSubscription -> ManageDonationsState.RedemptionState.FAILED
DonationRedemptionJobStatus.PendingKeepAlive -> {
if (latestPayment.isPendingBankTransfer()) {
ManageDonationsState.RedemptionState.IS_PENDING_BANK_TRANSFER
} else {
ManageDonationsState.RedemptionState.SUBSCRIPTION_REFRESH
}
}
is DonationRedemptionJobStatus.PendingExternalVerification -> {
if (latestPayment.isPendingBankTransfer()) {
if (latestPayment != null && (latestPayment.data.paymentMethodType == InAppPaymentData.PaymentMethodType.SEPA_DEBIT || latestPayment.data.paymentMethodType == InAppPaymentData.PaymentMethodType.IDEAL)) {
ManageDonationsState.RedemptionState.IS_PENDING_BANK_TRANSFER
} else {
ManageDonationsState.RedemptionState.IN_PROGRESS
@@ -185,10 +178,6 @@ class ManageDonationsViewModel : ViewModel() {
}
}
private fun InAppPaymentTable.InAppPayment?.isPendingBankTransfer(): Boolean {
return this != null && (data.paymentMethodType == InAppPaymentData.PaymentMethodType.SEPA_DEBIT || data.paymentMethodType == InAppPaymentData.PaymentMethodType.IDEAL)
}
private fun InAppPaymentTable.InAppPayment.toPendingOneTimeDonation(): PendingOneTimeDonation? {
if (type.recurring || data.amount == null || data.badge == null) {
return null
@@ -7,12 +7,7 @@ package org.thoughtcrime.securesms.components.settings.conversation
import androidx.annotation.WorkerThread
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.withStyle
import org.signal.core.util.Base64
import org.signal.core.util.Hex
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
@@ -103,18 +98,18 @@ data class InternalConversationSettingsState(
val capabilities: RecipientRecord.Capabilities? = SignalDatabase.recipients.getCapabilities(recipient.id)
if (capabilities != null) {
AnnotatedString("No capabilities right now.")
// Always leave one as an example in case we add one in the future
val style: SpanStyle = when (capabilities.usernameSyncMessages) {
Recipient.Capability.SUPPORTED -> SpanStyle(color = Color(0, 150, 0))
Recipient.Capability.NOT_SUPPORTED -> SpanStyle(color = Color.Red)
Recipient.Capability.UNKNOWN -> SpanStyle(fontStyle = FontStyle.Italic)
}
buildAnnotatedString {
withStyle(style = style) {
append("usernameSyncMessages")
}
}
// Left as an example in case we add one in the future
// val style: SpanStyle = when (capabilities.storageServiceEncryptionV2) {
// Recipient.Capability.SUPPORTED -> SpanStyle(color = Color(0, 150, 0))
// Recipient.Capability.NOT_SUPPORTED -> SpanStyle(color = Color.Red)
// Recipient.Capability.UNKNOWN -> SpanStyle(fontStyle = FontStyle.Italic)
// }
//
// buildAnnotatedString {
// withStyle(style = style) {
// append("SSREv2")
// }
// }
} else {
AnnotatedString("Recipient not found!")
}
@@ -7,13 +7,11 @@ import org.thoughtcrime.securesms.database.CallTable
import org.thoughtcrime.securesms.database.MessageTypes
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.databinding.ConversationSettingsCallPreferenceItemBinding
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.adapter.mapping.BindingFactory
import org.thoughtcrime.securesms.util.adapter.mapping.BindingViewHolder
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
import org.thoughtcrime.securesms.util.visible
/**
* Renders a single call preference row when displaying call info.
@@ -43,25 +41,6 @@ object CallPreference {
binding.callIcon.setImageResource(getCallIcon(model.call))
binding.callType.text = getCallType(model.call)
binding.callTime.text = getCallTime(model.record)
presentTimer(model.record)
}
private fun presentTimer(messageRecord: MessageRecord) {
if (messageRecord.expiresIn > 0) {
binding.callTimer.visible = true
binding.callTimer.setPercentComplete(0f)
if (messageRecord.expireStarted > 0) {
binding.callTimer.setExpirationTime(messageRecord.expireStarted, messageRecord.expiresIn)
binding.callTimer.startAnimation()
if (messageRecord.expireStarted + messageRecord.expiresIn <= System.currentTimeMillis()) {
AppDependencies.expiringMessageManager.checkSchedule()
}
}
} else {
binding.callTimer.visible = false
}
}
@DrawableRes
@@ -105,7 +105,7 @@ object RecipientPreference {
} else {
if (recipient.isSystemContact) {
SpannableStringBuilder(recipient.getDisplayName(context)).apply {
val drawable = context.requireDrawable(CoreUiR.drawable.symbol_person_circle_24).apply {
val drawable = context.requireDrawable(R.drawable.symbol_person_circle_24).apply {
setTint(ContextCompat.getColor(context, CoreUiR.color.signal_colorOnSurface))
}
SpanUtil.appendCenteredImageSpan(this, drawable, 16, 16)
@@ -7,6 +7,7 @@ import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Process;
@@ -115,7 +116,7 @@ public class VoiceNotePlaybackService extends MediaSessionService {
@Nullable
@Override
public MediaSession onGetSession(@NonNull MediaSession.ControllerInfo controllerInfo) {
if (controllerInfo.getUid() != Process.myUid()) {
if (Build.VERSION.SDK_INT >= 28 && controllerInfo.getUid() != Process.myUid()) {
Log.w(TAG, "Denying session to external caller: " + controllerInfo.getPackageName());
return null;
}
@@ -221,7 +221,7 @@ private fun Title(
style = MaterialTheme.typography.headlineMedium
)
Icon(
painter = painterResource(id = CoreUiR.drawable.symbol_person_circle_24),
painter = painterResource(id = R.drawable.symbol_person_circle_24),
contentDescription = null,
modifier = Modifier
.padding(start = 6.dp)
@@ -190,7 +190,7 @@ fun SelfPipContent(
Box(modifier = modifier) {
VideoRenderer(
participant = participant,
mirror = !participant.isScreenSharing && participant.cameraDirection == CameraState.Direction.FRONT,
mirror = participant.cameraDirection == CameraState.Direction.FRONT,
modifier = Modifier.fillMaxSize()
)
@@ -543,7 +543,7 @@ private fun LargeLocalVideoRenderer(
participant = localParticipant,
renderInPip = false,
raiseHandAllowed = false,
mirrorVideo = !localParticipant.isScreenSharing && localParticipant.cameraDirection == CameraState.Direction.FRONT,
mirrorVideo = localParticipant.cameraDirection == CameraState.Direction.FRONT,
showAudioIndicator = false,
onInfoMoreInfoClick = null,
modifier = modifier
@@ -49,7 +49,6 @@ import org.signal.core.ui.compose.Previews
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.webrtc.WebRtcLocalRenderState
import org.thoughtcrime.securesms.events.CallParticipant
import org.signal.core.ui.R as CoreUiR
/**
* Small moveable local video renderer that displays the user's video in a draggable and expandable view.
@@ -161,7 +160,7 @@ fun MoveableLocalVideoRenderer(
) {
Icon(
imageVector = ImageVector.vectorResource(
if (isFocused) R.drawable.symbol_minimize_24 else CoreUiR.drawable.symbol_maximize_24
if (isFocused) R.drawable.symbol_minimize_24 else R.drawable.symbol_maximize_24
),
tint = MaterialTheme.colorScheme.onSecondaryContainer,
contentDescription = stringResource(
@@ -75,7 +75,7 @@ fun PictureInPictureCallScreen(
renderInPip = true,
raiseHandAllowed = false,
onInfoMoreInfoClick = null,
mirrorVideo = isFullScreenLocalParticipant && !fullScreenParticipant.isScreenSharing,
mirrorVideo = isFullScreenLocalParticipant,
modifier = Modifier.fillMaxSize()
)
@@ -643,7 +643,7 @@ object ContactSearchModels {
val recipient = getRecipient(model)
val suffix: CharSequence? = if (recipient.isSystemContact && !recipient.showVerified) {
SpannableStringBuilder().apply {
val drawable = context.requireDrawable(CoreUiR.drawable.symbol_person_circle_24).apply {
val drawable = context.requireDrawable(R.drawable.symbol_person_circle_24).apply {
setTint(ContextCompat.getColor(context, CoreUiR.color.signal_colorOnSurface))
}
SpanUtil.appendCenteredImageSpan(this, drawable, 16, 16)
@@ -10,7 +10,7 @@ public enum AttachmentKeyboardButton {
GALLERY(R.string.AttachmentKeyboard_gallery, R.drawable.symbol_album_tilt_24),
FILE(R.string.AttachmentKeyboard_file, R.drawable.symbol_file_24),
PAYMENT(R.string.AttachmentKeyboard_payment, R.drawable.symbol_payment_24),
CONTACT(R.string.AttachmentKeyboard_contact, org.signal.core.ui.R.drawable.symbol_person_circle_24),
CONTACT(R.string.AttachmentKeyboard_contact, R.drawable.symbol_person_circle_24),
LOCATION(R.string.AttachmentKeyboard_location, R.drawable.symbol_location_circle_24),
POLL(R.string.AttachmentKeyboard_poll, R.drawable.symbol_poll_24);
@@ -19,6 +19,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.LifecycleOwner;
@@ -103,7 +104,6 @@ public final class ConversationUpdateItem extends FrameLayout
private EventListener eventListener;
private Button collapsedButton;
private float lastYDownRelativeToThis;
private int tint;
private final UpdateObserver updateObserver = new UpdateObserver();
@@ -221,8 +221,6 @@ public final class ConversationUpdateItem extends FrameLayout
observeDisplayBody(lifecycleOwner, spannableMessage);
observeDisplayBodyWithTimer(lifecycleOwner);
this.tint = updateDescription.getTint(getContext());
boolean donationRequest = conversationMessage.getMessageRecord().isReleaseChannelDonationRequest();
present(conversationMessage, nextMessageRecord, conversationRecipient, isMessageRequestAccepted);
@@ -489,9 +487,8 @@ public final class ConversationUpdateItem extends FrameLayout
SpannableStringBuilder builder = new SpannableStringBuilder(displayBody);
int color = tint != 0 ? tint : ContextCompat.getColor(getContext(), R.color.signal_icon_tint_secondary);
if (latestFrame != 0) {
Drawable drawable = DrawableUtil.tint(getContext().getDrawable(latestFrame), color);
Drawable drawable = DrawableUtil.tint(getContext().getDrawable(latestFrame), ContextCompat.getColor(getContext(), R.color.signal_icon_tint_secondary));
SpanUtil.appendCenteredImageSpan(builder, drawable, 12, 12);
}
@@ -787,7 +784,7 @@ public final class ConversationUpdateItem extends FrameLayout
return false;
}
return (messageRecord.isCollapsedGroupV2JoinUpdate(toBlock.requireServiceId()) && !nextMessageRecord.map(m -> m.isGroupV2JoinRequest(toBlock.requireServiceId())).orElse(false)) ||
return (messageRecord.isCollapsedGroupV2JoinUpdate() && !nextMessageRecord.map(m -> m.isGroupV2JoinRequest(toBlock.requireServiceId())).orElse(false)) ||
(messageRecord.isGroupV2JoinRequest(toBlock.requireServiceId()) && previousMessageRecord.map(m -> m.isCollapsedGroupV2JoinUpdate(toBlock.requireServiceId())).orElse(false));
}
@@ -41,10 +41,6 @@ class ExpirationTimer(
}
fun calculateProgress(): Float {
if (startedAt == 0L) {
return 0f
}
val progressed = System.currentTimeMillis() - startedAt
val percentComplete = progressed.toFloat() / expiresIn.toFloat()
@@ -2882,11 +2882,10 @@ class ConversationFragment :
requireContext(),
recipient,
{
val disabledInput = binding.conversationDisabledInput
messageRequestViewModel
.onReportSpam()
.doOnSubscribe { disabledInput.showBusy() }
.doOnTerminate { disabledInput.hideBusy() }
.doOnSubscribe { binding.conversationDisabledInput.showBusy() }
.doOnTerminate { binding.conversationDisabledInput.hideBusy() }
.subscribeBy {
Log.d(TAG, "report spam complete")
toast(R.string.ConversationFragment_reported_as_spam)
@@ -2896,11 +2895,10 @@ class ConversationFragment :
null
} else {
Runnable {
val disabledInput = binding.conversationDisabledInput
messageRequestViewModel
.onBlockAndReportSpam()
.doOnSubscribe { disabledInput.showBusy() }
.doOnTerminate { disabledInput.hideBusy() }
.doOnSubscribe { binding.conversationDisabledInput.showBusy() }
.doOnTerminate { binding.conversationDisabledInput.hideBusy() }
.subscribeBy { result ->
when (result) {
is Result.Success -> {
@@ -2959,6 +2957,7 @@ class ConversationFragment :
messageRequestViewModel
.onAccept()
.subscribeWithShowProgress("accept message request")
.addTo(disposables)
}
private fun onDeleteConversation() {
@@ -2977,9 +2976,8 @@ class ConversationFragment :
}
private fun Single<Result<Unit, GroupChangeFailureReason>>.subscribeWithShowProgress(logMessage: String): Disposable {
val disabledInput = binding.conversationDisabledInput
return doOnSubscribe { disabledInput.showBusy() }
.doOnTerminate { disabledInput.hideBusy() }
return doOnSubscribe { binding.conversationDisabledInput.showBusy() }
.doOnTerminate { binding.conversationDisabledInput.hideBusy() }
.subscribeBy { result ->
when (result) {
is Result.Success -> Log.d(TAG, "$logMessage complete")
@@ -246,7 +246,6 @@ class ConversationViewModel(
disposables += chatColors.update(chatColorsDataObservable.toFlowable(BackpressureStrategy.LATEST)) { c, _ -> c }
disposables += repository.getConversationThreadState(threadId, requestedStartingPosition)
.subscribeOn(Schedulers.io())
.subscribeBy(onSuccess = {
pagingController.set(it.items.controller)
_conversationThreadState.onNext(it)
@@ -16,7 +16,6 @@ import android.widget.TextView
import androidx.core.content.ContextCompat
import com.google.android.material.button.MaterialButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.messagerequests.MessageRequestState
import org.thoughtcrime.securesms.messagerequests.MessageRequestsBottomView
@@ -40,10 +39,6 @@ class DisabledInputView @JvmOverloads constructor(
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
companion object {
private val TAG = Log.tag(DisabledInputView::class.java)
}
private val inflater: LayoutInflater by lazy { LayoutInflater.from(context) }
private var expiredOrUnauthorized: View? = null
@@ -98,51 +93,30 @@ class DisabledInputView @JvmOverloads constructor(
setWallpaperEnabled(recipient.hasWallpaper)
setAcceptOnClickListener {
Log.i(TAG, "[message-request] Accept tapped. isIndividual: ${messageRequestState.isIndividual}, isGroupV2Add: ${messageRequestState.isGroupV2Add}, listener present: ${listener != null}")
if (messageRequestState.isIndividual) {
val signalWillNever = context.getString(R.string.MessageRequestBottomView_signal_will_never)
val body = context.getString(R.string.MessageRequestBottomView_accept_request_body, signalWillNever)
MaterialAlertDialogBuilder(context)
.setTitle(R.string.MessageRequestBottomView_accept_request)
.setMessage(SpanUtil.boldSubstring(body, signalWillNever))
.setCancelable(false)
.setPositiveButton(R.string.MessageRequestBottomView_accept) { _, _ ->
Log.i(TAG, "[message-request] Individual request confirmed. listener present: ${listener != null}")
listener?.onAcceptMessageRequestClicked()
}
.setNegativeButton(android.R.string.cancel) { _, _ -> Log.i(TAG, "[message-request] Individual request canceled.") }
.setPositiveButton(R.string.MessageRequestBottomView_accept) { _, _ -> listener?.onAcceptMessageRequestClicked() }
.setNegativeButton(android.R.string.cancel, null)
.show()
} else if (messageRequestState.isGroupV2Add) {
MaterialAlertDialogBuilder(context)
.setTitle(R.string.MessageRequestBottomView_join_group)
.setMessage(R.string.MessageRequestBottomView_review_requests_carefully_groups)
.setCancelable(false)
.setPositiveButton(R.string.MessageRequestBottomView_join) { _, _ ->
Log.i(TAG, "[message-request] Group join confirmed. listener present: ${listener != null}")
listener?.onAcceptMessageRequestClicked()
}
.setNegativeButton(android.R.string.cancel) { _, _ -> Log.i(TAG, "[message-request] Group join canceled.") }
.setPositiveButton(R.string.MessageRequestBottomView_join) { _, _ -> listener?.onAcceptMessageRequestClicked() }
.setNegativeButton(android.R.string.cancel, null)
.show()
} else {
listener?.onAcceptMessageRequestClicked()
}
}
setDeleteOnClickListener {
Log.i(TAG, "[message-request] Delete tapped. listener present: ${listener != null}")
listener?.onDeleteClicked()
}
setBlockOnClickListener {
Log.i(TAG, "[message-request] Block tapped. listener present: ${listener != null}")
listener?.onBlockClicked()
}
setUnblockOnClickListener {
Log.i(TAG, "[message-request] Unblock tapped. listener present: ${listener != null}")
listener?.onUnblockClicked()
}
setReportOnClickListener {
Log.i(TAG, "[message-request] Report tapped. listener present: ${listener != null}")
listener?.onReportSpamClicked()
}
setDeleteOnClickListener { listener?.onDeleteClicked() }
setBlockOnClickListener { listener?.onBlockClicked() }
setUnblockOnClickListener { listener?.onUnblockClicked() }
setReportOnClickListener { listener?.onReportSpamClicked() }
}
)
}
@@ -167,7 +167,6 @@ import org.signal.core.util.ServiceUtil;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
import org.thoughtcrime.securesms.util.SignalProxyUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.SnapToTopDataObserver;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.adapter.mapping.PagingMappingAdapter;
@@ -436,9 +435,7 @@ public class ConversationListFragment extends MainFragment implements Conversati
onSearchQueryUpdated(query);
}
if (SignalStore.account().isRegistered() &&
!TextSecurePreferences.isUnauthorizedReceived(requireContext()) &&
SignalStore.settings().getAutomaticVerificationEnabled() &&
if (SignalStore.settings().getAutomaticVerificationEnabled() &&
SignalStore.misc().getHasKeyTransparencyFailure() &&
!SignalStore.misc().getHasSeenKeyTransparencyFailure()) {
SelfVerificationFailureSheet.show(getParentFragmentManager());
@@ -245,7 +245,7 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
SpannableStringBuilder suffix = null;
if (appendSystemContactIcon && recipient.get().isSystemContact() && !recipient.get().getShowVerified()) {
suffix = new SpannableStringBuilder();
Drawable drawable = ContextUtil.requireDrawable(getContext(), org.signal.core.ui.R.drawable.symbol_person_circle_24);
Drawable drawable = ContextUtil.requireDrawable(getContext(), R.drawable.symbol_person_circle_24);
drawable.setTint(ContextCompat.getColor(getContext(), org.signal.core.ui.R.color.signal_colorOnSurface));
SpanUtil.appendCenteredImageSpan(suffix, drawable, 16, 16);
}
@@ -629,8 +629,12 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
return emphasisAdded(context, context.getString(R.string.ConversationListItem_key_exchange_message), defaultTint);
} else if (MessageTypes.isChatSessionRefresh(thread.getType())) {
return emphasisAdded(context, context.getString(R.string.ThreadRecord_chat_session_refreshed), Glyph.REFRESH, defaultTint);
} else if (MessageTypes.isNoRemoteSessionType(thread.getType())) {
return emphasisAdded(context, context.getString(R.string.MessageDisplayHelper_message_encrypted_for_non_existing_session), defaultTint);
} else if (MessageTypes.isEndSessionType(thread.getType())) {
return emphasisAdded(context, context.getString(R.string.ThreadRecord_secure_session_reset), defaultTint);
} else if (MessageTypes.isLegacyType(thread.getType())) {
return emphasisAdded(context, context.getString(R.string.MessageRecord_message_encrypted_with_a_legacy_protocol_version_that_is_no_longer_supported), defaultTint);
} else if (thread.isScheduledMessage()) {
return emphasisAdded(context, context.getString(R.string.ThreadRecord_scheduled_message), Glyph.CALENDAR, defaultTint);
} else if (MessageTypes.isDraftMessageType(thread.getType())) {
@@ -712,15 +716,15 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
MessageStyler.style(thread.getDate(), thread.getBodyRanges(), sourceBody);
CharSequence body = StringUtil.replace(sourceBody, '\n', " ");
LiveData<SpannableString> finalBody = Transformations.switchMap(createFinalBodyWithMediaIcon(context, body, thread, requestManager, thumbSize, thumbTarget), updatedBody -> {
LiveData<SpannableString> finalBody = Transformations.map(createFinalBodyWithMediaIcon(context, body, thread, requestManager, thumbSize, thumbTarget), updatedBody -> {
if (thread.getRecipient().isGroup()) {
RecipientId groupMessageSender = thread.getGroupMessageSender();
if (!groupMessageSender.isUnknown()) {
return Transformations.map(Recipient.live(groupMessageSender).getLiveDataResolved(), recipient -> createGroupMessageUpdateString(context, updatedBody, recipient));
return createGroupMessageUpdateString(context, updatedBody, Recipient.resolved(groupMessageSender));
}
}
return LiveDataUtil.just(new SpannableString(updatedBody));
return new SpannableString(updatedBody);
});
return whileLoadingShow(sourceBody, finalBody);
@@ -7,7 +7,6 @@ package org.thoughtcrime.securesms.database
import android.content.Context
import android.database.Cursor
import org.signal.core.util.SqlUtil
import org.signal.core.util.Util
import org.signal.core.util.delete
import org.signal.core.util.insertInto
@@ -85,23 +84,6 @@ class AttachmentMetadataTable(context: Context, databaseHelper: SignalDatabase)
.run()
}
fun cleanupById(ids: Collection<Long>) {
if (ids.isEmpty()) {
return
}
val idClause = SqlUtil.buildSingleCollectionQuery(ID, ids)
val attachmentReferenceClause = SqlUtil.buildSingleCollectionQuery(AttachmentTable.METADATA_ID, ids)
writableDatabase
.delete(TABLE_NAME)
.where(
"${idClause.where} AND $ID NOT IN (SELECT ${AttachmentTable.METADATA_ID} FROM ${AttachmentTable.TABLE_NAME} WHERE ${attachmentReferenceClause.where})",
idClause.whereArgs + attachmentReferenceClause.whereArgs
)
.run()
}
fun insertNewKeysForExistingAttachments() {
writableDatabase.withinTransaction {
do {
@@ -298,10 +298,7 @@ class AttachmentTable(
"CREATE INDEX IF NOT EXISTS attachment_archive_transfer_state ON $TABLE_NAME ($ARCHIVE_TRANSFER_STATE);",
"CREATE INDEX IF NOT EXISTS attachment_remote_digest_index ON $TABLE_NAME ($REMOTE_DIGEST);",
"CREATE INDEX IF NOT EXISTS attachment_metadata_id ON $TABLE_NAME ($METADATA_ID);",
"CREATE INDEX IF NOT EXISTS attachment_media_overview_size ON $TABLE_NAME ($DATA_SIZE DESC, $DISPLAY_ORDER DESC) WHERE $QUOTE = 0 AND $STICKER_PACK_ID IS NULL AND $DATA_FILE IS NOT NULL",
"CREATE INDEX IF NOT EXISTS attachment_archive_thumbnail_transfer_state ON $TABLE_NAME ($ARCHIVE_THUMBNAIL_TRANSFER_STATE)",
"CREATE INDEX IF NOT EXISTS attachment_thumbnail_file_index ON $TABLE_NAME ($THUMBNAIL_FILE) WHERE $THUMBNAIL_FILE IS NOT NULL",
"CREATE INDEX IF NOT EXISTS attachment_uuid_index ON $TABLE_NAME ($ATTACHMENT_UUID) WHERE $ATTACHMENT_UUID IS NOT NULL"
"CREATE INDEX IF NOT EXISTS attachment_media_overview_size ON $TABLE_NAME ($DATA_SIZE DESC, $DISPLAY_ORDER DESC) WHERE $QUOTE = 0 AND $STICKER_PACK_ID IS NULL AND $DATA_FILE IS NOT NULL"
)
private val DATA_FILE_INFO_PROJECTION = arrayOf(
@@ -375,7 +372,7 @@ class AttachmentTable(
val hashEnd = Base64.encodeWithPadding(hash)
val (existingFile: String?, existingSize: Long?, existingRandom: ByteArray?) = db.select(DATA_FILE, DATA_SIZE, DATA_RANDOM)
.from("$TABLE_NAME INDEXED BY $DATA_HASH_REMOTE_KEY_INDEX")
.from(TABLE_NAME)
.where("$DATA_HASH_END = ? AND $TRANSFER_STATE = $TRANSFER_PROGRESS_DONE AND $DATA_FILE NOT NULL AND $DATA_FILE != ?", hashEnd, file.absolutePath)
.limit(1)
.run()
@@ -1304,9 +1301,7 @@ class AttachmentTable(
val contentTypesToDelete: MutableSet<String> = mutableSetOf()
val deleteCount = writableDatabase.withinTransaction { db ->
val metadataIdsToCleanup: MutableSet<Long> = mutableSetOf()
db.select(DATA_FILE, CONTENT_TYPE, ID, METADATA_ID)
db.select(DATA_FILE, CONTENT_TYPE, ID)
.from(TABLE_NAME)
.where("$MESSAGE_ID = ?", mmsId)
.run()
@@ -1318,8 +1313,6 @@ class AttachmentTable(
val filePath = cursor.requireString(DATA_FILE)
val contentType = cursor.requireString(CONTENT_TYPE)
cursor.requireLongOrNull(METADATA_ID)?.let { metadataIdsToCleanup += it }
if (filePath != null && isSafeToDeleteDataFile(filePath, attachmentId)) {
filePathsToDelete += filePath
contentType?.let { contentTypesToDelete += it }
@@ -1330,7 +1323,7 @@ class AttachmentTable(
.where("$MESSAGE_ID = ?", mmsId)
.run()
SignalDatabase.attachmentMetadata.cleanupById(metadataIdsToCleanup)
SignalDatabase.attachmentMetadata.cleanup()
AppDependencies.databaseObserver.notifyAttachmentDeletedObservers()
@@ -1374,9 +1367,7 @@ class AttachmentTable(
var threadId: Long = -1
writableDatabase.withinTransaction { db ->
val metadataIdsToCleanup: MutableSet<Long> = mutableSetOf()
db.select(DATA_FILE, CONTENT_TYPE, ID, METADATA_ID)
db.select(DATA_FILE, CONTENT_TYPE, ID)
.from(TABLE_NAME)
.where("$MESSAGE_ID = ?", messageId)
.run()
@@ -1384,7 +1375,6 @@ class AttachmentTable(
val filePath = cursor.requireString(DATA_FILE)
val contentType = cursor.requireString(CONTENT_TYPE)
val id = AttachmentId(cursor.requireLong(ID))
cursor.requireLongOrNull(METADATA_ID)?.let { metadataIdsToCleanup += it }
if (filePath != null && isSafeToDeleteDataFile(filePath, id)) {
filePathsToDelete += filePath
@@ -1418,7 +1408,7 @@ class AttachmentTable(
.where("$MESSAGE_ID = ?", messageId)
.run()
SignalDatabase.attachmentMetadata.cleanupById(metadataIdsToCleanup)
SignalDatabase.attachmentMetadata.cleanup()
AppDependencies.databaseObserver.notifyAttachmentDeletedObservers()
@@ -1443,7 +1433,7 @@ class AttachmentTable(
var deletedMessageId: Long? = null
writableDatabase.withinTransaction { db ->
db.select(DATA_FILE, CONTENT_TYPE, MESSAGE_ID, METADATA_ID)
db.select(DATA_FILE, CONTENT_TYPE, MESSAGE_ID)
.from(TABLE_NAME)
.where("$ID = ?", id.id)
.run()
@@ -1456,13 +1446,12 @@ class AttachmentTable(
val filePath = cursor.requireString(DATA_FILE)
val contentType = cursor.requireString(CONTENT_TYPE)
deletedMessageId = cursor.requireLong(MESSAGE_ID)
val metadataId = cursor.requireLongOrNull(METADATA_ID)
db.delete(TABLE_NAME)
.where("$ID = ?", id.id)
.run()
SignalDatabase.attachmentMetadata.cleanupById(listOfNotNull(metadataId))
SignalDatabase.attachmentMetadata.cleanup()
if (filePath != null && isSafeToDeleteDataFile(filePath, id)) {
filePathsToDelete += filePath
@@ -1676,24 +1665,6 @@ class AttachmentTable(
}
}
/**
* Marks any restorable attachment whose [MESSAGE_ID] does not point to an existing message as failed.
* @return the number of rows updated
*/
fun markRestorableAttachmentsWithoutMessageAsFailed(): Int {
return writableDatabase
.update(TABLE_NAME)
.values(TRANSFER_STATE to TRANSFER_PROGRESS_FAILED)
.where(
"""
($TRANSFER_STATE = $TRANSFER_NEEDS_RESTORE OR $TRANSFER_STATE = $TRANSFER_RESTORE_IN_PROGRESS) AND
$MESSAGE_ID > 0 AND
$MESSAGE_ID NOT IN (SELECT ${MessageTable.ID} FROM ${MessageTable.TABLE_NAME})
"""
)
.run()
}
fun setRestoreTransferState(attachmentId: AttachmentId, state: Int) {
setRestoreTransferState(listOf(attachmentId), state)
}
@@ -1759,7 +1730,7 @@ class AttachmentTable(
// the quality of the attachment we received.
val hashMatch: DataFileInfo? = readableDatabase
.select(*DATA_FILE_INFO_PROJECTION)
.from("$TABLE_NAME INDEXED BY $DATA_HASH_REMOTE_KEY_INDEX")
.from(TABLE_NAME)
.where("$DATA_HASH_END = ? AND $DATA_HASH_END NOT NULL AND $TRANSFER_STATE = $TRANSFER_PROGRESS_DONE AND $DATA_FILE NOT NULL", fileWriteResult.hash)
.run()
.readToList { it.readDataFileInfo() }
@@ -30,7 +30,6 @@ import org.signal.core.util.update
import org.signal.core.util.withinTransaction
import org.signal.ringrtc.CallId
import org.signal.ringrtc.CallManager.RingUpdate
import org.thoughtcrime.securesms.database.CallTable.Companion.TIMESTAMP
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.CallLinkUpdateSendJob
@@ -110,66 +109,24 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
)
}
fun markAllCallEventsRead(timestamp: Long = Long.MAX_VALUE) {
val now = System.currentTimeMillis()
val allUnreadMissedCalls = readableDatabase
.select(MESSAGE_ID)
.from(TABLE_NAME)
.where("$TIMESTAMP <= ? AND $READ != ? AND $EVENT = ?", timestamp, ReadState.serialize(ReadState.READ), Event.serialize(Event.MISSED))
.run()
.readToList { cursor ->
cursor.requireLong(MESSAGE_ID)
}
val updateCount = writableDatabase
.update(TABLE_NAME)
.values(READ to ReadState.serialize(ReadState.READ))
.where("$TIMESTAMP <= ? AND $READ != ?", timestamp, ReadState.serialize(ReadState.READ))
.run()
val expiringCalls = SignalDatabase.messages.getUnstartedExpirations(allUnreadMissedCalls)
if (expiringCalls.isNotEmpty()) {
Log.i(TAG, "Found ${expiringCalls.size} calls that needs expiring.")
SignalDatabase.messages.markExpireStarted(expiringCalls.map { it.key to now })
for ((messageId, expiresIn) in expiringCalls) {
AppDependencies.expiringMessageManager.scheduleDeletion(messageId, true, now, expiresIn)
}
}
if (updateCount > 0) {
notifyConversationListListeners()
}
}
fun markAllCallEventsWithPeerBeforeTimestampRead(peer: RecipientId, timestamp: Long): Call? {
val now = System.currentTimeMillis()
val latestCallAsOfTimestamp = writableDatabase.withinTransaction { db ->
val unreadMissedCalls = db
.select(MESSAGE_ID)
.from(TABLE_NAME)
.where("$PEER = ? AND $TIMESTAMP <= ? AND $READ != ? AND $EVENT = ?", peer.toLong(), timestamp, ReadState.serialize(ReadState.READ), Event.serialize(Event.MISSED))
.run()
.readToList { cursor ->
cursor.requireLong(MESSAGE_ID)
}
val updated = db.update(TABLE_NAME)
.values(READ to ReadState.serialize(ReadState.READ))
.where("$PEER = ? AND $TIMESTAMP <= ?", peer.toLong(), timestamp)
.run()
val expiring = SignalDatabase.messages.getUnstartedExpirations(unreadMissedCalls)
if (expiring.isNotEmpty()) {
Log.i(TAG, "Found ${expiring.size} calls that needs expiring.")
SignalDatabase.messages.markExpireStarted(expiring.map { it.key to now })
for ((messageId, expiresIn) in expiring) {
AppDependencies.expiringMessageManager.scheduleDeletion(messageId, true, now, expiresIn)
}
}
if (updated == 0) {
null
} else {
@@ -200,7 +157,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
val messageType: Long = Call.getMessageType(type, direction, event)
writableDatabase.withinTransaction {
val result = SignalDatabase.messages.insertOneToOneCallLog(peer, messageType, timestamp, direction == Direction.OUTGOING)
val result = SignalDatabase.messages.insertCallLog(peer, messageType, timestamp, direction == Direction.OUTGOING)
val values = contentValuesOf(
CALL_ID to callId,
MESSAGE_ID to result.messageId,
@@ -245,7 +202,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
if (call.messageId == null) {
Log.w(TAG, "Call does not have an associated message id! No message to update.")
} else {
SignalDatabase.messages.updateOneToOneCallLog(call.messageId, call.messageType)
SignalDatabase.messages.updateCallLog(call.messageId, call.messageType)
}
AppDependencies.messageNotifier.updateNotification(context)
@@ -935,7 +892,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
Log.d(TAG, "Updating group call state: localJoined: $localJoined, isGroupCallActive: $isGroupCallActive")
val changed = writableDatabase.update(TABLE_NAME)
return writableDatabase.update(TABLE_NAME)
.values(
LOCAL_JOINED to localJoined,
GROUP_CALL_ACTIVE to isGroupCallActive
@@ -948,16 +905,6 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
isGroupCallActive.toInt()
)
.run() > 0
if (hasLocalUserJoined && !call.didLocalUserJoin && call.event == Event.RINGING) {
writableDatabase.update(TABLE_NAME)
.values(EVENT to Event.serialize(Event.ACCEPTED))
.where("$CALL_ID = ?", call.callId)
.run()
Log.d(TAG, "[updateGroupCallState] Transitioned group call ${call.callId} from RINGING to ACCEPTED on local join")
}
return changed
}
private fun handleGroupRingState(
@@ -989,14 +936,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
RingUpdate.EXPIRED_REQUEST, RingUpdate.CANCELLED_BY_RINGER -> {
when (call.event) {
Event.GENERIC_GROUP_CALL -> updateEventFromRingState(ringId, if (dueToNotificationProfile) Event.MISSED_NOTIFICATION_PROFILE else Event.MISSED, ringerRecipient)
Event.RINGING -> {
if (call.didLocalUserJoin) {
updateEventFromRingState(ringId, Event.ACCEPTED, ringerRecipient)
} else {
updateEventFromRingState(ringId, if (dueToNotificationProfile) Event.MISSED_NOTIFICATION_PROFILE else Event.MISSED, ringerRecipient)
}
}
Event.GENERIC_GROUP_CALL, Event.RINGING -> updateEventFromRingState(ringId, if (dueToNotificationProfile) Event.MISSED_NOTIFICATION_PROFILE else Event.MISSED, ringerRecipient)
Event.JOINED -> updateEventFromRingState(ringId, Event.ACCEPTED, ringerRecipient)
Event.OUTGOING_RING -> Log.w(TAG, "Received an expiration or cancellation while in OUTGOING_RING state. Ignoring.")
else -> Unit
@@ -1006,14 +946,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
RingUpdate.BUSY_LOCALLY -> {
when (call.event) {
Event.JOINED -> updateEventFromRingState(ringId, Event.ACCEPTED)
Event.GENERIC_GROUP_CALL -> updateEventFromRingState(ringId, Event.MISSED)
Event.RINGING -> {
if (call.didLocalUserJoin) {
updateEventFromRingState(ringId, Event.ACCEPTED)
} else {
updateEventFromRingState(ringId, Event.MISSED)
}
}
Event.GENERIC_GROUP_CALL, Event.RINGING -> updateEventFromRingState(ringId, Event.MISSED)
else -> {
updateEventFromRingState(ringId, call.event, ringerRecipient)
Log.w(TAG, "Received a busy event we can't process. Updating ringer only.")
@@ -1024,14 +957,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
RingUpdate.BUSY_ON_ANOTHER_DEVICE -> {
when (call.event) {
Event.JOINED -> updateEventFromRingState(ringId, Event.ACCEPTED)
Event.GENERIC_GROUP_CALL -> updateEventFromRingState(ringId, Event.MISSED)
Event.RINGING -> {
if (call.didLocalUserJoin) {
updateEventFromRingState(ringId, Event.ACCEPTED)
} else {
updateEventFromRingState(ringId, Event.MISSED)
}
}
Event.GENERIC_GROUP_CALL, Event.RINGING -> updateEventFromRingState(ringId, Event.MISSED)
else -> Log.w(TAG, "Received a busy event we can't process. Ignoring.")
}
}
@@ -138,6 +138,7 @@ import org.thoughtcrime.securesms.polls.PollRecord
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.revealable.ViewOnceExpirationInfo
import org.thoughtcrime.securesms.revealable.ViewOnceUtil
import org.thoughtcrime.securesms.sms.GroupV2UpdateMessageUtil
import org.thoughtcrime.securesms.stories.Stories.isFeatureEnabled
import org.thoughtcrime.securesms.util.DateUtils
@@ -315,11 +316,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
private const val INDEX_THREAD_COUNT = "message_thread_count_index"
private const val INDEX_THREAD_UNREAD_COUNT = "message_thread_unread_count_index"
private const val INDEX_STORY_TYPE = "message_story_type_index"
private const val INDEX_PARENT_STORY_ID = "message_parent_story_id_index"
private const val INDEX_ARCHIVED_STORY = "message_story_archived_index"
private const val INDEX_STARRED = "message_starred_index"
private const val INDEX_NOTIFICATION_STATE = "message_notification_state_index"
private const val INDEX_RATE_LIMITED = "message_rate_limited_index"
@JvmField
val CREATE_INDEXS = arrayOf(
@@ -352,11 +350,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
"CREATE INDEX IF NOT EXISTS $INDEX_ARCHIVED_STORY ON $TABLE_NAME ($STORY_ARCHIVED, $STORY_TYPE, $DATE_SENT) WHERE $STORY_TYPE > 0 AND $STORY_ARCHIVED > 0",
"CREATE INDEX IF NOT EXISTS $INDEX_STARRED ON $TABLE_NAME ($STARRED) WHERE $STARRED > 0",
"CREATE INDEX IF NOT EXISTS message_collapsed_state_index ON $TABLE_NAME ($COLLAPSED_STATE)",
"CREATE INDEX IF NOT EXISTS message_collapsed_head_id_index ON $TABLE_NAME ($COLLAPSED_HEAD_ID)",
"CREATE INDEX IF NOT EXISTS $INDEX_NOTIFICATION_STATE ON $TABLE_NAME ($DATE_RECEIVED) WHERE $NOTIFIED = 0 AND $STORY_TYPE = 0 AND $LATEST_REVISION_ID IS NULL",
"CREATE INDEX IF NOT EXISTS message_expire_started_index ON $TABLE_NAME ($EXPIRE_STARTED) WHERE $EXPIRE_STARTED > 0",
"CREATE INDEX IF NOT EXISTS message_view_once_index ON $TABLE_NAME ($VIEW_ONCE) WHERE $VIEW_ONCE > 0",
"CREATE INDEX IF NOT EXISTS $INDEX_RATE_LIMITED ON $TABLE_NAME ($ID) WHERE ($TYPE & ${MessageTypes.MESSAGE_RATE_LIMITED_BIT}) != 0"
"CREATE INDEX IF NOT EXISTS message_collapsed_head_id_index ON $TABLE_NAME ($COLLAPSED_HEAD_ID)"
)
private val MMS_PROJECTION_BASE = arrayOf(
@@ -658,32 +652,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
return queryMessages(where, null)
}
/**
* Given a list of ids, will return a list of the ids that have an expiration and what that expiration is.
*/
fun getUnstartedExpirations(messageIds: List<Long>): Map<Long, Long> {
val expirations: MutableMap<Long, Long> = hashMapOf()
SqlUtil.buildCollectionQuery(
column = ID,
values = messageIds,
prefix = "$EXPIRES_IN != 0 AND $EXPIRE_STARTED = 0 AND"
).forEach { query ->
readableDatabase
.select(ID, EXPIRES_IN)
.from(TABLE_NAME)
.where(query.where, query.whereArgs)
.run()
.use { cursor ->
while (cursor.moveToNext()) {
expirations[cursor.requireLong(ID)] = cursor.requireLong(EXPIRES_IN)
}
}
}
return expirations
}
/**
* Returns true iff
* - the message will expire within [ChatItemArchiveExporter.EXPIRATION_CUTOFF] once viewed
@@ -905,12 +873,11 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
return results
}
fun insertOneToOneCallLog(recipientId: RecipientId, type: Long, timestamp: Long, outgoing: Boolean): InsertResult {
fun insertCallLog(recipientId: RecipientId, type: Long, timestamp: Long, outgoing: Boolean): InsertResult {
val recipient = Recipient.resolved(recipientId)
val threadIdResult = threads.getOrCreateThreadIdResultFor(recipient.id, recipient.isGroup)
val threadId = threadIdResult.threadId
val dateReceived = System.currentTimeMillis()
val expiresIn = if (RemoteConfig.disappearMore) threads.getExpiresIn(threadId) else 0
val values = contentValuesOf(
FROM_RECIPIENT_ID to if (outgoing) Recipient.self().id.serialize() else recipientId.serialize(),
@@ -919,10 +886,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
DATE_RECEIVED to dateReceived,
DATE_SENT to timestamp,
READ to 1,
NOTIFIED to 1,
TYPE to type,
THREAD_ID to threadId,
EXPIRES_IN to expiresIn
THREAD_ID to threadId
)
val messageId = writableDatabase.insert(TABLE_NAME, null, values)
@@ -941,14 +906,13 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
)
}
fun updateOneToOneCallLog(messageId: Long, type: Long) {
fun updateCallLog(messageId: Long, type: Long) {
val message = getMessageRecordOrNull(messageId = messageId)
writableDatabase
.update(TABLE_NAME)
.values(
TYPE to type,
READ to 1,
NOTIFIED to 1
READ to 1
)
.where("$ID = ?", messageId)
.run()
@@ -961,13 +925,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
maybeCollapseMessage(db = writableDatabase, messageId = messageId, threadId = threadId, dateReceived = message.dateReceived, messageExtras = message.messageExtras, messageType = type)
}
// Start disappearing timer when a call is answered or declined (e.g. not missed)
if (message?.expiresIn != null && message.expiresIn != 0L && !MessageTypes.isMissedVideoCall(type) && !MessageTypes.isMissedAudioCall(type)) {
val now = System.currentTimeMillis()
markExpireStarted(messageId, now)
AppDependencies.expiringMessageManager.scheduleDeletion(messageId, message.isMms, now, message.expiresIn)
}
notifyConversationListeners(threadId)
AppDependencies.databaseObserver.notifyMessageUpdateObservers(MessageId(messageId))
}
@@ -1003,7 +960,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
DATE_RECEIVED to timestamp,
DATE_SENT to timestamp,
READ to if (markRead) 1 else 0,
NOTIFIED to if (markRead) 1 else 0,
BODY to Base64.encodeWithPadding(updateDetails),
TYPE to MessageTypes.GROUP_CALL_TYPE,
THREAD_ID to threadId
@@ -1103,8 +1059,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
val updateDetail = GroupCallUpdateDetailsUtil.parse(message.body)
val containsSelf = joinedUuids.contains(SignalStore.account.requireAci().rawUuid)
// Treat empty eraId from ring requests as matching for updating
val sameEraId = (updateDetail.eraId == eraId || updateDetail.eraId.isEmpty()) && !Util.isEmpty(eraId)
val sameEraId = updateDetail.eraId == eraId && !Util.isEmpty(eraId)
val inCallUuids = if (sameEraId) joinedUuids.map { it.toString() } else emptyList()
val body = GroupCallUpdateDetailsUtil.createUpdatedBody(updateDetail, inCallUuids, isCallFull, isRingingOnLocalDevice)
val contentValues = contentValuesOf(
@@ -1113,7 +1068,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
if (sameEraId && (containsSelf || updateDetail.localUserJoined)) {
contentValues.put(READ, 1)
contentValues.put(NOTIFIED, 1)
}
val query = buildTrueUpdateQuery(ID_WHERE, buildArgs(messageId), contentValues)
@@ -1151,8 +1105,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
val record = reader.getNext() ?: return@withinTransaction false
val groupCallUpdateDetails = GroupCallUpdateDetailsUtil.parse(record.body)
val containsSelf = peekJoinedUuids.contains(SignalStore.account.requireAci().rawUuid)
// Treat empty eraId from ring requests as matching for updating
val sameEraId = (groupCallUpdateDetails.eraId == peekGroupCallEraId || groupCallUpdateDetails.eraId.isEmpty()) && !Util.isEmpty(peekGroupCallEraId)
val sameEraId = groupCallUpdateDetails.eraId == peekGroupCallEraId && !Util.isEmpty(peekGroupCallEraId)
val inCallUuids = if (sameEraId) {
peekJoinedUuids.map { it.toString() }.toList()
@@ -1166,7 +1119,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
if (sameEraId && (containsSelf || groupCallUpdateDetails.localUserJoined)) {
contentValues.put(READ, 1)
contentValues.put(NOTIFIED, 1)
}
val query = buildTrueUpdateQuery(ID_WHERE, buildArgs(record.id), contentValues)
@@ -1235,7 +1187,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
DATE_RECEIVED to now,
DATE_SENT to now,
READ to 1,
NOTIFIED to 1,
TYPE to MessageTypes.PROFILE_CHANGE_TYPE,
THREAD_ID to threadId,
MESSAGE_EXTRAS to extras.encode()
@@ -1275,7 +1226,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
DATE_RECEIVED to now,
DATE_SENT to now,
READ to 1,
NOTIFIED to 1,
TYPE to MessageTypes.PROFILE_CHANGE_TYPE,
THREAD_ID to threadId,
MESSAGE_EXTRAS to extras.encode()
@@ -1309,7 +1259,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
DATE_RECEIVED to System.currentTimeMillis(),
DATE_SENT to System.currentTimeMillis(),
READ to 1,
NOTIFIED to 1,
TYPE to MessageTypes.GV1_MIGRATION_TYPE,
THREAD_ID to threadId
)
@@ -1344,7 +1293,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
DATE_RECEIVED to System.currentTimeMillis(),
DATE_SENT to System.currentTimeMillis(),
READ to 1,
NOTIFIED to 1,
TYPE to MessageTypes.CHANGE_NUMBER_TYPE,
THREAD_ID to threadId,
BODY to null
@@ -1369,7 +1317,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
DATE_RECEIVED to System.currentTimeMillis(),
DATE_SENT to System.currentTimeMillis(),
READ to 1,
NOTIFIED to 1,
TYPE to MessageTypes.RELEASE_CHANNEL_DONATION_REQUEST_TYPE,
THREAD_ID to threadId,
BODY to null
@@ -1387,7 +1334,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
DATE_RECEIVED to System.currentTimeMillis(),
DATE_SENT to System.currentTimeMillis(),
READ to 1,
NOTIFIED to 1,
TYPE to MessageTypes.THREAD_MERGE_TYPE,
THREAD_ID to threadId,
BODY to Base64.encodeWithPadding(event.encode())
@@ -1406,7 +1352,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
DATE_RECEIVED to System.currentTimeMillis(),
DATE_SENT to System.currentTimeMillis(),
READ to 1,
NOTIFIED to 1,
TYPE to MessageTypes.SESSION_SWITCHOVER_TYPE,
THREAD_ID to threadId,
BODY to Base64.encodeWithPadding(event.encode())
@@ -1428,7 +1373,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
DATE_RECEIVED to System.currentTimeMillis(),
DATE_SENT to System.currentTimeMillis(),
READ to 1,
NOTIFIED to 1,
TYPE to MessageTypes.SMS_EXPORT_TYPE,
THREAD_ID to threadId,
BODY to null
@@ -1706,7 +1650,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
return readableDatabase
.select(DATE_SENT)
.from("$TABLE_NAME INDEXED BY $INDEX_STORY_TYPE")
.from(TABLE_NAME)
.where("$IS_STORY_CLAUSE AND $THREAD_ID != ?", releaseChannelThreadId)
.limit(1)
.orderBy("$DATE_SENT ASC")
@@ -1726,45 +1670,76 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
return writableDatabase.withinTransaction { db ->
val releaseChannelThreadId = getReleaseChannelThreadId(hasSeenReleaseChannelStories)
data class ExpiredStory(val id: Long, val fromRecipientId: Long)
val storiesBeforeTimestampWhere = "$IS_STORY_CLAUSE AND $DATE_SENT < ? AND $THREAD_ID != ?"
val sharedArgs = buildArgs(timestamp, releaseChannelThreadId)
val expiredStories = db
.select(ID, FROM_RECIPIENT_ID)
.from("$TABLE_NAME INDEXED BY $INDEX_STORY_TYPE")
.where("$IS_STORY_CLAUSE AND $DATE_SENT < ? AND $THREAD_ID != ?", buildArgs(timestamp, releaseChannelThreadId))
.run()
.readToList { ExpiredStory(it.requireLong(ID), it.requireLong(FROM_RECIPIENT_ID)) }
val deleteStoryRepliesQuery = """
DELETE FROM $TABLE_NAME INDEXED BY $INDEX_STORY_TYPE
WHERE
$PARENT_STORY_ID > 0 AND
$PARENT_STORY_ID IN (
SELECT $ID
FROM $TABLE_NAME
WHERE $storiesBeforeTimestampWhere
)
"""
if (expiredStories.isEmpty()) {
return@withinTransaction 0
val disassociateQuoteQuery = """
UPDATE $TABLE_NAME
SET
$QUOTE_MISSING = 1,
$QUOTE_BODY = ''
WHERE
$PARENT_STORY_ID < 0 AND
ABS($PARENT_STORY_ID) IN (
SELECT $ID
FROM $TABLE_NAME
WHERE $storiesBeforeTimestampWhere
)
"""
val storyRepliesQuery = """
SELECT $ID FROM $TABLE_NAME
WHERE
$PARENT_STORY_ID < 0 AND
ABS($PARENT_STORY_ID) IN (
SELECT $ID
FROM $TABLE_NAME
WHERE $storiesBeforeTimestampWhere
)
"""
db.execSQL(deleteStoryRepliesQuery, sharedArgs)
db.execSQL(disassociateQuoteQuery, sharedArgs)
db.rawQuery(storyRepliesQuery, sharedArgs).forEach { cursor: Cursor ->
val mmsId = cursor.requireLong(ID)
attachments.deleteAttachmentsForMessage(mmsId)
}
val storyIds = expiredStories.map { it.id }
val directReplyClause = buildSingleCollectionQuery(PARENT_STORY_ID, storyIds)
val quotedReplyClause = buildSingleCollectionQuery(PARENT_STORY_ID, storyIds.map { -it })
db.delete("$TABLE_NAME INDEXED BY $INDEX_PARENT_STORY_ID")
.where(directReplyClause.where, directReplyClause.whereArgs)
db.select(FROM_RECIPIENT_ID)
.from(TABLE_NAME)
.where(storiesBeforeTimestampWhere, sharedArgs)
.run()
.readToList { RecipientId.from(it.requireLong(FROM_RECIPIENT_ID)) }
.forEach { id -> AppDependencies.databaseObserver.notifyStoryObservers(id) }
db.update("$TABLE_NAME INDEXED BY $INDEX_PARENT_STORY_ID")
.values(QUOTE_MISSING to 1, QUOTE_BODY to "")
.where(quotedReplyClause.where, quotedReplyClause.whereArgs)
val deletedStoryCount = db.select(ID)
.from(TABLE_NAME)
.where(storiesBeforeTimestampWhere, sharedArgs)
.run()
.use { cursor ->
while (cursor.moveToNext()) {
deleteMessage(cursor.requireLong(ID))
}
db.select(ID)
.from("$TABLE_NAME INDEXED BY $INDEX_PARENT_STORY_ID")
.where(quotedReplyClause.where, quotedReplyClause.whereArgs)
.run()
.forEach { cursor -> attachments.deleteAttachmentsForMessage(cursor.requireLong(ID)) }
cursor.count
}
expiredStories.forEach { AppDependencies.databaseObserver.notifyStoryObservers(RecipientId.from(it.fromRecipientId)) }
if (deletedStoryCount > 0) {
OptimizeMessageSearchIndexJob.enqueue()
}
storyIds.forEach { deleteMessage(it) }
OptimizeMessageSearchIndexJob.enqueue()
storyIds.size
deletedStoryCount
}
}
@@ -1835,40 +1810,71 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
fun deleteStoriesForRecipient(recipientId: RecipientId): Int {
return writableDatabase.withinTransaction { db ->
val threadId = threads.getThreadIdFor(recipientId) ?: return@withinTransaction 0
val storiesInRecipientThread = "$IS_STORY_CLAUSE AND $THREAD_ID = ?"
val sharedArgs = buildArgs(threadId)
val storyIds = db.select(ID)
.from("$TABLE_NAME INDEXED BY $INDEX_STORY_TYPE")
.where("$IS_STORY_CLAUSE AND $THREAD_ID = ?", threadId)
.run()
.readToList { it.requireLong(ID) }
val deleteStoryRepliesQuery = """
DELETE FROM $TABLE_NAME INDEXED BY $INDEX_STORY_TYPE
WHERE
$PARENT_STORY_ID > 0 AND
$PARENT_STORY_ID IN (
SELECT $ID
FROM $TABLE_NAME
WHERE $storiesInRecipientThread
)
"""
if (storyIds.isEmpty()) return@withinTransaction 0
val disassociateQuoteQuery = """
UPDATE $TABLE_NAME
SET
$QUOTE_MISSING = 1,
$QUOTE_BODY = ''
WHERE
$PARENT_STORY_ID < 0 AND
ABS($PARENT_STORY_ID) IN (
SELECT $ID
FROM $TABLE_NAME
WHERE $storiesInRecipientThread
)
"""
val directReplyClause = buildSingleCollectionQuery(PARENT_STORY_ID, storyIds)
val quotedReplyClause = buildSingleCollectionQuery(PARENT_STORY_ID, storyIds.map { -it })
val storyRepliesQuery = """
SELECT $ID FROM $TABLE_NAME
WHERE
$PARENT_STORY_ID < 0 AND
ABS($PARENT_STORY_ID) IN (
SELECT $ID
FROM $TABLE_NAME
WHERE $storiesInRecipientThread
)
"""
db.delete("$TABLE_NAME INDEXED BY $INDEX_PARENT_STORY_ID")
.where(directReplyClause.where, directReplyClause.whereArgs)
.run()
db.update("$TABLE_NAME INDEXED BY $INDEX_PARENT_STORY_ID")
.values(QUOTE_MISSING to 1, QUOTE_BODY to "")
.where(quotedReplyClause.where, quotedReplyClause.whereArgs)
.run()
db.select(ID)
.from("$TABLE_NAME INDEXED BY $INDEX_PARENT_STORY_ID")
.where(quotedReplyClause.where, quotedReplyClause.whereArgs)
.run()
.forEach { cursor -> attachments.deleteAttachmentsForMessage(cursor.requireLong(ID)) }
db.execSQL(deleteStoryRepliesQuery, sharedArgs)
db.execSQL(disassociateQuoteQuery, sharedArgs)
db.rawQuery(storyRepliesQuery, sharedArgs).forEach { cursor: Cursor ->
val mmsId = cursor.requireLong(ID)
attachments.deleteAttachmentsForMessage(mmsId)
}
AppDependencies.databaseObserver.notifyStoryObservers(recipientId)
storyIds.forEach { deleteMessage(it) }
val deletedStoryCount = db.select(ID)
.from("$TABLE_NAME INDEXED BY $INDEX_STORY_TYPE")
.where(storiesInRecipientThread, sharedArgs)
.run()
.use { cursor ->
while (cursor.moveToNext()) {
deleteMessage(cursor.requireLong(ID))
}
OptimizeMessageSearchIndexJob.enqueue()
cursor.count
}
storyIds.size
if (deletedStoryCount > 0) {
OptimizeMessageSearchIndexJob.enqueue()
}
deletedStoryCount
}
}
@@ -2624,7 +2630,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
val results = writableDatabase.rawQuery(
"""
UPDATE $TABLE_NAME INDEXED BY $index
SET $READ = 1, $NOTIFIED = 1, $REACTIONS_UNREAD = 0, $REACTIONS_LAST_SEEN = ${System.currentTimeMillis()}, $VOTES_UNREAD = 0, $VOTES_LAST_SEEN = ${System.currentTimeMillis()}
SET $READ = 1, $REACTIONS_UNREAD = 0, $REACTIONS_LAST_SEEN = ${System.currentTimeMillis()}, $VOTES_UNREAD = 0, $VOTES_LAST_SEEN = ${System.currentTimeMillis()}
WHERE $where
RETURNING $ID, $FROM_RECIPIENT_ID, $DATE_SENT, $DATE_RECEIVED, $TYPE, $EXPIRES_IN, $EXPIRE_STARTED, $THREAD_ID, $STORY_TYPE
""",
@@ -3424,7 +3430,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
contentValues.put(TYPE, type)
contentValues.put(THREAD_ID, threadId)
contentValues.put(READ, 1)
contentValues.put(NOTIFIED, 1)
contentValues.put(DATE_RECEIVED, dateReceived)
contentValues.put(SMS_SUBSCRIPTION_ID, message.subscriptionId)
contentValues.put(EXPIRES_IN, editedMessage?.expiresIn ?: message.expiresIn)
@@ -4226,12 +4231,15 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
}
fun getAllRateLimitedMessageIds(): Set<Long> {
return readableDatabase
.select(ID)
.from("$TABLE_NAME INDEXED BY $INDEX_RATE_LIMITED")
.where("($TYPE & ${MessageTypes.MESSAGE_RATE_LIMITED_BIT}) != 0")
.run()
.readToSet { it.requireLong(ID) }
val db = databaseHelper.signalReadableDatabase
val where = "(" + TYPE + " & " + MessageTypes.TOTAL_MASK + " & " + MessageTypes.MESSAGE_RATE_LIMITED_BIT + ") > 0"
val ids: MutableSet<Long> = HashSet()
db.query(TABLE_NAME, arrayOf(ID), where, null, null, null, null).use { cursor ->
while (cursor.moveToNext()) {
ids.add(CursorUtil.requireLong(cursor, ID))
}
}
return ids
}
fun deleteMessagesInThreadBeforeDate(threadId: Long, date: Long, inclusive: Boolean): Int {
@@ -4435,25 +4443,34 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
fun getNearestExpiringViewOnceMessage(): ViewOnceExpirationInfo? {
val query = """
SELECT
$TABLE_NAME.$ID,
$VIEW_ONCE,
$DATE_RECEIVED
FROM
$TABLE_NAME INNER JOIN ${AttachmentTable.TABLE_NAME} ON $TABLE_NAME.$ID = ${AttachmentTable.TABLE_NAME}.${AttachmentTable.MESSAGE_ID}
WHERE
$VIEW_ONCE > 0 AND
SELECT
$TABLE_NAME.$ID,
$VIEW_ONCE,
$DATE_RECEIVED
FROM
$TABLE_NAME INNER JOIN ${AttachmentTable.TABLE_NAME} ON $TABLE_NAME.$ID = ${AttachmentTable.TABLE_NAME}.${AttachmentTable.MESSAGE_ID}
WHERE
$VIEW_ONCE > 0 AND
(${AttachmentTable.DATA_FILE} NOT NULL OR ${AttachmentTable.TRANSFER_STATE} != ?)
ORDER BY $DATE_RECEIVED ASC
LIMIT 1
"""
val args = buildArgs(AttachmentTable.TRANSFER_PROGRESS_DONE)
return readableDatabase.rawQuery(query, args)
.readToSingleObject { cursor ->
ViewOnceExpirationInfo(cursor.requireLong(ID), cursor.requireLong(DATE_RECEIVED))
var info: ViewOnceExpirationInfo? = null
var nearestExpiration = Long.MAX_VALUE
readableDatabase.rawQuery(query, args).forEach { cursor ->
val id = cursor.requireLong(ID)
val dateReceived = cursor.requireLong(DATE_RECEIVED)
val expiresAt = dateReceived + ViewOnceUtil.MAX_LIFESPAN
if (info == null || expiresAt < nearestExpiration) {
info = ViewOnceExpirationInfo(id, dateReceived)
nearestExpiration = expiresAt
}
}
return info
}
/**
@@ -5596,7 +5613,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
val values = contentValuesOf(
READ to 1,
NOTIFIED to 1,
REACTIONS_UNREAD to 0,
REACTIONS_LAST_SEEN to System.currentTimeMillis(),
VOTES_UNREAD to 0,
@@ -5813,12 +5829,12 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
return readableDatabase
.select(*MMS_PROJECTION)
.from("$TABLE_NAME INDEXED BY $INDEX_NOTIFICATION_STATE")
.from(TABLE_NAME)
.where(
"""
$NOTIFIED = 0
AND $STORY_TYPE = 0
AND $LATEST_REVISION_ID IS NULL
$NOTIFIED = 0
AND $STORY_TYPE = 0
AND $LATEST_REVISION_ID IS NULL
AND (
($READ = 0 AND ($ORIGINAL_MESSAGE_ID IS NULL OR EXISTS (SELECT 1 FROM $TABLE_NAME AS m WHERE m.$ID = $TABLE_NAME.$ORIGINAL_MESSAGE_ID AND m.$READ = 0)))
OR $REACTIONS_UNREAD = 1
@@ -5975,7 +5991,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
if (!hasReactions) {
values.put(REACTIONS_UNREAD, 0)
} else if (!isRemoval && isOutgoing) {
} else if (!isRemoval) {
values.put(REACTIONS_UNREAD, 1)
}
@@ -6001,7 +6017,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
if (!hasVotes) {
values.put(VOTES_UNREAD, 0)
} else if (!isRemoval && isOutgoing) {
} else if (!isRemoval) {
values.put(VOTES_UNREAD, 1)
}
@@ -345,11 +345,19 @@ public interface MessageTypes {
}
static boolean isChatSessionRefresh(long type) {
return (type & ENCRYPTION_REMOTE_FAILED_BIT) != 0 ||
// These are legacy encryption error types that we just bundle into this type
(type & ENCRYPTION_REMOTE_NO_SESSION_BIT) != 0 ||
(type & ENCRYPTION_REMOTE_DUPLICATE_BIT) != 0 ||
(type & ENCRYPTION_REMOTE_LEGACY_BIT) != 0 ||
return (type & ENCRYPTION_REMOTE_FAILED_BIT) != 0;
}
static boolean isDuplicateMessageType(long type) {
return (type & ENCRYPTION_REMOTE_DUPLICATE_BIT) != 0;
}
static boolean isNoRemoteSessionType(long type) {
return (type & ENCRYPTION_REMOTE_NO_SESSION_BIT) != 0;
}
static boolean isLegacyType(long type) {
return (type & ENCRYPTION_REMOTE_LEGACY_BIT) != 0 ||
(type & ENCRYPTION_REMOTE_BIT) != 0;
}
@@ -424,7 +424,6 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
fun maskCapabilitiesToLong(capabilities: SignalServiceProfile.Capabilities): Long {
var value: Long = 0
value = Bitmask.update(value, Capabilities.STORAGE_SERVICE_ENCRYPTION_V2, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isStorageServiceEncryptionV2).serialize().toLong())
value = Bitmask.update(value, Capabilities.USERNAME_SYNC_MESSAGES, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isUsernameSyncMessages).serialize().toLong())
return value
}
}
@@ -4954,9 +4953,8 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
// const val DELETE_SYNC = 9
// const val VERSIONED_EXPIRATION_TIMER = 10
const val STORAGE_SERVICE_ENCRYPTION_V2 = 11
const val USERNAME_SYNC_MESSAGES = 12
// IMPORTANT: We cannot store more than 32 capabilities in the bitmask.
// IMPORTANT: We cannot sore more than 32 capabilities in the bitmask.
}
enum class VibrateState(val id: Int) {
@@ -10,7 +10,6 @@ import android.database.Cursor
import com.google.protobuf.InvalidProtocolBufferException
import org.signal.core.models.ServiceId
import org.signal.core.util.Base64
import org.signal.core.util.Bitmask
import org.signal.core.util.Util
import org.signal.core.util.logging.Log
import org.signal.core.util.optionalBlob
@@ -175,8 +174,7 @@ object RecipientTableCursorUtil {
fun readCapabilities(cursor: Cursor): RecipientRecord.Capabilities {
val capabilities = cursor.requireLong(RecipientTable.CAPABILITIES)
return RecipientRecord.Capabilities(
rawBits = capabilities,
usernameSyncMessages = Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientTable.Capabilities.USERNAME_SYNC_MESSAGES, RecipientTable.Capabilities.BIT_LENGTH).toInt())
rawBits = capabilities
)
}
@@ -56,11 +56,16 @@ class RemappedRecordTables internal constructor(context: Context?, databaseHelpe
fun getAllRecipientMappings(): Map<RecipientId, RecipientId> {
val recipientMap: MutableMap<RecipientId, RecipientId> = HashMap()
val mappings = getAllMappings(readableDatabase, Recipients.TABLE_NAME)
for (mapping in mappings) {
val oldId = RecipientId.from(mapping.oldId)
val newId = RecipientId.from(mapping.newId)
recipientMap[oldId] = newId
readableDatabase.withinTransaction { db ->
trimInvalidRecipientEntries(db)
trimInvalidThreadEntries(db)
val mappings = getAllMappings(db, Recipients.TABLE_NAME)
for (mapping in mappings) {
val oldId = RecipientId.from(mapping.oldId)
val newId = RecipientId.from(mapping.newId)
recipientMap[oldId] = newId
}
}
return recipientMap
@@ -69,21 +74,16 @@ class RemappedRecordTables internal constructor(context: Context?, databaseHelpe
fun getAllThreadMappings(): Map<Long, Long> {
val threadMap: MutableMap<Long, Long> = HashMap()
val mappings = getAllMappings(readableDatabase, Threads.TABLE_NAME)
for (mapping in mappings) {
threadMap[mapping.oldId] = mapping.newId
readableDatabase.withinTransaction { db ->
val mappings = getAllMappings(db, Threads.TABLE_NAME)
for (mapping in mappings) {
threadMap[mapping.oldId] = mapping.newId
}
}
return threadMap
}
fun trimStaleMappings() {
writableDatabase.withinTransaction { db ->
trimInvalidRecipientEntries(db)
trimInvalidThreadEntries(db)
}
}
fun addRecipientMapping(oldId: RecipientId, newId: RecipientId) {
addMapping(Recipients.TABLE_NAME, Mapping(oldId.toLong(), newId.toLong()))
}
@@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.database;
import androidx.annotation.NonNull;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.signal.network.util.Preconditions;
@@ -12,7 +11,6 @@ import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Merging together recipients and threads is messy business. We can easily replace *almost* all of
@@ -32,10 +30,8 @@ class RemappedRecords {
private static final RemappedRecords INSTANCE = new RemappedRecords();
private volatile Map<RecipientId, RecipientId> recipientMap;
private volatile Map<Long, Long> threadMap;
private final AtomicBoolean staleTrimScheduled = new AtomicBoolean(false);
private Map<RecipientId, RecipientId> recipientMap;
private Map<Long, Long> threadMap;
private RemappedRecords() {}
@@ -110,31 +106,13 @@ class RemappedRecords {
private void ensureRecipientMapIsPopulated() {
if (recipientMap == null) {
Map<RecipientId, RecipientId> loaded = SignalDatabase.remappedRecords().getAllRecipientMappings();
synchronized (this) {
if (recipientMap == null) {
recipientMap = loaded;
}
}
scheduleStaleTrimIfNeeded(loaded.isEmpty());
recipientMap = SignalDatabase.remappedRecords().getAllRecipientMappings();
}
}
private void ensureThreadMapIsPopulated() {
if (threadMap == null) {
Map<Long, Long> loaded = SignalDatabase.remappedRecords().getAllThreadMappings();
synchronized (this) {
if (threadMap == null) {
threadMap = loaded;
}
}
scheduleStaleTrimIfNeeded(loaded.isEmpty());
}
}
private void scheduleStaleTrimIfNeeded(boolean loadedMapWasEmpty) {
if (!loadedMapWasEmpty && staleTrimScheduled.compareAndSet(false, true)) {
SignalExecutors.BOUNDED.execute(() -> SignalDatabase.remappedRecords().trimStaleMappings());
threadMap = SignalDatabase.remappedRecords().getAllThreadMappings();
}
}
@@ -12,10 +12,8 @@ import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteQuery;
import net.zetetic.database.sqlcipher.SQLiteStatement;
import net.zetetic.database.sqlcipher.SQLiteQueryBuilder;
import net.zetetic.database.sqlcipher.SQLiteTransactionListener;
import org.signal.core.util.logging.Log;
import org.signal.core.util.tracing.Tracer;
import java.io.IOException;
@@ -27,7 +25,6 @@ import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* This is a wrapper around {@link net.zetetic.database.sqlcipher.SQLiteDatabase}. There's difficulties
@@ -36,14 +33,6 @@ import java.util.concurrent.TimeUnit;
*/
public class SQLiteDatabase implements SupportSQLiteDatabase {
private static final String TAG = Log.tag(SQLiteDatabase.class);
private static final long SLOW_WRITE_LOCK_WAIT_MS = TimeUnit.SECONDS.toMillis(3);
private static final long SLOW_TRANSACTION_HOLD_MS = 750;
private static final long SLOW_DIRECT_WRITE_MS = 250;
private static final long SLOW_DIRECT_DELETE_MS = TimeUnit.SECONDS.toMillis(1);
private static final long SLOW_QUERY_MS = TimeUnit.SECONDS.toMillis(1);
public static final int CONFLICT_ROLLBACK = 1;
public static final int CONFLICT_ABORT = 2;
public static final int CONFLICT_FAIL = 3;
@@ -59,9 +48,6 @@ public class SQLiteDatabase implements SupportSQLiteDatabase {
private final net.zetetic.database.sqlcipher.SQLiteDatabase wrapped;
private final Tracer tracer;
private static volatile boolean slowWriteLoggingEnabled = false;
private static final ThreadLocal<Long> TRANSACTION_HOLD_START_NS = new ThreadLocal<>();
private static final ThreadLocal<Set<Runnable>> PENDING_POST_SUCCESSFUL_TRANSACTION_TASKS;
private static final ThreadLocal<Set<Runnable>> POST_SUCCESSFUL_TRANSACTION_TASKS;
@@ -72,10 +58,6 @@ public class SQLiteDatabase implements SupportSQLiteDatabase {
PENDING_POST_SUCCESSFUL_TRANSACTION_TASKS.set(new LinkedHashSet<>());
}
public static void setSlowWriteLoggingEnabled(boolean enabled) {
slowWriteLoggingEnabled = enabled;
}
public SQLiteDatabase(net.zetetic.database.sqlcipher.SQLiteDatabase wrapped) {
this.wrapped = wrapped;
this.tracer = Tracer.getInstance();
@@ -96,20 +78,12 @@ public class SQLiteDatabase implements SupportSQLiteDatabase {
}
private void traceSql(String methodName, String query, boolean locked, Runnable returnable) {
traceSql(methodName, query, locked, null, null, returnable);
}
private void traceSql(String methodName, String query, boolean locked, String queryPlanSql, Object[] queryPlanArgs, Runnable returnable) {
if (locked) {
traceLockStart();
}
tracer.start(methodName, KEY_QUERY, query);
long startNs = slowWriteLoggingEnabled && locked ? System.nanoTime() : 0L;
returnable.run();
if (slowWriteLoggingEnabled && locked) {
warnIfSlowDirectWrite(methodName, null, query, queryPlanSql, queryPlanArgs, startNs);
}
tracer.end(methodName);
if (locked) {
@@ -122,10 +96,6 @@ public class SQLiteDatabase implements SupportSQLiteDatabase {
}
private <E> E traceSql(String methodName, String table, String query, boolean locked, Returnable<E> returnable) {
return traceSql(methodName, table, query, locked, null, null, returnable);
}
private <E> E traceSql(String methodName, String table, String query, boolean locked, String queryPlanSql, Object[] queryPlanArgs, Returnable<E> returnable) {
if (locked) {
traceLockStart();
}
@@ -139,19 +109,11 @@ public class SQLiteDatabase implements SupportSQLiteDatabase {
}
tracer.start(methodName, params);
long startNs = slowWriteLoggingEnabled ? System.nanoTime() : 0L;
E result = returnable.run();
if (result instanceof Cursor) {
// Triggers filling the window (which is about to be done anyway), but lets us capture that time inside the trace
((Cursor) result).getCount();
}
if (slowWriteLoggingEnabled) {
if (locked) {
warnIfSlowDirectWrite(methodName, table, query, queryPlanSql, queryPlanArgs, startNs);
} else {
warnIfSlowQuery(methodName, table, query, queryPlanSql, queryPlanArgs, startNs);
}
}
tracer.end(methodName);
if (locked) {
@@ -274,13 +236,13 @@ public class SQLiteDatabase implements SupportSQLiteDatabase {
@Override
public Cursor query(SupportSQLiteQuery query) {
DatabaseMonitor.onSql(query.getSql(), null);
return traceSql("query(SupportSQLiteQuery)", null, query.getSql(), false, query.getSql(), null, () -> wrapped.query(query));
return wrapped.query(query);
}
@Override
public Cursor query(SupportSQLiteQuery query, CancellationSignal cancellationSignal) {
DatabaseMonitor.onSql(query.getSql(), null);
return traceSql("query(SupportSQLiteQuery, CancellationSignal)", null, query.getSql(), false, query.getSql(), null, () -> wrapped.query(query, cancellationSignal));
return wrapped.query(query, cancellationSignal);
}
@Override
@@ -330,7 +292,6 @@ public class SQLiteDatabase implements SupportSQLiteDatabase {
trace("beginTransaction()", wrapped::beginTransaction);
} else {
trace("beginTransaction()", () -> {
long waitStartNs = slowWriteLoggingEnabled ? System.nanoTime() : 0L;
wrapped.beginTransactionWithListener(new SQLiteTransactionListener() {
@Override
public void onBegin() { }
@@ -349,31 +310,13 @@ public class SQLiteDatabase implements SupportSQLiteDatabase {
getPendingPostSuccessfulTransactionTasks().clear();
}
});
if (slowWriteLoggingEnabled) {
long waitMs = (System.nanoTime() - waitStartNs) / 1_000_000L;
if (waitMs >= SLOW_WRITE_LOCK_WAIT_MS) {
Log.w(TAG, "Slow write-lock acquire: waited " + waitMs + "ms to BEGIN", new Throwable());
}
TRANSACTION_HOLD_START_NS.set(System.nanoTime());
}
});
}
}
public void endTransaction() {
Long holdStartNs = slowWriteLoggingEnabled ? TRANSACTION_HOLD_START_NS.get() : null;
trace("endTransaction()", wrapped::endTransaction);
traceLockEnd();
if (holdStartNs != null && !wrapped.inTransaction()) {
TRANSACTION_HOLD_START_NS.remove();
if (slowWriteLoggingEnabled) {
long holdMs = (System.nanoTime() - holdStartNs) / 1_000_000L;
if (holdMs >= SLOW_TRANSACTION_HOLD_MS) {
Log.w(TAG, "Slow transaction: held write lock for " + holdMs + "ms", new Throwable());
SlowTransactionInternalNotifier.onSlowEvent();
}
}
}
Set<Runnable> tasks = getPostSuccessfulTransactionTasks();
for (Runnable r : new HashSet<>(tasks)) {
r.run();
@@ -387,42 +330,42 @@ public class SQLiteDatabase implements SupportSQLiteDatabase {
public Cursor query(boolean distinct, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit) {
DatabaseMonitor.onQuery(distinct, table, columns, selection, selectionArgs, groupBy, having, orderBy, limit);
return traceSql("query(9)", table, selection, false, buildQueryPlanSql(distinct, table, columns, selection, groupBy, having, orderBy, limit), selectionArgs, () -> wrapped.query(distinct, table, columns, selection, selectionArgs, groupBy, having, orderBy, limit));
return traceSql("query(9)", table, selection, false, () -> wrapped.query(distinct, table, columns, selection, selectionArgs, groupBy, having, orderBy, limit));
}
public Cursor queryWithFactory(net.zetetic.database.sqlcipher.SQLiteDatabase.CursorFactory cursorFactory, boolean distinct, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit) {
DatabaseMonitor.onQuery(distinct, table, columns, selection, selectionArgs, groupBy, having, orderBy, limit);
return traceSql("queryWithFactory()", table, selection, false, buildQueryPlanSql(distinct, table, columns, selection, groupBy, having, orderBy, limit), selectionArgs, () -> wrapped.queryWithFactory(cursorFactory, distinct, table, columns, selection, selectionArgs, groupBy, having, orderBy, limit));
return traceSql("queryWithFactory()", table, selection, false, () -> wrapped.queryWithFactory(cursorFactory, distinct, table, columns, selection, selectionArgs, groupBy, having, orderBy, limit));
}
public Cursor query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy) {
DatabaseMonitor.onQuery(false, table, columns, selection, selectionArgs, groupBy, having, orderBy, null);
return traceSql("query(7)", table, selection, false, buildQueryPlanSql(false, table, columns, selection, groupBy, having, orderBy, null), selectionArgs, () -> wrapped.query(table, columns, selection, selectionArgs, groupBy, having, orderBy));
return traceSql("query(7)", table, selection, false, () -> wrapped.query(table, columns, selection, selectionArgs, groupBy, having, orderBy));
}
public Cursor query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit) {
DatabaseMonitor.onQuery(false, table, columns, selection, selectionArgs, groupBy, having, orderBy, limit);
return traceSql("query(8)", table, selection, false, buildQueryPlanSql(false, table, columns, selection, groupBy, having, orderBy, limit), selectionArgs, () -> wrapped.query(table, columns, selection, selectionArgs, groupBy, having, orderBy, limit));
return traceSql("query(8)", table, selection, false, () -> wrapped.query(table, columns, selection, selectionArgs, groupBy, having, orderBy, limit));
}
public Cursor rawQuery(String sql, String[] selectionArgs) {
DatabaseMonitor.onSql(sql, selectionArgs);
return traceSql("rawQuery(2a)", null, sql, false, sql, selectionArgs, () -> wrapped.rawQuery(sql, selectionArgs));
return traceSql("rawQuery(2a)", sql, false, () -> wrapped.rawQuery(sql, selectionArgs));
}
public Cursor rawQuery(String sql, Object... args) {
DatabaseMonitor.onSql(sql, args);
return traceSql("rawQuery(2b)", null, sql, false, sql, args, () -> wrapped.rawQuery(sql, args));
return traceSql("rawQuery(2b)", sql, false,() -> wrapped.rawQuery(sql, args));
}
public Cursor rawQueryWithFactory(net.zetetic.database.sqlcipher.SQLiteDatabase.CursorFactory cursorFactory, String sql, String[] selectionArgs, String editTable) {
DatabaseMonitor.onSql(sql, selectionArgs);
return traceSql("rawQueryWithFactory()", null, sql, false, sql, selectionArgs, () -> wrapped.rawQueryWithFactory(cursorFactory, sql, selectionArgs, editTable));
return traceSql("rawQueryWithFactory()", sql, false, () -> wrapped.rawQueryWithFactory(cursorFactory, sql, selectionArgs, editTable));
}
public Cursor rawQuery(String sql, String[] selectionArgs, int initialRead, int maxRead) {
DatabaseMonitor.onSql(sql, selectionArgs);
return traceSql("rawQuery(4)", null, sql, false, sql, selectionArgs, () -> rawQuery(sql, selectionArgs, initialRead, maxRead));
return traceSql("rawQuery(4)", sql, false, () -> rawQuery(sql, selectionArgs, initialRead, maxRead));
}
public long insert(String table, String nullColumnHack, ContentValues values) {
@@ -447,17 +390,17 @@ public class SQLiteDatabase implements SupportSQLiteDatabase {
public int delete(String table, String whereClause, String[] whereArgs) {
DatabaseMonitor.onDelete(table, whereClause, whereArgs);
return traceSql("delete()", table, whereClause, true, buildDeletePlanSql(table, whereClause), whereArgs, () -> wrapped.delete(table, whereClause, whereArgs));
return traceSql("delete()", table, whereClause, true, () -> wrapped.delete(table, whereClause, whereArgs));
}
public int update(String table, ContentValues values, String whereClause, String[] whereArgs) {
DatabaseMonitor.onUpdate(table, values, whereClause, whereArgs);
return traceSql("update()", table, whereClause, true, buildUpdatePlanSql(table, values, whereClause, CONFLICT_NONE), buildUpdatePlanArgs(values, whereArgs), () -> wrapped.update(table, values, whereClause, whereArgs));
return traceSql("update()", table, whereClause, true, () -> wrapped.update(table, values, whereClause, whereArgs));
}
public int updateWithOnConflict(String table, ContentValues values, String whereClause, String[] whereArgs, int conflictAlgorithm) {
DatabaseMonitor.onUpdate(table, values, whereClause, whereArgs);
return traceSql("updateWithOnConflict()", table, whereClause, true, buildUpdatePlanSql(table, values, whereClause, conflictAlgorithm), buildUpdatePlanArgs(values, whereArgs), () -> wrapped.updateWithOnConflict(table, values, whereClause, whereArgs, conflictAlgorithm));
return traceSql("updateWithOnConflict()", table, whereClause, true, () -> wrapped.updateWithOnConflict(table, values, whereClause, whereArgs, conflictAlgorithm));
}
public void execSQL(String sql) throws SQLException {
@@ -576,149 +519,6 @@ public class SQLiteDatabase implements SupportSQLiteDatabase {
wrapped.setLocale(locale);
}
private static String buildQueryPlanSql(boolean distinct, String table, String[] columns, String selection, String groupBy, String having, String orderBy, String limit) {
try {
return SQLiteQueryBuilder.buildQueryString(distinct, table, columns, selection, groupBy, having, orderBy, limit);
} catch (Throwable t) {
return null;
}
}
private static String buildDeletePlanSql(String table, String whereClause) {
try {
StringBuilder sql = new StringBuilder(120);
sql.append("DELETE FROM ").append(table);
if (whereClause != null && whereClause.length() > 0) {
sql.append(" WHERE ").append(whereClause);
}
return sql.toString();
} catch (Throwable t) {
return null;
}
}
private static String buildUpdatePlanSql(String table, ContentValues values, String whereClause, int conflictAlgorithm) {
try {
StringBuilder sql = new StringBuilder(120);
sql.append("UPDATE").append(getConflictClause(conflictAlgorithm)).append(" ").append(table).append(" SET ");
boolean needsSeparator = false;
for (Map.Entry<String, Object> entry : values.valueSet()) {
if (needsSeparator) {
sql.append(",");
}
sql.append(entry.getKey()).append("=?");
needsSeparator = true;
}
if (whereClause != null && whereClause.length() > 0) {
sql.append(" WHERE ").append(whereClause);
}
return sql.toString();
} catch (Throwable t) {
return null;
}
}
private static Object[] buildUpdatePlanArgs(ContentValues values, String[] whereArgs) {
try {
int valuesSize = values.size();
int whereSize = whereArgs != null ? whereArgs.length : 0;
Object[] bindArgs = new Object[valuesSize + whereSize];
int index = 0;
for (Map.Entry<String, Object> entry : values.valueSet()) {
bindArgs[index++] = entry.getValue();
}
if (whereArgs != null) {
for (String whereArg : whereArgs) {
bindArgs[index++] = whereArg;
}
}
return bindArgs;
} catch (Throwable t) {
return null;
}
}
private static String getConflictClause(int conflictAlgorithm) {
switch (conflictAlgorithm) {
case CONFLICT_ROLLBACK:
return " OR ROLLBACK";
case CONFLICT_ABORT:
return " OR ABORT";
case CONFLICT_FAIL:
return " OR FAIL";
case CONFLICT_IGNORE:
return " OR IGNORE";
case CONFLICT_REPLACE:
return " OR REPLACE";
case CONFLICT_NONE:
default:
return "";
}
}
private void warnIfSlowDirectWrite(String methodName, String table, String query, String queryPlanSql, Object[] queryPlanArgs, long startNs) {
if (!slowWriteLoggingEnabled || wrapped.inTransaction()) {
return;
}
long elapsedMs = (System.nanoTime() - startNs) / 1_000_000L;
long threshold = "delete()".equals(methodName) ? SLOW_DIRECT_DELETE_MS : SLOW_DIRECT_WRITE_MS;
if (elapsedMs >= threshold) {
Log.w(TAG, "Slow direct write: " + methodName + " on " + table + " took " + elapsedMs + "ms (query=" + query + ")", new Throwable());
logQueryPlan(methodName, queryPlanSql, queryPlanArgs);
}
}
private void warnIfSlowQuery(String methodName, String table, String query, String queryPlanSql, Object[] queryPlanArgs, long startNs) {
if (!slowWriteLoggingEnabled) {
return;
}
long elapsedMs = (System.nanoTime() - startNs) / 1_000_000L;
if (elapsedMs >= SLOW_QUERY_MS) {
Log.w(TAG, "Slow query: " + methodName + " on " + table + " took " + elapsedMs + "ms (query=" + query + ")", new Throwable());
logQueryPlan(methodName, queryPlanSql, queryPlanArgs);
SlowTransactionInternalNotifier.onSlowEvent();
}
}
private void logQueryPlan(String methodName, String queryPlanSql, Object[] queryPlanArgs) {
if (queryPlanSql == null) {
return;
}
try (Cursor cursor = queryPlanArgs != null ? wrapped.rawQuery("EXPLAIN QUERY PLAN " + queryPlanSql, queryPlanArgs)
: wrapped.rawQuery("EXPLAIN QUERY PLAN " + queryPlanSql, (String[]) null))
{
StringBuilder plan = new StringBuilder();
while (cursor.moveToNext()) {
if (plan.length() > 0) {
plan.append('\n');
}
plan.append(cursor.getInt(0))
.append('|')
.append(cursor.getInt(1))
.append('|')
.append(cursor.getInt(2))
.append('|')
.append(cursor.getString(3));
}
Log.w(TAG, "Slow query plan: " + methodName + " (query=" + queryPlanSql + ")\n" + plan);
} catch (Throwable t) {
Log.w(TAG, "Failed to log slow query plan: " + methodName + " (query=" + queryPlanSql + ")", t);
}
}
private static class ConvertedTransactionListener implements SQLiteTransactionListener {
private final android.database.sqlite.SQLiteTransactionListener listener;
@@ -1,96 +0,0 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.database
import android.Manifest
import android.app.Notification
import android.app.PendingIntent
import android.content.Intent
import android.content.pm.PackageManager
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import org.signal.core.util.PendingIntentFlags
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity
import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.notifications.NotificationIds
import org.thoughtcrime.securesms.util.RemoteConfig
import java.util.concurrent.atomic.AtomicInteger
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
/**
* Notifier that surfaces SQLite write-lock contention. Gated behind the [RemoteConfig.slowDatabaseNotifications] flag.
*/
object SlowTransactionInternalNotifier {
private const val THRESHOLD = 5
private val NOTIFY_INTERVAL = 30.minutes
private val IGNORED_STACK_TRACE_CLASSES = listOf(
"BackupRepository",
"BackupMessagesJob",
"ArchiveAttachmentReconciliationJob",
"SubmitDebugLogRepository"
)
private val count = AtomicInteger(0)
@Volatile
private var lastNotify: Duration = 0.milliseconds
@JvmStatic
fun onSlowEvent() {
if (!RemoteConfig.slowDatabaseNotifications) {
return
}
if (isExpectedSlowOperation()) {
return
}
if (count.incrementAndGet() < THRESHOLD) {
return
}
val now = System.currentTimeMillis().milliseconds
if (lastNotify + NOTIFY_INTERVAL > now) {
return
}
val context = AppDependencies.application
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
return
}
val observed = count.getAndSet(0)
lastNotify = now
val notification: Notification = NotificationCompat.Builder(context, NotificationChannels.getInstance().FAILURES)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("[Internal-only] Slow database activity")
.setContentText("$observed slow database operations (transactions/queries) observed. Please tap to get a debug log.")
.setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, SubmitDebugLogActivity::class.java), PendingIntentFlags.mutable()))
.build()
NotificationManagerCompat.from(context).notify(NotificationIds.INTERNAL_ERROR, notification)
}
private fun isExpectedSlowOperation(): Boolean {
return Thread
.currentThread()
.stackTrace
.any { element ->
IGNORED_STACK_TRACE_CLASSES.any {
element.className.contains(it)
}
}
}
}
@@ -2086,7 +2086,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
}
}
fun getExpiresIn(threadId: Long): Long {
private fun getExpiresIn(threadId: Long): Long {
return readableDatabase
.select(EXPIRES_IN)
.from(TABLE_NAME)
@@ -170,9 +170,6 @@ import org.thoughtcrime.securesms.database.helpers.migration.V314_FixMessageRequ
import org.thoughtcrime.securesms.database.helpers.migration.V315_CleanupE164SenderKeyShared
import org.thoughtcrime.securesms.database.helpers.migration.V316_AddVerifiedGroupNameHashMigration
import org.thoughtcrime.securesms.database.helpers.migration.V317_AddMessageThreadDateReceivedUnreadIndex
import org.thoughtcrime.securesms.database.helpers.migration.V318_AddMessageNotificationStateIndex
import org.thoughtcrime.securesms.database.helpers.migration.V319_AddAttachmentAndMessageIndexes
import org.thoughtcrime.securesms.database.helpers.migration.V320_AddAttachmentThumbnailFileAndUuidIndexes
import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSqliteDatabase
/**
@@ -350,13 +347,10 @@ object SignalDatabaseMigrations {
314 to V314_FixMessageRequestAcceptedToRecipient,
315 to V315_CleanupE164SenderKeyShared,
316 to V316_AddVerifiedGroupNameHashMigration,
317 to V317_AddMessageThreadDateReceivedUnreadIndex,
318 to V318_AddMessageNotificationStateIndex,
319 to V319_AddAttachmentAndMessageIndexes,
320 to V320_AddAttachmentThumbnailFileAndUuidIndexes
317 to V317_AddMessageThreadDateReceivedUnreadIndex
)
const val DATABASE_VERSION = 320
const val DATABASE_VERSION = 317
@JvmStatic
fun migrate(context: Application, db: SignalSqliteDatabase, oldVersion: Int, newVersion: Int) {
@@ -1,23 +0,0 @@
package org.thoughtcrime.securesms.database.helpers.migration
import android.app.Application
import org.thoughtcrime.securesms.database.SQLiteDatabase
/**
* Fix bad notified state across the message table so that we can use an index to improve query performance
* when fetching notification state.
*
* Note: this intentionally does *not* clean up "dead" rows (read messages where notified is still 0) that bloat
* the partial index. That cleanup will happen over time as an app migration to prevent long migration startups.
*/
@Suppress("ClassName")
object V318_AddMessageNotificationStateIndex : SignalDatabaseMigration {
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
val outgoingBaseTypes = "(2, 11, 21, 22, 23, 24, 25, 26, 28)"
db.execSQL("UPDATE message SET reactions_unread = 0 WHERE reactions_unread = 1 AND (type & 31) NOT IN $outgoingBaseTypes")
db.execSQL("UPDATE message SET votes_unread = 0 WHERE votes_unread = 1 AND (type & 31) NOT IN $outgoingBaseTypes")
db.execSQL("CREATE INDEX IF NOT EXISTS message_notification_state_index ON message (date_received) WHERE notified = 0 AND story_type = 0 AND latest_revision_id IS NULL")
}
}
@@ -1,13 +0,0 @@
package org.thoughtcrime.securesms.database.helpers.migration
import android.app.Application
import org.thoughtcrime.securesms.database.SQLiteDatabase
@Suppress("ClassName")
object V319_AddAttachmentAndMessageIndexes : SignalDatabaseMigration {
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL("CREATE INDEX IF NOT EXISTS message_expire_started_index ON message (expire_started) WHERE expire_started > 0")
db.execSQL("CREATE INDEX IF NOT EXISTS message_view_once_index ON message (view_once) WHERE view_once > 0")
db.execSQL("CREATE INDEX IF NOT EXISTS attachment_archive_thumbnail_transfer_state ON attachment (archive_thumbnail_transfer_state)")
}
}
@@ -1,13 +0,0 @@
package org.thoughtcrime.securesms.database.helpers.migration
import android.app.Application
import org.thoughtcrime.securesms.database.SQLiteDatabase
@Suppress("ClassName")
object V320_AddAttachmentThumbnailFileAndUuidIndexes : SignalDatabaseMigration {
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL("CREATE INDEX IF NOT EXISTS attachment_thumbnail_file_index ON attachment (thumbnail_file) WHERE thumbnail_file IS NOT NULL")
db.execSQL("CREATE INDEX IF NOT EXISTS attachment_uuid_index ON attachment (attachment_uuid) WHERE attachment_uuid IS NOT NULL")
db.execSQL("CREATE INDEX IF NOT EXISTS message_rate_limited_index ON message (_id) WHERE (type & 128) != 0")
}
}
@@ -54,7 +54,6 @@ import org.signal.archive.proto.GroupMemberRemovedUpdate;
import org.signal.archive.proto.GroupMembershipAccessLevelChangeUpdate;
import org.signal.archive.proto.GroupNameUpdate;
import org.signal.archive.proto.GroupSelfInvitationRevokedUpdate;
import org.signal.archive.proto.GroupSequenceOfRequestsAndCancelsUpdate;
import org.signal.archive.proto.GroupTerminateChangeUpdate;
import org.signal.archive.proto.GroupUnknownInviteeUpdate;
import org.signal.archive.proto.GroupV2AccessLevel;
@@ -200,8 +199,6 @@ final class GroupsV2UpdateMessageProducer {
describeGroupJoinRequestApprovedUpdate(update.groupJoinRequestApprovalUpdate, updates);
} else if (update.groupJoinRequestCanceledUpdate != null) {
describeGroupJoinRequestCanceledUpdate(update.groupJoinRequestCanceledUpdate, updates);
} else if (update.groupSequenceOfRequestsAndCancelsUpdate != null) {
describeGroupSequenceOfRequestsAndCancelsUpdate(update.groupSequenceOfRequestsAndCancelsUpdate, updates);
} else if (update.groupInviteLinkResetUpdate != null) {
describeInviteLinkResetUpdate(update.groupInviteLinkResetUpdate, updates);
} else if (update.groupInviteLinkEnabledUpdate != null) {
@@ -381,18 +378,6 @@ final class GroupsV2UpdateMessageProducer {
}
}
private void describeGroupSequenceOfRequestsAndCancelsUpdate(@NonNull GroupSequenceOfRequestsAndCancelsUpdate update, @NonNull List<UpdateDescription> updates) {
if (selfIds.matches(update.requestorAci)) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_canceled_your_request_to_join_the_group), Glyph.GROUP));
} else {
updates.add(updateDescription(R.plurals.MessageRecord_s_requested_and_cancelled_their_request_to_join_via_the_group_link,
update.count,
update.requestorAci,
update.count,
Glyph.GROUP));
}
}
private void describeGroupJoinRequestApprovedUpdate(@NonNull GroupJoinRequestApprovalUpdate update, @NonNull List<UpdateDescription> updates) {
boolean requestingMemberIsYou = selfIds.matches(update.requestorAci);
@@ -42,12 +42,12 @@ public final class LiveUpdateMessage {
}
List<LiveData<Recipient>> allMentionedRecipients = updateDescription.getMentioned().stream()
.map(uuid -> Recipient.live(RecipientId.from(uuid)).getLiveDataResolved()).collect(Collectors.toList());
.map(uuid -> Recipient.resolved(RecipientId.from(uuid)).live().getLiveData()).collect(Collectors.toList());
LiveData<?> mentionedRecipientChangeStream = allMentionedRecipients.isEmpty() ? LiveDataUtil.just(new Object())
: LiveDataUtil.merge(allMentionedRecipients);
return LiveDataUtil.mapAsync(mentionedRecipientChangeStream, event -> toSpannable(context, updateDescription, updateDescription.getSpannable(), defaultTint, adjustPosition));
return Transformations.map(mentionedRecipientChangeStream, event -> toSpannable(context, updateDescription, updateDescription.getSpannable(), defaultTint, adjustPosition));
}
/**
@@ -180,6 +180,10 @@ public abstract class MessageRecord extends DisplayRecord {
return MessageTypes.isSecureType(type);
}
public boolean isLegacyMessage() {
return MessageTypes.isLegacyType(type);
}
@Override
public boolean isFailed() {
return super.isFailed() || isFailedAdminDelete();
@@ -507,10 +511,6 @@ public abstract class MessageRecord extends DisplayRecord {
return UpdateDescription.staticDescriptionWithExpiration(string, glyph);
}
protected static @NonNull UpdateDescription staticUpdateDescriptionWithExpiration(@NonNull String string, Glyph glyph, @ColorInt int lightTint, @ColorInt int darkTint) {
return UpdateDescription.staticDescriptionWithExpiration(string, glyph, lightTint, darkTint);
}
protected static @NonNull UpdateDescription staticUpdateDescription(@NonNull String string,
Glyph glyph,
@ColorInt int lightTint,
@@ -228,6 +228,12 @@ public class MmsMessageRecord extends MessageRecord {
public SpannableString getDisplayBody(@NonNull Context context) {
if (MessageTypes.isChatSessionRefresh(type)) {
return emphasisAdded(context.getString(R.string.MessageRecord_chat_session_refreshed));
} else if (MessageTypes.isDuplicateMessageType(type)) {
return emphasisAdded(context.getString(R.string.SmsMessageRecord_duplicate_message));
} else if (MessageTypes.isNoRemoteSessionType(type)) {
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_message_encrypted_for_non_existing_session));
} else if (isLegacyMessage()) {
return emphasisAdded(context.getString(R.string.MessageRecord_message_encrypted_with_a_legacy_protocol_version_that_is_no_longer_supported));
} else if (isPaymentNotification() && payment != null) {
return new SpannableString(context.getString(R.string.MessageRecord__payment_s, payment.getAmount().toString(FormatterOptions.defaults())));
} else if (isPaymentTombstone() || isPaymentNotification()) {
@@ -259,13 +265,13 @@ public class MmsMessageRecord extends MessageRecord {
if (call.getEvent() == CallTable.Event.NOT_ACCEPTED) {
int message = isVideoCall ? R.string.MessageRecord_unanswered_video_call : R.string.MessageRecord_unanswered_voice_call;
return staticUpdateDescriptionWithExpiration(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(message), callDateString),
return staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(message), callDateString),
icon,
ContextCompat.getColor(context, R.color.core_red_shade),
ContextCompat.getColor(context, R.color.core_red));
} else {
int updateString = isVideoCall ? R.string.MessageRecord_outgoing_video_call : R.string.MessageRecord_outgoing_voice_call;
return staticUpdateDescriptionWithExpiration(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(updateString), callDateString), icon);
return staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(updateString), callDateString), icon);
}
} else {
boolean isVideoCall = call.getType() == CallTable.Type.VIDEO_CALL;
@@ -273,7 +279,7 @@ public class MmsMessageRecord extends MessageRecord {
if (accepted || !call.isDisplayedAsMissedCallInUi()) {
int updateString = isVideoCall ? R.string.MessageRecord_incoming_video_call : R.string.MessageRecord_incoming_voice_call;
Glyph icon = isVideoCall ? Glyph.VIDEO_CAMERA : Glyph.PHONE;
return staticUpdateDescriptionWithExpiration(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(updateString), callDateString), icon);
return staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(updateString), callDateString), icon);
} else {
Glyph icon = isVideoCall ? Glyph.VIDEO_CAMERA : Glyph.PHONE;
int message;
@@ -285,7 +291,7 @@ public class MmsMessageRecord extends MessageRecord {
message = isVideoCall ? R.string.MessageRecord_missed_video_call : R.string.MessageRecord_missed_voice_call;
}
return staticUpdateDescriptionWithExpiration(context.getString(R.string.MessageRecord_call_message_with_date,
return staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date,
context.getString(message),
callDateString),
icon,
@@ -121,14 +121,12 @@ data class RecipientRecord(
)
data class Capabilities(
val rawBits: Long,
val usernameSyncMessages: Recipient.Capability
val rawBits: Long
) {
companion object {
@JvmField
val UNKNOWN = Capabilities(
rawBits = 0,
usernameSyncMessages = Recipient.Capability.UNKNOWN
rawBits = 0
)
}
}
@@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.database.model;
import android.content.Context;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
@@ -11,7 +10,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import org.signal.core.ui.util.ThemeUtil;
import org.thoughtcrime.securesms.fonts.SignalSymbols.Glyph;
import org.signal.core.models.ServiceId;
@@ -100,14 +98,7 @@ public final class UpdateDescription {
* Create an update description that's string value is fixed with a start glyph and has the ability to expire when a disappearing timer is set.
*/
public static UpdateDescription staticDescriptionWithExpiration(@NonNull String staticString, Glyph glyph) {
return staticDescriptionWithExpiration(staticString, glyph, 0, 0);
}
/**
* Create an update description that's string value is fixed with a start glyph and has the ability to expire when a disappearing timer is set.
*/
public static UpdateDescription staticDescriptionWithExpiration(@NonNull String staticString, Glyph glyph, @ColorInt int lightTint, @ColorInt int darkTint) {
return new UpdateDescription(Collections.emptyList(), null, new SpannableString(staticString), glyph, true, lightTint, darkTint);
return new UpdateDescription(Collections.emptyList(), null, new SpannableString(staticString), glyph, true,0, 0);
}
/**
@@ -170,11 +161,6 @@ public final class UpdateDescription {
return darkTint;
}
public @ColorInt int getTint(Context context) {
boolean isDarkTheme = ThemeUtil.isDarkTheme(context);
return isDarkTheme ? getDarkTint() : getLightTint();
}
public boolean hasExpiration() {
return canExpire;
}
@@ -399,8 +399,8 @@ class AttachmentDownloadJob private constructor(
}
return try {
val remoteId = SignalServiceAttachmentRemoteId.from(attachment.remoteLocation)
val cdnNumber = attachment.cdn.cdnNumber
val remoteId = SignalServiceAttachmentRemoteId.from(attachment.remoteLocation, cdnNumber)
val key = Base64.decode(attachment.remoteKey)
@@ -1,96 +0,0 @@
package org.thoughtcrime.securesms.jobs
import org.signal.core.util.fullWalCheckpoint
import org.signal.core.util.logging.Log
import org.signal.core.util.update
import org.thoughtcrime.securesms.database.MessageTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobmanager.Job
import kotlin.time.Duration.Companion.seconds
/**
* Incrementally cleans up "dead" notification state in the message table by marking read, non-notified messages as
* notified. These rows would otherwise sit in the message_notification_state_index forever.
*/
class BackfillNotifiedStateJob private constructor(parameters: Parameters) : Job(parameters) {
companion object {
const val KEY = "BackfillNotifiedStateJob"
private val TAG = Log.tag(BackfillNotifiedStateJob::class.java)
private const val BATCH_SIZE = 1000
private val TIME_BUDGET = 3.seconds
private val RETRY_BACKOFF = 30.seconds
@JvmStatic
fun enqueue() {
AppDependencies.jobManager.add(BackfillNotifiedStateJob())
}
}
constructor() : this(
Parameters.Builder()
.setQueue(KEY)
.setMaxInstancesForFactory(1)
.setMaxAttempts(Parameters.UNLIMITED)
.setInitialDelay(30.seconds.inWholeMilliseconds)
.build()
)
override fun serialize(): ByteArray? = null
override fun getFactoryKey(): String = KEY
override fun onFailure() = Unit
override fun run(): Result {
val endTime = System.currentTimeMillis() + TIME_BUDGET.inWholeMilliseconds
var totalUpdated = 0
var lastBatchUpdateCount: Int
do {
lastBatchUpdateCount = updateBatch()
totalUpdated += lastBatchUpdateCount
} while (lastBatchUpdateCount > 0 && System.currentTimeMillis() < endTime)
Log.i(TAG, "Updated $totalUpdated rows this run.")
if (lastBatchUpdateCount > 0) {
return Result.retry(RETRY_BACKOFF.inWholeMilliseconds)
}
Log.i(TAG, "Backfill complete. Attempting to shrink WAL")
if (!SignalDatabase.writableDatabase.fullWalCheckpoint()) {
Log.w(TAG, "Failed to do a full WAL checkpoint after finished backfill.")
}
return Result.success()
}
/**
* Marks up to [BATCH_SIZE] read, non-notified messages as notified in a single transaction. Returns the number of
* rows updated, which is 0 once there is nothing left to clean up.
*/
private fun updateBatch(): Int {
return SignalDatabase.writableDatabase
.update(MessageTable.TABLE_NAME)
.values(MessageTable.NOTIFIED to 1)
.where(
"""
${MessageTable.ID} IN (
SELECT ${MessageTable.ID}
FROM ${MessageTable.TABLE_NAME}
WHERE ${MessageTable.NOTIFIED} = 0 AND ${MessageTable.READ} = 1 AND ${MessageTable.REACTIONS_UNREAD} = 0 AND ${MessageTable.VOTES_UNREAD} = 0
LIMIT $BATCH_SIZE
)
"""
)
.run()
}
class Factory : Job.Factory<BackfillNotifiedStateJob> {
override fun create(parameters: Parameters, serializedData: ByteArray?): BackfillNotifiedStateJob {
return BackfillNotifiedStateJob(parameters)
}
}
}
@@ -61,11 +61,6 @@ class BackupRestoreMediaJob private constructor(parameters: Parameters) : BaseJo
val batchSize = 500
val restoreTime = System.currentTimeMillis()
val orphanedCount = SignalDatabase.attachments.markRestorableAttachmentsWithoutMessageAsFailed()
if (orphanedCount > 0) {
Log.w(TAG, "$orphanedCount orphaned restorable attachments marked failed")
}
do {
val restoreThumbnailJobs: MutableList<RestoreAttachmentThumbnailJob> = mutableListOf()
val restoreFullAttachmentJobs: MutableList<RestoreAttachmentJob> = mutableListOf()
@@ -18,7 +18,6 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.net.SignalNetwork
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds
@@ -88,9 +87,6 @@ class CheckKeyTransparencyJob private constructor(
} else if (!SignalStore.account.isRegistered) {
Log.i(TAG, "Account not registered. Exiting.")
false
} else if (TextSecurePreferences.isUnauthorizedReceived(AppDependencies.application)) {
Log.i(TAG, "Account is unauthorized. Exiting.")
false
} else if (!SignalStore.settings.automaticVerificationEnabled) {
Log.i(TAG, "Automatic verification disabled. Exiting.")
false
@@ -121,11 +117,11 @@ class CheckKeyTransparencyJob private constructor(
aciIdentityKey = SignalStore.account.aciIdentityKey.publicKey,
e164 = recipient.e164!!,
unidentifiedAccessKey = ProfileKeyUtil.profileKeyOrNull(recipient.profileKey).let { UnidentifiedAccess.deriveAccessKeyFrom(it) },
usernameHash = SignalStore.account.username?.let { Username(it).hash }.takeIf { Recipient.self().usernameSyncMessagesCapability.isSupported },
usernameHash = SignalStore.account.username?.let { Username(it).hash },
keyTransparencyStore = KeyTransparencyStore
)
Log.i(TAG, "Key transparency complete, result: $result. Included username in check: ${Recipient.self().usernameSyncMessagesCapability.isSupported}")
Log.i(TAG, "Key transparency complete, result: $result")
return when (result) {
is RequestResult.Success -> {
SignalStore.misc.hasKeyTransparencyFailure = false
@@ -171,11 +167,9 @@ class CheckKeyTransparencyJob private constructor(
* For others, it will only show once and only be cleared on the next successful verification.
*/
private fun markFailure() {
if (SignalStore.account.isRegistered && !TextSecurePreferences.isUnauthorizedReceived(AppDependencies.application)) {
SignalStore.misc.hasKeyTransparencyFailure = true
if (RemoteConfig.internalUser) {
SignalStore.misc.hasSeenKeyTransparencyFailure = false
}
SignalStore.misc.hasKeyTransparencyFailure = true
if (RemoteConfig.internalUser) {
SignalStore.misc.hasSeenKeyTransparencyFailure = false
}
}
@@ -26,12 +26,6 @@ class CheckRestoreMediaLeftJob private constructor(parameters: Parameters) : Job
const val KEY = "CheckRestoreMediaLeftJob"
private val TAG = Log.tag(CheckRestoreMediaLeftJob::class)
private val FINALIZE_LOCK = Any()
private const val STALLED_RECOVERY_QUEUE = "CheckRestoreMediaLeftJob::StalledRecovery"
fun enqueueStalledRecoveryCheck() {
AppDependencies.jobManager.add(CheckRestoreMediaLeftJob(STALLED_RECOVERY_QUEUE))
}
}
constructor(queue: String) : this(
@@ -52,63 +46,28 @@ class CheckRestoreMediaLeftJob private constructor(parameters: Parameters) : Job
if (remainingAttachmentSize == 0L) {
Log.d(TAG, "Media restore complete: there are no remaining restorable attachments.")
onMediaRestoreComplete()
ArchiveRestoreProgress.allMediaRestored()
BackupMediaRestoreService.stop(context)
if (SignalStore.backup.deletionState == DeletionState.AWAITING_MEDIA_DOWNLOAD) {
SignalStore.backup.deletionState = DeletionState.MEDIA_DOWNLOAD_FINISHED
}
if (!SignalStore.backup.backsUpMedia) {
SignalDatabase.attachments.markQuotesThatNeedReconstruction()
AppDependencies.jobManager.add(QuoteThumbnailReconstructionJob())
}
} else if (runAttempt == 0) {
Log.w(TAG, "Still have remaining data to restore, will retry before checking job queues, queue: ${parameters.queue} estimated remaining: $remainingAttachmentSize")
return Result.retry(30.seconds.inWholeMilliseconds)
return Result.retry(15.seconds.inWholeMilliseconds)
} else {
handleRemainingAfterMaxAttempts(remainingAttachmentSize)
Log.w(TAG, "Max retries reached, queue: ${parameters.queue} estimated remaining: $remainingAttachmentSize")
// todo [local-backup] inspect jobs/queues and raise some alarm/abort?
}
return Result.success()
}
/**
* Reached the retry limit while attachments still appear restorable. Figure out whether there is anything left actively working on the
* restore before deciding the restore is finished.
*/
private fun handleRemainingAfterMaxAttempts(remainingAttachmentSize: Long) {
synchronized(FINALIZE_LOCK) {
val otherCheckJobs = AppDependencies.jobManager.find { it.factoryKey == KEY && it.id != id }
if (otherCheckJobs.isNotEmpty()) {
Log.w(TAG, "Max retries reached but ${otherCheckJobs.size} other check job(s) remain, deferring to them. queue: ${parameters.queue} estimated remaining: $remainingAttachmentSize")
return
}
val orphanedCount = SignalDatabase.attachments.markRestorableAttachmentsWithoutMessageAsFailed()
if (orphanedCount > 0) {
Log.w(TAG, "$orphanedCount orphaned restorable attachments marked failed")
}
val restoreQueues = AppDependencies.jobManager
.find { it.factoryKey == RestoreAttachmentJob.KEY || it.factoryKey == RestoreLocalAttachmentJob.KEY }
.mapNotNull { it.queueKey }
.toSet()
if (restoreQueues.isNotEmpty()) {
Log.w(TAG, "Max retries reached but restore jobs remain in ${restoreQueues.size} queue(s), re-enqueueing check jobs. estimated remaining: $remainingAttachmentSize")
AppDependencies.jobManager.addAll(restoreQueues.map { CheckRestoreMediaLeftJob(it) })
return
}
Log.w(TAG, "Max retries reached and no restore jobs remain, treating restore as complete. estimated remaining: $remainingAttachmentSize")
onMediaRestoreComplete()
}
}
private fun onMediaRestoreComplete() {
ArchiveRestoreProgress.allMediaRestored()
BackupMediaRestoreService.stop(context)
if (SignalStore.backup.deletionState == DeletionState.AWAITING_MEDIA_DOWNLOAD) {
SignalStore.backup.deletionState = DeletionState.MEDIA_DOWNLOAD_FINISHED
}
if (!SignalStore.backup.backsUpMedia) {
SignalDatabase.attachments.markQuotesThatNeedReconstruction()
AppDependencies.jobManager.add(QuoteThumbnailReconstructionJob())
}
}
override fun onFailure() = Unit
class Factory : Job.Factory<CheckRestoreMediaLeftJob?> {
@@ -11,14 +11,9 @@ import arrow.core.Either
import arrow.core.getOrElse
import arrow.core.raise.Raise
import arrow.core.raise.either
import okio.ByteString
import okio.ByteString.Companion.toByteString
import okio.utf8Size
import org.signal.core.models.ServiceId
import org.signal.core.util.logging.Log
import org.signal.core.util.orNull
import org.signal.libsignal.net.ChallengeOption
import org.signal.libsignal.protocol.SignalProtocolAddress
import org.signal.network.service.MessageService
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.attachments.Attachment
@@ -202,7 +197,7 @@ class IndividualSendJobV2 private constructor(parameters: Parameters, private va
val syntheticResult = SendMessageResult.success(
SignalServiceAddress(recipient.requireServiceId(), recipient.e164.orNull()),
success.devices,
success.sentSealedSender,
success.sentUnidentified,
false,
0L,
Optional.of(content)
@@ -221,7 +216,7 @@ class IndividualSendJobV2 private constructor(parameters: Parameters, private va
SignalDatabase.pendingPniSignatureMessages.insertIfNecessary(recipient.id, message.sentTimeMillis, syntheticResult)
}
SignalDatabase.messages.markAsSent(messageId, success.sentSealedSender)
SignalDatabase.messages.markAsSent(messageId, success.sentUnidentified)
PushSendJob.markAttachmentsUploaded(messageId, message)
SignalDatabase.threads.updateSilently(threadId, false)
@@ -233,11 +228,11 @@ class IndividualSendJobV2 private constructor(parameters: Parameters, private va
}
val accessMode = recipient.sealedSenderAccessMode
if (success.sentSealedSender && accessMode == SealedSenderAccessMode.UNKNOWN && recipient.profileKey == null) {
if (success.sentUnidentified && accessMode == SealedSenderAccessMode.UNKNOWN && recipient.profileKey == null) {
SignalDatabase.recipients.setSealedSenderAccessMode(recipient.id, SealedSenderAccessMode.UNRESTRICTED)
} else if (success.sentSealedSender && accessMode == SealedSenderAccessMode.UNKNOWN) {
} else if (success.sentUnidentified && accessMode == SealedSenderAccessMode.UNKNOWN) {
SignalDatabase.recipients.setSealedSenderAccessMode(recipient.id, SealedSenderAccessMode.ENABLED)
} else if (!success.sentSealedSender && accessMode != SealedSenderAccessMode.DISABLED) {
} else if (!success.sentUnidentified && accessMode != SealedSenderAccessMode.DISABLED) {
SignalDatabase.recipients.setSealedSenderAccessMode(recipient.id, SealedSenderAccessMode.DISABLED)
}
@@ -260,41 +255,36 @@ class IndividualSendJobV2 private constructor(parameters: Parameters, private va
ifLeft = { error ->
when (error) {
is MessageService.SendError.IdentityMismatch -> {
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Identity mismatch for ${error.serviceId}", error.exception)
val externalRecipient = Recipient.external(error.serviceId.toString())
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Identity mismatch for ${error.recipient.identifier}", error.cause)
val externalRecipient = Recipient.external(error.recipient.identifier)
if (externalRecipient == null) {
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Failed to create a Recipient for the identifier!")
} else {
SignalDatabase.messages.addMismatchedIdentity(messageId, externalRecipient.id, error.exception.untrustedIdentity)
SignalDatabase.messages.addMismatchedIdentity(messageId, externalRecipient.id, error.cause.untrustedIdentity)
SignalDatabase.messages.markAsSentFailed(messageId)
RetrieveProfileJob.enqueue(externalRecipient.id, true)
}
Result.success()
}
is MessageService.SendError.NotRegistered -> {
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Recipient not registered", error)
MessageService.SendError.NotRegistered -> {
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Recipient not registered")
SignalDatabase.messages.markAsSentFailed(messageId)
PushSendJob.notifyMediaMessageDeliveryFailed(context, messageId)
AppDependencies.jobManager.add(DirectoryRefreshJob(false))
Result.success()
}
is MessageService.SendError.Unauthorized -> {
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Unauthorized send", error)
MessageService.SendError.Unauthorized -> {
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Unauthorized send")
Result.failure()
}
is MessageService.SendError.ChallengeRequired -> {
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Challenge required (options=${error.options})", error)
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Challenge required (options=${error.options})")
val proofResponse = ProofRequiredResponse().apply {
token = error.token
options = error.options.map {
when (it) {
ChallengeOption.PUSH_CHALLENGE -> "pushChallenge"
ChallengeOption.CAPTCHA -> "captcha"
}
}
options = error.options
}
val proofException = ProofRequiredException(proofResponse, error.retryAfter?.inWholeSeconds ?: 0L)
val threadRecipient = SignalDatabase.threads.getRecipientForThreadId(threadId)
@@ -305,23 +295,23 @@ class IndividualSendJobV2 private constructor(parameters: Parameters, private va
}
}
is MessageService.SendError.ServerRejected -> {
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Server rejected the send", error)
MessageService.SendError.ServerRejected -> {
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Server rejected the send")
Result.failure()
}
is MessageService.SendError.ContentTooLarge -> {
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Content too large (${error.size} > ${error.maxAllowed} bytes). Failing.", error)
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Content too large (${error.size} > ${error.maxAllowed} bytes); failing.")
Result.failure()
}
is MessageService.SendError.SessionAttemptsExhausted -> {
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Exhausted device-resolution attempts. Retrying", error)
MessageService.SendError.SessionAttemptsExhausted -> {
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Exhausted device-resolution attempts; retrying")
Result.retry(nextRunAttemptBackoff(runAttempt + 1))
}
is MessageService.SendError.PreKeyUnavailable -> {
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Prekey unavailable: ${error.reason}", error)
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Prekey unavailable: ${error.reason}")
Result.retry(nextRunAttemptBackoff(runAttempt + 1))
}
@@ -329,16 +319,16 @@ class IndividualSendJobV2 private constructor(parameters: Parameters, private va
val defaultBackoff = nextRunAttemptBackoff(runAttempt + 1)
val serverBackoff = error.retryAfter?.inWholeMilliseconds ?: 0L
val backoff = maxOf(defaultBackoff, serverBackoff)
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Rate limited, retryAfter=${error.retryAfter}, using backoff=${backoff}ms", error)
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Rate limited, retryAfter=${error.retryAfter}, using backoff=${backoff}ms")
Result.retry(backoff)
}
is MessageService.SendError.NetworkError -> {
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Network error", error.exception)
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Network error", error.cause)
Result.retry(nextRunAttemptBackoff(runAttempt + 1))
}
is MessageService.SendError.ApplicationError -> when (val cause = error.exception) {
is MessageService.SendError.ApplicationError -> when (val cause = error.cause) {
is RuntimeException -> {
Log.e(TAG, "${logPrefix(message.sentTimeMillis)} Encountered a fatal application error. Crash imminent.", cause)
Result.fatalFailure(cause)
@@ -403,7 +393,7 @@ class IndividualSendJobV2 private constructor(parameters: Parameters, private va
}
return AppDependencies.messageService.sendMessage(
serviceId = recipient.requireServiceId(),
recipient = SignalServiceAddress(recipient.requireServiceId(), recipient.e164.orNull()),
envelopeContent = envelopeContent,
timestamp = dataMessage.timestamp!!,
sealedSenderAccess = SealedSenderAccessUtil.getSealedSenderAccessFor(recipient),
@@ -419,39 +409,25 @@ class IndividualSendJobV2 private constructor(parameters: Parameters, private va
val editMessage = primaryResult.envelopeContent.content.get().editMessage
val timestamp = dataMessage?.timestamp ?: editMessage?.dataMessage?.timestamp ?: raise(MessageService.SendError.ApplicationError(IllegalStateException("No timestamp on primary message send!")))
val recipientServiceId = targetRecipient.requireServiceId()
val pniIdentityKey: ByteString? = if (recipientServiceId is ServiceId.PNI) {
AppDependencies
.protocolStore
.aci()
.identities()
.getIdentity(SignalProtocolAddress(recipientServiceId.toString(), SignalServiceAddress.DEFAULT_DEVICE_ID))?.publicKey?.serialize()?.toByteString()
} else {
null
}
val syncContent = Content(
syncMessage = SyncMessage(
sent = SyncMessage.Sent(
destinationServiceId = targetRecipient.serviceId.get().toString(),
timestamp = timestamp,
message = dataMessage,
editMessage = editMessage,
unidentifiedStatus = listOf(
SyncMessage.Sent.UnidentifiedDeliveryStatus(
destinationServiceIdBinary = recipientServiceId.toByteString(),
unidentified = primaryResult.sentSealedSender,
destinationPniIdentityKey = pniIdentityKey
)
)
editMessage = editMessage
)
)
)
val syncEnvelope = EnvelopeContent.encrypted(syncContent, ContentHint.IMPLICIT, Optional.empty())
return AppDependencies.messageService.sendSyncMessage(
return AppDependencies.messageService.sendMessage(
recipient = SignalServiceAddress(SignalStore.account.requireAci()),
envelopeContent = syncEnvelope,
timestamp = timestamp,
sealedSenderAccess = null, // We don't use sealed sender for sync messages
story = false,
isOnline = false,
urgent = true,
onEncrypted = { SignalLocalMetrics.IndividualMessageSend.onSyncMessageEncrypted(messageId) }
).bind()
@@ -55,7 +55,6 @@ import org.thoughtcrime.securesms.migrations.AvatarIdRemovalMigrationJob;
import org.thoughtcrime.securesms.migrations.AvatarMigrationJob;
import org.thoughtcrime.securesms.migrations.BackfillCollapsedEventsMigrationJob;
import org.thoughtcrime.securesms.migrations.BackfillDigestsForDuplicatesMigrationJob;
import org.thoughtcrime.securesms.migrations.BackfillNotifiedStateMigrationJob;
import org.thoughtcrime.securesms.migrations.BackupJitterMigrationJob;
import org.thoughtcrime.securesms.migrations.BackupNotificationMigrationJob;
import org.thoughtcrime.securesms.migrations.BadE164MigrationJob;
@@ -145,9 +144,8 @@ public final class JobManagerFactories {
put(AutomaticSessionResetJob.KEY, new AutomaticSessionResetJob.Factory());
put(AvatarGroupsV1DownloadJob.KEY, new AvatarGroupsV1DownloadJob.Factory());
put(AvatarGroupsV2DownloadJob.KEY, new AvatarGroupsV2DownloadJob.Factory());
put(BackfillCollapsedMessageJob.KEY, new BackfillCollapsedMessageJob.Factory());
put(BackfillCollapsedMessageJob.KEY, new BackfillCollapsedMessageJob.Factory());
put(BackfillDigestsForDataFileJob.KEY, new BackfillDigestsForDataFileJob.Factory());
put(BackfillNotifiedStateJob.KEY, new BackfillNotifiedStateJob.Factory());
put(BackupDeleteJob.KEY, new BackupDeleteJob.Factory());
put(BackupMessagesJob.KEY, new BackupMessagesJob.Factory());
put(BackupRestoreMediaJob.KEY, new BackupRestoreMediaJob.Factory());
@@ -207,7 +205,6 @@ public final class JobManagerFactories {
put(LocalPlaintextArchiveJob.KEY, new LocalPlaintextArchiveJob.Factory());
put(LocalBackupJobApi29.KEY, new LocalBackupJobApi29.Factory());
put(MarkerJob.KEY, new MarkerJob.Factory());
put(MessageSendLogCleanupJob.KEY, new MessageSendLogCleanupJob.Factory());
put(MultiDeviceAttachmentBackfillMissingJob.KEY, new MultiDeviceAttachmentBackfillMissingJob.Factory());
put(MultiDeviceAttachmentBackfillUpdateJob.KEY, new MultiDeviceAttachmentBackfillUpdateJob.Factory());
put(MultiDeviceBlockedUpdateJob.KEY, new MultiDeviceBlockedUpdateJob.Factory());
@@ -226,7 +223,6 @@ public final class JobManagerFactories {
put(MultiDeviceStickerPackSyncJob.KEY, new MultiDeviceStickerPackSyncJob.Factory());
put(MultiDeviceStorageSyncRequestJob.KEY, new MultiDeviceStorageSyncRequestJob.Factory());
put(MultiDeviceSubscriptionSyncRequestJob.KEY, new MultiDeviceSubscriptionSyncRequestJob.Factory());
put(MultiDeviceUsernameChangeSyncJob.KEY, new MultiDeviceUsernameChangeSyncJob.Factory());
put(MultiDeviceVerifiedUpdateJob.KEY, new MultiDeviceVerifiedUpdateJob.Factory());
put(MultiDeviceViewOnceOpenJob.KEY, new MultiDeviceViewOnceOpenJob.Factory());
put(MultiDeviceViewedUpdateJob.KEY, new MultiDeviceViewedUpdateJob.Factory());
@@ -319,7 +315,6 @@ public final class JobManagerFactories {
put(AvatarMigrationJob.KEY, new AvatarMigrationJob.Factory());
put(BackfillCollapsedEventsMigrationJob.KEY, new BackfillCollapsedEventsMigrationJob.Factory());
put(BackfillDigestsForDuplicatesMigrationJob.KEY, new BackfillDigestsForDuplicatesMigrationJob.Factory());
put(BackfillNotifiedStateMigrationJob.KEY, new BackfillNotifiedStateMigrationJob.Factory());
put(BackupJitterMigrationJob.KEY, new BackupJitterMigrationJob.Factory());
put(BackupNotificationMigrationJob.KEY, new BackupNotificationMigrationJob.Factory());
put(BackupRefreshJob.KEY, new BackupRefreshJob.Factory());
@@ -1,52 +0,0 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.jobs
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.util.RemoteConfig
import java.util.concurrent.TimeUnit
/**
* Trims expired entries out of the message send log after a delay.
*/
class MessageSendLogCleanupJob private constructor(parameters: Parameters) : Job(parameters) {
companion object {
const val KEY = "MessageSendLogCleanupJob"
@JvmStatic
fun enqueue() {
AppDependencies.jobManager.add(
MessageSendLogCleanupJob(
Parameters.Builder()
.setInitialDelay(TimeUnit.MINUTES.toMillis(1))
.setLifespan(TimeUnit.HOURS.toMillis(1))
.setMaxInstancesForFactory(1)
.setQueue(KEY)
.build()
)
)
}
}
override fun serialize(): ByteArray? = null
override fun getFactoryKey(): String = KEY
override fun onFailure() = Unit
override fun run(): Result {
SignalDatabase.messageLog.trimOldMessages(System.currentTimeMillis(), RemoteConfig.retryRespondMaxAge)
return Result.success()
}
class Factory : Job.Factory<MessageSendLogCleanupJob> {
override fun create(parameters: Parameters, serializedData: ByteArray?): MessageSendLogCleanupJob {
return MessageSendLogCleanupJob(parameters)
}
}
}
@@ -1,95 +0,0 @@
package org.thoughtcrime.securesms.jobs
import androidx.annotation.WorkerThread
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
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.pad
import org.thoughtcrime.securesms.recipients.Recipient
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException
import org.whispersystems.signalservice.internal.push.Content
import org.whispersystems.signalservice.internal.push.SyncMessage
import java.io.IOException
import java.util.Optional
import kotlin.time.Duration.Companion.days
/**
* Sends a sync message to alert linked devices of a username change so they can reset KT.
*/
class MultiDeviceUsernameChangeSyncJob private constructor(
parameters: Parameters
) : Job(parameters) {
companion object {
const val KEY = "MultiDeviceUsernameChangeSyncJob"
private val TAG = Log.tag(MultiDeviceUsernameChangeSyncJob::class.java)
@WorkerThread
@JvmStatic
fun enqueueUsernameChangeSync() {
if (!SignalStore.account.isMultiDevice) {
return
}
AppDependencies.jobManager.add(
MultiDeviceUsernameChangeSyncJob(
parameters = Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
.setMaxAttempts(Parameters.UNLIMITED)
.setLifespan(1.days.inWholeMilliseconds)
.build()
)
)
}
}
override fun serialize(): ByteArray? = null
override fun getFactoryKey(): String = KEY
override fun run(): Result {
if (!Recipient.self().isRegistered) {
Log.w(TAG, "Not registered")
return Result.failure()
}
if (!SignalStore.account.isMultiDevice) {
Log.w(TAG, "Not multi-device")
return Result.failure()
}
val syncMessageContent = Content(
syncMessage = SyncMessage.Builder()
.pad()
.usernameChange(SyncMessage.UsernameChange())
.build()
)
return try {
Log.d(TAG, "Sending username change sync")
val success = AppDependencies.signalServiceMessageSender.sendSyncMessage(syncMessageContent, true, Optional.empty()).isSuccess
if (success) {
Result.success()
} else {
Log.w(TAG, "Unsuccessful username change send. Retrying.")
Result.retry(defaultBackoff())
}
} catch (e: IOException) {
Log.w(TAG, "Unable to send username change sync due to io exception", e)
Result.retry(defaultBackoff())
} catch (e: UntrustedIdentityException) {
Log.w(TAG, "Unable to send username change sync due to untrusted exception", e)
Result.failure()
}
}
override fun onFailure() = Unit
class Factory : Job.Factory<MultiDeviceUsernameChangeSyncJob> {
override fun create(parameters: Parameters, serializedData: ByteArray?): MultiDeviceUsernameChangeSyncJob {
return MultiDeviceUsernameChangeSyncJob(parameters)
}
}
}
@@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.jobs
import org.signal.core.util.fullWalCheckpoint
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies
@@ -64,9 +63,6 @@ class OptimizeMessageSearchIndexJob private constructor(parameters: Parameters)
}
val success = SignalDatabase.messageSearch.optimizeIndex(5.seconds.inWholeMilliseconds)
if (!SignalDatabase.writableDatabase.fullWalCheckpoint()) {
Log.w(TAG, "Failed to do a full WAL checkpoint after deletion.")
}
if (!success) {
throw RetryLaterException()
@@ -282,7 +282,7 @@ abstract class PushSendJob protected constructor(parameters: Parameters) : BaseJ
}
try {
val remoteId = SignalServiceAttachmentRemoteId.from(attachment.remoteLocation!!, attachment.cdn.cdnNumber)
val remoteId = SignalServiceAttachmentRemoteId.from(attachment.remoteLocation!!)
val key = Base64.decode(attachment.remoteKey!!)
var width = attachment.width
@@ -1,45 +0,0 @@
package org.thoughtcrime.securesms.jobs
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException
import org.whispersystems.signalservice.api.messages.SendMessageResult
import java.io.IOException
/**
* Shared send logic for the receipt jobs ([SendReadReceiptJob], [SendDeliveryReceiptJob], [SendViewedReceiptJob]) that
* will repair on first failure and then try again.
*/
object ReceiptSender {
private val TAG = Log.tag(ReceiptSender::class.java)
/**
* @return the result of the send, or `null` if the receipt was dropped because the session could not be repaired.
*/
@JvmStatic
@Throws(IOException::class, UntrustedIdentityException::class)
fun sendWithSessionRepair(recipientId: RecipientId, operation: ReceiptSendOperation): SendMessageResult? {
return try {
operation.send()
} catch (e: IllegalStateException) {
Log.w(TAG, "Failed to send receipt, likely due to a missing or corrupt session. Archiving sessions and retrying.", e)
AppDependencies.protocolStore.aci().sessions().archiveSessions(recipientId)
AppDependencies.protocolStore.pni().sessions().archiveSessions(recipientId)
try {
operation.send()
} catch (retryError: IllegalStateException) {
Log.w(TAG, "Failed to send receipt even after archiving sessions. Dropping.", retryError)
null
}
}
}
fun interface ReceiptSendOperation {
@Throws(IOException::class, UntrustedIdentityException::class)
fun send(): SendMessageResult
}
}
@@ -46,11 +46,6 @@ class RestoreLocalAttachmentJob private constructor(
fun enqueueRestoreLocalAttachmentsJobs(mediaNameToFileInfo: Map<String, DocumentFileInfo>) {
val jobManager = AppDependencies.jobManager
val orphanedCount = SignalDatabase.attachments.markRestorableAttachmentsWithoutMessageAsFailed()
if (orphanedCount > 0) {
Log.w(TAG, "Failed $orphanedCount orphaned restorable attachment(s) with no backing message before enqueueing restores.")
}
do {
val possibleRestorableAttachments: List<LocalRestorableAttachment> = SignalDatabase.attachments.getRestorableLocalAttachments(500)
val notRestorableAttachments = ArrayList<AttachmentId>(possibleRestorableAttachments.size)
@@ -77,7 +72,7 @@ class RestoreLocalAttachmentJob private constructor(
// Intentionally enqueues one at a time for safer attachment transfer state management
Log.d(TAG, "Adding ${restoreAttachmentJobs.size} restore local attachment jobs")
restoreAttachmentJobs.forEach { jobManager.add(it) }
} while (possibleRestorableAttachments.isNotEmpty())
} while (restoreAttachmentJobs.isNotEmpty())
ArchiveRestoreProgress.onRestoringMedia()
@@ -6,7 +6,6 @@ import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.zkgroup.groupsend.GroupSendFullToken;
import org.signal.network.exceptions.PushNetworkException;
import org.thoughtcrime.securesms.crypto.SealedSenderAccessUtil;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.MessageId;
@@ -26,6 +25,7 @@ import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.signal.network.exceptions.PushNetworkException;
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;
import java.io.IOException;
@@ -125,12 +125,12 @@ public class SendDeliveryReceiptJob extends BaseJob {
Collections.singletonList(messageSentTimestamp),
timestamp);
SendMessageResult result = ReceiptSender.sendWithSessionRepair(recipientId, () -> messageSender.sendReceipt(remoteAddress,
SealedSenderAccessUtil.getSealedSenderAccessFor(recipient, this::getGroupSendFullToken),
receiptMessage,
recipient.getNeedsPniSignature()));
SendMessageResult result = messageSender.sendReceipt(remoteAddress,
SealedSenderAccessUtil.getSealedSenderAccessFor(recipient, this::getGroupSendFullToken),
receiptMessage,
recipient.getNeedsPniSignature());
if (result != null && messageId != null) {
if (messageId != null) {
SignalDatabase.messageLog().insertIfPossible(recipientId, timestamp, result, ContentHint.IMPLICIT, messageId, false);
}
}
@@ -152,8 +152,6 @@ public class SendDeliveryReceiptJob extends BaseJob {
public boolean onShouldRetry(@NonNull Exception e) {
if (e instanceof ServerRejectedException) return false;
if (e instanceof PushNetworkException) return true;
if (e instanceof IOException) return true;
return false;
}
@@ -191,13 +191,13 @@ public class SendReadReceiptJob extends BaseJob {
SignalServiceAddress remoteAddress = RecipientUtil.toSignalServiceAddress(context, recipient);
SignalServiceReceiptMessage receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ, messageSentTimestamps, timestamp);
SendMessageResult result = ReceiptSender.sendWithSessionRepair(recipientId, () -> messageSender.sendReceipt(remoteAddress,
SealedSenderAccessUtil.getSealedSenderAccessFor(recipient,
() -> SignalDatabase.groups().getGroupSendFullToken(threadId, recipientId)),
receiptMessage,
recipient.getNeedsPniSignature()));
SendMessageResult result = messageSender.sendReceipt(remoteAddress,
SealedSenderAccessUtil.getSealedSenderAccessFor(recipient,
() -> SignalDatabase.groups().getGroupSendFullToken(threadId, recipientId)),
receiptMessage,
recipient.getNeedsPniSignature());
if (result != null && Util.hasItems(messageIds)) {
if (Util.hasItems(messageIds)) {
SignalDatabase.messageLog().insertIfPossible(recipientId, timestamp, result, ContentHint.IMPLICIT, messageIds, false);
}
}
@@ -209,13 +209,13 @@ public class SendViewedReceiptJob extends BaseJob {
messageSentTimestamps,
timestamp);
SendMessageResult result = ReceiptSender.sendWithSessionRepair(recipientId, () -> messageSender.sendReceipt(remoteAddress,
SealedSenderAccessUtil.getSealedSenderAccessFor(recipient,
() -> SignalDatabase.groups().getGroupSendFullToken(threadId, recipientId)),
receiptMessage,
recipient.getNeedsPniSignature()));
SendMessageResult result = messageSender.sendReceipt(remoteAddress,
SealedSenderAccessUtil.getSealedSenderAccessFor(recipient,
() -> SignalDatabase.groups().getGroupSendFullToken(threadId, recipientId)),
receiptMessage,
recipient.getNeedsPniSignature());
if (result != null && Util.hasItems(foundMessageIds)) {
if (Util.hasItems(foundMessageIds)) {
SignalDatabase.messageLog().insertIfPossible(recipientId, timestamp, result, ContentHint.IMPLICIT, foundMessageIds, false);
}
}
@@ -400,8 +400,6 @@ class UploadAttachmentToArchiveJob private constructor(
SignalDatabase.attachments.setArchiveTransferStateFailure(attachmentId, AttachmentTable.ArchiveTransferState.TEMPORARY_FAILURE)
}
}
ArchiveUploadProgress.onAttachmentFinished(attachmentId)
}
private fun setArchiveTransferStateWithDelayedNotification(attachmentId: AttachmentId, transferState: AttachmentTable.ArchiveTransferState) {
@@ -76,7 +76,6 @@ import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.SupportEmailUtil
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.signal.core.ui.R as CoreUiR
private const val PLACEHOLDER = "__ICON_PLACEHOLDER__"
@@ -504,7 +503,7 @@ fun DeviceRow(device: Device, isInternalUser: Boolean, setDeviceToRemove: (Devic
.fillMaxWidth()
) {
Image(
painter = painterResource(id = CoreUiR.drawable.symbol_devices_24),
painter = painterResource(id = R.drawable.symbol_devices_24),
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface),
contentScale = ContentScale.Inside,
@@ -2,13 +2,11 @@ package org.thoughtcrime.securesms.lock.v2
import android.app.Activity
import android.content.DialogInterface
import android.os.Bundle
import android.view.View
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModelProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.TemporaryScreenshotSecurity
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.lock.v2.ConfirmSvrPinViewModel.SaveAnimation
import org.thoughtcrime.securesms.megaphone.Megaphones
@@ -18,11 +16,6 @@ import org.thoughtcrime.securesms.util.SpanUtil
internal class ConfirmSvrPinFragment : BaseSvrPinFragment<ConfirmSvrPinViewModel>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
TemporaryScreenshotSecurity.bindToViewLifecycleOwner(this)
}
override fun initializeViewStates() {
val args = ConfirmSvrPinFragmentArgs.fromBundle(requireArguments())
if (args.isPinChange) {
@@ -1,7 +1,5 @@
package org.thoughtcrime.securesms.lock.v2
import android.os.Bundle
import android.view.View
import android.view.animation.Animation
import android.view.animation.TranslateAnimation
import android.widget.EditText
@@ -10,18 +8,12 @@ import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.Navigation.findNavController
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.TemporaryScreenshotSecurity
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinViewModel.NavigationEvent
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinViewModel.PinErrorEvent
import org.thoughtcrime.securesms.util.SpanUtil
import org.thoughtcrime.securesms.util.navigation.safeNavigate
class CreateSvrPinFragment : BaseSvrPinFragment<CreateSvrPinViewModel?>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
TemporaryScreenshotSecurity.bindToViewLifecycleOwner(this)
}
override fun initializeViewStates() {
val args = CreateSvrPinFragmentArgs.fromBundle(requireArguments())
if (args.isPinChange) {
@@ -16,7 +16,6 @@ import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.window.core.layout.WindowSizeClass
import org.signal.core.ui.WindowBreakpoint
import org.signal.core.ui.getWindowBreakpoint
import org.signal.core.ui.rememberIsSplitPane
@@ -79,7 +78,7 @@ data class MainContentLayoutData(
val isSplitPane = resources.rememberIsSplitPane()
return remember(windowSizeClass, mode, breakpoint, isSplitPane) {
val isLargeWindowSize = breakpoint is WindowBreakpoint.Large
val isLargeWindowSize = breakpoint.isLargeWindow
MainContentLayoutData(
shape = when {
@@ -236,7 +236,7 @@ class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment), Schedul
if (MediaUtil.isImageType(media.contentType) && editorData != null && editorData is ImageEditorFragment.Data) {
val model = editorData.readModel()
if (model != null) {
ImageEditorFragment.renderToSingleSessionBlob(requireContext(), model)
ImageEditorFragment.renderToSingleUseBlob(requireContext(), model)
} else {
media.uri
}
@@ -32,7 +32,7 @@ class TextStoryPostSendRepository {
val outputStream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
bitmap.recycle()
BlobProvider.getInstance().forData(outputStream.toByteArray()).createForSingleSessionInMemory()
BlobProvider.getInstance().forData(outputStream.toByteArray()).createForSingleUseInMemory()
}.subscribeOn(Schedulers.computation())
}
@@ -222,7 +222,7 @@ enum class OnboardingListItem(
),
ADD_PHOTO(
title = R.string.Megaphones_add_a_profile_photo,
icon = CoreUiR.drawable.symbol_person_circle_24,
icon = R.drawable.symbol_person_circle_24,
cardColor = R.color.onboarding_background_4
),
APPEARANCE(
@@ -14,7 +14,6 @@ import org.signal.core.util.Util
import org.signal.core.util.UuidUtil
import org.signal.core.util.isNotEmpty
import org.signal.core.util.orNull
import org.signal.libsignal.net.KeyTransparency
import org.signal.libsignal.protocol.IdentityKey
import org.signal.libsignal.protocol.IdentityKeyPair
import org.signal.libsignal.protocol.InvalidKeyException
@@ -43,7 +42,6 @@ import org.thoughtcrime.securesms.database.PaymentMetaDataUtil
import org.thoughtcrime.securesms.database.SentStorySyncManifest
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.database.model.KeyTransparencyStore
import org.thoughtcrime.securesms.database.model.Mention
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
@@ -60,7 +58,6 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.PollTerminate
import org.thoughtcrime.securesms.database.model.toBodyRangeList
import org.thoughtcrime.securesms.database.withAttachments
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.dependencies.KeyTransparencyApi
import org.thoughtcrime.securesms.groups.BadGroupIdException
import org.thoughtcrime.securesms.groups.GroupChangeBusyException
import org.thoughtcrime.securesms.groups.GroupId
@@ -191,7 +188,6 @@ object SyncMessageProcessor {
syncMessage.deleteForMe != null -> handleSynchronizeDeleteForMe(context, syncMessage.deleteForMe!!, envelope.clientTimestamp!!, earlyMessageCacheEntry)
syncMessage.attachmentBackfillRequest != null -> handleSynchronizeAttachmentBackfillRequest(syncMessage.attachmentBackfillRequest!!, envelope.clientTimestamp!!)
syncMessage.attachmentBackfillResponse != null -> warn(envelope.clientTimestamp!!, "Contains a backfill response, but we don't handle these!")
syncMessage.usernameChange != null -> handleSynchronizeUsernameChange(envelope.clientTimestamp!!)
else -> warn(envelope.clientTimestamp!!, "Contains no known sync types...")
}
}
@@ -2030,12 +2026,6 @@ object SyncMessageProcessor {
return threadId
}
private fun handleSynchronizeUsernameChange(timestamp: Long) {
log(timestamp, "[handleSynchronizeUsernameChange] Synchronize username change. Resetting KT.")
KeyTransparencyApi.reset(aci = SignalStore.account.requireAci().libSignalAci, field = KeyTransparency.AccountDataField.USERNAME_HASH, keyTransparencyStore = KeyTransparencyStore)
}
private fun ConversationIdentifier.toRecipientId(): RecipientId? {
val threadServiceId = ServiceId.parseOrNull(this.threadServiceId, this.threadServiceIdBinary)
return when {
@@ -10,12 +10,12 @@ import androidx.lifecycle.MutableLiveData;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.signal.core.util.Util;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.stickers.BlessedPacks;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.signal.core.util.Util;
import org.thoughtcrime.securesms.util.VersionTracker;
import java.util.LinkedHashMap;
@@ -203,11 +203,9 @@ public class ApplicationMigrations {
static final int READ_INDEX_DB_MIGRATION = 159;
// Need to skip 160 due to release ordering issues
static final int SVR2_ENCLAVE_UPDATE_6 = 161;
static final int NOTIFICATION_INDEX_MIGRATION = 162;
static final int NOTIFICATION_STATE_CLEANUP = 163;
}
public static final int CURRENT_VERSION = 163;
public static final int CURRENT_VERSION = 161;
/**
* This *must* be called after the {@link JobManager} has been instantiated, but *before* the call
@@ -942,14 +940,6 @@ public class ApplicationMigrations {
jobs.put(Version.SVR2_ENCLAVE_UPDATE_6, new Svr2MirrorMigrationJob());
}
if (lastSeenVersion < Version.NOTIFICATION_INDEX_MIGRATION) {
jobs.put(Version.NOTIFICATION_INDEX_MIGRATION, new DatabaseMigrationJob());
}
if (lastSeenVersion < Version.NOTIFICATION_STATE_CLEANUP) {
jobs.put(Version.NOTIFICATION_STATE_CLEANUP, new BackfillNotifiedStateMigrationJob());
}
return jobs;
}
@@ -1,32 +0,0 @@
package org.thoughtcrime.securesms.migrations
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobs.BackfillNotifiedStateJob
/**
* Kicks off a background job to clean up dead notification state left behind by V318. See [BackfillNotifiedStateJob].
*/
internal class BackfillNotifiedStateMigrationJob(
parameters: Parameters = Parameters.Builder().build()
) : MigrationJob(parameters) {
companion object {
const val KEY = "BackfillNotifiedStateMigrationJob"
}
override fun getFactoryKey(): String = KEY
override fun isUiBlocking(): Boolean = false
override fun performMigration() {
BackfillNotifiedStateJob.enqueue()
}
override fun shouldRetry(e: Exception): Boolean = false
class Factory : Job.Factory<BackfillNotifiedStateMigrationJob> {
override fun create(parameters: Parameters, serializedData: ByteArray?): BackfillNotifiedStateMigrationJob {
return BackfillNotifiedStateMigrationJob(parameters)
}
}
}
@@ -164,7 +164,7 @@ object SlowNotificationHeuristics {
return false
}
if (failures.size.toFloat() / (failures.size + successes.size) >= failurePercentage) {
if (failures.size / (failures.size + successes.size) >= failurePercentage) {
Log.w(TAG, "User often unable start FCM service. ${failures.size} failed : ${successes.size} successful")
return true
}
@@ -68,7 +68,7 @@ class VitalsViewModel(private val context: Application) : AndroidViewModel(conte
return@fromCallable State.PROMPT_SPECIFIC_BATTERY_SAVER_DIALOG
}
if (havingDelayedNotifications && SlowNotificationHeuristics.shouldPromptBatterySaver() && SlowNotificationHeuristics.isBatteryOptimizationsOn()) {
if (havingDelayedNotifications && SlowNotificationHeuristics.shouldPromptBatterySaver()) {
return@fromCallable State.PROMPT_GENERAL_BATTERY_SAVER_DIALOG
}
@@ -24,7 +24,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.signal.core.util.ClearClipboardAlarmReceiver;
import org.signal.core.util.PendingIntentFlags;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.TemporaryScreenshotSecurity;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.payments.Mnemonic;
import org.signal.core.util.ServiceUtil;
@@ -48,8 +47,6 @@ public class PaymentsRecoveryPhraseFragment extends Fragment {
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
TemporaryScreenshotSecurity.bindToViewLifecycleOwner(this);
Toolbar toolbar = view.findViewById(R.id.payments_recovery_phrase_fragment_toolbar);
RecyclerView recyclerView = view.findViewById(R.id.payments_recovery_phrase_fragment_recycler);
TextView message = view.findViewById(R.id.payments_recovery_phrase_fragment_message);
@@ -20,13 +20,9 @@ import org.signal.network.NetworkResult
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.main.UsernameLinkResetResult
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.MultiDeviceUsernameChangeSyncJob
import org.thoughtcrime.securesms.keyvalue.AccountValues
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.net.SignalNetwork
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.confirmUsernameAndCreateNewLink
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.reserveUsername
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.updateUsernameDisplayForCurrentLink
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.util.NetworkUtil
@@ -448,9 +444,6 @@ object UsernameRepository {
SignalStore.account.usernameSyncErrorCount = 0
SignalStore.misc.needsUsernameRestore = false
if (Recipient.self().usernameSyncMessagesCapability.isSupported) {
MultiDeviceUsernameChangeSyncJob.enqueueUsernameChangeSync()
}
SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
StorageSyncHelper.scheduleSyncForDataChange()
Log.i(TAG, "[updateUsernameDisplayForCurrentLink] Successfully updated username.")
@@ -484,9 +477,6 @@ object UsernameRepository {
SignalStore.account.usernameSyncErrorCount = 0
SignalStore.misc.needsUsernameRestore = false
if (Recipient.self().usernameSyncMessagesCapability.isSupported) {
MultiDeviceUsernameChangeSyncJob.enqueueUsernameChangeSync()
}
SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
StorageSyncHelper.scheduleSyncForDataChange()
Log.i(TAG, "[confirmUsernameAndCreateNewLink] Successfully confirmed username.")
@@ -544,10 +534,6 @@ object UsernameRepository {
SignalStore.account.usernameSyncState = AccountValues.UsernameSyncState.IN_SYNC
SignalStore.account.usernameSyncErrorCount = 0
SignalStore.misc.needsUsernameRestore = false
if (Recipient.self().usernameSyncMessagesCapability.isSupported) {
MultiDeviceUsernameChangeSyncJob.enqueueUsernameChangeSync()
}
SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
StorageSyncHelper.scheduleSyncForDataChange()
Log.i(TAG, "[deleteUsername] Successfully deleted the username.")
@@ -352,9 +352,6 @@ class Recipient(
sealedSenderAccessModeValue
}
/** The user's capability to receive username sync messages */
val usernameSyncMessagesCapability: Capability = capabilities.usernameSyncMessages
/** The wallpaper to render as the chat background, if present. */
val wallpaper: ChatWallpaper?
get() {
@@ -312,7 +312,7 @@ private fun Content(
if (!model.isSelf && model.systemContact) {
AboutRow(
startIcon = ImageVector.vectorResource(id = CoreUiR.drawable.symbol_person_circle_24),
startIcon = ImageVector.vectorResource(id = R.drawable.symbol_person_circle_24),
text = stringResource(id = R.string.AboutSheet__s_is_in_your_system_contacts, model.shortName),
modifier = Modifier.fillMaxWidth()
)

Some files were not shown because too many files have changed in this diff Show More