Convert StickerManagementRepository to kotlin.

Converts `StickerManagementRepository` to kotlin, so `getStickerPacks()` can return a `Flow` that emits updates after the database is changed.

This change simplifies the implementation of `StickerManagmentViewModelV2`, since `StickerManagementRepository.getStickerPacks()` will now automatically register and unregister the database observer.
This commit is contained in:
Jeffrey Starke
2025-04-17 16:23:19 -04:00
committed by Cody Henthorne
parent 050dcb3eb1
commit 2cfe321274
8 changed files with 204 additions and 167 deletions

View File

@@ -305,7 +305,7 @@ class StickerTable(
notifyStickerListeners()
}
fun updatePackOrder(packsInOrder: MutableList<StickerPackRecord>) {
fun updatePackOrder(packsInOrder: List<StickerPackRecord>) {
writableDatabase.withinTransaction { db ->
for ((i, pack) in packsInOrder.withIndex()) {
db.update(TABLE_NAME)
@@ -470,6 +470,12 @@ class StickerTable(
)
}
fun asSequence(): Sequence<StickerPackRecord> = sequence {
while (getNext() != null) {
yield(getCurrent())
}
}
override fun close() {
cursor.close()
}

View File

@@ -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();

View File

@@ -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<PackResult> callback) {
SignalExecutors.SERIAL.execute(() -> {
List<StickerPackRecord> installedPacks = new ArrayList<>();
List<StickerPackRecord> availablePacks = new ArrayList<>();
List<StickerPackRecord> 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<StickerPackRecord> packsInOrder) {
SignalExecutors.SERIAL.execute(() -> {
stickerDatabase.updatePackOrder(packsInOrder);
});
}
static class PackResult {
private final List<StickerPackRecord> installedPacks;
private final List<StickerPackRecord> availablePacks;
private final List<StickerPackRecord> blessedPacks;
PackResult(@NonNull List<StickerPackRecord> installedPacks,
@NonNull List<StickerPackRecord> availablePacks,
@NonNull List<StickerPackRecord> blessedPacks)
{
this.installedPacks = installedPacks;
this.availablePacks = availablePacks;
this.blessedPacks = blessedPacks;
}
@NonNull List<StickerPackRecord> getInstalledPacks() {
return installedPacks;
}
@NonNull List<StickerPackRecord> getAvailablePacks() {
return availablePacks;
}
@NonNull List<StickerPackRecord> getBlessedPacks() {
return blessedPacks;
}
}
interface Callback<T> {
void onComplete(T result);
}
}

View File

@@ -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<StickerPackResult>) {
coroutineScope.launch {
callback.onComplete(loadStickerPacks())
}
}
/**
* Emits the sticker packs along with any updates.
*/
fun getStickerPacks(): Flow<StickerPackResult> = 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<StickerPackRecord>()
val availablePacks = mutableListOf<StickerPackRecord>()
val blessedPacks = mutableListOf<StickerPackRecord>()
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<StickerPackRecord>) {
coroutineScope.launch {
setStickerPacksOrder(packsInOrder)
}
}
suspend fun setStickerPacksOrder(packsInOrder: List<StickerPackRecord>) = withContext(Dispatchers.IO) {
stickersDbTable.updatePackOrder(packsInOrder)
}
interface Callback<T> {
fun onComplete(result: T)
}
}
data class StickerPackResult(
val installedPacks: List<StickerPackRecord>,
val availablePacks: List<StickerPackRecord>,
val blessedPacks: List<StickerPackRecord>
)

View File

@@ -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<PackResult> packs;
private final MutableLiveData<StickerPackResult> 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<PackResult> getStickerPacks() {
@NonNull LiveData<StickerPackResult> 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<StickerPackRecord> packsInOrder) {
repository.setPackOrder(packsInOrder);
repository.setStickerPacksOrderAsync(packsInOrder);
}
@Override

View File

@@ -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<StickerManagementUiState> = _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) }
)
}
}
}

View File

@@ -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 -> {

View File

@@ -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