diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/StickerTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/StickerTable.kt index 2877cd004a..bd517ec666 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/StickerTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/StickerTable.kt @@ -305,7 +305,7 @@ class StickerTable( notifyStickerListeners() } - fun updatePackOrder(packsInOrder: MutableList) { + fun updatePackOrder(packsInOrder: List) { writableDatabase.withinTransaction { db -> for ((i, pack) in packsInOrder.withIndex()) { db.update(TABLE_NAME) @@ -470,6 +470,12 @@ class StickerTable( ) } + fun asSequence(): Sequence = sequence { + while (getNext() != null) { + yield(getCurrent()) + } + } + override fun close() { cursor.close() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementActivity.java b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementActivity.java index 3e4968c0f4..9f675efed4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementActivity.java @@ -123,7 +123,7 @@ public final class StickerManagementActivity extends PassphraseRequiredActivity } private void initViewModel() { - StickerManagementRepository repository = new StickerManagementRepository(); + StickerManagementRepository repository = StickerManagementRepository.INSTANCE; viewModel = new ViewModelProvider(this, new StickerManagementViewModel.Factory(getApplication(), repository)).get(StickerManagementViewModel.class); viewModel.init(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementRepository.java b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementRepository.java deleted file mode 100644 index 1b51f61ad7..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementRepository.java +++ /dev/null @@ -1,137 +0,0 @@ -package org.thoughtcrime.securesms.stickers; - -import android.database.Cursor; - -import androidx.annotation.NonNull; - -import org.signal.core.util.concurrent.SignalExecutors; -import org.thoughtcrime.securesms.database.AttachmentTable; -import org.thoughtcrime.securesms.database.SignalDatabase; -import org.thoughtcrime.securesms.database.StickerTable; -import org.thoughtcrime.securesms.database.StickerTable.StickerPackRecordReader; -import org.thoughtcrime.securesms.database.model.StickerPackRecord; -import org.thoughtcrime.securesms.dependencies.AppDependencies; -import org.thoughtcrime.securesms.jobmanager.JobManager; -import org.thoughtcrime.securesms.jobs.MultiDeviceStickerPackOperationJob; -import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob; -import org.thoughtcrime.securesms.keyvalue.SignalStore; - -import java.util.ArrayList; -import java.util.List; - -final class StickerManagementRepository { - - private final StickerTable stickerDatabase; - private final AttachmentTable attachmentDatabase; - - StickerManagementRepository() { - this.stickerDatabase = SignalDatabase.stickers(); - this.attachmentDatabase = SignalDatabase.attachments(); - } - - void deleteOrphanedStickerPacks() { - SignalExecutors.SERIAL.execute(stickerDatabase::deleteOrphanedPacks); - } - - void fetchUnretrievedReferencePacks() { - SignalExecutors.SERIAL.execute(() -> { - JobManager jobManager = AppDependencies.getJobManager(); - - try (Cursor cursor = attachmentDatabase.getUnavailableStickerPacks()) { - while (cursor != null && cursor.moveToNext()) { - String packId = cursor.getString(cursor.getColumnIndexOrThrow(AttachmentTable.STICKER_PACK_ID)); - String packKey = cursor.getString(cursor.getColumnIndexOrThrow(AttachmentTable.STICKER_PACK_KEY)); - - jobManager.add(StickerPackDownloadJob.forReference(packId, packKey)); - } - } - }); - } - - void getStickerPacks(@NonNull Callback callback) { - SignalExecutors.SERIAL.execute(() -> { - List installedPacks = new ArrayList<>(); - List availablePacks = new ArrayList<>(); - List blessedPacks = new ArrayList<>(); - - try (StickerPackRecordReader reader = new StickerPackRecordReader(stickerDatabase.getAllStickerPacks())) { - StickerPackRecord record; - while ((record = reader.getNext()) != null) { - if (record.isInstalled) { - installedPacks.add(record); - } else if (BlessedPacks.contains(record.packId)) { - blessedPacks.add(record); - } else { - availablePacks.add(record); - } - } - } - - callback.onComplete(new PackResult(installedPacks, availablePacks, blessedPacks)); - }); - } - - void uninstallStickerPack(@NonNull String packId, @NonNull String packKey) { - SignalExecutors.SERIAL.execute(() -> { - stickerDatabase.uninstallPack(packId); - - if (SignalStore.account().hasLinkedDevices()) { - AppDependencies.getJobManager().add(new MultiDeviceStickerPackOperationJob(packId, packKey, MultiDeviceStickerPackOperationJob.Type.REMOVE)); - } - }); - } - - void installStickerPack(@NonNull String packId, @NonNull String packKey, boolean notify) { - SignalExecutors.SERIAL.execute(() -> { - JobManager jobManager = AppDependencies.getJobManager(); - - if (stickerDatabase.isPackAvailableAsReference(packId)) { - stickerDatabase.markPackAsInstalled(packId, notify); - } - - jobManager.add(StickerPackDownloadJob.forInstall(packId, packKey, notify)); - - if (SignalStore.account().hasLinkedDevices()) { - jobManager.add(new MultiDeviceStickerPackOperationJob(packId, packKey, MultiDeviceStickerPackOperationJob.Type.INSTALL)); - } - }); - } - - void setPackOrder(@NonNull List packsInOrder) { - SignalExecutors.SERIAL.execute(() -> { - stickerDatabase.updatePackOrder(packsInOrder); - }); - } - - static class PackResult { - - private final List installedPacks; - private final List availablePacks; - private final List blessedPacks; - - PackResult(@NonNull List installedPacks, - @NonNull List availablePacks, - @NonNull List blessedPacks) - { - this.installedPacks = installedPacks; - this.availablePacks = availablePacks; - this.blessedPacks = blessedPacks; - } - - @NonNull List getInstalledPacks() { - return installedPacks; - } - - @NonNull List getAvailablePacks() { - return availablePacks; - } - - @NonNull List getBlessedPacks() { - return blessedPacks; - } - } - - interface Callback { - void onComplete(T result); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementRepository.kt new file mode 100644 index 0000000000..3b38b83538 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementRepository.kt @@ -0,0 +1,163 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.stickers + +import androidx.annotation.Discouraged +import androidx.annotation.WorkerThread +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.signal.core.util.requireNonNullString +import org.thoughtcrime.securesms.database.AttachmentTable +import org.thoughtcrime.securesms.database.DatabaseObserver +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.database.StickerTable +import org.thoughtcrime.securesms.database.StickerTable.StickerPackRecordReader +import org.thoughtcrime.securesms.database.model.StickerPackRecord +import org.thoughtcrime.securesms.dependencies.AppDependencies +import org.thoughtcrime.securesms.jobmanager.JobManager +import org.thoughtcrime.securesms.jobs.MultiDeviceStickerPackOperationJob +import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob +import org.thoughtcrime.securesms.keyvalue.SignalStore + +/** + * Handles the retrieval and modification of sticker pack data. + */ +object StickerManagementRepository { + private val jobManager: JobManager = AppDependencies.jobManager + private val databaseObserver: DatabaseObserver = AppDependencies.databaseObserver + private val stickersDbTable: StickerTable = SignalDatabase.stickers + private val attachmentsDbTable: AttachmentTable = SignalDatabase.attachments + private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO) + + @Discouraged("For Java use only. In Kotlin, use the getStickerPacks() overload that returns a Flow instead.") + @WorkerThread + fun getStickerPacks(callback: Callback) { + coroutineScope.launch { + callback.onComplete(loadStickerPacks()) + } + } + + /** + * Emits the sticker packs along with any updates. + */ + fun getStickerPacks(): Flow = callbackFlow { + trySend(loadStickerPacks()) + + val stickersDbObserver = DatabaseObserver.Observer { + launch { + deleteOrphanedStickerPacks() + trySend(loadStickerPacks()) + } + } + + databaseObserver.registerStickerPackObserver(stickersDbObserver) + awaitClose { + databaseObserver.unregisterObserver(stickersDbObserver) + } + } + + private suspend fun loadStickerPacks(): StickerPackResult = withContext(Dispatchers.IO) { + StickerPackRecordReader(stickersDbTable.getAllStickerPacks()).use { reader -> + val installedPacks = mutableListOf() + val availablePacks = mutableListOf() + val blessedPacks = mutableListOf() + + reader.asSequence().forEach { record -> + when { + record.isInstalled -> installedPacks.add(record) + BlessedPacks.contains(record.packId) -> blessedPacks.add(record) + else -> availablePacks.add(record) + } + } + + StickerPackResult( + installedPacks = installedPacks, + availablePacks = availablePacks, + blessedPacks = blessedPacks + ) + } + } + + @Discouraged("For Java use only. In Kotlin, use deleteOrphanedStickerPacks() instead.") + fun deleteOrphanedStickerPacksAsync() { + coroutineScope.launch { + deleteOrphanedStickerPacks() + } + } + + suspend fun deleteOrphanedStickerPacks() = withContext(Dispatchers.IO) { + stickersDbTable.deleteOrphanedPacks() + } + + fun fetchUnretrievedReferencePacks() { + attachmentsDbTable.getUnavailableStickerPacks().use { cursor -> + while (cursor.moveToNext()) { + val packId: String = cursor.requireNonNullString(AttachmentTable.STICKER_PACK_ID) + val packKey: String = cursor.requireNonNullString(AttachmentTable.STICKER_PACK_KEY) + jobManager.add(StickerPackDownloadJob.forReference(packId, packKey)) + } + } + } + + fun installStickerPackAsync(packId: String, packKey: String, notify: Boolean) { + coroutineScope.launch { + installStickerPack(packId, packKey, notify) + } + } + + suspend fun installStickerPack(packId: String, packKey: String, notify: Boolean) = withContext(Dispatchers.IO) { + if (stickersDbTable.isPackAvailableAsReference(packId)) { + stickersDbTable.markPackAsInstalled(packId, notify) + } + + jobManager.add(StickerPackDownloadJob.forInstall(packId, packKey, notify)) + + if (SignalStore.account.hasLinkedDevices) { + jobManager.add(MultiDeviceStickerPackOperationJob(packId, packKey, MultiDeviceStickerPackOperationJob.Type.INSTALL)) + } + } + + @Discouraged("For Java use only. In Kotlin, use uninstallStickerPack() instead.") + fun uninstallStickerPackAsync(packId: String, packKey: String) { + coroutineScope.launch { + uninstallStickerPack(packId, packKey) + } + } + + suspend fun uninstallStickerPack(packId: String, packKey: String) = withContext(Dispatchers.IO) { + stickersDbTable.uninstallPack(packId) + + if (SignalStore.account.hasLinkedDevices) { + AppDependencies.jobManager.add(MultiDeviceStickerPackOperationJob(packId, packKey, MultiDeviceStickerPackOperationJob.Type.REMOVE)) + } + } + + @Discouraged("For Java use only. In Kotlin, use setStickerPackOrder() instead.") + fun setStickerPacksOrderAsync(packsInOrder: List) { + coroutineScope.launch { + setStickerPacksOrder(packsInOrder) + } + } + + suspend fun setStickerPacksOrder(packsInOrder: List) = withContext(Dispatchers.IO) { + stickersDbTable.updatePackOrder(packsInOrder) + } + + interface Callback { + fun onComplete(result: T) + } +} + +data class StickerPackResult( + val installedPacks: List, + val availablePacks: List, + val blessedPacks: List +) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementViewModel.java index 7bbb45fa47..dde236966c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementViewModel.java @@ -11,7 +11,6 @@ import androidx.lifecycle.ViewModelProvider; import org.thoughtcrime.securesms.database.DatabaseObserver; import org.thoughtcrime.securesms.database.model.StickerPackRecord; import org.thoughtcrime.securesms.dependencies.AppDependencies; -import org.thoughtcrime.securesms.stickers.StickerManagementRepository.PackResult; import java.util.List; @@ -19,7 +18,7 @@ final class StickerManagementViewModel extends ViewModel { private final Application application; private final StickerManagementRepository repository; - private final MutableLiveData packs; + private final MutableLiveData packs; private final DatabaseObserver.Observer observer; private StickerManagementViewModel(@NonNull Application application, @NonNull StickerManagementRepository repository) { @@ -27,7 +26,7 @@ final class StickerManagementViewModel extends ViewModel { this.repository = repository; this.packs = new MutableLiveData<>(); this.observer = () -> { - repository.deleteOrphanedStickerPacks(); + repository.deleteOrphanedStickerPacksAsync(); repository.getStickerPacks(packs::postValue); }; @@ -35,29 +34,29 @@ final class StickerManagementViewModel extends ViewModel { } void init() { - repository.deleteOrphanedStickerPacks(); + repository.deleteOrphanedStickerPacksAsync(); repository.fetchUnretrievedReferencePacks(); } void onVisible() { - repository.deleteOrphanedStickerPacks(); + repository.deleteOrphanedStickerPacksAsync(); } - @NonNull LiveData getStickerPacks() { + @NonNull LiveData getStickerPacks() { repository.getStickerPacks(packs::postValue); return packs; } void onStickerPackUninstallClicked(@NonNull String packId, @NonNull String packKey) { - repository.uninstallStickerPack(packId, packKey); + repository.uninstallStickerPackAsync(packId, packKey); } void onStickerPackInstallClicked(@NonNull String packId, @NonNull String packKey) { - repository.installStickerPack(packId, packKey, false); + repository.installStickerPackAsync(packId, packKey, false); } void onOrderChanged(List packsInOrder) { - repository.setPackOrder(packsInOrder); + repository.setStickerPacksOrderAsync(packsInOrder); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementViewModelV2.kt b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementViewModelV2.kt index 5d673accf2..384b3725a2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementViewModelV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementViewModelV2.kt @@ -6,35 +6,41 @@ package org.thoughtcrime.securesms.stickers import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import org.thoughtcrime.securesms.database.model.StickerPackRecord import org.thoughtcrime.securesms.stickers.AvailableStickerPack.DownloadStatus class StickerManagementViewModelV2 : ViewModel() { - private val stickerManagementRepo = StickerManagementRepository() + private val stickerManagementRepo = StickerManagementRepository private val _uiState = MutableStateFlow(StickerManagementUiState()) val uiState: StateFlow = _uiState.asStateFlow() init { - stickerManagementRepo.deleteOrphanedStickerPacks() - stickerManagementRepo.fetchUnretrievedReferencePacks() - loadStickerPacks() + viewModelScope.launch { + stickerManagementRepo.deleteOrphanedStickerPacks() + stickerManagementRepo.fetchUnretrievedReferencePacks() + loadStickerPacks() + } } - private fun loadStickerPacks() { - stickerManagementRepo.getStickerPacks { result -> - _uiState.value = _uiState.value.copy( - availableBlessedPacks = result.blessedPacks - .map { AvailableStickerPack(record = it, isBlessed = true, downloadStatus = DownloadStatus.NotDownloaded) }, - availablePacks = result.availablePacks - .map { AvailableStickerPack(record = it, isBlessed = false, downloadStatus = DownloadStatus.NotDownloaded) }, - installedPacks = result.installedPacks - .mapIndexed { index, record -> InstalledStickerPack(record = record, isBlessed = BlessedPacks.contains(record.packId), sortOrder = index) } - ) - } + private suspend fun loadStickerPacks() { + StickerManagementRepository.getStickerPacks() + .collectLatest { result -> + _uiState.value = _uiState.value.copy( + availableBlessedPacks = result.blessedPacks + .map { AvailableStickerPack(record = it, isBlessed = true, downloadStatus = DownloadStatus.NotDownloaded) }, + availablePacks = result.availablePacks + .map { AvailableStickerPack(record = it, isBlessed = false, downloadStatus = DownloadStatus.NotDownloaded) }, + installedPacks = result.installedPacks + .mapIndexed { index, record -> InstalledStickerPack(record = record, isBlessed = BlessedPacks.contains(record.packId), sortOrder = index) } + ) + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackPreviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackPreviewActivity.java index 88c60b42be..dbba0d7c25 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackPreviewActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackPreviewActivity.java @@ -177,7 +177,7 @@ public final class StickerPackPreviewActivity extends PassphraseRequiredActivity private void initViewModel(@NonNull String packId, @NonNull String packKey) { viewModel = new ViewModelProvider(this, new StickerPackPreviewViewModel.Factory(getApplication(), new StickerPackPreviewRepository(this), - new StickerManagementRepository()) + StickerManagementRepository.INSTANCE) ).get(StickerPackPreviewViewModel.class); viewModel.getStickerManifest(packId, packKey).observe(this, manifest -> { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackPreviewViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackPreviewViewModel.java index 2df5887522..b03e20edf3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackPreviewViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackPreviewViewModel.java @@ -29,7 +29,7 @@ final class StickerPackPreviewViewModel extends ViewModel { private StickerPackPreviewViewModel(@NonNull Application application, @NonNull StickerPackPreviewRepository previewRepository, - @NonNull StickerManagementRepository managementRepository) + @NonNull StickerManagementRepository managementRepository) { this.application = application; this.previewRepository = previewRepository; @@ -54,11 +54,11 @@ final class StickerPackPreviewViewModel extends ViewModel { } void onInstallClicked() { - managementRepository.installStickerPack(packId, packKey, true); + managementRepository.installStickerPackAsync(packId, packKey, true); } void onRemoveClicked() { - managementRepository.uninstallStickerPack(packId, packKey); + managementRepository.uninstallStickerPackAsync(packId, packKey); } @Override