mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 04:58:45 +00:00
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:
committed by
Cody Henthorne
parent
050dcb3eb1
commit
2cfe321274
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 -> {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user