mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-06-10 01:06:06 +01:00
Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2adf84a895 | |||
| 30ed0aa11a | |||
| ec9ae9e3b1 | |||
| 6a23896077 | |||
| f5a1d79eb5 | |||
| 4f0f0938d8 | |||
| 0136971963 | |||
| f810d731dd | |||
| 7c7c364fef | |||
| aa9591211b | |||
| bbd48547e5 | |||
| 757b521744 | |||
| a6311c87c1 | |||
| 045bd9287b | |||
| f1a72dd01a | |||
| af4d0a0ef0 | |||
| 7dcaa933f2 | |||
| 2c88945e6b | |||
| f9b9ce6c14 | |||
| 1d8fbad17e | |||
| 6872a14378 | |||
| 3a1eb4bd88 | |||
| d9f93294e4 | |||
| f063c43b52 | |||
| a7ed672634 | |||
| 1371663163 | |||
| 1f0c24a5d5 | |||
| b732cbe00b | |||
| 85d60dd0da | |||
| c020bfeb7a | |||
| 3ad446c6c9 | |||
| bc9c560f96 | |||
| 2b54dc4715 | |||
| 1443457eca | |||
| ffbc4465bb | |||
| 4e5ddad78f | |||
| 47a69d667c | |||
| 9d3a51def2 | |||
| b8c964846c | |||
| b02210c166 | |||
| c2f8261419 | |||
| 089d47936b | |||
| 48f55bba0a | |||
| 348387f2d0 | |||
| 30c0ef255a | |||
| 64d3ba9e5b | |||
| 930a2f052a | |||
| efec070728 | |||
| 99aa8a602b | |||
| 4a68e0c469 | |||
| be80619a3b | |||
| a0d605d1b1 | |||
| 64f30bff47 | |||
| 843b656fb6 | |||
| f5f5bf0a67 | |||
| bf73954f42 | |||
| 1fd651ee50 | |||
| f76292769a | |||
| 51c4afe5f5 | |||
| fe435433fd | |||
| fa8098a9aa | |||
| 81e09e65cb | |||
| 1059fcafba | |||
| 91d3fa8ad5 | |||
| 89bffe39ae | |||
| de2a5ea440 | |||
| a54d62c09d | |||
| e8785218a5 | |||
| e6e6075c9b | |||
| 95b69faa58 | |||
| b7f09ef923 | |||
| 50884e144e | |||
| 274feb168e | |||
| 5b1f5a2a20 | |||
| 2cb9685024 | |||
| 0f4fc74829 | |||
| f5e8e15785 | |||
| 3ff1501090 | |||
| 0907898105 | |||
| 5fd8101180 |
@@ -5,7 +5,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- '7.**'
|
||||
- '8.**'
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
# gh api repos/actions/checkout/commits/v6 --jq '.sha'
|
||||
with:
|
||||
submodules: true
|
||||
@@ -27,13 +27,26 @@ 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
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
# gh api repos/actions/checkout/commits/v6 --jq '.sha'
|
||||
with:
|
||||
submodules: true
|
||||
@@ -28,7 +28,15 @@ jobs:
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
cache: gradle
|
||||
|
||||
- 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 }}
|
||||
|
||||
- name: Install NDK
|
||||
run: echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "ndk;${{ env.NDK_VERSION }}"
|
||||
@@ -53,7 +61,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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
# gh api repos/actions/checkout/commits/v6 --jq '.sha'
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
# gh api repos/actions/checkout/commits/v6 --jq '.sha'
|
||||
- name: Build image
|
||||
run: |
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
actions: write
|
||||
|
||||
steps:
|
||||
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10
|
||||
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10
|
||||
# gh api repos/actions/stale/commits/v10 --jq '.sha'
|
||||
with:
|
||||
days-before-stale: 60
|
||||
|
||||
@@ -27,8 +27,8 @@ plugins {
|
||||
val staticIps = Properties().apply { file("static-ips.properties").reader().use { load(it) } }
|
||||
staticIps.stringPropertyNames().forEach { rootProject.extra[it] = staticIps.getProperty(it) }
|
||||
|
||||
val canonicalVersionCode = 1699
|
||||
val canonicalVersionName = "8.13.1"
|
||||
val canonicalVersionCode = 1703
|
||||
val canonicalVersionName = "8.14.3"
|
||||
val currentHotfixVersion = 0
|
||||
val maxHotfixVersions = 100
|
||||
|
||||
|
||||
+1
-1
@@ -138,7 +138,7 @@ class ConversationItemPreviewer {
|
||||
private fun attachment(): SignalServiceAttachmentPointer {
|
||||
return SignalServiceAttachmentPointer(
|
||||
Cdn.CDN_3.cdnNumber,
|
||||
SignalServiceAttachmentRemoteId.from(""),
|
||||
SignalServiceAttachmentRemoteId.from("", Cdn.CDN_3.cdnNumber),
|
||||
"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))
|
||||
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, 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(""),
|
||||
SignalServiceAttachmentRemoteId.from("", Cdn.S3.cdnNumber),
|
||||
"image/webp",
|
||||
null,
|
||||
Optional.empty(),
|
||||
@@ -170,7 +170,7 @@ object TestMessages {
|
||||
private fun voiceAttachment(): SignalServiceAttachmentPointer {
|
||||
return SignalServiceAttachmentPointer(
|
||||
Cdn.S3.cdnNumber,
|
||||
SignalServiceAttachmentRemoteId.from(""),
|
||||
SignalServiceAttachmentRemoteId.from("", Cdn.S3.cdnNumber),
|
||||
"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))
|
||||
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, 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))
|
||||
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, 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)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,8 @@ object AppCapabilities {
|
||||
storage = storageCapable,
|
||||
versionedExpirationTimer = true,
|
||||
attachmentBackfill = true,
|
||||
spqr = true
|
||||
spqr = true,
|
||||
usernameChangeSyncMessage = false // TODO(michelle): Turn on once all clients support it and add a migration
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,10 @@ 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;
|
||||
@@ -51,6 +53,7 @@ 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;
|
||||
@@ -59,6 +62,7 @@ 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;
|
||||
@@ -75,6 +79,7 @@ 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;
|
||||
@@ -83,7 +88,6 @@ 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;
|
||||
@@ -107,10 +111,8 @@ 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;
|
||||
@@ -121,7 +123,6 @@ 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;
|
||||
@@ -229,7 +230,7 @@ public class ApplicationContext extends Application implements AppForegroundObse
|
||||
.addPostRender(RefreshSvrCredentialsJob::enqueueIfNecessary)
|
||||
.addPostRender(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this))
|
||||
.addPostRender(EmojiSearchIndexDownloadJob::scheduleIfNecessary)
|
||||
.addPostRender(() -> SignalDatabase.messageLog().trimOldMessages(System.currentTimeMillis(), RemoteConfig.retryRespondMaxAge()))
|
||||
.addPostRender(MessageSendLogCleanupJob::enqueue)
|
||||
.addPostRender(() -> JumboEmoji.updateCurrentVersion(this))
|
||||
.addPostRender(RetrieveRemoteAnnouncementsJob::enqueue)
|
||||
.addPostRender(AndroidTelecomUtil::registerPhoneAccount)
|
||||
@@ -420,6 +421,7 @@ 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));
|
||||
@@ -510,6 +512,8 @@ 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!!)
|
||||
val remoteId = SignalServiceAttachmentRemoteId.from(attachment.remoteLocation!!, attachment.cdn.cdnNumber)
|
||||
|
||||
var attachmentWidth = attachment.width
|
||||
var attachmentHeight = attachment.height
|
||||
|
||||
@@ -38,8 +38,8 @@ object AvatarPickerStorage {
|
||||
.getAllAvatars()
|
||||
.filterIsInstance<Avatar.Photo>()
|
||||
|
||||
val inDatabaseFileNames = photoAvatars.map { PartAuthority.getAvatarPickerFilename(it.uri) }
|
||||
val onDiskFileNames = avatarFiles.map { it.name }
|
||||
val inDatabaseFileNames = photoAvatars.mapTo(mutableSetOf()) { PartAuthority.getAvatarPickerFilename(it.uri) }
|
||||
val onDiskFileNames = avatarFiles.mapTo(mutableSetOf()) { it.name }
|
||||
|
||||
val inDatabaseButNotOnDisk = inDatabaseFileNames - onDiskFileNames
|
||||
val onDiskButNotInDatabase = onDiskFileNames - inDatabaseFileNames
|
||||
|
||||
@@ -6,14 +6,18 @@
|
||||
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
|
||||
@@ -46,6 +50,8 @@ 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(
|
||||
@@ -61,7 +67,7 @@ object ArchiveUploadProgress {
|
||||
/**
|
||||
* Observe this to get updates on the current upload progress.
|
||||
*/
|
||||
val progress: Flow<ArchiveUploadProgressState> = _progress
|
||||
val progress: SharedFlow<ArchiveUploadProgressState> = _progress
|
||||
.throttleLatest(500.milliseconds) {
|
||||
uploadProgress.state == ArchiveUploadProgressState.State.None ||
|
||||
(uploadProgress.state == ArchiveUploadProgressState.State.UploadBackupFile && uploadProgress.backupFileUploadedBytes == 0L) ||
|
||||
@@ -114,6 +120,11 @@ 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,6 +31,9 @@ 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
|
||||
@@ -157,6 +160,22 @@ 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,6 +1465,7 @@ object BackupRepository {
|
||||
}
|
||||
|
||||
SignalDatabase.remappedRecords.clearCache()
|
||||
SignalDatabase.remappedRecords.trimStaleMappings()
|
||||
AppDependencies.recipientCache.clear()
|
||||
AppDependencies.recipientCache.warmUp()
|
||||
SignalDatabase.threads.clearCache()
|
||||
|
||||
+1
-1
@@ -123,7 +123,7 @@ fun DatabaseAttachment.createArchiveAttachmentPointer(useArchiveCdn: Boolean): S
|
||||
throw InvalidAttachmentException("empty content id")
|
||||
}
|
||||
|
||||
SignalServiceAttachmentRemoteId.from(remoteLocation) to cdn.cdnNumber
|
||||
SignalServiceAttachmentRemoteId.from(remoteLocation, cdn.cdnNumber) to cdn.cdnNumber
|
||||
}
|
||||
|
||||
val key = Base64.decode(remoteKey)
|
||||
|
||||
+1
-1
@@ -87,7 +87,7 @@ fun FilePointer?.toLocalAttachment(
|
||||
AttachmentType.TRANSIT -> {
|
||||
val signalAttachmentPointer = SignalServiceAttachmentPointer(
|
||||
cdnNumber = locatorInfo.transitCdnNumber ?: Cdn.CDN_0.cdnNumber,
|
||||
remoteId = SignalServiceAttachmentRemoteId.from(locatorInfo.transitCdnKey!!),
|
||||
remoteId = SignalServiceAttachmentRemoteId.from(locatorInfo.transitCdnKey!!, locatorInfo.transitCdnNumber ?: Cdn.CDN_0.cdnNumber),
|
||||
contentType = contentType,
|
||||
key = locatorInfo.key.toByteArray(),
|
||||
size = Optional.ofNullable(locatorInfo.size),
|
||||
|
||||
+2
@@ -10,6 +10,7 @@ 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
|
||||
@@ -25,6 +26,7 @@ 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,10 +38,12 @@ 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;
|
||||
@@ -63,7 +65,6 @@ 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;
|
||||
@@ -91,32 +92,33 @@ 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 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 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 MicrophoneRecorderView microphoneRecorderView;
|
||||
private SlideToCancel slideToCancel;
|
||||
private RecordTime recordTime;
|
||||
private ValueAnimator quoteAnimator;
|
||||
private ValueAnimator editMessageAnimator;
|
||||
private VoiceNoteDraftView voiceNoteDraftView;
|
||||
private MicrophoneRecorderView microphoneRecorderView;
|
||||
private SlideToCancel slideToCancel;
|
||||
private RecordTime recordTime;
|
||||
private ValueAnimator quoteAnimator;
|
||||
private ValueAnimator editMessageAnimator;
|
||||
private Stub<VoiceNoteDraftView> voiceNoteDraftViewStub;
|
||||
|
||||
private @Nullable Listener listener;
|
||||
private boolean emojiVisible;
|
||||
private boolean wallpaperEnabled;
|
||||
|
||||
private boolean hideForMessageRequestState;
|
||||
private boolean hideForGroupState;
|
||||
@@ -127,6 +129,12 @@ 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);
|
||||
}
|
||||
@@ -143,12 +151,10 @@ 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.quoteView = findViewById(R.id.quote_view);
|
||||
this.linkPreview = findViewById(R.id.link_preview);
|
||||
this.quoteViewStub = new Stub<>(findViewById(R.id.quote_view));
|
||||
this.linkPreviewStub = new Stub<>(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);
|
||||
@@ -158,7 +164,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.voiceNoteDraftView = findViewById(R.id.voice_note_draft_view);
|
||||
this.voiceNoteDraftViewStub = new Stub<>(findViewById(R.id.voice_note_draft_view_stub));
|
||||
this.slideToCancel = new SlideToCancel(findViewById(R.id.slide_to_cancel));
|
||||
this.microphoneRecorderView = findViewById(R.id.recorder_view);
|
||||
this.microphoneRecorderView.setHandler(this);
|
||||
@@ -175,14 +181,6 @@ 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));
|
||||
@@ -197,7 +195,6 @@ 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());
|
||||
@@ -214,34 +211,35 @@ public class InputPanel extends ConstraintLayout
|
||||
@NonNull SlideDeck attachments,
|
||||
@NonNull QuoteModel.Type quoteType)
|
||||
{
|
||||
this.quoteView.setQuote(requestManager, id, author, body, false, attachments, null, quoteType, true, null);
|
||||
QuoteView quoteView = requireQuoteView();
|
||||
|
||||
quoteView.setQuote(requestManager, id, author, body, false, attachments, null, quoteType, true, null);
|
||||
if (listener != null) {
|
||||
this.quoteView.setOnClickListener(v -> listener.onQuoteClicked(id, author.getId()));
|
||||
quoteView.setOnClickListener(v -> listener.onQuoteClicked(id, author.getId()));
|
||||
}
|
||||
|
||||
int originalHeight = this.quoteView.getVisibility() == VISIBLE ? this.quoteView.getMeasuredHeight()
|
||||
: 0;
|
||||
int originalHeight = quoteView.getVisibility() == VISIBLE ? quoteView.getMeasuredHeight() : 0;
|
||||
|
||||
this.quoteView.setVisibility(VISIBLE);
|
||||
quoteView.setVisibility(VISIBLE);
|
||||
|
||||
int maxWidth = composeContainer.getWidth();
|
||||
if (quoteView.getLayoutParams() instanceof MarginLayoutParams) {
|
||||
MarginLayoutParams layoutParams = (MarginLayoutParams) quoteView.getLayoutParams();
|
||||
maxWidth -= layoutParams.leftMargin + layoutParams.rightMargin;
|
||||
}
|
||||
this.quoteView.measure(MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST), 0);
|
||||
quoteView.measure(MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST), 0);
|
||||
|
||||
if (quoteAnimator != null) {
|
||||
quoteAnimator.cancel();
|
||||
}
|
||||
|
||||
quoteAnimator = createHeightAnimator(quoteView, originalHeight, this.quoteView.getMeasuredHeight(), null);
|
||||
quoteAnimator = createHeightAnimator(quoteView, originalHeight, quoteView.getMeasuredHeight(), null);
|
||||
|
||||
quoteAnimator.start();
|
||||
|
||||
if (this.linkPreview.getVisibility() == View.VISIBLE) {
|
||||
if (linkPreviewStub.getVisibility() == View.VISIBLE) {
|
||||
int cornerRadius = readDimen(R.dimen.message_corner_collapse_radius);
|
||||
this.linkPreview.setCorners(cornerRadius, cornerRadius);
|
||||
linkPreviewStub.get().setCorners(cornerRadius, cornerRadius);
|
||||
}
|
||||
|
||||
if (listener != null) {
|
||||
@@ -250,6 +248,12 @@ public class InputPanel extends ConstraintLayout
|
||||
}
|
||||
|
||||
public void clearQuote() {
|
||||
if (!quoteViewStub.resolved()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QuoteView quoteView = quoteViewStub.get();
|
||||
|
||||
if (quoteAnimator != null) {
|
||||
quoteAnimator.cancel();
|
||||
}
|
||||
@@ -259,9 +263,9 @@ public class InputPanel extends ConstraintLayout
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
quoteView.dismiss();
|
||||
|
||||
if (linkPreview.getVisibility() == View.VISIBLE) {
|
||||
if (linkPreviewStub.getVisibility() == View.VISIBLE) {
|
||||
int cornerRadius = readDimen(R.dimen.message_corner_radius);
|
||||
linkPreview.setCorners(cornerRadius, cornerRadius);
|
||||
linkPreviewStub.get().setCorners(cornerRadius, cornerRadius);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -273,6 +277,20 @@ 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,
|
||||
@@ -294,11 +312,12 @@ public class InputPanel extends ConstraintLayout
|
||||
return animator;
|
||||
}
|
||||
|
||||
public boolean hasSaveableContent() {
|
||||
return getQuote().isPresent() || voiceNoteDraftView.getDraft() != null;
|
||||
}
|
||||
|
||||
public Optional<QuoteModel> getQuote() {
|
||||
if (!quoteViewStub.resolved()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
QuoteView quoteView = quoteViewStub.get();
|
||||
if (quoteView.getQuoteId() > 0 && quoteView.getVisibility() == View.VISIBLE) {
|
||||
return Optional.of(new QuoteModel(quoteView.getQuoteId(),
|
||||
quoteView.getAuthor().getId(),
|
||||
@@ -314,41 +333,53 @@ public class InputPanel extends ConstraintLayout
|
||||
}
|
||||
|
||||
public boolean hasLinkPreview() {
|
||||
return linkPreview.getVisibility() == View.VISIBLE;
|
||||
return linkPreviewStub.getVisibility() == View.VISIBLE;
|
||||
}
|
||||
|
||||
public void setLinkPreviewLoading() {
|
||||
this.linkPreview.setVisibility(View.VISIBLE);
|
||||
this.linkPreview.setLoading();
|
||||
LinkPreviewView linkPreview = requireLinkPreview();
|
||||
linkPreview.setVisibility(View.VISIBLE);
|
||||
linkPreview.setLoading();
|
||||
}
|
||||
|
||||
public void setLinkPreviewNoPreview(@Nullable LinkPreviewRepository.Error customError) {
|
||||
this.linkPreview.setVisibility(View.VISIBLE);
|
||||
this.linkPreview.setNoPreview(customError);
|
||||
LinkPreviewView linkPreview = requireLinkPreview();
|
||||
linkPreview.setVisibility(View.VISIBLE);
|
||||
linkPreview.setNoPreview(customError);
|
||||
}
|
||||
|
||||
public void setLinkPreview(@NonNull RequestManager requestManager, @NonNull Optional<LinkPreview> preview) {
|
||||
if (preview.isPresent()) {
|
||||
this.linkPreview.setVisibility(View.VISIBLE);
|
||||
this.linkPreview.setLinkPreview(requestManager, preview.get(), true);
|
||||
LinkPreviewView linkPreview = requireLinkPreview();
|
||||
linkPreview.setVisibility(View.VISIBLE);
|
||||
linkPreview.setLinkPreview(requestManager, preview.get(), true);
|
||||
} else {
|
||||
this.linkPreview.setVisibility(View.GONE);
|
||||
linkPreviewStub.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
int cornerRadius = quoteView.getVisibility() == VISIBLE ? readDimen(R.dimen.message_corner_collapse_radius)
|
||||
: readDimen(R.dimen.message_corner_radius);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
this.linkPreview.setCorners(cornerRadius, cornerRadius);
|
||||
private @NonNull LinkPreviewView requireLinkPreview() {
|
||||
boolean wasResolved = linkPreviewStub.resolved();
|
||||
LinkPreviewView view = linkPreviewStub.get();
|
||||
|
||||
if (!wasResolved) {
|
||||
view.setCloseClickedListener(() -> {
|
||||
if (listener != null) listener.onLinkPreviewCanceled();
|
||||
});
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -403,7 +434,10 @@ public class InputPanel extends ConstraintLayout
|
||||
quickCameraToggle.setColorFilter(iconTint);
|
||||
composeText.setTextColor(textColor);
|
||||
composeText.setHintTextColor(textHintColor);
|
||||
quoteView.setWallpaperEnabled(enabled);
|
||||
wallpaperEnabled = enabled;
|
||||
if (quoteViewStub.resolved()) {
|
||||
quoteViewStub.get().setWallpaperEnabled(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
public void enterEditModeIfPossible(@NonNull RequestManager requestManager, @NonNull ConversationMessage conversationMessageToEdit, boolean fromDraft, boolean clearQuote) {
|
||||
@@ -493,7 +527,9 @@ public class InputPanel extends ConstraintLayout
|
||||
if (messageToEdit != null) {
|
||||
composeText.setText("");
|
||||
messageToEdit = null;
|
||||
quoteView.setMessageType(QuoteView.MessageType.PREVIEW);
|
||||
if (quoteViewStub.resolved()) {
|
||||
quoteViewStub.get().setMessageType(QuoteView.MessageType.PREVIEW);
|
||||
}
|
||||
clearQuote();
|
||||
}
|
||||
updateEditModeUi();
|
||||
@@ -647,7 +683,7 @@ public class InputPanel extends ConstraintLayout
|
||||
}
|
||||
|
||||
public @NonNull Observer<VoiceNotePlaybackState> getPlaybackStateObserver() {
|
||||
return voiceNoteDraftView.getPlaybackStateObserver();
|
||||
return playbackStateObserverProxy;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
@@ -666,7 +702,7 @@ public class InputPanel extends ConstraintLayout
|
||||
future.addListener(new AssertedSuccessListener<Void>() {
|
||||
@Override
|
||||
public void onSuccess(Void result) {
|
||||
if (voiceNoteDraftView.getDraft() == null) {
|
||||
if (!voiceNoteDraftViewStub.resolved() || voiceNoteDraftViewStub.get().getDraft() == null) {
|
||||
fadeInNormalComposeViews();
|
||||
}
|
||||
}
|
||||
@@ -680,10 +716,6 @@ public class InputPanel extends ConstraintLayout
|
||||
mediaKeyboard.setToMedia();
|
||||
}
|
||||
|
||||
public void setToIme() {
|
||||
mediaKeyboard.setToIme();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKeyEvent(KeyEvent keyEvent) {
|
||||
composeText.dispatchKeyEvent(keyEvent);
|
||||
@@ -715,20 +747,35 @@ 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 {
|
||||
voiceNoteDraftView.clearDraft();
|
||||
ViewUtil.fadeOut(voiceNoteDraftView, FADE_TIME);
|
||||
if (voiceNoteDraftViewStub.resolved()) {
|
||||
VoiceNoteDraftView voiceNoteDraftView = voiceNoteDraftViewStub.get();
|
||||
voiceNoteDraftView.clearDraft();
|
||||
ViewUtil.fadeOut(voiceNoteDraftView, FADE_TIME);
|
||||
}
|
||||
fadeInNormalComposeViews();
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable DraftTable.Draft getVoiceNoteDraft() {
|
||||
return voiceNoteDraftView.getDraft();
|
||||
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;
|
||||
}
|
||||
|
||||
private void hideNormalComposeViews() {
|
||||
|
||||
@@ -4,8 +4,6 @@ import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -43,9 +41,6 @@ import okhttp3.HttpUrl;
|
||||
*/
|
||||
public class LinkPreviewView extends FrameLayout {
|
||||
|
||||
private static final String STATE_ROOT = "linkPreviewView.state.root";
|
||||
private static final String STATE_STATE = "linkPreviewView.state.state";
|
||||
|
||||
private static final int TYPE_CONVERSATION = 0;
|
||||
private static final int TYPE_COMPOSE = 1;
|
||||
|
||||
@@ -114,30 +109,6 @@ public class LinkPreviewView extends FrameLayout {
|
||||
setWillNotDraw(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull Parcelable onSaveInstanceState() {
|
||||
Parcelable root = super.onSaveInstanceState();
|
||||
Bundle bundle = new Bundle();
|
||||
|
||||
bundle.putParcelable(STATE_ROOT, root);
|
||||
bundle.putParcelable(STATE_STATE, thumbnailState);
|
||||
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(Parcelable state) {
|
||||
if (state instanceof Bundle) {
|
||||
Parcelable root = ((Bundle) state).getParcelable(STATE_ROOT);
|
||||
thumbnailState = ((Bundle) state).getParcelable(STATE_STATE);
|
||||
|
||||
thumbnailState.applyState(thumbnail);
|
||||
super.onRestoreInstanceState(root);
|
||||
} else {
|
||||
super.onRestoreInstanceState(state);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dispatchDraw(Canvas canvas) {
|
||||
super.dispatchDraw(canvas);
|
||||
@@ -251,7 +222,7 @@ public class LinkPreviewView extends FrameLayout {
|
||||
thumbnailState.applyState(thumbnail);
|
||||
} else {
|
||||
cornerMask.setRadii(topStart, topEnd, 0, 0);
|
||||
thumbnailState.copy(
|
||||
thumbnailState = thumbnailState.copy(
|
||||
topStart,
|
||||
defaultRadius,
|
||||
defaultRadius,
|
||||
|
||||
@@ -434,6 +434,7 @@ public class QuoteView extends ConstraintLayout implements RecipientForeverObser
|
||||
|
||||
if (TextUtils.isEmpty(quoteTargetContentType)) {
|
||||
thumbnailView.setVisibility(GONE);
|
||||
attachmentVideoOVerlayStub.setVisibility(GONE);
|
||||
attachmentNameViewStub.setVisibility(GONE);
|
||||
|
||||
if (dismissStub.resolved()) {
|
||||
|
||||
+3
-2
@@ -81,6 +81,7 @@ 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 {
|
||||
|
||||
@@ -295,7 +296,7 @@ private fun AppSettingsContent(
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = stringResource(R.string.AccountSettingsFragment__account),
|
||||
icon = painterResource(R.drawable.symbol_person_circle_24),
|
||||
icon = painterResource(CoreUiR.drawable.symbol_person_circle_24),
|
||||
onClick = {
|
||||
callbacks.navigate(AppSettingsRoute.AccountRoute.Account)
|
||||
}
|
||||
@@ -305,7 +306,7 @@ private fun AppSettingsContent(
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = stringResource(R.string.preferences__linked_devices),
|
||||
icon = painterResource(R.drawable.symbol_devices_24),
|
||||
icon = painterResource(CoreUiR.drawable.symbol_devices_24),
|
||||
onClick = {
|
||||
callbacks.navigate(AppSettingsRoute.LinkDeviceRoute.LinkDevice)
|
||||
},
|
||||
|
||||
+1
-1
@@ -242,7 +242,7 @@ private fun BackupsSettingsContent(
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__on_device_backups),
|
||||
icon = ImageVector.vectorResource(R.drawable.symbol_device_phone_24),
|
||||
icon = ImageVector.vectorResource(CoreUiR.drawable.symbol_device_phone_24),
|
||||
label = stringResource(R.string.RemoteBackupsSettingsFragment__save_your_backups_to),
|
||||
onClick = onOnDeviceBackupsRowClick
|
||||
)
|
||||
|
||||
+63
-3
@@ -6,9 +6,12 @@ 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
|
||||
@@ -74,8 +77,10 @@ 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
|
||||
@@ -84,13 +89,14 @@ import kotlin.math.max
|
||||
import kotlin.random.Random
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__internal_preferences) {
|
||||
class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__internal_preferences, R.menu.internal_settings) {
|
||||
|
||||
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?
|
||||
@@ -107,6 +113,7 @@ 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)) {
|
||||
@@ -125,8 +132,11 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
viewModel = ViewModelProvider(this, factory)[InternalSettingsViewModel::class.java]
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) {
|
||||
adapter.submitList(getConfiguration(it).toMappingModelList()) {
|
||||
if (scrollToPosition != 0) {
|
||||
val mappingModelList = getConfiguration(it).toMappingModelList()
|
||||
val filteredList = viewModel.filterPreferences(requireContext(), mappingModelList, it.searchQuery)
|
||||
|
||||
adapter.submitList(filteredList) {
|
||||
if (scrollToPosition != 0 && it.searchQuery.isBlank()) {
|
||||
layoutManager?.scrollToPositionWithOffset(scrollToPosition, 0)
|
||||
scrollToPosition = 0
|
||||
}
|
||||
@@ -134,6 +144,56 @@ 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"))
|
||||
|
||||
+2
-1
@@ -32,5 +32,6 @@ data class InternalSettingsState(
|
||||
val forceSplitPane: Boolean,
|
||||
val forceSinglePane: Boolean,
|
||||
val useNewMediaActivity: Boolean,
|
||||
val disableInternalUser: Boolean
|
||||
val disableInternalUser: Boolean,
|
||||
val searchQuery: String = ""
|
||||
)
|
||||
|
||||
+99
-1
@@ -1,10 +1,14 @@
|
||||
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
|
||||
@@ -12,7 +16,10 @@ 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()
|
||||
@@ -167,7 +174,47 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
store.update { getState().copy(emojiVersion = it.emojiVersion) }
|
||||
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
|
||||
}
|
||||
|
||||
private fun getState() = InternalSettingsState(
|
||||
@@ -225,6 +272,57 @@ 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)))
|
||||
|
||||
+13
-2
@@ -162,11 +162,18 @@ 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 != null && (latestPayment.data.paymentMethodType == InAppPaymentData.PaymentMethodType.SEPA_DEBIT || latestPayment.data.paymentMethodType == InAppPaymentData.PaymentMethodType.IDEAL)) {
|
||||
if (latestPayment.isPendingBankTransfer()) {
|
||||
ManageDonationsState.RedemptionState.IS_PENDING_BANK_TRANSFER
|
||||
} else {
|
||||
ManageDonationsState.RedemptionState.IN_PROGRESS
|
||||
@@ -178,6 +185,10 @@ 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
|
||||
|
||||
+17
-12
@@ -7,7 +7,12 @@ 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
|
||||
@@ -98,18 +103,18 @@ data class InternalConversationSettingsState(
|
||||
val capabilities: RecipientRecord.Capabilities? = SignalDatabase.recipients.getCapabilities(recipient.id)
|
||||
if (capabilities != null) {
|
||||
AnnotatedString("No capabilities right now.")
|
||||
// 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")
|
||||
// }
|
||||
// }
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
AnnotatedString("Recipient not found!")
|
||||
}
|
||||
|
||||
+21
@@ -7,11 +7,13 @@ 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.
|
||||
@@ -41,6 +43,25 @@ 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
|
||||
|
||||
+1
-1
@@ -105,7 +105,7 @@ object RecipientPreference {
|
||||
} else {
|
||||
if (recipient.isSystemContact) {
|
||||
SpannableStringBuilder(recipient.getDisplayName(context)).apply {
|
||||
val drawable = context.requireDrawable(R.drawable.symbol_person_circle_24).apply {
|
||||
val drawable = context.requireDrawable(CoreUiR.drawable.symbol_person_circle_24).apply {
|
||||
setTint(ContextCompat.getColor(context, CoreUiR.color.signal_colorOnSurface))
|
||||
}
|
||||
SpanUtil.appendCenteredImageSpan(this, drawable, 16, 16)
|
||||
|
||||
+1
-2
@@ -7,7 +7,6 @@ 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;
|
||||
|
||||
@@ -116,7 +115,7 @@ public class VoiceNotePlaybackService extends MediaSessionService {
|
||||
@Nullable
|
||||
@Override
|
||||
public MediaSession onGetSession(@NonNull MediaSession.ControllerInfo controllerInfo) {
|
||||
if (Build.VERSION.SDK_INT >= 28 && controllerInfo.getUid() != Process.myUid()) {
|
||||
if (controllerInfo.getUid() != Process.myUid()) {
|
||||
Log.w(TAG, "Denying session to external caller: " + controllerInfo.getPackageName());
|
||||
return null;
|
||||
}
|
||||
|
||||
+1
-1
@@ -221,7 +221,7 @@ private fun Title(
|
||||
style = MaterialTheme.typography.headlineMedium
|
||||
)
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.symbol_person_circle_24),
|
||||
painter = painterResource(id = CoreUiR.drawable.symbol_person_circle_24),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(start = 6.dp)
|
||||
|
||||
+1
-1
@@ -190,7 +190,7 @@ fun SelfPipContent(
|
||||
Box(modifier = modifier) {
|
||||
VideoRenderer(
|
||||
participant = participant,
|
||||
mirror = participant.cameraDirection == CameraState.Direction.FRONT,
|
||||
mirror = !participant.isScreenSharing && participant.cameraDirection == CameraState.Direction.FRONT,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
|
||||
|
||||
@@ -543,7 +543,7 @@ private fun LargeLocalVideoRenderer(
|
||||
participant = localParticipant,
|
||||
renderInPip = false,
|
||||
raiseHandAllowed = false,
|
||||
mirrorVideo = localParticipant.cameraDirection == CameraState.Direction.FRONT,
|
||||
mirrorVideo = !localParticipant.isScreenSharing && localParticipant.cameraDirection == CameraState.Direction.FRONT,
|
||||
showAudioIndicator = false,
|
||||
onInfoMoreInfoClick = null,
|
||||
modifier = modifier
|
||||
|
||||
+2
-1
@@ -49,6 +49,7 @@ 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.
|
||||
@@ -160,7 +161,7 @@ fun MoveableLocalVideoRenderer(
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(
|
||||
if (isFocused) R.drawable.symbol_minimize_24 else R.drawable.symbol_maximize_24
|
||||
if (isFocused) R.drawable.symbol_minimize_24 else CoreUiR.drawable.symbol_maximize_24
|
||||
),
|
||||
tint = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
contentDescription = stringResource(
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@ fun PictureInPictureCallScreen(
|
||||
renderInPip = true,
|
||||
raiseHandAllowed = false,
|
||||
onInfoMoreInfoClick = null,
|
||||
mirrorVideo = isFullScreenLocalParticipant,
|
||||
mirrorVideo = isFullScreenLocalParticipant && !fullScreenParticipant.isScreenSharing,
|
||||
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(R.drawable.symbol_person_circle_24).apply {
|
||||
val drawable = context.requireDrawable(CoreUiR.drawable.symbol_person_circle_24).apply {
|
||||
setTint(ContextCompat.getColor(context, CoreUiR.color.signal_colorOnSurface))
|
||||
}
|
||||
SpanUtil.appendCenteredImageSpan(this, drawable, 16, 16)
|
||||
|
||||
+1
-1
@@ -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, R.drawable.symbol_person_circle_24),
|
||||
CONTACT(R.string.AttachmentKeyboard_contact, org.signal.core.ui.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);
|
||||
|
||||
|
||||
+6
-3
@@ -19,7 +19,6 @@ 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;
|
||||
@@ -104,6 +103,7 @@ 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,6 +221,8 @@ 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);
|
||||
@@ -487,8 +489,9 @@ 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), ContextCompat.getColor(getContext(), R.color.signal_icon_tint_secondary));
|
||||
Drawable drawable = DrawableUtil.tint(getContext().getDrawable(latestFrame), color);
|
||||
SpanUtil.appendCenteredImageSpan(builder, drawable, 12, 12);
|
||||
}
|
||||
|
||||
@@ -784,7 +787,7 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||
return false;
|
||||
}
|
||||
|
||||
return (messageRecord.isCollapsedGroupV2JoinUpdate() && !nextMessageRecord.map(m -> m.isGroupV2JoinRequest(toBlock.requireServiceId())).orElse(false)) ||
|
||||
return (messageRecord.isCollapsedGroupV2JoinUpdate(toBlock.requireServiceId()) && !nextMessageRecord.map(m -> m.isGroupV2JoinRequest(toBlock.requireServiceId())).orElse(false)) ||
|
||||
(messageRecord.isGroupV2JoinRequest(toBlock.requireServiceId()) && previousMessageRecord.map(m -> m.isCollapsedGroupV2JoinUpdate(toBlock.requireServiceId())).orElse(false));
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,10 @@ class ExpirationTimer(
|
||||
}
|
||||
|
||||
fun calculateProgress(): Float {
|
||||
if (startedAt == 0L) {
|
||||
return 0f
|
||||
}
|
||||
|
||||
val progressed = System.currentTimeMillis() - startedAt
|
||||
val percentComplete = progressed.toFloat() / expiresIn.toFloat()
|
||||
|
||||
|
||||
+9
-7
@@ -2882,10 +2882,11 @@ class ConversationFragment :
|
||||
requireContext(),
|
||||
recipient,
|
||||
{
|
||||
val disabledInput = binding.conversationDisabledInput
|
||||
messageRequestViewModel
|
||||
.onReportSpam()
|
||||
.doOnSubscribe { binding.conversationDisabledInput.showBusy() }
|
||||
.doOnTerminate { binding.conversationDisabledInput.hideBusy() }
|
||||
.doOnSubscribe { disabledInput.showBusy() }
|
||||
.doOnTerminate { disabledInput.hideBusy() }
|
||||
.subscribeBy {
|
||||
Log.d(TAG, "report spam complete")
|
||||
toast(R.string.ConversationFragment_reported_as_spam)
|
||||
@@ -2895,10 +2896,11 @@ class ConversationFragment :
|
||||
null
|
||||
} else {
|
||||
Runnable {
|
||||
val disabledInput = binding.conversationDisabledInput
|
||||
messageRequestViewModel
|
||||
.onBlockAndReportSpam()
|
||||
.doOnSubscribe { binding.conversationDisabledInput.showBusy() }
|
||||
.doOnTerminate { binding.conversationDisabledInput.hideBusy() }
|
||||
.doOnSubscribe { disabledInput.showBusy() }
|
||||
.doOnTerminate { disabledInput.hideBusy() }
|
||||
.subscribeBy { result ->
|
||||
when (result) {
|
||||
is Result.Success -> {
|
||||
@@ -2957,7 +2959,6 @@ class ConversationFragment :
|
||||
messageRequestViewModel
|
||||
.onAccept()
|
||||
.subscribeWithShowProgress("accept message request")
|
||||
.addTo(disposables)
|
||||
}
|
||||
|
||||
private fun onDeleteConversation() {
|
||||
@@ -2976,8 +2977,9 @@ class ConversationFragment :
|
||||
}
|
||||
|
||||
private fun Single<Result<Unit, GroupChangeFailureReason>>.subscribeWithShowProgress(logMessage: String): Disposable {
|
||||
return doOnSubscribe { binding.conversationDisabledInput.showBusy() }
|
||||
.doOnTerminate { binding.conversationDisabledInput.hideBusy() }
|
||||
val disabledInput = binding.conversationDisabledInput
|
||||
return doOnSubscribe { disabledInput.showBusy() }
|
||||
.doOnTerminate { disabledInput.hideBusy() }
|
||||
.subscribeBy { result ->
|
||||
when (result) {
|
||||
is Result.Success -> Log.d(TAG, "$logMessage complete")
|
||||
|
||||
@@ -246,6 +246,7 @@ 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,6 +16,7 @@ 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
|
||||
@@ -39,6 +40,10 @@ 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
|
||||
@@ -93,30 +98,51 @@ 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))
|
||||
.setPositiveButton(R.string.MessageRequestBottomView_accept) { _, _ -> listener?.onAcceptMessageRequestClicked() }
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.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.") }
|
||||
.show()
|
||||
} else if (messageRequestState.isGroupV2Add) {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.MessageRequestBottomView_join_group)
|
||||
.setMessage(R.string.MessageRequestBottomView_review_requests_carefully_groups)
|
||||
.setPositiveButton(R.string.MessageRequestBottomView_join) { _, _ -> listener?.onAcceptMessageRequestClicked() }
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.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.") }
|
||||
.show()
|
||||
} else {
|
||||
listener?.onAcceptMessageRequestClicked()
|
||||
}
|
||||
}
|
||||
setDeleteOnClickListener { listener?.onDeleteClicked() }
|
||||
setBlockOnClickListener { listener?.onBlockClicked() }
|
||||
setUnblockOnClickListener { listener?.onUnblockClicked() }
|
||||
setReportOnClickListener { listener?.onReportSpamClicked() }
|
||||
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()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
+4
-1
@@ -167,6 +167,7 @@ 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;
|
||||
@@ -435,7 +436,9 @@ public class ConversationListFragment extends MainFragment implements Conversati
|
||||
onSearchQueryUpdated(query);
|
||||
}
|
||||
|
||||
if (SignalStore.settings().getAutomaticVerificationEnabled() &&
|
||||
if (SignalStore.account().isRegistered() &&
|
||||
!TextSecurePreferences.isUnauthorizedReceived(requireContext()) &&
|
||||
SignalStore.settings().getAutomaticVerificationEnabled() &&
|
||||
SignalStore.misc().getHasKeyTransparencyFailure() &&
|
||||
!SignalStore.misc().getHasSeenKeyTransparencyFailure()) {
|
||||
SelfVerificationFailureSheet.show(getParentFragmentManager());
|
||||
|
||||
+4
-8
@@ -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(), R.drawable.symbol_person_circle_24);
|
||||
Drawable drawable = ContextUtil.requireDrawable(getContext(), org.signal.core.ui.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,12 +629,8 @@ 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())) {
|
||||
@@ -716,15 +712,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.map(createFinalBodyWithMediaIcon(context, body, thread, requestManager, thumbSize, thumbTarget), updatedBody -> {
|
||||
LiveData<SpannableString> finalBody = Transformations.switchMap(createFinalBodyWithMediaIcon(context, body, thread, requestManager, thumbSize, thumbTarget), updatedBody -> {
|
||||
if (thread.getRecipient().isGroup()) {
|
||||
RecipientId groupMessageSender = thread.getGroupMessageSender();
|
||||
if (!groupMessageSender.isUnknown()) {
|
||||
return createGroupMessageUpdateString(context, updatedBody, Recipient.resolved(groupMessageSender));
|
||||
return Transformations.map(Recipient.live(groupMessageSender).getLiveDataResolved(), recipient -> createGroupMessageUpdateString(context, updatedBody, recipient));
|
||||
}
|
||||
}
|
||||
|
||||
return new SpannableString(updatedBody);
|
||||
return LiveDataUtil.just(new SpannableString(updatedBody));
|
||||
});
|
||||
|
||||
return whileLoadingShow(sourceBody, finalBody);
|
||||
|
||||
@@ -7,6 +7,7 @@ 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
|
||||
@@ -84,6 +85,23 @@ 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,7 +298,10 @@ 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_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"
|
||||
)
|
||||
|
||||
private val DATA_FILE_INFO_PROJECTION = arrayOf(
|
||||
@@ -372,7 +375,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)
|
||||
.from("$TABLE_NAME INDEXED BY $DATA_HASH_REMOTE_KEY_INDEX")
|
||||
.where("$DATA_HASH_END = ? AND $TRANSFER_STATE = $TRANSFER_PROGRESS_DONE AND $DATA_FILE NOT NULL AND $DATA_FILE != ?", hashEnd, file.absolutePath)
|
||||
.limit(1)
|
||||
.run()
|
||||
@@ -1301,7 +1304,9 @@ class AttachmentTable(
|
||||
val contentTypesToDelete: MutableSet<String> = mutableSetOf()
|
||||
|
||||
val deleteCount = writableDatabase.withinTransaction { db ->
|
||||
db.select(DATA_FILE, CONTENT_TYPE, ID)
|
||||
val metadataIdsToCleanup: MutableSet<Long> = mutableSetOf()
|
||||
|
||||
db.select(DATA_FILE, CONTENT_TYPE, ID, METADATA_ID)
|
||||
.from(TABLE_NAME)
|
||||
.where("$MESSAGE_ID = ?", mmsId)
|
||||
.run()
|
||||
@@ -1313,6 +1318,8 @@ 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 }
|
||||
@@ -1323,7 +1330,7 @@ class AttachmentTable(
|
||||
.where("$MESSAGE_ID = ?", mmsId)
|
||||
.run()
|
||||
|
||||
SignalDatabase.attachmentMetadata.cleanup()
|
||||
SignalDatabase.attachmentMetadata.cleanupById(metadataIdsToCleanup)
|
||||
|
||||
AppDependencies.databaseObserver.notifyAttachmentDeletedObservers()
|
||||
|
||||
@@ -1367,7 +1374,9 @@ class AttachmentTable(
|
||||
var threadId: Long = -1
|
||||
|
||||
writableDatabase.withinTransaction { db ->
|
||||
db.select(DATA_FILE, CONTENT_TYPE, ID)
|
||||
val metadataIdsToCleanup: MutableSet<Long> = mutableSetOf()
|
||||
|
||||
db.select(DATA_FILE, CONTENT_TYPE, ID, METADATA_ID)
|
||||
.from(TABLE_NAME)
|
||||
.where("$MESSAGE_ID = ?", messageId)
|
||||
.run()
|
||||
@@ -1375,6 +1384,7 @@ 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
|
||||
@@ -1408,7 +1418,7 @@ class AttachmentTable(
|
||||
.where("$MESSAGE_ID = ?", messageId)
|
||||
.run()
|
||||
|
||||
SignalDatabase.attachmentMetadata.cleanup()
|
||||
SignalDatabase.attachmentMetadata.cleanupById(metadataIdsToCleanup)
|
||||
|
||||
AppDependencies.databaseObserver.notifyAttachmentDeletedObservers()
|
||||
|
||||
@@ -1433,7 +1443,7 @@ class AttachmentTable(
|
||||
var deletedMessageId: Long? = null
|
||||
|
||||
writableDatabase.withinTransaction { db ->
|
||||
db.select(DATA_FILE, CONTENT_TYPE, MESSAGE_ID)
|
||||
db.select(DATA_FILE, CONTENT_TYPE, MESSAGE_ID, METADATA_ID)
|
||||
.from(TABLE_NAME)
|
||||
.where("$ID = ?", id.id)
|
||||
.run()
|
||||
@@ -1446,12 +1456,13 @@ 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.cleanup()
|
||||
SignalDatabase.attachmentMetadata.cleanupById(listOfNotNull(metadataId))
|
||||
|
||||
if (filePath != null && isSafeToDeleteDataFile(filePath, id)) {
|
||||
filePathsToDelete += filePath
|
||||
@@ -1665,6 +1676,24 @@ 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)
|
||||
}
|
||||
@@ -1730,7 +1759,7 @@ class AttachmentTable(
|
||||
// the quality of the attachment we received.
|
||||
val hashMatch: DataFileInfo? = readableDatabase
|
||||
.select(*DATA_FILE_INFO_PROJECTION)
|
||||
.from(TABLE_NAME)
|
||||
.from("$TABLE_NAME INDEXED BY $DATA_HASH_REMOTE_KEY_INDEX")
|
||||
.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,6 +30,7 @@ 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
|
||||
@@ -109,24 +110,66 @@ 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 {
|
||||
@@ -157,7 +200,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||
val messageType: Long = Call.getMessageType(type, direction, event)
|
||||
|
||||
writableDatabase.withinTransaction {
|
||||
val result = SignalDatabase.messages.insertCallLog(peer, messageType, timestamp, direction == Direction.OUTGOING)
|
||||
val result = SignalDatabase.messages.insertOneToOneCallLog(peer, messageType, timestamp, direction == Direction.OUTGOING)
|
||||
val values = contentValuesOf(
|
||||
CALL_ID to callId,
|
||||
MESSAGE_ID to result.messageId,
|
||||
@@ -202,7 +245,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.updateCallLog(call.messageId, call.messageType)
|
||||
SignalDatabase.messages.updateOneToOneCallLog(call.messageId, call.messageType)
|
||||
}
|
||||
|
||||
AppDependencies.messageNotifier.updateNotification(context)
|
||||
@@ -892,7 +935,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||
|
||||
Log.d(TAG, "Updating group call state: localJoined: $localJoined, isGroupCallActive: $isGroupCallActive")
|
||||
|
||||
return writableDatabase.update(TABLE_NAME)
|
||||
val changed = writableDatabase.update(TABLE_NAME)
|
||||
.values(
|
||||
LOCAL_JOINED to localJoined,
|
||||
GROUP_CALL_ACTIVE to isGroupCallActive
|
||||
@@ -905,6 +948,16 @@ 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(
|
||||
@@ -936,7 +989,14 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||
|
||||
RingUpdate.EXPIRED_REQUEST, RingUpdate.CANCELLED_BY_RINGER -> {
|
||||
when (call.event) {
|
||||
Event.GENERIC_GROUP_CALL, Event.RINGING -> updateEventFromRingState(ringId, if (dueToNotificationProfile) Event.MISSED_NOTIFICATION_PROFILE else Event.MISSED, ringerRecipient)
|
||||
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.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
|
||||
@@ -946,7 +1006,14 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||
RingUpdate.BUSY_LOCALLY -> {
|
||||
when (call.event) {
|
||||
Event.JOINED -> updateEventFromRingState(ringId, Event.ACCEPTED)
|
||||
Event.GENERIC_GROUP_CALL, Event.RINGING -> updateEventFromRingState(ringId, Event.MISSED)
|
||||
Event.GENERIC_GROUP_CALL -> updateEventFromRingState(ringId, Event.MISSED)
|
||||
Event.RINGING -> {
|
||||
if (call.didLocalUserJoin) {
|
||||
updateEventFromRingState(ringId, Event.ACCEPTED)
|
||||
} else {
|
||||
updateEventFromRingState(ringId, Event.MISSED)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
updateEventFromRingState(ringId, call.event, ringerRecipient)
|
||||
Log.w(TAG, "Received a busy event we can't process. Updating ringer only.")
|
||||
@@ -957,7 +1024,14 @@ 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, Event.RINGING -> updateEventFromRingState(ringId, Event.MISSED)
|
||||
Event.GENERIC_GROUP_CALL -> updateEventFromRingState(ringId, Event.MISSED)
|
||||
Event.RINGING -> {
|
||||
if (call.didLocalUserJoin) {
|
||||
updateEventFromRingState(ringId, Event.ACCEPTED)
|
||||
} else {
|
||||
updateEventFromRingState(ringId, Event.MISSED)
|
||||
}
|
||||
}
|
||||
else -> Log.w(TAG, "Received a busy event we can't process. Ignoring.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +138,6 @@ 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
|
||||
@@ -316,8 +315,11 @@ 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(
|
||||
@@ -350,7 +352,11 @@ 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 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"
|
||||
)
|
||||
|
||||
private val MMS_PROJECTION_BASE = arrayOf(
|
||||
@@ -652,6 +658,32 @@ 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
|
||||
@@ -873,11 +905,12 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
return results
|
||||
}
|
||||
|
||||
fun insertCallLog(recipientId: RecipientId, type: Long, timestamp: Long, outgoing: Boolean): InsertResult {
|
||||
fun insertOneToOneCallLog(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(),
|
||||
@@ -886,8 +919,10 @@ 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
|
||||
THREAD_ID to threadId,
|
||||
EXPIRES_IN to expiresIn
|
||||
)
|
||||
|
||||
val messageId = writableDatabase.insert(TABLE_NAME, null, values)
|
||||
@@ -906,13 +941,14 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
)
|
||||
}
|
||||
|
||||
fun updateCallLog(messageId: Long, type: Long) {
|
||||
fun updateOneToOneCallLog(messageId: Long, type: Long) {
|
||||
val message = getMessageRecordOrNull(messageId = messageId)
|
||||
writableDatabase
|
||||
.update(TABLE_NAME)
|
||||
.values(
|
||||
TYPE to type,
|
||||
READ to 1
|
||||
READ to 1,
|
||||
NOTIFIED to 1
|
||||
)
|
||||
.where("$ID = ?", messageId)
|
||||
.run()
|
||||
@@ -925,6 +961,13 @@ 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))
|
||||
}
|
||||
@@ -960,6 +1003,7 @@ 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
|
||||
@@ -1059,7 +1103,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
|
||||
val updateDetail = GroupCallUpdateDetailsUtil.parse(message.body)
|
||||
val containsSelf = joinedUuids.contains(SignalStore.account.requireAci().rawUuid)
|
||||
val sameEraId = updateDetail.eraId == eraId && !Util.isEmpty(eraId)
|
||||
// Treat empty eraId from ring requests as matching for updating
|
||||
val sameEraId = (updateDetail.eraId == eraId || updateDetail.eraId.isEmpty()) && !Util.isEmpty(eraId)
|
||||
val inCallUuids = if (sameEraId) joinedUuids.map { it.toString() } else emptyList()
|
||||
val body = GroupCallUpdateDetailsUtil.createUpdatedBody(updateDetail, inCallUuids, isCallFull, isRingingOnLocalDevice)
|
||||
val contentValues = contentValuesOf(
|
||||
@@ -1068,6 +1113,7 @@ 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)
|
||||
@@ -1105,7 +1151,8 @@ 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)
|
||||
val sameEraId = groupCallUpdateDetails.eraId == peekGroupCallEraId && !Util.isEmpty(peekGroupCallEraId)
|
||||
// Treat empty eraId from ring requests as matching for updating
|
||||
val sameEraId = (groupCallUpdateDetails.eraId == peekGroupCallEraId || groupCallUpdateDetails.eraId.isEmpty()) && !Util.isEmpty(peekGroupCallEraId)
|
||||
|
||||
val inCallUuids = if (sameEraId) {
|
||||
peekJoinedUuids.map { it.toString() }.toList()
|
||||
@@ -1119,6 +1166,7 @@ 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)
|
||||
@@ -1187,6 +1235,7 @@ 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()
|
||||
@@ -1226,6 +1275,7 @@ 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()
|
||||
@@ -1259,6 +1309,7 @@ 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
|
||||
)
|
||||
@@ -1293,6 +1344,7 @@ 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
|
||||
@@ -1317,6 +1369,7 @@ 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
|
||||
@@ -1334,6 +1387,7 @@ 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())
|
||||
@@ -1352,6 +1406,7 @@ 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())
|
||||
@@ -1373,6 +1428,7 @@ 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
|
||||
@@ -1650,7 +1706,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
|
||||
return readableDatabase
|
||||
.select(DATE_SENT)
|
||||
.from(TABLE_NAME)
|
||||
.from("$TABLE_NAME INDEXED BY $INDEX_STORY_TYPE")
|
||||
.where("$IS_STORY_CLAUSE AND $THREAD_ID != ?", releaseChannelThreadId)
|
||||
.limit(1)
|
||||
.orderBy("$DATE_SENT ASC")
|
||||
@@ -1670,76 +1726,45 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
return writableDatabase.withinTransaction { db ->
|
||||
val releaseChannelThreadId = getReleaseChannelThreadId(hasSeenReleaseChannelStories)
|
||||
|
||||
val storiesBeforeTimestampWhere = "$IS_STORY_CLAUSE AND $DATE_SENT < ? AND $THREAD_ID != ?"
|
||||
val sharedArgs = buildArgs(timestamp, releaseChannelThreadId)
|
||||
data class ExpiredStory(val id: Long, val fromRecipientId: Long)
|
||||
|
||||
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
|
||||
)
|
||||
"""
|
||||
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 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)
|
||||
if (expiredStories.isEmpty()) {
|
||||
return@withinTransaction 0
|
||||
}
|
||||
|
||||
db.select(FROM_RECIPIENT_ID)
|
||||
.from(TABLE_NAME)
|
||||
.where(storiesBeforeTimestampWhere, sharedArgs)
|
||||
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)
|
||||
.run()
|
||||
.readToList { RecipientId.from(it.requireLong(FROM_RECIPIENT_ID)) }
|
||||
.forEach { id -> AppDependencies.databaseObserver.notifyStoryObservers(id) }
|
||||
|
||||
val deletedStoryCount = db.select(ID)
|
||||
.from(TABLE_NAME)
|
||||
.where(storiesBeforeTimestampWhere, sharedArgs)
|
||||
db.update("$TABLE_NAME INDEXED BY $INDEX_PARENT_STORY_ID")
|
||||
.values(QUOTE_MISSING to 1, QUOTE_BODY to "")
|
||||
.where(quotedReplyClause.where, quotedReplyClause.whereArgs)
|
||||
.run()
|
||||
.use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
deleteMessage(cursor.requireLong(ID))
|
||||
}
|
||||
|
||||
cursor.count
|
||||
}
|
||||
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)) }
|
||||
|
||||
if (deletedStoryCount > 0) {
|
||||
OptimizeMessageSearchIndexJob.enqueue()
|
||||
}
|
||||
expiredStories.forEach { AppDependencies.databaseObserver.notifyStoryObservers(RecipientId.from(it.fromRecipientId)) }
|
||||
|
||||
deletedStoryCount
|
||||
storyIds.forEach { deleteMessage(it) }
|
||||
|
||||
OptimizeMessageSearchIndexJob.enqueue()
|
||||
|
||||
storyIds.size
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1810,71 +1835,40 @@ 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 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
|
||||
)
|
||||
"""
|
||||
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 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
|
||||
)
|
||||
"""
|
||||
if (storyIds.isEmpty()) return@withinTransaction 0
|
||||
|
||||
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
|
||||
)
|
||||
"""
|
||||
val directReplyClause = buildSingleCollectionQuery(PARENT_STORY_ID, storyIds)
|
||||
val quotedReplyClause = buildSingleCollectionQuery(PARENT_STORY_ID, storyIds.map { -it })
|
||||
|
||||
db.execSQL(deleteStoryRepliesQuery, sharedArgs)
|
||||
db.execSQL(disassociateQuoteQuery, sharedArgs)
|
||||
db.rawQuery(storyRepliesQuery, sharedArgs).forEach { cursor: Cursor ->
|
||||
val mmsId = cursor.requireLong(ID)
|
||||
attachments.deleteAttachmentsForMessage(mmsId)
|
||||
}
|
||||
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)) }
|
||||
|
||||
AppDependencies.databaseObserver.notifyStoryObservers(recipientId)
|
||||
|
||||
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))
|
||||
}
|
||||
storyIds.forEach { deleteMessage(it) }
|
||||
|
||||
cursor.count
|
||||
}
|
||||
OptimizeMessageSearchIndexJob.enqueue()
|
||||
|
||||
if (deletedStoryCount > 0) {
|
||||
OptimizeMessageSearchIndexJob.enqueue()
|
||||
}
|
||||
|
||||
deletedStoryCount
|
||||
storyIds.size
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2630,7 +2624,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
val results = writableDatabase.rawQuery(
|
||||
"""
|
||||
UPDATE $TABLE_NAME INDEXED BY $index
|
||||
SET $READ = 1, $REACTIONS_UNREAD = 0, $REACTIONS_LAST_SEEN = ${System.currentTimeMillis()}, $VOTES_UNREAD = 0, $VOTES_LAST_SEEN = ${System.currentTimeMillis()}
|
||||
SET $READ = 1, $NOTIFIED = 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
|
||||
""",
|
||||
@@ -3430,6 +3424,7 @@ 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)
|
||||
@@ -4231,15 +4226,12 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
}
|
||||
|
||||
fun getAllRateLimitedMessageIds(): Set<Long> {
|
||||
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
|
||||
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) }
|
||||
}
|
||||
|
||||
fun deleteMessagesInThreadBeforeDate(threadId: Long, date: Long, inclusive: Boolean): Int {
|
||||
@@ -4443,34 +4435,25 @@ 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)
|
||||
|
||||
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 readableDatabase.rawQuery(query, args)
|
||||
.readToSingleObject { cursor ->
|
||||
ViewOnceExpirationInfo(cursor.requireLong(ID), cursor.requireLong(DATE_RECEIVED))
|
||||
}
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -5613,6 +5596,7 @@ 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,
|
||||
@@ -5829,12 +5813,12 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
|
||||
return readableDatabase
|
||||
.select(*MMS_PROJECTION)
|
||||
.from(TABLE_NAME)
|
||||
.from("$TABLE_NAME INDEXED BY $INDEX_NOTIFICATION_STATE")
|
||||
.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
|
||||
@@ -5991,7 +5975,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
|
||||
if (!hasReactions) {
|
||||
values.put(REACTIONS_UNREAD, 0)
|
||||
} else if (!isRemoval) {
|
||||
} else if (!isRemoval && isOutgoing) {
|
||||
values.put(REACTIONS_UNREAD, 1)
|
||||
}
|
||||
|
||||
@@ -6017,7 +6001,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
|
||||
if (!hasVotes) {
|
||||
values.put(VOTES_UNREAD, 0)
|
||||
} else if (!isRemoval) {
|
||||
} else if (!isRemoval && isOutgoing) {
|
||||
values.put(VOTES_UNREAD, 1)
|
||||
}
|
||||
|
||||
|
||||
@@ -345,19 +345,11 @@ public interface MessageTypes {
|
||||
}
|
||||
|
||||
static boolean isChatSessionRefresh(long type) {
|
||||
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 ||
|
||||
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 ||
|
||||
(type & ENCRYPTION_REMOTE_BIT) != 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -424,6 +424,7 @@ 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
|
||||
}
|
||||
}
|
||||
@@ -4953,8 +4954,9 @@ 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 sore more than 32 capabilities in the bitmask.
|
||||
// IMPORTANT: We cannot store more than 32 capabilities in the bitmask.
|
||||
}
|
||||
|
||||
enum class VibrateState(val id: Int) {
|
||||
|
||||
@@ -10,6 +10,7 @@ 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
|
||||
@@ -174,7 +175,8 @@ object RecipientTableCursorUtil {
|
||||
fun readCapabilities(cursor: Cursor): RecipientRecord.Capabilities {
|
||||
val capabilities = cursor.requireLong(RecipientTable.CAPABILITIES)
|
||||
return RecipientRecord.Capabilities(
|
||||
rawBits = capabilities
|
||||
rawBits = capabilities,
|
||||
usernameSyncMessages = Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientTable.Capabilities.USERNAME_SYNC_MESSAGES, RecipientTable.Capabilities.BIT_LENGTH).toInt())
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -56,16 +56,11 @@ class RemappedRecordTables internal constructor(context: Context?, databaseHelpe
|
||||
fun getAllRecipientMappings(): Map<RecipientId, RecipientId> {
|
||||
val recipientMap: MutableMap<RecipientId, RecipientId> = HashMap()
|
||||
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
return recipientMap
|
||||
@@ -74,16 +69,21 @@ class RemappedRecordTables internal constructor(context: Context?, databaseHelpe
|
||||
fun getAllThreadMappings(): Map<Long, Long> {
|
||||
val threadMap: MutableMap<Long, Long> = HashMap()
|
||||
|
||||
readableDatabase.withinTransaction { db ->
|
||||
val mappings = getAllMappings(db, Threads.TABLE_NAME)
|
||||
for (mapping in mappings) {
|
||||
threadMap[mapping.oldId] = mapping.newId
|
||||
}
|
||||
val mappings = getAllMappings(readableDatabase, 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,6 +2,7 @@ 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;
|
||||
@@ -11,6 +12,7 @@ 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
|
||||
@@ -30,8 +32,10 @@ class RemappedRecords {
|
||||
|
||||
private static final RemappedRecords INSTANCE = new RemappedRecords();
|
||||
|
||||
private Map<RecipientId, RecipientId> recipientMap;
|
||||
private Map<Long, Long> threadMap;
|
||||
private volatile Map<RecipientId, RecipientId> recipientMap;
|
||||
private volatile Map<Long, Long> threadMap;
|
||||
|
||||
private final AtomicBoolean staleTrimScheduled = new AtomicBoolean(false);
|
||||
|
||||
private RemappedRecords() {}
|
||||
|
||||
@@ -106,13 +110,31 @@ class RemappedRecords {
|
||||
|
||||
private void ensureRecipientMapIsPopulated() {
|
||||
if (recipientMap == null) {
|
||||
recipientMap = SignalDatabase.remappedRecords().getAllRecipientMappings();
|
||||
Map<RecipientId, RecipientId> loaded = SignalDatabase.remappedRecords().getAllRecipientMappings();
|
||||
synchronized (this) {
|
||||
if (recipientMap == null) {
|
||||
recipientMap = loaded;
|
||||
}
|
||||
}
|
||||
scheduleStaleTrimIfNeeded(loaded.isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureThreadMapIsPopulated() {
|
||||
if (threadMap == null) {
|
||||
threadMap = SignalDatabase.remappedRecords().getAllThreadMappings();
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,10 @@ 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;
|
||||
@@ -25,6 +27,7 @@ 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
|
||||
@@ -33,6 +36,14 @@ import java.util.Set;
|
||||
*/
|
||||
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;
|
||||
@@ -48,6 +59,9 @@ 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;
|
||||
|
||||
@@ -58,6 +72,10 @@ 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();
|
||||
@@ -78,12 +96,20 @@ 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) {
|
||||
@@ -96,6 +122,10 @@ 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();
|
||||
}
|
||||
@@ -109,11 +139,19 @@ 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) {
|
||||
@@ -236,13 +274,13 @@ public class SQLiteDatabase implements SupportSQLiteDatabase {
|
||||
@Override
|
||||
public Cursor query(SupportSQLiteQuery query) {
|
||||
DatabaseMonitor.onSql(query.getSql(), null);
|
||||
return wrapped.query(query);
|
||||
return traceSql("query(SupportSQLiteQuery)", null, query.getSql(), false, query.getSql(), null, () -> wrapped.query(query));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(SupportSQLiteQuery query, CancellationSignal cancellationSignal) {
|
||||
DatabaseMonitor.onSql(query.getSql(), null);
|
||||
return wrapped.query(query, cancellationSignal);
|
||||
return traceSql("query(SupportSQLiteQuery, CancellationSignal)", null, query.getSql(), false, query.getSql(), null, () -> wrapped.query(query, cancellationSignal));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -292,6 +330,7 @@ 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() { }
|
||||
@@ -310,13 +349,31 @@ 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();
|
||||
@@ -330,42 +387,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, () -> wrapped.query(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));
|
||||
}
|
||||
|
||||
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, () -> wrapped.queryWithFactory(cursorFactory, 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));
|
||||
}
|
||||
|
||||
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, () -> wrapped.query(table, columns, selection, selectionArgs, groupBy, having, orderBy));
|
||||
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));
|
||||
}
|
||||
|
||||
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, () -> wrapped.query(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));
|
||||
}
|
||||
|
||||
public Cursor rawQuery(String sql, String[] selectionArgs) {
|
||||
DatabaseMonitor.onSql(sql, selectionArgs);
|
||||
return traceSql("rawQuery(2a)", sql, false, () -> wrapped.rawQuery(sql, selectionArgs));
|
||||
return traceSql("rawQuery(2a)", null, sql, false, sql, selectionArgs, () -> wrapped.rawQuery(sql, selectionArgs));
|
||||
}
|
||||
|
||||
public Cursor rawQuery(String sql, Object... args) {
|
||||
DatabaseMonitor.onSql(sql, args);
|
||||
return traceSql("rawQuery(2b)", sql, false,() -> wrapped.rawQuery(sql, args));
|
||||
return traceSql("rawQuery(2b)", null, sql, false, sql, args, () -> 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()", sql, false, () -> wrapped.rawQueryWithFactory(cursorFactory, sql, selectionArgs, editTable));
|
||||
return traceSql("rawQueryWithFactory()", null, sql, false, sql, selectionArgs, () -> 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)", sql, false, () -> rawQuery(sql, selectionArgs, initialRead, maxRead));
|
||||
return traceSql("rawQuery(4)", null, sql, false, sql, selectionArgs, () -> rawQuery(sql, selectionArgs, initialRead, maxRead));
|
||||
}
|
||||
|
||||
public long insert(String table, String nullColumnHack, ContentValues values) {
|
||||
@@ -390,17 +447,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, () -> wrapped.delete(table, whereClause, whereArgs));
|
||||
return traceSql("delete()", table, whereClause, true, buildDeletePlanSql(table, whereClause), whereArgs, () -> 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, () -> wrapped.update(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));
|
||||
}
|
||||
|
||||
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, () -> wrapped.updateWithOnConflict(table, values, whereClause, whereArgs, conflictAlgorithm));
|
||||
return traceSql("updateWithOnConflict()", table, whereClause, true, buildUpdatePlanSql(table, values, whereClause, conflictAlgorithm), buildUpdatePlanArgs(values, whereArgs), () -> wrapped.updateWithOnConflict(table, values, whereClause, whereArgs, conflictAlgorithm));
|
||||
}
|
||||
|
||||
public void execSQL(String sql) throws SQLException {
|
||||
@@ -519,6 +576,149 @@ 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;
|
||||
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
}
|
||||
|
||||
private fun getExpiresIn(threadId: Long): Long {
|
||||
fun getExpiresIn(threadId: Long): Long {
|
||||
return readableDatabase
|
||||
.select(EXPIRES_IN)
|
||||
.from(TABLE_NAME)
|
||||
|
||||
+8
-2
@@ -170,6 +170,9 @@ 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
|
||||
|
||||
/**
|
||||
@@ -347,10 +350,13 @@ object SignalDatabaseMigrations {
|
||||
314 to V314_FixMessageRequestAcceptedToRecipient,
|
||||
315 to V315_CleanupE164SenderKeyShared,
|
||||
316 to V316_AddVerifiedGroupNameHashMigration,
|
||||
317 to V317_AddMessageThreadDateReceivedUnreadIndex
|
||||
317 to V317_AddMessageThreadDateReceivedUnreadIndex,
|
||||
318 to V318_AddMessageNotificationStateIndex,
|
||||
319 to V319_AddAttachmentAndMessageIndexes,
|
||||
320 to V320_AddAttachmentThumbnailFileAndUuidIndexes
|
||||
)
|
||||
|
||||
const val DATABASE_VERSION = 317
|
||||
const val DATABASE_VERSION = 320
|
||||
|
||||
@JvmStatic
|
||||
fun migrate(context: Application, db: SignalSqliteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
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)")
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
+15
@@ -54,6 +54,7 @@ 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;
|
||||
@@ -199,6 +200,8 @@ 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) {
|
||||
@@ -378,6 +381,18 @@ 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.resolved(RecipientId.from(uuid)).live().getLiveData()).collect(Collectors.toList());
|
||||
.map(uuid -> Recipient.live(RecipientId.from(uuid)).getLiveDataResolved()).collect(Collectors.toList());
|
||||
|
||||
LiveData<?> mentionedRecipientChangeStream = allMentionedRecipients.isEmpty() ? LiveDataUtil.just(new Object())
|
||||
: LiveDataUtil.merge(allMentionedRecipients);
|
||||
|
||||
return Transformations.map(mentionedRecipientChangeStream, event -> toSpannable(context, updateDescription, updateDescription.getSpannable(), defaultTint, adjustPosition));
|
||||
return LiveDataUtil.mapAsync(mentionedRecipientChangeStream, event -> toSpannable(context, updateDescription, updateDescription.getSpannable(), defaultTint, adjustPosition));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -180,10 +180,6 @@ 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();
|
||||
@@ -511,6 +507,10 @@ 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,12 +228,6 @@ 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()) {
|
||||
@@ -265,13 +259,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 staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(message), callDateString),
|
||||
return staticUpdateDescriptionWithExpiration(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 staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(updateString), callDateString), icon);
|
||||
return staticUpdateDescriptionWithExpiration(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(updateString), callDateString), icon);
|
||||
}
|
||||
} else {
|
||||
boolean isVideoCall = call.getType() == CallTable.Type.VIDEO_CALL;
|
||||
@@ -279,7 +273,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 staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(updateString), callDateString), icon);
|
||||
return staticUpdateDescriptionWithExpiration(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;
|
||||
@@ -291,7 +285,7 @@ public class MmsMessageRecord extends MessageRecord {
|
||||
message = isVideoCall ? R.string.MessageRecord_missed_video_call : R.string.MessageRecord_missed_voice_call;
|
||||
}
|
||||
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date,
|
||||
return staticUpdateDescriptionWithExpiration(context.getString(R.string.MessageRecord_call_message_with_date,
|
||||
context.getString(message),
|
||||
callDateString),
|
||||
icon,
|
||||
|
||||
@@ -121,12 +121,14 @@ data class RecipientRecord(
|
||||
)
|
||||
|
||||
data class Capabilities(
|
||||
val rawBits: Long
|
||||
val rawBits: Long,
|
||||
val usernameSyncMessages: Recipient.Capability
|
||||
) {
|
||||
companion object {
|
||||
@JvmField
|
||||
val UNKNOWN = Capabilities(
|
||||
rawBits = 0
|
||||
rawBits = 0,
|
||||
usernameSyncMessages = Recipient.Capability.UNKNOWN
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
@@ -10,6 +11,7 @@ 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;
|
||||
|
||||
@@ -98,7 +100,14 @@ 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 new UpdateDescription(Collections.emptyList(), null, new SpannableString(staticString), glyph, true,0, 0);
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -161,6 +170,11 @@ 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)
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
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,6 +61,11 @@ 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,6 +18,7 @@ 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
|
||||
@@ -87,6 +88,9 @@ 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
|
||||
@@ -117,11 +121,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 },
|
||||
usernameHash = SignalStore.account.username?.let { Username(it).hash }.takeIf { Recipient.self().usernameSyncMessagesCapability.isSupported },
|
||||
keyTransparencyStore = KeyTransparencyStore
|
||||
)
|
||||
|
||||
Log.i(TAG, "Key transparency complete, result: $result")
|
||||
Log.i(TAG, "Key transparency complete, result: $result. Included username in check: ${Recipient.self().usernameSyncMessagesCapability.isSupported}")
|
||||
return when (result) {
|
||||
is RequestResult.Success -> {
|
||||
SignalStore.misc.hasKeyTransparencyFailure = false
|
||||
@@ -167,9 +171,11 @@ class CheckKeyTransparencyJob private constructor(
|
||||
* For others, it will only show once and only be cleared on the next successful verification.
|
||||
*/
|
||||
private fun markFailure() {
|
||||
SignalStore.misc.hasKeyTransparencyFailure = true
|
||||
if (RemoteConfig.internalUser) {
|
||||
SignalStore.misc.hasSeenKeyTransparencyFailure = false
|
||||
if (SignalStore.account.isRegistered && !TextSecurePreferences.isUnauthorizedReceived(AppDependencies.application)) {
|
||||
SignalStore.misc.hasKeyTransparencyFailure = true
|
||||
if (RemoteConfig.internalUser) {
|
||||
SignalStore.misc.hasSeenKeyTransparencyFailure = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,12 @@ 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(
|
||||
@@ -46,28 +52,63 @@ class CheckRestoreMediaLeftJob private constructor(parameters: Parameters) : Job
|
||||
|
||||
if (remainingAttachmentSize == 0L) {
|
||||
Log.d(TAG, "Media restore complete: there are no remaining restorable attachments.")
|
||||
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())
|
||||
}
|
||||
onMediaRestoreComplete()
|
||||
} 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(15.seconds.inWholeMilliseconds)
|
||||
return Result.retry(30.seconds.inWholeMilliseconds)
|
||||
} else {
|
||||
Log.w(TAG, "Max retries reached, queue: ${parameters.queue} estimated remaining: $remainingAttachmentSize")
|
||||
// todo [local-backup] inspect jobs/queues and raise some alarm/abort?
|
||||
handleRemainingAfterMaxAttempts(remainingAttachmentSize)
|
||||
}
|
||||
|
||||
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,9 +11,14 @@ 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
|
||||
@@ -197,7 +202,7 @@ class IndividualSendJobV2 private constructor(parameters: Parameters, private va
|
||||
val syntheticResult = SendMessageResult.success(
|
||||
SignalServiceAddress(recipient.requireServiceId(), recipient.e164.orNull()),
|
||||
success.devices,
|
||||
success.sentUnidentified,
|
||||
success.sentSealedSender,
|
||||
false,
|
||||
0L,
|
||||
Optional.of(content)
|
||||
@@ -216,7 +221,7 @@ class IndividualSendJobV2 private constructor(parameters: Parameters, private va
|
||||
SignalDatabase.pendingPniSignatureMessages.insertIfNecessary(recipient.id, message.sentTimeMillis, syntheticResult)
|
||||
}
|
||||
|
||||
SignalDatabase.messages.markAsSent(messageId, success.sentUnidentified)
|
||||
SignalDatabase.messages.markAsSent(messageId, success.sentSealedSender)
|
||||
PushSendJob.markAttachmentsUploaded(messageId, message)
|
||||
|
||||
SignalDatabase.threads.updateSilently(threadId, false)
|
||||
@@ -228,11 +233,11 @@ class IndividualSendJobV2 private constructor(parameters: Parameters, private va
|
||||
}
|
||||
|
||||
val accessMode = recipient.sealedSenderAccessMode
|
||||
if (success.sentUnidentified && accessMode == SealedSenderAccessMode.UNKNOWN && recipient.profileKey == null) {
|
||||
if (success.sentSealedSender && accessMode == SealedSenderAccessMode.UNKNOWN && recipient.profileKey == null) {
|
||||
SignalDatabase.recipients.setSealedSenderAccessMode(recipient.id, SealedSenderAccessMode.UNRESTRICTED)
|
||||
} else if (success.sentUnidentified && accessMode == SealedSenderAccessMode.UNKNOWN) {
|
||||
} else if (success.sentSealedSender && accessMode == SealedSenderAccessMode.UNKNOWN) {
|
||||
SignalDatabase.recipients.setSealedSenderAccessMode(recipient.id, SealedSenderAccessMode.ENABLED)
|
||||
} else if (!success.sentUnidentified && accessMode != SealedSenderAccessMode.DISABLED) {
|
||||
} else if (!success.sentSealedSender && accessMode != SealedSenderAccessMode.DISABLED) {
|
||||
SignalDatabase.recipients.setSealedSenderAccessMode(recipient.id, SealedSenderAccessMode.DISABLED)
|
||||
}
|
||||
|
||||
@@ -255,36 +260,41 @@ 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.recipient.identifier}", error.cause)
|
||||
val externalRecipient = Recipient.external(error.recipient.identifier)
|
||||
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Identity mismatch for ${error.serviceId}", error.exception)
|
||||
val externalRecipient = Recipient.external(error.serviceId.toString())
|
||||
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.cause.untrustedIdentity)
|
||||
SignalDatabase.messages.addMismatchedIdentity(messageId, externalRecipient.id, error.exception.untrustedIdentity)
|
||||
SignalDatabase.messages.markAsSentFailed(messageId)
|
||||
RetrieveProfileJob.enqueue(externalRecipient.id, true)
|
||||
}
|
||||
Result.success()
|
||||
}
|
||||
|
||||
MessageService.SendError.NotRegistered -> {
|
||||
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Recipient not registered")
|
||||
is MessageService.SendError.NotRegistered -> {
|
||||
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Recipient not registered", error)
|
||||
SignalDatabase.messages.markAsSentFailed(messageId)
|
||||
PushSendJob.notifyMediaMessageDeliveryFailed(context, messageId)
|
||||
AppDependencies.jobManager.add(DirectoryRefreshJob(false))
|
||||
Result.success()
|
||||
}
|
||||
|
||||
MessageService.SendError.Unauthorized -> {
|
||||
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Unauthorized send")
|
||||
is MessageService.SendError.Unauthorized -> {
|
||||
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Unauthorized send", error)
|
||||
Result.failure()
|
||||
}
|
||||
|
||||
is MessageService.SendError.ChallengeRequired -> {
|
||||
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Challenge required (options=${error.options})")
|
||||
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Challenge required (options=${error.options})", error)
|
||||
val proofResponse = ProofRequiredResponse().apply {
|
||||
token = error.token
|
||||
options = error.options
|
||||
options = error.options.map {
|
||||
when (it) {
|
||||
ChallengeOption.PUSH_CHALLENGE -> "pushChallenge"
|
||||
ChallengeOption.CAPTCHA -> "captcha"
|
||||
}
|
||||
}
|
||||
}
|
||||
val proofException = ProofRequiredException(proofResponse, error.retryAfter?.inWholeSeconds ?: 0L)
|
||||
val threadRecipient = SignalDatabase.threads.getRecipientForThreadId(threadId)
|
||||
@@ -295,23 +305,23 @@ class IndividualSendJobV2 private constructor(parameters: Parameters, private va
|
||||
}
|
||||
}
|
||||
|
||||
MessageService.SendError.ServerRejected -> {
|
||||
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Server rejected the send")
|
||||
is MessageService.SendError.ServerRejected -> {
|
||||
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Server rejected the send", error)
|
||||
Result.failure()
|
||||
}
|
||||
|
||||
is MessageService.SendError.ContentTooLarge -> {
|
||||
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Content too large (${error.size} > ${error.maxAllowed} bytes); failing.")
|
||||
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Content too large (${error.size} > ${error.maxAllowed} bytes). Failing.", error)
|
||||
Result.failure()
|
||||
}
|
||||
|
||||
MessageService.SendError.SessionAttemptsExhausted -> {
|
||||
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Exhausted device-resolution attempts; retrying")
|
||||
is MessageService.SendError.SessionAttemptsExhausted -> {
|
||||
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Exhausted device-resolution attempts. Retrying", error)
|
||||
Result.retry(nextRunAttemptBackoff(runAttempt + 1))
|
||||
}
|
||||
|
||||
is MessageService.SendError.PreKeyUnavailable -> {
|
||||
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Prekey unavailable: ${error.reason}")
|
||||
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Prekey unavailable: ${error.reason}", error)
|
||||
Result.retry(nextRunAttemptBackoff(runAttempt + 1))
|
||||
}
|
||||
|
||||
@@ -319,16 +329,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")
|
||||
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Rate limited, retryAfter=${error.retryAfter}, using backoff=${backoff}ms", error)
|
||||
Result.retry(backoff)
|
||||
}
|
||||
|
||||
is MessageService.SendError.NetworkError -> {
|
||||
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Network error", error.cause)
|
||||
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Network error", error.exception)
|
||||
Result.retry(nextRunAttemptBackoff(runAttempt + 1))
|
||||
}
|
||||
|
||||
is MessageService.SendError.ApplicationError -> when (val cause = error.cause) {
|
||||
is MessageService.SendError.ApplicationError -> when (val cause = error.exception) {
|
||||
is RuntimeException -> {
|
||||
Log.e(TAG, "${logPrefix(message.sentTimeMillis)} Encountered a fatal application error. Crash imminent.", cause)
|
||||
Result.fatalFailure(cause)
|
||||
@@ -393,7 +403,7 @@ class IndividualSendJobV2 private constructor(parameters: Parameters, private va
|
||||
}
|
||||
|
||||
return AppDependencies.messageService.sendMessage(
|
||||
recipient = SignalServiceAddress(recipient.requireServiceId(), recipient.e164.orNull()),
|
||||
serviceId = recipient.requireServiceId(),
|
||||
envelopeContent = envelopeContent,
|
||||
timestamp = dataMessage.timestamp!!,
|
||||
sealedSenderAccess = SealedSenderAccessUtil.getSealedSenderAccessFor(recipient),
|
||||
@@ -409,25 +419,39 @@ 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
|
||||
editMessage = editMessage,
|
||||
unidentifiedStatus = listOf(
|
||||
SyncMessage.Sent.UnidentifiedDeliveryStatus(
|
||||
destinationServiceIdBinary = recipientServiceId.toByteString(),
|
||||
unidentified = primaryResult.sentSealedSender,
|
||||
destinationPniIdentityKey = pniIdentityKey
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val syncEnvelope = EnvelopeContent.encrypted(syncContent, ContentHint.IMPLICIT, Optional.empty())
|
||||
|
||||
return AppDependencies.messageService.sendMessage(
|
||||
recipient = SignalServiceAddress(SignalStore.account.requireAci()),
|
||||
return AppDependencies.messageService.sendSyncMessage(
|
||||
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,6 +55,7 @@ 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;
|
||||
@@ -144,8 +145,9 @@ 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());
|
||||
@@ -205,6 +207,7 @@ 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());
|
||||
@@ -223,6 +226,7 @@ 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());
|
||||
@@ -315,6 +319,7 @@ 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());
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
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,5 +1,6 @@
|
||||
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
|
||||
@@ -63,6 +64,9 @@ 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!!)
|
||||
val remoteId = SignalServiceAttachmentRemoteId.from(attachment.remoteLocation!!, attachment.cdn.cdnNumber)
|
||||
val key = Base64.decode(attachment.remoteKey!!)
|
||||
|
||||
var width = attachment.width
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
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,6 +46,11 @@ 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)
|
||||
@@ -72,7 +77,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 (restoreAttachmentJobs.isNotEmpty())
|
||||
} while (possibleRestorableAttachments.isNotEmpty())
|
||||
|
||||
ArchiveRestoreProgress.onRestoringMedia()
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ 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;
|
||||
@@ -25,7 +26,6 @@ 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 = messageSender.sendReceipt(remoteAddress,
|
||||
SealedSenderAccessUtil.getSealedSenderAccessFor(recipient, this::getGroupSendFullToken),
|
||||
receiptMessage,
|
||||
recipient.getNeedsPniSignature());
|
||||
SendMessageResult result = ReceiptSender.sendWithSessionRepair(recipientId, () -> messageSender.sendReceipt(remoteAddress,
|
||||
SealedSenderAccessUtil.getSealedSenderAccessFor(recipient, this::getGroupSendFullToken),
|
||||
receiptMessage,
|
||||
recipient.getNeedsPniSignature()));
|
||||
|
||||
if (messageId != null) {
|
||||
if (result != null && messageId != null) {
|
||||
SignalDatabase.messageLog().insertIfPossible(recipientId, timestamp, result, ContentHint.IMPLICIT, messageId, false);
|
||||
}
|
||||
}
|
||||
@@ -152,6 +152,8 @@ 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 = messageSender.sendReceipt(remoteAddress,
|
||||
SealedSenderAccessUtil.getSealedSenderAccessFor(recipient,
|
||||
() -> SignalDatabase.groups().getGroupSendFullToken(threadId, recipientId)),
|
||||
receiptMessage,
|
||||
recipient.getNeedsPniSignature());
|
||||
SendMessageResult result = ReceiptSender.sendWithSessionRepair(recipientId, () -> messageSender.sendReceipt(remoteAddress,
|
||||
SealedSenderAccessUtil.getSealedSenderAccessFor(recipient,
|
||||
() -> SignalDatabase.groups().getGroupSendFullToken(threadId, recipientId)),
|
||||
receiptMessage,
|
||||
recipient.getNeedsPniSignature()));
|
||||
|
||||
if (Util.hasItems(messageIds)) {
|
||||
if (result != null && 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 = messageSender.sendReceipt(remoteAddress,
|
||||
SealedSenderAccessUtil.getSealedSenderAccessFor(recipient,
|
||||
() -> SignalDatabase.groups().getGroupSendFullToken(threadId, recipientId)),
|
||||
receiptMessage,
|
||||
recipient.getNeedsPniSignature());
|
||||
SendMessageResult result = ReceiptSender.sendWithSessionRepair(recipientId, () -> messageSender.sendReceipt(remoteAddress,
|
||||
SealedSenderAccessUtil.getSealedSenderAccessFor(recipient,
|
||||
() -> SignalDatabase.groups().getGroupSendFullToken(threadId, recipientId)),
|
||||
receiptMessage,
|
||||
recipient.getNeedsPniSignature()));
|
||||
|
||||
if (Util.hasItems(foundMessageIds)) {
|
||||
if (result != null && Util.hasItems(foundMessageIds)) {
|
||||
SignalDatabase.messageLog().insertIfPossible(recipientId, timestamp, result, ContentHint.IMPLICIT, foundMessageIds, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.pin.Svr3Migration
|
||||
import org.thoughtcrime.securesms.pin.SvrRepository
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.api.svr.SecureValueRecovery.BackupResponse
|
||||
import org.whispersystems.signalservice.api.svr.SecureValueRecovery.PinChangeSession
|
||||
import org.whispersystems.signalservice.api.svr.SecureValueRecoveryV2
|
||||
@@ -55,8 +56,18 @@ class Svr2MirrorJob private constructor(parameters: Parameters, private var seri
|
||||
override fun getFactoryKey(): String = KEY
|
||||
|
||||
override fun run(): Result {
|
||||
if (!SignalStore.account.isRegistered) {
|
||||
Log.w(TAG, "Not registered. Skipping.")
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
if (TextSecurePreferences.isUnauthorizedReceived(context)) {
|
||||
Log.w(TAG, "Not authorized. Skipping.")
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
if (SignalStore.account.isLinkedDevice) {
|
||||
Log.i(TAG, "Not primary device, skipping mirror")
|
||||
Log.i(TAG, "Not primary device. Skipping.")
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
@@ -128,7 +139,7 @@ class Svr2MirrorJob private constructor(parameters: Parameters, private var seri
|
||||
}
|
||||
|
||||
private fun Throwable.isUnauthorized(): Boolean {
|
||||
return this is NonSuccessfulResponseCodeException && this.code == 401
|
||||
return this is NonSuccessfulResponseCodeException && (this.code == 401 || this.code == 403)
|
||||
}
|
||||
|
||||
override fun onFailure() = Unit
|
||||
|
||||
@@ -400,6 +400,8 @@ class UploadAttachmentToArchiveJob private constructor(
|
||||
SignalDatabase.attachments.setArchiveTransferStateFailure(attachmentId, AttachmentTable.ArchiveTransferState.TEMPORARY_FAILURE)
|
||||
}
|
||||
}
|
||||
|
||||
ArchiveUploadProgress.onAttachmentFinished(attachmentId)
|
||||
}
|
||||
|
||||
private fun setArchiveTransferStateWithDelayedNotification(attachmentId: AttachmentId, transferState: AttachmentTable.ArchiveTransferState) {
|
||||
|
||||
@@ -76,6 +76,7 @@ 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__"
|
||||
|
||||
@@ -503,7 +504,7 @@ fun DeviceRow(device: Device, isInternalUser: Boolean, setDeviceToRemove: (Devic
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.symbol_devices_24),
|
||||
painter = painterResource(id = CoreUiR.drawable.symbol_devices_24),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface),
|
||||
contentScale = ContentScale.Inside,
|
||||
|
||||
@@ -2,11 +2,13 @@ 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
|
||||
@@ -16,6 +18,11 @@ 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,5 +1,7 @@
|
||||
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
|
||||
@@ -8,12 +10,18 @@ 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,6 +16,7 @@ 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
|
||||
|
||||
@@ -78,7 +79,7 @@ data class MainContentLayoutData(
|
||||
val isSplitPane = resources.rememberIsSplitPane()
|
||||
|
||||
return remember(windowSizeClass, mode, breakpoint, isSplitPane) {
|
||||
val isLargeWindowSize = breakpoint.isLargeWindow
|
||||
val isLargeWindowSize = breakpoint is WindowBreakpoint.Large
|
||||
|
||||
MainContentLayoutData(
|
||||
shape = when {
|
||||
|
||||
+1
-1
@@ -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.renderToSingleUseBlob(requireContext(), model)
|
||||
ImageEditorFragment.renderToSingleSessionBlob(requireContext(), model)
|
||||
} else {
|
||||
media.uri
|
||||
}
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ class TextStoryPostSendRepository {
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
|
||||
bitmap.recycle()
|
||||
BlobProvider.getInstance().forData(outputStream.toByteArray()).createForSingleUseInMemory()
|
||||
BlobProvider.getInstance().forData(outputStream.toByteArray()).createForSingleSessionInMemory()
|
||||
}.subscribeOn(Schedulers.computation())
|
||||
}
|
||||
|
||||
|
||||
@@ -222,7 +222,7 @@ enum class OnboardingListItem(
|
||||
),
|
||||
ADD_PHOTO(
|
||||
title = R.string.Megaphones_add_a_profile_photo,
|
||||
icon = R.drawable.symbol_person_circle_24,
|
||||
icon = CoreUiR.drawable.symbol_person_circle_24,
|
||||
cardColor = R.color.onboarding_background_4
|
||||
),
|
||||
APPEARANCE(
|
||||
|
||||
@@ -14,6 +14,7 @@ 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
|
||||
@@ -42,6 +43,7 @@ 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
|
||||
@@ -58,6 +60,7 @@ 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
|
||||
@@ -188,6 +191,7 @@ 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...")
|
||||
}
|
||||
}
|
||||
@@ -2026,6 +2030,12 @@ 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,9 +203,11 @@ 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 = 161;
|
||||
public static final int CURRENT_VERSION = 163;
|
||||
|
||||
/**
|
||||
* This *must* be called after the {@link JobManager} has been instantiated, but *before* the call
|
||||
@@ -940,6 +942,14 @@ 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;
|
||||
}
|
||||
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -164,7 +164,7 @@ object SlowNotificationHeuristics {
|
||||
return false
|
||||
}
|
||||
|
||||
if (failures.size / (failures.size + successes.size) >= failurePercentage) {
|
||||
if (failures.size.toFloat() / (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()) {
|
||||
if (havingDelayedNotifications && SlowNotificationHeuristics.shouldPromptBatterySaver() && SlowNotificationHeuristics.isBatteryOptimizationsOn()) {
|
||||
return@fromCallable State.PROMPT_GENERAL_BATTERY_SAVER_DIALOG
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user