diff --git a/WooCommerce-Wear/src/main/java/com/woocommerce/android/wear/di/FluxCModule.kt b/WooCommerce-Wear/src/main/java/com/woocommerce/android/wear/di/FluxCModule.kt index 83958ee1d05b..d6bc70de05c0 100644 --- a/WooCommerce-Wear/src/main/java/com/woocommerce/android/wear/di/FluxCModule.kt +++ b/WooCommerce-Wear/src/main/java/com/woocommerce/android/wear/di/FluxCModule.kt @@ -4,6 +4,7 @@ import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import org.wordpress.android.fluxc.di.WCDatabaseModule +import org.wordpress.android.fluxc.module.MediaModule import org.wordpress.android.fluxc.module.OkHttpClientModule import org.wordpress.android.fluxc.module.ReleaseNetworkModule @@ -12,7 +13,8 @@ import org.wordpress.android.fluxc.module.ReleaseNetworkModule includes = [ ReleaseNetworkModule::class, OkHttpClientModule::class, - WCDatabaseModule::class + WCDatabaseModule::class, + MediaModule::class ] ) abstract class FluxCModule diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/di/FluxCModule.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/di/FluxCModule.kt index 9d305391e0c1..a84f81c9d811 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/di/FluxCModule.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/di/FluxCModule.kt @@ -4,6 +4,7 @@ import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import org.wordpress.android.fluxc.di.WCDatabaseModule +import org.wordpress.android.fluxc.module.MediaModule import org.wordpress.android.fluxc.module.OkHttpClientModule import org.wordpress.android.fluxc.module.ReleaseNetworkModule @@ -12,7 +13,8 @@ import org.wordpress.android.fluxc.module.ReleaseNetworkModule includes = [ ReleaseNetworkModule::class, OkHttpClientModule::class, - WCDatabaseModule::class + WCDatabaseModule::class, + MediaModule::class ] ) abstract class FluxCModule diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/media/FileUploadUtils.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/media/FileUploadUtils.kt index 05d68c67f982..cd8cabe25f90 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/media/FileUploadUtils.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/media/FileUploadUtils.kt @@ -85,15 +85,9 @@ object FileUploadUtils { path, mimeType, filenameWithExtension, - null ) val instantiatedMedia = mediaStore.instantiateMediaModel(media) - return if (instantiatedMedia != null) { - instantiatedMedia - } else { - WooLog.w(T.MEDIA, "We couldn't instantiate the media") - null - } + return instantiatedMedia } /** diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/media/MediaFilesRepository.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/media/MediaFilesRepository.kt index c300d34c01d4..1a8f961a939c 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/media/MediaFilesRepository.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/media/MediaFilesRepository.kt @@ -49,7 +49,7 @@ class MediaFilesRepository @Inject constructor( private val resourceProvider: ResourceProvider, private val mediaPickerUtils: MediaPickerUtils ) { - suspend fun fetchMedia(localUri: String): MediaModel? { + suspend fun getLocalMedia(localUri: String): MediaModel? { return withContext(dispatchers.io) { val mediaModel = FileUploadUtils.mediaModelFromLocalUri( context, @@ -120,7 +120,7 @@ class MediaFilesRepository @Inject constructor( fun uploadFile(localUri: String): Flow { return flow { - val mediaModel = fetchMedia(localUri) + val mediaModel = getLocalMedia(localUri) if (mediaModel == null) { WooLog.w(T.MEDIA, "MediaFilesRepository > null media") diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/media/ProductImagesUploadWorker.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/media/ProductImagesUploadWorker.kt index 1a604bb58fac..600ecd3ac508 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/media/ProductImagesUploadWorker.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/media/ProductImagesUploadWorker.kt @@ -2,9 +2,7 @@ package com.woocommerce.android.media import com.woocommerce.android.di.AppCoroutineScope import com.woocommerce.android.media.MediaFilesRepository.UploadResult.* -import com.woocommerce.android.media.ProductImagesUploadWorker.Event import com.woocommerce.android.media.ProductImagesUploadWorker.Event.MediaUploadEvent -import com.woocommerce.android.media.ProductImagesUploadWorker.Work import com.woocommerce.android.model.Product import com.woocommerce.android.model.toAppModel import com.woocommerce.android.ui.products.details.ProductDetailRepository @@ -172,7 +170,7 @@ class ProductImagesUploadWorker @Inject constructor( private suspend fun fetchMedia(work: Work.FetchMedia) { WooLog.d(T.MEDIA, "ProductImagesUploadWorker -> fetch media ${work.localUri}") - val fetchedMedia = mediaFilesRepository.fetchMedia(work.localUri) + val fetchedMedia = mediaFilesRepository.getLocalMedia(work.localUri) if (fetchedMedia == null) { WooLog.w(T.MEDIA, "ProductImagesUploadWorker -> fetching media failed") diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/media/ProductImagesUploadWorkerTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/media/ProductImagesUploadWorkerTest.kt index e4af37273e3f..ae1979250626 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/media/ProductImagesUploadWorkerTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/media/ProductImagesUploadWorkerTest.kt @@ -35,7 +35,7 @@ import org.mockito.kotlin.never import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever -import org.wordpress.android.fluxc.model.MediaModel +import org.wordpress.android.fluxc.media.MediaTestUtils import org.wordpress.android.fluxc.store.MediaStore.MediaErrorType.GENERIC_ERROR import org.wordpress.android.util.DateTimeUtils import java.util.Date @@ -45,20 +45,19 @@ class ProductImagesUploadWorkerTest : BaseUnitTest() { companion object { private const val REMOTE_PRODUCT_ID = 1L private const val TEST_URI = "test" - private val FETCHED_MEDIA = MediaModel(0, 0) - private val UPLOADED_MEDIA = MediaModel(0, 0).apply { - fileName = "" - filePath = "" - url = "" - uploadDate = DateTimeUtils.iso8601FromDate(Date()) - } + private val FETCHED_MEDIA = MediaTestUtils.createRemoteTestMedia().build() + private val UPLOADED_MEDIA = MediaTestUtils.createRemoteTestMedia() + .fileName("") + .url("") + .uploadDate(DateTimeUtils.iso8601FromDate(Date())) + .build() } private val notificationHandler: ProductImagesNotificationHandler = mock() private val productImagesServiceWrapper: ProductImagesServiceWrapper = mock() private lateinit var worker: ProductImagesUploadWorker private val mediaFilesRepository: MediaFilesRepository = mock { - onBlocking { fetchMedia(TEST_URI) } doReturn FETCHED_MEDIA + onBlocking { getLocalMedia(TEST_URI) } doReturn FETCHED_MEDIA onBlocking { uploadMedia(any(), any()) } doReturn flowOf(UploadResult.UploadSuccess(UPLOADED_MEDIA)) } private val productDetailRepository: ProductDetailRepository = mock() @@ -111,7 +110,7 @@ class ProductImagesUploadWorkerTest : BaseUnitTest() { } worker.enqueueWork(Work.FetchMedia(REMOTE_PRODUCT_ID, TEST_URI)) - verify(mediaFilesRepository).fetchMedia(TEST_URI) + verify(mediaFilesRepository).getLocalMedia(TEST_URI) assertThat(eventsList[0]).isEqualTo(FetchSucceeded(REMOTE_PRODUCT_ID, TEST_URI, FETCHED_MEDIA)) job.cancel() } @@ -122,7 +121,9 @@ class ProductImagesUploadWorkerTest : BaseUnitTest() { val job = launch { worker.events.toList(eventsList) } - worker.enqueueWork(Work.UploadMedia(REMOTE_PRODUCT_ID, TEST_URI, MediaModel(0, 0))) + worker.enqueueWork( + Work.UploadMedia(REMOTE_PRODUCT_ID, TEST_URI, MediaTestUtils.createRemoteTestMedia().build()) + ) advanceUntilIdle() verify(mediaFilesRepository).uploadMedia(any(), any()) @@ -133,9 +134,11 @@ class ProductImagesUploadWorkerTest : BaseUnitTest() { @Test fun `when media upload progress changes, then update notification`() = testBlocking { whenever(mediaFilesRepository.uploadMedia(any(), any())) - .thenReturn(flowOf(UploadProgress(0.5f), UploadSuccess(MediaModel(0, 0)))) + .thenReturn(flowOf(UploadProgress(0.5f), UploadSuccess(MediaTestUtils.createRemoteTestMedia().build()))) - worker.enqueueWork(Work.UploadMedia(REMOTE_PRODUCT_ID, TEST_URI, MediaModel(0, 0))) + worker.enqueueWork( + Work.UploadMedia(REMOTE_PRODUCT_ID, TEST_URI, MediaTestUtils.createRemoteTestMedia().build()) + ) advanceUntilIdle() verify(notificationHandler).setProgress(0.5f) @@ -153,7 +156,9 @@ class ProductImagesUploadWorkerTest : BaseUnitTest() { val job = launch { worker.events.toList(eventsList) } - worker.enqueueWork(Work.UploadMedia(REMOTE_PRODUCT_ID, TEST_URI, MediaModel(0, 0))) + worker.enqueueWork( + Work.UploadMedia(REMOTE_PRODUCT_ID, TEST_URI, MediaTestUtils.createRemoteTestMedia().build()) + ) advanceUntilIdle() assertThat(eventsList).contains(UploadFailed(REMOTE_PRODUCT_ID, TEST_URI, error)) @@ -166,7 +171,9 @@ class ProductImagesUploadWorkerTest : BaseUnitTest() { val job = launch { worker.events.toList(eventsList) } - worker.enqueueWork(Work.UploadMedia(REMOTE_PRODUCT_ID, TEST_URI, MediaModel(0, 0))) + worker.enqueueWork( + Work.UploadMedia(REMOTE_PRODUCT_ID, TEST_URI, MediaTestUtils.createRemoteTestMedia().build()) + ) advanceUntilIdle() assertThat(eventsList).contains(ProductUploadsCompleted(REMOTE_PRODUCT_ID)) diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/media/MediaFileUploadHandlerTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/media/MediaFileUploadHandlerTest.kt index 5c419938c7cc..a65fead5025f 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/media/MediaFileUploadHandlerTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/media/MediaFileUploadHandlerTest.kt @@ -29,9 +29,7 @@ import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify -import org.wordpress.android.fluxc.model.MediaModel -import org.wordpress.android.fluxc.model.MediaModel.MediaUploadState.FAILED -import org.wordpress.android.fluxc.model.MediaModel.MediaUploadState.UPLOADED +import org.wordpress.android.fluxc.media.MediaTestUtils import org.wordpress.android.fluxc.store.MediaStore.MediaErrorType import org.wordpress.android.fluxc.store.MediaStore.MediaErrorType.NULL_MEDIA_ARG import org.wordpress.android.util.DateTimeUtils @@ -80,7 +78,7 @@ class MediaFileUploadHandlerTest : BaseUnitTest() { fun `when media is fetched, then start uploading it`() = testBlocking { mediaFileUploadHandler.enqueueUpload(REMOTE_PRODUCT_ID, listOf(TEST_URI)) - val fetchedMedia = MediaModel(0, 0) + val fetchedMedia = MediaTestUtils.createRemoteTestMedia().build() eventsFlow.tryEmit( Event.MediaUploadEvent.FetchSucceeded( REMOTE_PRODUCT_ID, @@ -125,14 +123,12 @@ class MediaFileUploadHandlerTest : BaseUnitTest() { mediaFileUploadHandler.enqueueUpload(REMOTE_PRODUCT_ID, listOf(TEST_URI)) launch { - val successfulUpload = mediaFileUploadHandler.observeSuccessfulUploads(REMOTE_PRODUCT_ID).first() - assertThat(successfulUpload.uploadState).isEqualTo(UPLOADED.toString()) + mediaFileUploadHandler.observeSuccessfulUploads(REMOTE_PRODUCT_ID).first() } - val mediaModel = MediaModel(0, 0).apply { - postId = REMOTE_PRODUCT_ID - setUploadState(UPLOADED) - } + val mediaModel = MediaTestUtils.createRemoteTestMedia() + .postId(REMOTE_PRODUCT_ID) + .build() eventsFlow.tryEmit( Event.MediaUploadEvent.UploadSucceeded( REMOTE_PRODUCT_ID, @@ -146,13 +142,12 @@ class MediaFileUploadHandlerTest : BaseUnitTest() { fun `given there is no external observer, when uploads finish, then start product update`() = testBlocking { mediaFileUploadHandler.enqueueUpload(REMOTE_PRODUCT_ID, listOf(TEST_URI)) - val mediaModel = MediaModel(0, 0).apply { - postId = REMOTE_PRODUCT_ID - fileName = "test" - url = "url" - uploadDate = DateTimeUtils.iso8601FromDate(Date()) - setUploadState(UPLOADED) - } + val mediaModel = MediaTestUtils.createRemoteTestMedia() + .fileName("test") + .url("url") + .uploadDate(DateTimeUtils.iso8601FromDate(Date())) + .postId(REMOTE_PRODUCT_ID) + .build() eventsFlow.tryEmit( Event.MediaUploadEvent.UploadSucceeded( REMOTE_PRODUCT_ID, @@ -170,13 +165,12 @@ class MediaFileUploadHandlerTest : BaseUnitTest() { val testUri2 = "file:///test2" mediaFileUploadHandler.enqueueUpload(REMOTE_PRODUCT_ID, listOf(TEST_URI, testUri2)) - val mediaModel = MediaModel(0, 0).apply { - postId = REMOTE_PRODUCT_ID - fileName = "test" - url = "url" - uploadDate = DateTimeUtils.iso8601FromDate(Date()) - setUploadState(UPLOADED) - } + val mediaModel = MediaTestUtils.createRemoteTestMedia() + .fileName("test") + .url("url") + .uploadDate(DateTimeUtils.iso8601FromDate(Date())) + .postId(REMOTE_PRODUCT_ID) + .build() eventsFlow.tryEmit( Event.MediaUploadEvent.UploadSucceeded( REMOTE_PRODUCT_ID, @@ -203,10 +197,9 @@ class MediaFileUploadHandlerTest : BaseUnitTest() { val job = launch { mediaFileUploadHandler.observeSuccessfulUploads(REMOTE_PRODUCT_ID).collect() } - val mediaModel = MediaModel(0, 0).apply { - postId = REMOTE_PRODUCT_ID - setUploadState(FAILED) - } + val mediaModel = MediaTestUtils.createRemoteTestMedia() + .postId(REMOTE_PRODUCT_ID) + .build() eventsFlow.tryEmit( Event.MediaUploadEvent.UploadFailed( @@ -229,10 +222,9 @@ class MediaFileUploadHandlerTest : BaseUnitTest() { fun `given there is no external observer, when an upload fails, then show notification`() = testBlocking { mediaFileUploadHandler.enqueueUpload(REMOTE_PRODUCT_ID, listOf(TEST_URI)) - val mediaModel = MediaModel(0, 0).apply { - postId = REMOTE_PRODUCT_ID - setUploadState(FAILED) - } + val mediaModel = MediaTestUtils.createRemoteTestMedia() + .postId(REMOTE_PRODUCT_ID) + .build() eventsFlow.tryEmit( Event.MediaUploadEvent.UploadFailed( REMOTE_PRODUCT_ID, @@ -261,12 +253,11 @@ class MediaFileUploadHandlerTest : BaseUnitTest() { @Test fun `when assigning uploads to created product, then update the id for the successful ones`() = testBlocking { mediaFileUploadHandler.enqueueUpload(ProductDetailViewModel.DEFAULT_ADD_NEW_PRODUCT_ID, listOf(TEST_URI)) - val mediaModel = MediaModel(0, 0).apply { - fileName = "test" - url = "url" - uploadDate = DateTimeUtils.iso8601FromDate(Date()) - setUploadState(UPLOADED) - } + val mediaModel = MediaTestUtils.createRemoteTestMedia() + .fileName("test") + .url("url") + .uploadDate(DateTimeUtils.iso8601FromDate(Date())) + .build() eventsFlow.tryEmit( Event.MediaUploadEvent.UploadSucceeded( ProductDetailViewModel.DEFAULT_ADD_NEW_PRODUCT_ID, diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/products/details/ProductDetailViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/products/details/ProductDetailViewModelTest.kt index a6271befc738..d898ce9b9081 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/products/details/ProductDetailViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/products/details/ProductDetailViewModelTest.kt @@ -62,7 +62,7 @@ import org.mockito.kotlin.spy import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever -import org.wordpress.android.fluxc.model.MediaModel +import org.wordpress.android.fluxc.media.MediaTestUtils import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.store.MediaStore import org.wordpress.android.fluxc.store.WCProductStore @@ -1090,10 +1090,10 @@ class ProductDetailViewModelTest : BaseUnitTest() { on { it.observeCurrentUploadErrors(any()) } doReturn emptyFlow() on { it.observeCurrentUploads(any()) } doReturn flowOf(emptyList()) on { it.observeSuccessfulUploads(any()) } doReturn uris.map { - MediaModel(0, 0).apply { - url = it - uploadDate = "2022-09-27 18:00:00.000" - } + MediaTestUtils.createRemoteTestMedia() + .url(it) + .uploadDate("2022-09-27 18:00:00.000") + .build() }.asFlow() } diff --git a/libs/fluxc-plugin/src/main/kotlin/org/wordpress/android/fluxc/model/WCProductImageModel.kt b/libs/fluxc-plugin/src/main/kotlin/org/wordpress/android/fluxc/model/WCProductImageModel.kt index 30fa1a0dc213..b8e4cd4bba67 100644 --- a/libs/fluxc-plugin/src/main/kotlin/org/wordpress/android/fluxc/model/WCProductImageModel.kt +++ b/libs/fluxc-plugin/src/main/kotlin/org/wordpress/android/fluxc/model/WCProductImageModel.kt @@ -1,7 +1,6 @@ package org.wordpress.android.fluxc.model import com.google.gson.JsonObject -import org.wordpress.android.fluxc.utils.DateUtils class WCProductImageModel(val id: Long) { var dateCreated: String = "" @@ -9,18 +8,6 @@ class WCProductImageModel(val id: Long) { var alt: String = "" var name: String = "" - companion object { - fun fromMediaModel(media: MediaModel): WCProductImageModel { - with(WCProductImageModel(media.mediaId)) { - dateCreated = media.uploadDate ?: DateUtils.getCurrentDateString() - src = media.url - alt = media.alt - name = media.fileName ?: "" - return this - } - } - } - fun toJson(): JsonObject { return JsonObject().also { json -> json.addProperty("id", id) diff --git a/libs/fluxc-tests/src/test/java/org/wordpress/android/fluxc/media/MediaSqlUtilsTest.java b/libs/fluxc-tests/src/test/java/org/wordpress/android/fluxc/media/MediaSqlUtilsTest.java deleted file mode 100644 index 1ef26971525c..000000000000 --- a/libs/fluxc-tests/src/test/java/org/wordpress/android/fluxc/media/MediaSqlUtilsTest.java +++ /dev/null @@ -1,141 +0,0 @@ -package org.wordpress.android.fluxc.media; - -import static org.assertj.core.api.Assertions.assertThat; - -import android.content.Context; - -import com.wellsql.generated.MediaModelTable; -import com.yarolegovich.wellsql.WellSql; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; -import org.wordpress.android.fluxc.SingleStoreWellSqlConfigForTests; -import org.wordpress.android.fluxc.model.MediaModel; -import org.wordpress.android.fluxc.model.MediaModel.MediaUploadState; -import org.wordpress.android.fluxc.model.SiteModel; -import org.wordpress.android.fluxc.persistence.MediaSqlUtils; -import org.wordpress.android.fluxc.persistence.WellSqlConfig; -import org.wordpress.android.fluxc.utils.MimeType.Type; - -import java.util.ArrayList; -import java.util.List; -import java.util.Random; - -@RunWith(RobolectricTestRunner.class) -public class MediaSqlUtilsTest { - private static final int TEST_LOCAL_SITE_ID = 42; - private static final int SMALL_TEST_POOL = 10; - - private final Random mRandom = new Random(System.currentTimeMillis()); - - @Before - public void setUp() { - Context appContext = RuntimeEnvironment.getApplication().getApplicationContext(); - - WellSqlConfig config = new SingleStoreWellSqlConfigForTests(appContext, MediaModel.class); - WellSql.init(config); - config.reset(); - } - - // Inserts a media item with various known fields then retrieves and validates those fields - @Test - public void testInsertMedia() { - long testId = Math.abs(mRandom.nextLong()); - String testTitle = getTestString(); - String testDescription = getTestString(); - String testCaption = getTestString(); - MediaModel testMedia = getTestMedia(testId, testTitle, testDescription, testCaption); - assertThat(1).isEqualTo(MediaSqlUtils.insertOrUpdateMedia(testMedia)); - List media = MediaSqlUtils.getSiteMediaWithId(getTestSiteWithLocalId(TEST_LOCAL_SITE_ID), testId); - assertThat(media).hasSize(1); - assertThat(media.get(0)).isNotNull(); - assertThat(media.get(0).getMediaId()).isEqualTo(testId); - assertThat(media.get(0).getTitle()).isEqualTo(testTitle); - assertThat(media.get(0).getDescription()).isEqualTo(testDescription); - assertThat(media.get(0).getCaption()).isEqualTo(testCaption); - } - - // Inserts media of multiple MIME types then retrieves only images and verifies - @Test - public void testGetSiteImages() { - List imageIds = new ArrayList<>(SMALL_TEST_POOL); - List videoIds = new ArrayList<>(SMALL_TEST_POOL); - for (int i = 0; i < imageIds.size(); ++i) { - imageIds.add(mRandom.nextLong()); - videoIds.add(mRandom.nextLong()); - MediaModel image = getTestMedia(imageIds.get(i)); - image.setMimeType("image/jpg"); - MediaModel video = getTestMedia(videoIds.get(i)); - video.setMimeType("video/mp4"); - assertThat(MediaSqlUtils.insertOrUpdateMedia(image)).isEqualTo(0); - assertThat(MediaSqlUtils.insertOrUpdateMedia(video)).isEqualTo(0); - } - List images = MediaSqlUtils.getSiteImages(getTestSiteWithLocalId(TEST_LOCAL_SITE_ID)); - assertThat(imageIds.size()).isEqualTo(images.size()); - for (int i = 0; i < imageIds.size(); ++i) { - assertThat(images.get(0).getMimeType().contains(Type.IMAGE.getValue())).isTrue(); - assertThat(imageIds).contains(images.get(i).getMediaId()); - } - } - - // Inserts many images then retrieves all images with a supplied exclusion filter - @Test - public void testGetSiteImagesExclusionFilter() { - long[] imageIds = insertImageTestItems(); - List exclusion = new ArrayList<>(); - for (int i = 0; i < SMALL_TEST_POOL; i += 2) { - exclusion.add(imageIds[i]); - } - List includedImages = MediaSqlUtils - .getSiteImagesExcluding(getTestSiteWithLocalId(TEST_LOCAL_SITE_ID), exclusion); - assertThat(includedImages).hasSize(SMALL_TEST_POOL - exclusion.size()); - for (int i = 0; i < includedImages.size(); ++i) { - assertThat(exclusion).doesNotContain(includedImages.get(i).getMediaId()); - } - } - - // Utilities - - private long[] insertImageTestItems() { - long[] testItemIds = new long[MediaSqlUtilsTest.SMALL_TEST_POOL]; - for (int i = 0; i < MediaSqlUtilsTest.SMALL_TEST_POOL; ++i) { - testItemIds[i] = Math.abs(mRandom.nextInt()); - MediaModel image = getTestMedia(testItemIds[i]); - image.setMimeType("image/jpg"); - image.setUploadState(MediaUploadState.UPLOADED); - assertThat(1).isEqualTo(MediaSqlUtils.insertOrUpdateMedia(image)); - } - return testItemIds; - } - - private MediaModel getTestMedia(long mediaId) { - return new MediaModel( - TEST_LOCAL_SITE_ID, - mediaId - ); - } - - private MediaModel getTestMedia(long mediaId, String title, String description, String caption) { - MediaModel media = new MediaModel( - TEST_LOCAL_SITE_ID, - mediaId - ); - media.setTitle(title); - media.setDescription(description); - media.setCaption(caption); - return media; - } - - private String getTestString() { - return "BaseTestString-" + mRandom.nextInt(); - } - - private SiteModel getTestSiteWithLocalId(int localSiteId) { - SiteModel siteModel = new SiteModel(); - siteModel.setId(localSiteId); - return siteModel; - } -} diff --git a/libs/fluxc-tests/src/test/java/org/wordpress/android/fluxc/media/MediaTestUtils.java b/libs/fluxc-tests/src/test/java/org/wordpress/android/fluxc/media/MediaTestUtils.java deleted file mode 100644 index e76c5f127f28..000000000000 --- a/libs/fluxc-tests/src/test/java/org/wordpress/android/fluxc/media/MediaTestUtils.java +++ /dev/null @@ -1,71 +0,0 @@ -package org.wordpress.android.fluxc.media; - -import org.wordpress.android.fluxc.model.MediaModel; -import org.wordpress.android.fluxc.persistence.MediaSqlUtils; -import org.wordpress.android.fluxc.utils.MediaUtils; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -import static org.junit.Assert.assertEquals; - -public class MediaTestUtils { - public static int insertMediaIntoDatabase(MediaModel media) { - return MediaSqlUtils.insertOrUpdateMedia(media); - } - - public static List insertRandomMediaIntoDatabase(int localSiteId, int count) { - List insertedMedia = generateRandomizedMediaList(count, localSiteId); - for (MediaModel media : insertedMedia) { - assertEquals(1, MediaSqlUtils.insertOrUpdateMedia(media)); - } - return insertedMedia; - } - - public static MediaModel generateMedia(String title, String desc, String caption, String alt) { - MediaModel media = new MediaModel( - 0, - 0 - ); - media.setTitle(title); - media.setDescription(desc); - media.setCaption(caption); - media.setAlt(alt); - return media; - } - - public static MediaModel generateMediaFromPath(int localSiteId, long mediaId, String filePath) { - MediaModel media = new MediaModel( - localSiteId, - mediaId - ); - media.setFilePath(filePath); - media.setFileName(MediaUtils.getFileName(filePath)); - String extension = MediaUtils.getExtension(filePath); - media.setMimeType(MediaUtils.getMimeTypeForExtension(extension)); - media.setTitle(media.getFileName()); - return media; - } - - public static MediaModel generateRandomizedMedia(int localSiteId) { - MediaModel media = generateMedia(randomStr(5), randomStr(5), randomStr(5), randomStr(5)); - media.setLocalSiteId(localSiteId); - return media; - } - - public static List generateRandomizedMediaList(int size, int localSiteId) { - List mediaList = new ArrayList<>(); - for (int i = 0; i < size; ++i) { - MediaModel newMedia = generateRandomizedMedia(localSiteId); - newMedia.setMediaId(i); - mediaList.add(newMedia); - } - return mediaList; - } - - public static String randomStr(int length) { - String randomString = UUID.randomUUID().toString(); - return length > randomString.length() ? randomString : randomString.substring(0, length); - } -} diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/model/MediaModel.java b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/model/MediaModel.java index 86af8ad26e5b..923b5f21badf 100644 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/model/MediaModel.java +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/model/MediaModel.java @@ -3,132 +3,37 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.yarolegovich.wellsql.core.Identifiable; -import com.yarolegovich.wellsql.core.annotation.Column; -import com.yarolegovich.wellsql.core.annotation.PrimaryKey; -import com.yarolegovich.wellsql.core.annotation.Table; - import org.wordpress.android.fluxc.Payload; import org.wordpress.android.fluxc.network.BaseRequest.BaseNetworkError; import org.wordpress.android.util.StringUtils; import java.io.Serializable; +import java.util.Objects; -// WARN: This class is used within WordPress-MediaPicker-Android, do not remove! -@Table -public class MediaModel extends Payload implements Identifiable, Serializable { - private static final long serialVersionUID = -1396457338496002846L; - - public enum MediaUploadState { - QUEUED, UPLOADING, DELETING, DELETED, FAILED, UPLOADED; - - @NonNull - public static MediaUploadState fromString(@Nullable String stringState) { - if (stringState != null) { - for (MediaUploadState state : MediaUploadState.values()) { - if (stringState.equalsIgnoreCase(state.toString())) { - return state; - } - } - } - return UPLOADED; - } - } +public class MediaModel extends Payload implements Serializable { - @PrimaryKey - @Column private int mId; + private int mId; // Associated IDs - @Column private int mLocalSiteId; - @Column private int mLocalPostId; // The local post the media was uploaded from, for lookup after media uploads - @Column private long mMediaId; // The remote ID of the media - @Column private long mPostId; // The remote post ID ('parent') of the media + private int mLocalSiteId; + private long mMediaId; // The remote ID of the media + private long mPostId; // The remote post ID ('parent') of the media // Upload date, ISO 8601-formatted date in UTC - @Nullable @Column private String mUploadDate; + @Nullable private final String mUploadDate; // Remote Url's - @NonNull @Column private String mUrl; - @Nullable @Column private String mThumbnailUrl; + @NonNull private final String mUrl; // File descriptors - @Nullable @Column private String mFileName; - @Nullable @Column private String mFilePath; - @Nullable @Column private String mMimeType; + @Nullable private final String mFileName; + @Nullable private String mFilePath; + @Nullable private final String mMimeType; // Descriptive strings - @Nullable @Column private String mTitle; - @NonNull @Column private String mCaption; - @NonNull @Column private String mDescription; - @NonNull @Column private String mAlt; - - // Local only - @Nullable @Column private String mUploadState; - @Column private boolean mMarkedLocallyAsFeatured; - - /** - * Enum representing various media fields with their default field names. - * The default values can be changed by modifying the string parameter - * passed to the enum constructor. - */ - public enum MediaFields { - PARENT_ID("parent_id"), - TITLE("title"), - DESCRIPTION("description"), - CAPTION("caption"), - ALT("alt"); - - @NonNull private final String mFieldName; - - // Constructor - MediaFields(@NonNull String fieldName) { - this.mFieldName = fieldName; - } - - // Getter - @NonNull - public String getFieldName() { - return this.mFieldName; - } - } - - @NonNull private MediaFields[] mFieldsToUpdate = MediaFields.values(); - - @Deprecated - @SuppressWarnings("DeprecatedIsStillUsed") - public MediaModel() { - this.mId = 0; - this.mLocalSiteId = 0; - this.mLocalPostId = 0; - this.mMediaId = 0; - this.mPostId = 0; - this.mUploadDate = null; - this.mUrl = ""; - this.mThumbnailUrl = null; - this.mFileName = null; - this.mFilePath = null; - this.mMimeType = null; - this.mTitle = null; - this.mCaption = ""; - this.mDescription = ""; - this.mAlt = ""; - this.mUploadState = null; - this.mMarkedLocallyAsFeatured = false; - } - - /** - * Use when getting an existing media. - */ - public MediaModel( - int localSiteId, - long mediaId) { - this.mLocalSiteId = localSiteId; - this.mMediaId = mediaId; - this.mUrl = ""; - this.mCaption = ""; - this.mDescription = ""; - this.mAlt = ""; - } + @Nullable private final String mTitle; + @NonNull private final String mCaption; + @NonNull private final String mDescription; /** * Use when converting local uri into a media, and then, to upload a new or update an existing media. @@ -139,8 +44,7 @@ public MediaModel( @Nullable String fileName, @Nullable String filePath, @Nullable String mimeType, - @Nullable String title, - @Nullable MediaUploadState uploadState) { + @Nullable String title) { this.mLocalSiteId = localSiteId; this.mUploadDate = uploadDate; this.mUrl = ""; @@ -150,86 +54,32 @@ public MediaModel( this.mTitle = title; this.mCaption = ""; this.mDescription = ""; - this.mAlt = ""; - this.mUploadState = uploadState != null ? uploadState.toString() : null; - } - - /** - * Use when converting editor image metadata into a media. - */ - public MediaModel( - @NonNull String url, - @Nullable String fileName, - @Nullable String title, - @NonNull String caption, - @NonNull String alt) { - this.mUrl = url; - this.mFileName = fileName; - this.mTitle = title; - this.mCaption = caption; - this.mDescription = ""; - this.mAlt = alt; } /** - * Use when converting a media file into a media. + * Used for receiving media from the remote */ - public MediaModel( - int id, - int localSiteId, - long mediaId, - @NonNull String url, - @Nullable String thumbnailUrl, - @Nullable String fileName, - @Nullable String filePath, - @Nullable String mimeType, - @Nullable String title, - @NonNull String caption, - @NonNull String description, - @NonNull String alt, - @NonNull MediaUploadState uploadState) { - this.mId = id; - this.mLocalSiteId = localSiteId; - this.mMediaId = mediaId; - this.mUrl = url; - this.mThumbnailUrl = thumbnailUrl; - this.mFileName = fileName; - this.mFilePath = filePath; - this.mMimeType = mimeType; - this.mTitle = title; - this.mCaption = caption; - this.mDescription = description; - this.mAlt = alt; - this.mUploadState = uploadState.toString(); - } - public MediaModel( int localSiteId, long mediaId, long postId, @Nullable String uploadDate, @NonNull String url, - @Nullable String thumbnailUrl, @Nullable String fileName, @Nullable String mimeType, @Nullable String title, @NonNull String caption, - @NonNull String description, - @NonNull String alt, - @NonNull MediaUploadState uploadState) { + @NonNull String description) { this.mLocalSiteId = localSiteId; this.mMediaId = mediaId; this.mPostId = postId; this.mUploadDate = uploadDate; this.mUrl = url; - this.mThumbnailUrl = thumbnailUrl; this.mFileName = fileName; this.mMimeType = mimeType; this.mTitle = title; this.mCaption = caption; this.mDescription = description; - this.mAlt = alt; - this.mUploadState = uploadState.toString(); } @Override @@ -242,29 +92,40 @@ public boolean equals(@Nullable Object other) { return getId() == otherMedia.getId() && getLocalSiteId() == otherMedia.getLocalSiteId() - && getLocalPostId() == otherMedia.getLocalPostId() && getMediaId() == otherMedia.getMediaId() && getPostId() == otherMedia.getPostId() - && getMarkedLocallyAsFeatured() == otherMedia.getMarkedLocallyAsFeatured() && StringUtils.equals(getUploadDate(), otherMedia.getUploadDate()) && StringUtils.equals(getUrl(), otherMedia.getUrl()) - && StringUtils.equals(getThumbnailUrl(), otherMedia.getThumbnailUrl()) && StringUtils.equals(getFileName(), otherMedia.getFileName()) && StringUtils.equals(getFilePath(), otherMedia.getFilePath()) && StringUtils.equals(getMimeType(), otherMedia.getMimeType()) && StringUtils.equals(getTitle(), otherMedia.getTitle()) && StringUtils.equals(getDescription(), otherMedia.getDescription()) - && StringUtils.equals(getCaption(), otherMedia.getCaption()) - && StringUtils.equals(getAlt(), otherMedia.getAlt()) - && StringUtils.equals(getUploadState(), otherMedia.getUploadState()); + && StringUtils.equals(getCaption(), otherMedia.getCaption()); } @Override + public int hashCode() { + return Objects.hash( + mId, + mLocalSiteId, + mMediaId, + mPostId, + mUploadDate, + mUrl, + mFileName, + mFilePath, + mMimeType, + mTitle, + mCaption, + mDescription + ); + } + public void setId(int id) { mId = id; } - @Override public int getId() { return mId; } @@ -277,14 +138,6 @@ public int getLocalSiteId() { return mLocalSiteId; } - public void setLocalPostId(int localPostId) { - mLocalPostId = localPostId; - } - - public int getLocalPostId() { - return mLocalPostId; - } - public void setMediaId(long mediaId) { mMediaId = mediaId; } @@ -301,124 +154,43 @@ public long getPostId() { return mPostId; } - public void setUploadDate(@Nullable String uploadDate) { - mUploadDate = uploadDate; - } - @Nullable public String getUploadDate() { return mUploadDate; } - public void setUrl(@NonNull String url) { - mUrl = url; - } - @NonNull public String getUrl() { return mUrl; } - public void setThumbnailUrl(@Nullable String thumbnailUrl) { - mThumbnailUrl = thumbnailUrl; - } - - @Nullable - public String getThumbnailUrl() { - return mThumbnailUrl; - } - - public void setFileName(@Nullable String fileName) { - mFileName = fileName; - } - @Nullable public String getFileName() { return mFileName; } - public void setFilePath(@Nullable String filePath) { - mFilePath = filePath; - } - @Nullable public String getFilePath() { return mFilePath; } - public void setMimeType(@Nullable String mimeType) { - mMimeType = mimeType; - } - @Nullable public String getMimeType() { return mMimeType; } - public void setTitle(@Nullable String title) { - mTitle = title; - } - @Nullable public String getTitle() { return mTitle; } - public void setCaption(@NonNull String caption) { - mCaption = caption; - } - @NonNull public String getCaption() { return mCaption; } - public void setDescription(@NonNull String description) { - mDescription = description; - } - @NonNull public String getDescription() { return mDescription; } - - public void setAlt(@NonNull String alt) { - mAlt = alt; - } - - @NonNull - public String getAlt() { - return mAlt; - } - - public void setUploadState(@Nullable String uploadState) { - mUploadState = uploadState; - } - - public void setUploadState(@NonNull MediaUploadState uploadState) { - mUploadState = uploadState.toString(); - } - - @Nullable - public String getUploadState() { - return mUploadState; - } - - @NonNull - public MediaFields[] getFieldsToUpdate() { - return mFieldsToUpdate; - } - - @SuppressWarnings("unused") - public void setFieldsToUpdate(@NonNull MediaFields[] fieldsToUpdate) { - this.mFieldsToUpdate = fieldsToUpdate; - } - - public boolean getMarkedLocallyAsFeatured() { - return mMarkedLocallyAsFeatured; - } - - public void setMarkedLocallyAsFeatured(boolean markedLocallyAsFeatured) { - mMarkedLocallyAsFeatured = markedLocallyAsFeatured; - } } diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/module/MediaModule.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/module/MediaModule.kt new file mode 100644 index 000000000000..926d4d4c50c7 --- /dev/null +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/module/MediaModule.kt @@ -0,0 +1,21 @@ +package org.wordpress.android.fluxc.module + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.wordpress.android.fluxc.store.MediaIdGenerator +import org.wordpress.android.fluxc.store.TimestampMediaIdGenerator +import kotlin.time.Clock +import kotlin.time.ExperimentalTime + +@OptIn(ExperimentalTime::class) +@Module +abstract class MediaModule { + @Binds + internal abstract fun bindMediaIdGenerator(generator: TimestampMediaIdGenerator): MediaIdGenerator + + companion object { + @Provides + fun provideClock(): Clock = Clock.System + } +} diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/media/BaseWPV2MediaRestClient.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/media/BaseWPV2MediaRestClient.kt index 199695bd619c..f54c010ee1a4 100644 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/media/BaseWPV2MediaRestClient.kt +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/media/BaseWPV2MediaRestClient.kt @@ -9,7 +9,6 @@ import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onStart import okhttp3.Call @@ -24,7 +23,6 @@ import org.wordpress.android.fluxc.annotations.endpoint.WPAPIEndpoint import org.wordpress.android.fluxc.generated.MediaActionBuilder import org.wordpress.android.fluxc.generated.endpoint.WPAPI import org.wordpress.android.fluxc.model.MediaModel -import org.wordpress.android.fluxc.model.MediaModel.MediaUploadState.FAILED import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.network.rest.wpapi.WPAPIResponse import org.wordpress.android.fluxc.network.rest.wpcom.WPComGsonRequest @@ -103,7 +101,6 @@ abstract class BaseWPV2MediaRestClient( @Suppress("TooGenericExceptionCaught", "SwallowedException") private fun syncUploadMedia(site: SiteModel, media: MediaModel): Flow { fun ProducerScope.handleFailure(media: MediaModel, error: MediaError) { - media.setUploadState(FAILED) val payload = ProgressPayload(media, 1f, false, error) trySendBlocking(payload) close() diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/media/MediaWPRESTResponse.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/media/MediaWPRESTResponse.kt index 0c3fa40a7085..6c2178d1797b 100644 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/media/MediaWPRESTResponse.kt +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/media/MediaWPRESTResponse.kt @@ -5,9 +5,7 @@ package org.wordpress.android.fluxc.network.rest.wpapi.media import com.google.gson.annotations.SerializedName import org.apache.commons.text.StringEscapeUtils import org.wordpress.android.fluxc.model.MediaModel -import org.wordpress.android.fluxc.model.MediaModel.MediaUploadState import org.wordpress.android.fluxc.network.rest.JsonObjectOrNull -import org.wordpress.android.fluxc.network.rest.wpcom.media.MediaWPComRestResponse import org.wordpress.android.util.DateTimeUtils import java.text.SimpleDateFormat import java.util.Locale @@ -20,7 +18,6 @@ data class MediaWPRESTResponse( val post: Long? = null, val description: Attribute, val caption: Attribute, - @SerializedName("alt_text") val altText: String, @SerializedName("mime_type") val mimeType: String, @SerializedName("media_details") val mediaDetails: MediaDetails?, @SerializedName("source_url") val sourceURL: String? @@ -51,16 +48,9 @@ fun MediaWPRESTResponse.toMediaModel(localSiteId: Int) = MediaModel( SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ROOT).parse(dateGmt) ), sourceURL.orEmpty(), - mediaDetails?.sizes?.thumbnail?.sourceURL, mediaDetails?.file, mimeType, StringEscapeUtils.unescapeHtml4(title.rendered), StringEscapeUtils.unescapeHtml4(caption.rendered), - StringEscapeUtils.unescapeHtml4(description.rendered), - StringEscapeUtils.unescapeHtml4(altText), - if (MediaWPComRestResponse.DELETED_STATUS == status) { - MediaUploadState.DELETED - } else { - MediaUploadState.UPLOADED - } + StringEscapeUtils.unescapeHtml4(description.rendered) ) diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/media/WPRestUploadRequestBody.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/media/WPRestUploadRequestBody.kt index 3263336cadfa..2f970d9d7214 100644 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/media/WPRestUploadRequestBody.kt +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/media/WPRestUploadRequestBody.kt @@ -17,7 +17,6 @@ private const val FILE_FORM_KEY = "file" private const val TITLE_FORM_KEY = "title" private const val DESCRIPTION_FORM_KEY = "description" private const val CAPTION_FORM_KEY = "caption" -private const val ALT_FORM_KEY = "alt_text" private const val POST_ID_FORM_KEY = "post" class WPRestUploadRequestBody( @@ -44,7 +43,6 @@ class WPRestUploadRequestBody( .addParamIfNotEmpty(TITLE_FORM_KEY, media.title) .addParamIfNotEmpty(DESCRIPTION_FORM_KEY, media.description) .addParamIfNotEmpty(CAPTION_FORM_KEY, media.caption) - .addParamIfNotEmpty(ALT_FORM_KEY, media.alt) .addParamIfNotEmpty(POST_ID_FORM_KEY, media.postId.takeIf { it > 0L }?.toString()) val filePath = media.filePath diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/media/MediaResponseUtils.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/media/MediaResponseUtils.kt deleted file mode 100644 index 37b5ead6f05f..000000000000 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/media/MediaResponseUtils.kt +++ /dev/null @@ -1,52 +0,0 @@ -package org.wordpress.android.fluxc.network.rest.wpcom.media - -import android.text.TextUtils -import org.apache.commons.text.StringEscapeUtils -import org.wordpress.android.fluxc.model.MediaModel -import org.wordpress.android.fluxc.model.MediaModel.MediaUploadState -import org.wordpress.android.fluxc.network.rest.wpcom.media.MediaWPComRestResponse.MultipleMediaResponse -import javax.inject.Inject - -class MediaResponseUtils -@Inject constructor() { - /** - * Creates a [MediaModel] list from a WP.com REST response to a request for all media. - */ - fun getMediaListFromRestResponse( - from: MultipleMediaResponse, - localSiteId: Int - ): List { - return from.media.mapNotNull { - getMediaFromRestResponse(it, localSiteId) - } - } - - /** - * Creates a [MediaModel] from a WP.com REST response to a fetch request. - */ - fun getMediaFromRestResponse(from: MediaWPComRestResponse, siteId: Int) = MediaModel( - siteId, - from.ID, - from.post_ID, - from.date, - from.URL, - from.thumbnails?.let { - if (!TextUtils.isEmpty(it.fmt_std)) { - it.fmt_std - } else { - it.thumbnail - } - }, - from.file, - from.mime_type, - StringEscapeUtils.unescapeHtml4(from.title), - StringEscapeUtils.unescapeHtml4(from.caption), - StringEscapeUtils.unescapeHtml4(from.description), - StringEscapeUtils.unescapeHtml4(from.alt), - if (MediaWPComRestResponse.DELETED_STATUS == from.status) { - MediaUploadState.DELETED - } else { - MediaUploadState.UPLOADED - } - ) -} diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/media/MediaWPComRestResponse.java b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/media/MediaWPComRestResponse.java deleted file mode 100644 index acb1ee9547d3..000000000000 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/media/MediaWPComRestResponse.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.wordpress.android.fluxc.network.rest.wpcom.media; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.wordpress.android.fluxc.network.Response; - -import java.util.List; - -/** - * Response to GET request for media items - *

- * @see doc - */ -@SuppressWarnings("NotNullFieldNotInitialized") -public class MediaWPComRestResponse implements Response { - public static final String DELETED_STATUS = "deleted"; - - public static class MultipleMediaResponse { - @NonNull public List media; - } - - public static class Thumbnails { - @Nullable public String thumbnail; - @Nullable public String fmt_std; - } - - public long ID; - @NonNull public String date; - public long post_ID; - @NonNull public String URL; - @NonNull public String file; - @NonNull public String mime_type; - @NonNull public String title; - @NonNull public String caption; - @NonNull public String description; - @NonNull public String alt; - @Nullable public Thumbnails thumbnails; - @Nullable public String status; -} diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/media/RestUploadRequestBody.java b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/media/RestUploadRequestBody.java deleted file mode 100644 index f31a8022ba3d..000000000000 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/media/RestUploadRequestBody.java +++ /dev/null @@ -1,103 +0,0 @@ -package org.wordpress.android.fluxc.network.rest.wpcom.media; - -import androidx.annotation.NonNull; - -import org.wordpress.android.fluxc.model.MediaModel; -import org.wordpress.android.fluxc.network.BaseUploadRequestBody; -import org.wordpress.android.util.AppLog; - -import java.io.File; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.util.Map; - -import okhttp3.MediaType; -import okhttp3.MultipartBody; -import okhttp3.RequestBody; -import okio.BufferedSink; -import okio.Okio; - -/** - * Wrapper for {@link MultipartBody} that reports upload progress as body data is written. - *

- * A {@link ProgressListener} is required, use {@link MultipartBody} if progress is not needed. - *

- * @see doc - */ -public class RestUploadRequestBody extends BaseUploadRequestBody { - private static final String MEDIA_DATA_KEY = "media[0]"; - private static final String MEDIA_ATTRIBUTES_KEY = "attrs[0]"; - private static final String MEDIA_PARAM_FORMAT = MEDIA_ATTRIBUTES_KEY + "[%s]"; - - @NonNull private final MultipartBody mMultipartBody; - - public RestUploadRequestBody( - @NonNull MediaModel media, - @NonNull Map params, - @NonNull ProgressListener listener) { - super(media, listener); - mMultipartBody = buildMultipartBody(params); - } - - @Override - protected float getProgress(long bytesWritten) { - return (float) bytesWritten / contentLength(); - } - - @Override - public long contentLength() { - try { - return mMultipartBody.contentLength(); - } catch (IOException e) { - AppLog.w(AppLog.T.MEDIA, "Error determining mMultipartBody content length: " + e); - } - return -1L; - } - - @NonNull - @Override - public MediaType contentType() { - return mMultipartBody.contentType(); - } - - @Override - public void writeTo(@NonNull BufferedSink sink) throws IOException { - CountingSink countingSink = new CountingSink(sink); - BufferedSink bufferedSink = Okio.buffer(countingSink); - mMultipartBody.writeTo(bufferedSink); - bufferedSink.flush(); - } - - @NonNull - @SuppressWarnings("deprecation") - private MultipartBody buildMultipartBody(@NonNull Map params) { - MediaModel media = getMedia(); - MultipartBody.Builder builder = new MultipartBody.Builder().setType(MultipartBody.FORM); - - // add media attributes - for (String key : params.keySet()) { - Object value = params.get(key); - if (value != null) { - builder.addFormDataPart(String.format(MEDIA_PARAM_FORMAT, key), value.toString()); - } - } - - // add media file data - String filePath = media.getFilePath(); - String mimeType = media.getMimeType(); - if (filePath != null && mimeType != null) { - File mediaFile = new File(filePath); - RequestBody body = RequestBody.create(MediaType.parse(mimeType), mediaFile); - String fileName = media.getFileName(); - try { - fileName = URLEncoder.encode(media.getFileName(), "UTF-8"); - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } - builder.addFormDataPart(MEDIA_DATA_KEY, fileName, body); - } - - return builder.build(); - } -} diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/persistence/MediaSqlUtils.java b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/persistence/MediaSqlUtils.java deleted file mode 100644 index 8ba78270a371..000000000000 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/persistence/MediaSqlUtils.java +++ /dev/null @@ -1,310 +0,0 @@ -package org.wordpress.android.fluxc.persistence; - -import android.text.TextUtils; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.wellsql.generated.MediaModelTable; -import com.yarolegovich.wellsql.ConditionClauseBuilder; -import com.yarolegovich.wellsql.DeleteQuery; -import com.yarolegovich.wellsql.SelectQuery; -import com.yarolegovich.wellsql.WellSql; - -import org.wordpress.android.fluxc.model.MediaModel; -import org.wordpress.android.fluxc.model.MediaModel.MediaUploadState; -import org.wordpress.android.fluxc.model.SiteModel; -import org.wordpress.android.fluxc.utils.MimeType.Type; - -import java.util.ArrayList; -import java.util.List; - -public class MediaSqlUtils { - @NonNull - public static List getMediaWithStates( - @NonNull SiteModel site, - @NonNull List uploadStates) { - return getMediaWithStatesQuery(site, uploadStates).getAsModel(); - } - - @NonNull - public static List getMediaWithStatesAndMimeType( - @NonNull SiteModel site, - @NonNull List uploadStates, - @NonNull String mimeType) { - return WellSql.select(MediaModel.class) - .where().beginGroup() - .equals(MediaModelTable.LOCAL_SITE_ID, site.getId()) - .contains(MediaModelTable.MIME_TYPE, mimeType) - .isIn(MediaModelTable.UPLOAD_STATE, uploadStates) - .endGroup().endWhere() - .orderBy(MediaModelTable.UPLOAD_DATE, SelectQuery.ORDER_DESCENDING) - .getAsModel(); - } - - @NonNull - private static SelectQuery getMediaWithStatesQuery( - @NonNull SiteModel site, - @NonNull List uploadStates) { - return WellSql.select(MediaModel.class) - .where().beginGroup() - .equals(MediaModelTable.LOCAL_SITE_ID, site.getId()) - .isIn(MediaModelTable.UPLOAD_STATE, uploadStates) - .endGroup().endWhere() - .orderBy(MediaModelTable.UPLOAD_DATE, SelectQuery.ORDER_DESCENDING); - } - - @NonNull - public static List getSiteMediaWithId(@NonNull SiteModel siteModel, long mediaId) { - return WellSql.select(MediaModel.class).where().beginGroup() - .equals(MediaModelTable.LOCAL_SITE_ID, siteModel.getId()) - .equals(MediaModelTable.MEDIA_ID, mediaId) - .endGroup().endWhere() - .orderBy(MediaModelTable.UPLOAD_DATE, SelectQuery.ORDER_DESCENDING) - .getAsModel(); - } - - @NonNull - public static List searchSiteImages( - @NonNull SiteModel siteModel, - @NonNull String searchTerm) { - return searchSiteMediaByMimeTypeQuery(siteModel, searchTerm, Type.IMAGE.getValue()).getAsModel(); - } - - @NonNull - public static List searchSiteAudio( - @NonNull SiteModel siteModel, - @NonNull String searchTerm) { - return searchSiteMediaByMimeTypeQuery(siteModel, searchTerm, Type.AUDIO.getValue()).getAsModel(); - } - - @NonNull - public static List searchSiteVideos( - @NonNull SiteModel siteModel, - @NonNull String searchTerm) { - return searchSiteMediaByMimeTypeQuery(siteModel, searchTerm, Type.VIDEO.getValue()).getAsModel(); - } - - @NonNull - public static List searchSiteDocuments( - @NonNull SiteModel siteModel, - @NonNull String searchTerm) { - return searchSiteMediaByMimeTypeQuery(siteModel, searchTerm, Type.APPLICATION.getValue()).getAsModel(); - } - - @NonNull - private static SelectQuery searchSiteMediaByMimeTypeQuery( - @NonNull SiteModel siteModel, - @NonNull String searchTerm, - @NonNull String mimeTypePrefix) { - return WellSql.select(MediaModel.class) - .where().beginGroup() - .equals(MediaModelTable.LOCAL_SITE_ID, siteModel.getId()) - .contains(MediaModelTable.MIME_TYPE, mimeTypePrefix) - .beginGroup() - .contains(MediaModelTable.TITLE, searchTerm) - .or().contains(MediaModelTable.CAPTION, searchTerm) - .or().contains(MediaModelTable.DESCRIPTION, searchTerm) - .endGroup() - .endGroup().endWhere() - .orderBy(MediaModelTable.UPLOAD_DATE, SelectQuery.ORDER_DESCENDING); - } - - @NonNull - public static List getSiteImages(@NonNull SiteModel siteModel) { - return getSiteImagesQuery(siteModel).getAsModel(); - } - - @NonNull - private static SelectQuery getSiteImagesQuery(@NonNull SiteModel siteModel) { - return getSiteMediaByMimeTypeQuery(siteModel, Type.IMAGE.getValue()); - } - - @NonNull - public static List getSiteImagesExcluding( - @NonNull SiteModel siteModel, - @NonNull List filter) { - return getSiteImagesExcludingQuery(siteModel, filter).getAsModel(); - } - - @NonNull - public static List getSiteVideos(@NonNull SiteModel siteModel) { - return getSiteVideosQuery(siteModel).getAsModel(); - } - - @NonNull - public static List getSiteDocuments(@NonNull SiteModel siteModel) { - return getSiteDocumentsQuery(siteModel).getAsModel(); - } - - @NonNull - public static List getSiteAudio(@NonNull SiteModel siteModel) { - return getSiteAudioQuery(siteModel).getAsModel(); - } - - @NonNull - public static SelectQuery getSiteImagesExcludingQuery( - @NonNull SiteModel siteModel, - @NonNull List filter) { - return WellSql.select(MediaModel.class) - .where().beginGroup() - .equals(MediaModelTable.LOCAL_SITE_ID, siteModel.getId()) - .contains(MediaModelTable.MIME_TYPE, Type.IMAGE.getValue()) - .isNotIn(MediaModelTable.MEDIA_ID, filter) - .endGroup().endWhere() - .orderBy(MediaModelTable.UPLOAD_DATE, SelectQuery.ORDER_DESCENDING); - } - - @NonNull - private static SelectQuery getSiteVideosQuery(@NonNull SiteModel siteModel) { - return getSiteMediaByMimeTypeQuery(siteModel, Type.VIDEO.getValue()); - } - - @NonNull - private static SelectQuery getSiteAudioQuery(@NonNull SiteModel siteModel) { - return getSiteMediaByMimeTypeQuery(siteModel, Type.AUDIO.getValue()); - } - - @NonNull - private static SelectQuery getSiteDocumentsQuery(@NonNull SiteModel siteModel) { - return getSiteMediaByMimeTypeQuery(siteModel, Type.APPLICATION.getValue()); - } - - @NonNull - private static SelectQuery getSiteMediaByMimeTypeQuery( - @NonNull SiteModel siteModel, - @NonNull String mimeTypePrefix) { - return WellSql.select(MediaModel.class) - .where().beginGroup() - .equals(MediaModelTable.LOCAL_SITE_ID, siteModel.getId()) - .contains(MediaModelTable.MIME_TYPE, mimeTypePrefix) - .endGroup().endWhere() - .orderBy(MediaModelTable.UPLOAD_DATE, SelectQuery.ORDER_DESCENDING); - } - - public static int insertOrUpdateMedia(@Nullable MediaModel media) { - if (media == null) return 0; - - List existingMedia; - if (media.getMediaId() == 0) { - // If the remote media ID is 0, this is a local media file and we should only match by local ID - // Otherwise, we'd match all local media files for that site - existingMedia = WellSql.select(MediaModel.class) - .where() - .equals(MediaModelTable.ID, media.getId()) - .endWhere().getAsModel(); - } else { - // For remote media, we can uniquely identify the media by either its local ID - // or its remote media ID + its (local) site ID - existingMedia = WellSql.select(MediaModel.class) - .where().beginGroup() - .equals(MediaModelTable.ID, media.getId()) - .or() - .beginGroup() - .equals(MediaModelTable.LOCAL_SITE_ID, media.getLocalSiteId()) - .equals(MediaModelTable.MEDIA_ID, media.getMediaId()) - .endGroup() - .endGroup().endWhere().getAsModel(); - } - - if (existingMedia.isEmpty()) { - // insert, media item does not exist - WellSql.insert(media).asSingleTransaction(true).execute(); - return 1; - } else { - if (existingMedia.size() > 1) { - // We've ended up with a duplicate entry, probably due to a push/fetch race condition - // One matches based on local ID (this is the one we're trying to update with a remote media ID) - // The other matches based on local site ID + remote media ID, and we got it from a fetch - // Just remove the entry without a remote media ID (the one matching the current media's local ID) - return WellSql.delete(MediaModel.class).whereId(media.getId()); - } - // update, media item already exists - int oldId = existingMedia.get(0).getId(); - return WellSql.update(MediaModel.class).whereId(oldId) - .put(media, new UpdateAllExceptId<>(MediaModel.class)).execute(); - } - } - - @NonNull - public static MediaModel insertMediaForResult(@NonNull MediaModel media) { - WellSql.insert(media).asSingleTransaction(true).execute(); - return media; - } - - public static int deleteMedia(@Nullable MediaModel media) { - if (media == null) return 0; - if (media.getMediaId() == 0) { - // If the remote media ID is 0, this is a local media file and we should only match by local ID - // Otherwise, we'd match all local media files for that site - return WellSql.delete(MediaModel.class) - .where().beginGroup() - .equals(MediaModelTable.ID, media.getId()) - .endGroup().endWhere() - .execute(); - } else { - // For remote media, we can uniquely identify the media by either its local ID - // or its remote media ID + its (local) site ID - return WellSql.delete(MediaModel.class) - .where().beginGroup() - .equals(MediaModelTable.ID, media.getId()) - .or() - .beginGroup() - .equals(MediaModelTable.LOCAL_SITE_ID, media.getLocalSiteId()) - .equals(MediaModelTable.MEDIA_ID, media.getMediaId()) - .endGroup() - .endGroup().endWhere() - .execute(); - } - } - - public static void deleteAllUploadedSiteMedia(@NonNull SiteModel siteModel) { - WellSql.delete(MediaModel.class) - .where().beginGroup() - .equals(MediaModelTable.LOCAL_SITE_ID, siteModel.getId()) - .equals(MediaModelTable.UPLOAD_STATE, MediaUploadState.UPLOADED.toString()) - .endGroup().endWhere().execute(); - } - - public static void deleteAllUploadedSiteMediaWithMimeType( - @NonNull SiteModel siteModel, - @NonNull String mimeType) { - WellSql.delete(MediaModel.class) - .where().beginGroup() - .equals(MediaModelTable.LOCAL_SITE_ID, siteModel.getId()) - .equals(MediaModelTable.UPLOAD_STATE, MediaUploadState.UPLOADED.toString()) - .contains(MediaModelTable.MIME_TYPE, mimeType) - .endGroup().endWhere().execute(); - } - - public static void deleteUploadedSiteMediaNotInList( - @NonNull SiteModel site, - @NonNull List mediaList, - @NonNull String mimeType) { - if (mediaList.isEmpty()) { - if (!TextUtils.isEmpty(mimeType)) { - MediaSqlUtils.deleteAllUploadedSiteMediaWithMimeType(site, mimeType); - } else { - MediaSqlUtils.deleteAllUploadedSiteMedia(site); - } - return; - } - - List idList = new ArrayList<>(); - for (MediaModel media : mediaList) { - idList.add(media.getId()); - } - - ConditionClauseBuilder> builder = WellSql.delete(MediaModel.class) - .where().beginGroup() - .isNotIn(MediaModelTable.ID, idList) - .equals(MediaModelTable.LOCAL_SITE_ID, site.getId()) - .equals(MediaModelTable.UPLOAD_STATE, MediaUploadState.UPLOADED.toString()); - - if (!TextUtils.isEmpty(mimeType)) { - builder.contains(MediaModelTable.MIME_TYPE, mimeType); - } - - builder.endGroup().endWhere().execute(); - } -} diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/persistence/WellSqlConfig.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/persistence/WellSqlConfig.kt index 9319a96aaa18..e8321cdec480 100644 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/persistence/WellSqlConfig.kt +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/persistence/WellSqlConfig.kt @@ -40,7 +40,7 @@ open class WellSqlConfig : DefaultWellConfig { annotation class AddOn override fun getDbVersion(): Int { - return 238 + return 239 } override fun getDbName(): String { @@ -2285,6 +2285,10 @@ open class WellSqlConfig : DefaultWellConfig { 238 -> migrate(version) { db.execSQL("DROP TABLE IF EXISTS MediaUploadModel") } + + 239 -> migrate(version) { + db.execSQL("DROP TABLE IF EXISTS MediaModel") + } } } db.setTransactionSuccessful() diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/store/MediaCacheOperations.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/store/MediaCacheOperations.kt new file mode 100644 index 000000000000..2a1914e0fc86 --- /dev/null +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/store/MediaCacheOperations.kt @@ -0,0 +1,78 @@ +package org.wordpress.android.fluxc.store + +import org.wordpress.android.fluxc.model.MediaModel +import java.util.Locale +import javax.inject.Inject + +internal class MediaCacheOperations @Inject constructor( + private val cache: RemoteMediaCache +) { + fun getSiteImages(siteId: Int): List { + return filterByMimeType(siteId, "image") + } + + fun getSiteVideos(siteId: Int): List { + return filterByMimeType(siteId, "video") + } + + fun getSiteAudio(siteId: Int): List { + return filterByMimeType(siteId, "audio") + } + + fun getSiteDocuments(siteId: Int): List { + return filterByMimeType(siteId, "application") + } + + fun searchSiteImages(siteId: Int, searchTerm: String): List { + return searchByMimeTypeAndTerm(siteId, "image", searchTerm) + } + + fun searchSiteVideos(siteId: Int, searchTerm: String): List { + return searchByMimeTypeAndTerm(siteId, "video", searchTerm) + } + + fun searchSiteAudio(siteId: Int, searchTerm: String): List { + return searchByMimeTypeAndTerm(siteId, "audio", searchTerm) + } + + fun searchSiteDocuments(siteId: Int, searchTerm: String): List { + return searchByMimeTypeAndTerm(siteId, "application", searchTerm) + } + + fun getUploadedMediaCount(siteId: Int, mimeTypePrefix: String?): Int { + if (mimeTypePrefix == null) { + return getCacheSize(siteId) + } + return filterByMimeType(siteId, mimeTypePrefix).size + } + + private fun getCacheSize(siteId: Int): Int { + return cache.getMediaList(siteId)?.size ?: 0 + } + + private fun filterByMimeType(siteId: Int, mimeTypePrefix: String): List { + val allMedia = cache.getMediaList(siteId) ?: return emptyList() + return allMedia.filter { media -> + media.mimeType?.startsWith(mimeTypePrefix) == true + } + } + + private fun searchByMimeTypeAndTerm( + siteId: Int, + mimeTypePrefix: String, + searchTerm: String + ): List { + val allMedia = cache.getMediaList(siteId) ?: return emptyList() + val lowerSearchTerm = searchTerm.lowercase(Locale.ROOT) + return allMedia.filter { media -> + media.mimeType?.startsWith(mimeTypePrefix) == true && + matchesSearchTerm(media, lowerSearchTerm) + } + } + + private fun matchesSearchTerm(media: MediaModel, lowerSearchTerm: String): Boolean { + return (media.title?.contains(lowerSearchTerm, ignoreCase = true) == true) || + media.caption.contains(lowerSearchTerm, ignoreCase = true) || + media.description.contains(lowerSearchTerm, ignoreCase = true) + } +} diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/store/MediaIdGenerator.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/store/MediaIdGenerator.kt new file mode 100644 index 000000000000..c25cd2e9ecd6 --- /dev/null +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/store/MediaIdGenerator.kt @@ -0,0 +1,7 @@ +package org.wordpress.android.fluxc.store + +import org.wordpress.android.fluxc.model.LocalOrRemoteId.LocalId + +internal interface MediaIdGenerator { + fun generate(filePath: String): LocalId +} diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/store/MediaStore.java b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/store/MediaStore.java index caed1b38b4ce..3383aa2701d7 100644 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/store/MediaStore.java +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/store/MediaStore.java @@ -14,13 +14,11 @@ import org.wordpress.android.fluxc.annotations.action.IAction; import org.wordpress.android.fluxc.logging.FluxCCrashLogger; import org.wordpress.android.fluxc.model.MediaModel; -import org.wordpress.android.fluxc.model.MediaModel.MediaUploadState; import org.wordpress.android.fluxc.model.SiteModel; import org.wordpress.android.fluxc.network.BaseRequest.BaseNetworkError; import org.wordpress.android.fluxc.network.rest.wpapi.applicationpasswords.ApplicationPasswordsConfiguration; import org.wordpress.android.fluxc.network.rest.wpapi.media.ApplicationPasswordsMediaRestClient; import org.wordpress.android.fluxc.network.rest.wpcom.media.wpv2.WPComV2MediaRestClient; -import org.wordpress.android.fluxc.persistence.MediaSqlUtils; import org.wordpress.android.fluxc.store.media.MediaErrorSubType; import org.wordpress.android.fluxc.store.media.MediaErrorSubType.MalformedMediaArgSubType; import org.wordpress.android.fluxc.store.media.MediaErrorSubType.MalformedMediaArgSubType.Type; @@ -40,19 +38,8 @@ import javax.inject.Inject; import javax.inject.Singleton; -// WARN: This class is used within WordPress-MediaPicker-Android, do not remove! @Singleton public class MediaStore extends Store { - public static final List NOT_DELETED_STATES = new ArrayList<>(); - - static { - NOT_DELETED_STATES.add(MediaUploadState.DELETING.toString()); - NOT_DELETED_STATES.add(MediaUploadState.FAILED.toString()); - NOT_DELETED_STATES.add(MediaUploadState.QUEUED.toString()); - NOT_DELETED_STATES.add(MediaUploadState.UPLOADED.toString()); - NOT_DELETED_STATES.add(MediaUploadState.UPLOADING.toString()); - } - public static class MediaPayload extends Payload { @NonNull public SiteModel site; @Nullable public MediaModel media; @@ -286,7 +273,6 @@ public OnMediaChanged( } } - // WARN: This class is used within WordPress-MediaPicker-Android, do not remove! public static class OnMediaListFetched extends OnChanged { @NonNull public SiteModel site; public boolean canLoadMore; @@ -420,6 +406,9 @@ public static MediaErrorType fromString(@Nullable String string) { private final WPComV2MediaRestClient mWPComV2MediaRestClient; private final ApplicationPasswordsMediaRestClient mApplicationPasswordsMediaRestClient; + @NonNull private final RemoteMediaCache mRemoteMediaCache; + @NonNull private final MediaCacheOperations mMediaCacheOperations; + @NonNull private final MediaIdGenerator mMediaIdGenerator; private final ApplicationPasswordsConfiguration mApplicationPasswordsConfiguration; @@ -430,12 +419,18 @@ public static MediaErrorType fromString(@Nullable String string) { WPComV2MediaRestClient wpv2MediaRestClient, ApplicationPasswordsMediaRestClient applicationPasswordsMediaRestClient, ApplicationPasswordsConfiguration applicationPasswordsConfiguration, - @NonNull FluxCCrashLogger crashLogger) { + @NonNull FluxCCrashLogger crashLogger, + @NonNull RemoteMediaCache remoteMediaCache, + @NonNull MediaCacheOperations mediaCacheOperations, + @NonNull MediaIdGenerator mediaIdGenerator) { super(dispatcher); mWPComV2MediaRestClient = wpv2MediaRestClient; mApplicationPasswordsMediaRestClient = applicationPasswordsMediaRestClient; mApplicationPasswordsConfiguration = applicationPasswordsConfiguration; mCrashLogger = crashLogger; + mRemoteMediaCache = remoteMediaCache; + mMediaCacheOperations = mediaCacheOperations; + mMediaIdGenerator = mediaIdGenerator; } @Subscribe(threadMode = ThreadMode.ASYNC) @@ -486,69 +481,58 @@ public void onRegister() { // Getters // - @Nullable + @NonNull public MediaModel instantiateMediaModel(@NonNull MediaModel media) { - MediaModel insertedMedia = MediaSqlUtils.insertMediaForResult(media); - - if (insertedMedia.getId() == -1) { - return null; - } - - return insertedMedia; - } - - @Nullable - public MediaModel getSiteMediaWithId(@NonNull SiteModel siteModel, long mediaId) { - List media = MediaSqlUtils.getSiteMediaWithId(siteModel, mediaId); - return media.size() > 0 ? media.get(0) : null; + media.setId(mMediaIdGenerator.generate(media.getFilePath()).getValue()); + return media; } @NonNull public List getSiteImages(@NonNull SiteModel siteModel) { - return MediaSqlUtils.getSiteImages(siteModel); + return mMediaCacheOperations.getSiteImages(siteModel.getId()); } @NonNull public List getSiteVideos(@NonNull SiteModel siteModel) { - return MediaSqlUtils.getSiteVideos(siteModel); + return mMediaCacheOperations.getSiteVideos(siteModel.getId()); } @NonNull public List getSiteAudio(@NonNull SiteModel siteModel) { - return MediaSqlUtils.getSiteAudio(siteModel); + return mMediaCacheOperations.getSiteAudio(siteModel.getId()); } @NonNull public List getSiteDocuments(@NonNull SiteModel siteModel) { - return MediaSqlUtils.getSiteDocuments(siteModel); + return mMediaCacheOperations.getSiteDocuments(siteModel.getId()); } @NonNull public List searchSiteImages( @NonNull SiteModel siteModel, @NonNull String searchTerm) { - return MediaSqlUtils.searchSiteImages(siteModel, searchTerm); + return mMediaCacheOperations.searchSiteImages(siteModel.getId(), searchTerm); } @NonNull public List searchSiteVideos( @NonNull SiteModel siteModel, @NonNull String searchTerm) { - return MediaSqlUtils.searchSiteVideos(siteModel, searchTerm); + return mMediaCacheOperations.searchSiteVideos(siteModel.getId(), searchTerm); } @NonNull public List searchSiteAudio( @NonNull SiteModel siteModel, @NonNull String searchTerm) { - return MediaSqlUtils.searchSiteAudio(siteModel, searchTerm); + return mMediaCacheOperations.searchSiteAudio(siteModel.getId(), searchTerm); } @NonNull public List searchSiteDocuments( @NonNull SiteModel siteModel, @NonNull String searchTerm) { - return MediaSqlUtils.searchSiteDocuments(siteModel, searchTerm); + return mMediaCacheOperations.searchSiteDocuments(siteModel.getId(), searchTerm); } // @@ -560,10 +544,9 @@ void updateMedia(@Nullable MediaModel media, boolean emit) { if (media == null) { event.error = new MediaError(MediaErrorType.NULL_MEDIA_ARG); - } else if (MediaSqlUtils.insertOrUpdateMedia(media) > 0) { - event.mediaList.add(media); } else { - event.error = new MediaError(MediaErrorType.DB_QUERY_FAILURE); + mRemoteMediaCache.addOrUpdate(media.getLocalSiteId(), media); + event.mediaList.add(media); } if (emit) { @@ -601,8 +584,6 @@ private void performUploadMedia(@NonNull UploadMediaPayload payload) { if (argError.getType() != Type.NO_ERROR) { String message = "Media doesn't have required data: " + argError.getType().getErrorLogDescription(); AppLog.e(AppLog.T.MEDIA, message); - payload.media.setUploadState(MediaUploadState.FAILED); - MediaSqlUtils.insertOrUpdateMedia(payload.media); notifyMediaUploadError( MediaErrorType.MALFORMED_MEDIA_ARG, argError.getType().getErrorLogDescription(), @@ -612,9 +593,6 @@ private void performUploadMedia(@NonNull UploadMediaPayload payload) { return; } - payload.media.setUploadState(MediaUploadState.UPLOADING); - MediaSqlUtils.insertOrUpdateMedia(payload.media); - if (payload.stripLocation) { MediaUtils.stripLocation(payload.media.getFilePath()); } @@ -632,14 +610,8 @@ private void performUploadMedia(@NonNull UploadMediaPayload payload) { private void performFetchMediaList(@NonNull FetchMediaListPayload payload) { int offset = 0; if (payload.loadMore) { - List list = new ArrayList<>(); - list.add(MediaUploadState.UPLOADED.toString()); - if (payload.mimeType != null) { - offset = MediaSqlUtils.getMediaWithStatesAndMimeType(payload.site, list, payload.mimeType.getValue()) - .size(); - } else { - offset = MediaSqlUtils.getMediaWithStates(payload.site, list).size(); - } + String mimeTypeValue = payload.mimeType != null ? payload.mimeType.getValue() : null; + offset = mMediaCacheOperations.getUploadedMediaCount(payload.site.getId(), mimeTypeValue); } if (payload.site.getOrigin() == SiteModel.ORIGIN_WPCOM_REST) { mWPComV2MediaRestClient.fetchMediaList(payload.site, payload.number, offset, payload.mimeType); @@ -654,10 +626,7 @@ private void performFetchMediaList(@NonNull FetchMediaListPayload payload) { private void performCancelUpload(@NonNull CancelMediaPayload payload) { MediaModel media = payload.media; if (payload.delete) { - MediaSqlUtils.deleteMedia(media); - } else { - media.setUploadState(MediaUploadState.FAILED); - MediaSqlUtils.insertOrUpdateMedia(media); + mRemoteMediaCache.remove(payload.site.getId(), media.getMediaId()); } if (payload.site.getOrigin() == SiteModel.ORIGIN_WPCOM_REST) { @@ -671,7 +640,7 @@ private void performCancelUpload(@NonNull CancelMediaPayload payload) { } private void handleMediaUploaded(@NonNull ProgressPayload payload) { - if (payload.isError() || payload.canceled || payload.completed) { + if (payload.completed && !payload.isError() && !payload.canceled) { updateMedia(payload.media, false); } OnMediaUploaded onMediaUploaded = new OnMediaUploaded( @@ -697,39 +666,19 @@ private void handleMediaCanceled(@NonNull ProgressPayload payload) { } private void updateFetchedMediaList(@NonNull FetchMediaListResponsePayload payload) { - // if we loaded another page, simply add the fetched media and be done - if (payload.loadedMore) { - for (MediaModel media : payload.mediaList) { - updateMedia(media, false); - } - return; - } + List currentCache = mRemoteMediaCache.getMediaList(payload.site.getId()); - // build separate lists of existing and new media - List existingMediaList = new ArrayList<>(); - List newMediaList = new ArrayList<>(); - for (MediaModel fetchedMedia : payload.mediaList) { - MediaModel media = getSiteMediaWithId(payload.site, fetchedMedia.getMediaId()); - if (media != null) { - // retain the local ID, then update this media item - fetchedMedia.setId(media.getId()); - existingMediaList.add(fetchedMedia); - updateMedia(fetchedMedia, false); - } else { - newMediaList.add(fetchedMedia); + if (payload.loadedMore) { + // Append to existing cache + if (currentCache == null) { + currentCache = new ArrayList<>(); } - } - - // remove media that is NOT in the existing list - String mimeTypeValue = ""; - if (payload.mimeType != null) { - mimeTypeValue = payload.mimeType.getValue(); - } - MediaSqlUtils.deleteUploadedSiteMediaNotInList(payload.site, existingMediaList, mimeTypeValue); - - // add new media - for (MediaModel media : newMediaList) { - updateMedia(media, false); + List updatedList = new ArrayList<>(currentCache); + updatedList.addAll(payload.mediaList); + mRemoteMediaCache.cacheMediaList(payload.site.getId(), updatedList); + } else { + // Replace entire cache with fresh data + mRemoteMediaCache.cacheMediaList(payload.site.getId(), new ArrayList<>(payload.mediaList)); } } @@ -749,7 +698,7 @@ private void handleMediaListFetched(@NonNull FetchMediaListResponsePayload paylo private void handleMediaFetched(@NonNull MediaPayload payload) { OnMediaChanged onMediaChanged = new OnMediaChanged(MediaAction.FETCH_MEDIA, payload.error); if (payload.media != null) { - MediaSqlUtils.insertOrUpdateMedia(payload.media); + mRemoteMediaCache.addOrUpdate(payload.site.getId(), payload.media); onMediaChanged.mediaList = new ArrayList<>(); onMediaChanged.mediaList.add(payload.media); } diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/store/RemoteMediaCache.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/store/RemoteMediaCache.kt new file mode 100644 index 000000000000..8ed6f8742fe5 --- /dev/null +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/store/RemoteMediaCache.kt @@ -0,0 +1,38 @@ +package org.wordpress.android.fluxc.store + +import org.wordpress.android.fluxc.model.MediaModel +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class RemoteMediaCache @Inject constructor() { + private val cache = ConcurrentHashMap>() + + fun getMediaList(localSiteId: Int): List? { + return cache[localSiteId] + } + + fun cacheMediaList(localSiteId: Int, mediaList: List) { + cache[localSiteId] = mediaList + } + + fun addOrUpdate(localSiteId: Int, media: MediaModel) { + cache.compute(localSiteId) { _, currentList -> + val mutableList = (currentList ?: emptyList()).toMutableList() + val existingIndex = mutableList.indexOfFirst { it.mediaId == media.mediaId } + if (existingIndex != -1) { + mutableList[existingIndex] = media + } else { + mutableList.add(media) + } + mutableList + } + } + + fun remove(localSiteId: Int, mediaId: Long) { + cache.computeIfPresent(localSiteId) { _, currentList -> + currentList.filter { it.mediaId != mediaId } + } + } +} diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/store/TimestampMediaIdGenerator.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/store/TimestampMediaIdGenerator.kt new file mode 100644 index 000000000000..7345d8a431d6 --- /dev/null +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/store/TimestampMediaIdGenerator.kt @@ -0,0 +1,14 @@ +package org.wordpress.android.fluxc.store + +import org.wordpress.android.fluxc.model.LocalOrRemoteId.LocalId +import javax.inject.Inject +import kotlin.time.Clock +import kotlin.time.ExperimentalTime + +@OptIn(ExperimentalTime::class) +internal class TimestampMediaIdGenerator @Inject constructor(private val clock: Clock) : MediaIdGenerator { + override fun generate(filePath: String): LocalId { + val combined = "$filePath:${clock.now().toEpochMilliseconds()}" + return LocalId(combined.hashCode()) + } +} diff --git a/libs/fluxc/src/test/java/org/wordpress/android/fluxc/store/MediaLibraryCacheTest.kt b/libs/fluxc/src/test/java/org/wordpress/android/fluxc/store/MediaLibraryCacheTest.kt new file mode 100644 index 000000000000..1c91ef9070c6 --- /dev/null +++ b/libs/fluxc/src/test/java/org/wordpress/android/fluxc/store/MediaLibraryCacheTest.kt @@ -0,0 +1,162 @@ +package org.wordpress.android.fluxc.store + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.wordpress.android.fluxc.media.MediaTestUtils +import org.wordpress.android.fluxc.model.MediaModel +import java.util.concurrent.CountDownLatch + +class MediaLibraryCacheTest { + private lateinit var cache: RemoteMediaCache + private lateinit var testMedia1: MediaModel + private lateinit var testMedia2: MediaModel + + @Before + fun setup() { + cache = RemoteMediaCache() + testMedia1 = createTestMedia(1, "image1.jpg") + testMedia2 = createTestMedia(2, "image2.jpg") + } + + @Test + fun `when getting media list for uncached site, then it returns null`() { + val result = cache.getMediaList(1) + assertThat(result).isNull() + } + + @Test + fun `when caching media list, then it stores and retrieves correctly`() { + val mediaList = listOf(testMedia1, testMedia2) + + cache.cacheMediaList(1, mediaList) + val result = cache.getMediaList(1) + + assertThat(result).isEqualTo(mediaList) + } + + @Test + fun `when caching for different sites, then it stores different results`() { + val list1 = listOf(testMedia1) + val list2 = listOf(testMedia2) + + cache.cacheMediaList(1, list1) + cache.cacheMediaList(2, list2) + + assertThat(cache.getMediaList(1)).isEqualTo(list1) + assertThat(cache.getMediaList(2)).isEqualTo(list2) + } + + @Test + fun `when caching media list, then it overwrites previous value for same site`() { + val list1 = listOf(testMedia1) + val list2 = listOf(testMedia2) + + cache.cacheMediaList(1, list1) + cache.cacheMediaList(1, list2) + + val result = cache.getMediaList(1) + assertThat(result).isEqualTo(list2) + } + + @Test + fun `when caching empty list, then it stores correctly`() { + cache.cacheMediaList(1, emptyList()) + + val result = cache.getMediaList(1) + assertThat(result).isEmpty() + } + + @Test + fun `when caching large list, then it stores correctly`() { + val largeList = (1..100).map { createTestMedia(it, "image$it.jpg") } + + cache.cacheMediaList(1, largeList) + val result = cache.getMediaList(1) + + assertThat(result).isEqualTo(largeList) + } + + @Test + fun `when adding new media, then it appends to list`() { + val existingList = listOf(testMedia1) + cache.cacheMediaList(1, existingList) + + cache.addOrUpdate(1, testMedia2) + + val result = cache.getMediaList(1) + assertThat(result).containsExactly(testMedia1, testMedia2) + } + + @Test + fun `when updating existing media by media id, then it replaces media`() { + val siteId = 123 + val original = createTestMedia(1, "original.jpg") + val updated = createTestMedia(1, "updated.jpg") + cache.cacheMediaList(siteId, listOf(original)) + + cache.addOrUpdate(siteId, updated) + + val result = cache.getMediaList(siteId) + assertThat(result).containsExactly(updated) + } + + @Test + fun `when adding to empty cache, then it creates new list`() { + cache.addOrUpdate(1, testMedia1) + + val result = cache.getMediaList(1) + assertThat(result).containsExactly(testMedia1) + } + + @Test + fun `when removing media by remote id, then it filters out media`() { + val mediaToRemove = testMedia1 + cache.cacheMediaList(1, listOf(mediaToRemove, testMedia2)) + + cache.remove(1, mediaToRemove.mediaId) + + val result = cache.getMediaList(1) + assertThat(result).containsExactly(testMedia2) + } + + @Test + fun `when removing from empty cache, then it does nothing`() { + cache.remove(1, 2) + + val result = cache.getMediaList(1) + assertThat(result).isNull() + } + + @Test + fun `when multiple threads add media concurrently, then all updates are preserved`() { + val siteId = 1 + val threadCount = 10 + val latch = CountDownLatch(threadCount) + val startLatch = CountDownLatch(1) + + val mediaItems = (1..threadCount).map { createTestMedia(it, "image$it.jpg") } + + // Launch threads that all try to add media at the same time + mediaItems.map { media -> + Thread { + startLatch.await() + cache.addOrUpdate(siteId, media) + latch.countDown() + }.apply { start() } + } + + startLatch.countDown() + latch.await() + + assertThat(cache.getMediaList(siteId)) + .hasSize(threadCount) + .containsExactlyInAnyOrderElementsOf(mediaItems) + } + + private fun createTestMedia(id: Int, fileName: String) = + MediaTestUtils.createRemoteTestMedia() + .mediaId(id.toLong()) + .fileName(fileName) + .build() +} diff --git a/libs/fluxc-tests/src/test/java/org/wordpress/android/fluxc/media/MediaStoreTest.java b/libs/fluxc/src/test/java/org/wordpress/android/fluxc/store/MediaStoreTest.java similarity index 73% rename from libs/fluxc-tests/src/test/java/org/wordpress/android/fluxc/media/MediaStoreTest.java rename to libs/fluxc/src/test/java/org/wordpress/android/fluxc/store/MediaStoreTest.java index 02894352d72b..14ab8502523d 100644 --- a/libs/fluxc-tests/src/test/java/org/wordpress/android/fluxc/media/MediaStoreTest.java +++ b/libs/fluxc/src/test/java/org/wordpress/android/fluxc/store/MediaStoreTest.java @@ -1,53 +1,51 @@ -package org.wordpress.android.fluxc.media; +package org.wordpress.android.fluxc.store; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertTrue; import static org.wordpress.android.fluxc.media.MediaTestUtils.generateMediaFromPath; -import static org.wordpress.android.fluxc.media.MediaTestUtils.insertMediaIntoDatabase; +import static org.wordpress.android.fluxc.model.LocalOrRemoteId.LocalId; -import android.content.Context; - -import com.yarolegovich.wellsql.WellSql; - -import org.junit.Before; +import org.jetbrains.annotations.NotNull; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; import org.wordpress.android.fluxc.Dispatcher; -import org.wordpress.android.fluxc.SingleStoreWellSqlConfigForTests; +import org.wordpress.android.fluxc.logging.FakeCrashLogging; import org.wordpress.android.fluxc.model.MediaModel; import org.wordpress.android.fluxc.model.SiteModel; import org.wordpress.android.fluxc.network.rest.wpapi.media.ApplicationPasswordsMediaRestClient; import org.wordpress.android.fluxc.network.rest.wpcom.media.wpv2.WPComV2MediaRestClient; -import org.wordpress.android.fluxc.persistence.WellSqlConfig; -import org.wordpress.android.fluxc.store.MediaStore; import org.wordpress.android.fluxc.utils.MediaUtils; -import org.wordpress.android.fluxc.logging.FakeCrashLogging; import java.util.List; @RunWith(RobolectricTestRunner.class) public class MediaStoreTest { + private final RemoteMediaCache mRemoteMediaCache = new RemoteMediaCache(); + private final MediaCacheOperations mMediaCacheOperations = new MediaCacheOperations(mRemoteMediaCache); + + private static class FakeMediaIdGenerator implements MediaIdGenerator { + private int nextId = 1; + @Override + public @NotNull LocalId generate(@NotNull String filePath) { + return new LocalId(nextId++); + } + } + @SuppressWarnings("KotlinInternalInJava") private final MediaStore mMediaStore = new MediaStore(new Dispatcher(), Mockito.mock(WPComV2MediaRestClient.class), Mockito.mock(ApplicationPasswordsMediaRestClient.class), Mockito.mock(org.wordpress.android.fluxc.network.rest.wpapi.applicationpasswords .ApplicationPasswordsConfiguration.class), - FakeCrashLogging.INSTANCE + FakeCrashLogging.INSTANCE, + mRemoteMediaCache, + mMediaCacheOperations, + new FakeMediaIdGenerator() ); - @Before - public void setUp() { - Context context = RuntimeEnvironment.getApplication().getApplicationContext(); - WellSqlConfig config = new SingleStoreWellSqlConfigForTests(context, MediaModel.class); - WellSql.init(config); - config.reset(); - } - @Test public void testGetSiteImages() { final String testVideoPath = "/test/test_video.mp4"; @@ -61,8 +59,8 @@ public void testGetSiteImages() { assertTrue(MediaUtils.isVideoMimeType(videoMedia.getMimeType())); MediaModel imageMedia = generateMediaFromPath(testSiteId, testImageId, testImagePath); assertTrue(MediaUtils.isImageMimeType(imageMedia.getMimeType())); - insertMediaIntoDatabase(videoMedia); - insertMediaIntoDatabase(imageMedia); + mRemoteMediaCache.addOrUpdate(testSiteId, videoMedia); + mRemoteMediaCache.addOrUpdate(testSiteId, imageMedia); final List storeImages = mMediaStore.getSiteImages(getTestSiteWithLocalId(testSiteId)); assertNotNull(storeImages); @@ -83,24 +81,22 @@ public void testSearchSiteImages() { final long testAudioId = 540; // generate media of different types - MediaModel imageMedia = generateMediaFromPath(testSiteId, testImageId, testImagePath); - imageMedia.setTitle("Awesome Image"); - imageMedia.setDescription("This is an image test"); + MediaModel imageMedia = generateMediaFromPath(testSiteId, testImageId, testImagePath, + "Awesome Image", "This is an image test", null); assertTrue(MediaUtils.isImageMimeType(imageMedia.getMimeType())); - MediaModel videoMedia = generateMediaFromPath(testSiteId, testVideoId, testVideoPath); - videoMedia.setTitle("Video Title"); - videoMedia.setCaption("Test Caption"); + MediaModel videoMedia = generateMediaFromPath(testSiteId, testVideoId, testVideoPath, + "Video Title", null, "Test Caption"); assertTrue(MediaUtils.isVideoMimeType(videoMedia.getMimeType())); - MediaModel audioMedia = generateMediaFromPath(testSiteId, testAudioId, testAudioPath); - audioMedia.setDescription("This is an audio test"); + MediaModel audioMedia = generateMediaFromPath(testSiteId, testAudioId, testAudioPath, + null, "This is an audio test", null); assertTrue(MediaUtils.isAudioMimeType(audioMedia.getMimeType())); // insert media of different types - insertMediaIntoDatabase(videoMedia); - insertMediaIntoDatabase(imageMedia); - insertMediaIntoDatabase(audioMedia); + mRemoteMediaCache.addOrUpdate(testSiteId, videoMedia); + mRemoteMediaCache.addOrUpdate(testSiteId, imageMedia); + mRemoteMediaCache.addOrUpdate(testSiteId, audioMedia); // verify the correct media is returned final List storeImages = mMediaStore @@ -125,22 +121,22 @@ public void testSearchSiteVideos() { final long testDocumentId = 125; // generate media of different types - MediaModel videoMedia1 = generateMediaFromPath(testSiteId, testVideoId1, testVideoPath1); - videoMedia1.setTitle("My trip title"); + MediaModel videoMedia1 = generateMediaFromPath(testSiteId, testVideoId1, testVideoPath1, + "My trip title", null, null); assertTrue(MediaUtils.isVideoMimeType(videoMedia1.getMimeType())); - MediaModel videoMedia2 = generateMediaFromPath(testSiteId, testVideoId2, testVideoPath2); - videoMedia2.setTitle("Test video title"); + MediaModel videoMedia2 = generateMediaFromPath(testSiteId, testVideoId2, testVideoPath2, + "Test video title", null, null); assertTrue(MediaUtils.isVideoMimeType(videoMedia2.getMimeType())); - MediaModel documentMedia = generateMediaFromPath(testSiteId, testDocumentId, testDocumentPath); - documentMedia.setTitle("My first test"); + MediaModel documentMedia = generateMediaFromPath(testSiteId, testDocumentId, testDocumentPath, + "My first test", null, null); assertTrue(MediaUtils.isApplicationMimeType(documentMedia.getMimeType())); // insert media of different types - insertMediaIntoDatabase(videoMedia1); - insertMediaIntoDatabase(videoMedia2); - insertMediaIntoDatabase(documentMedia); + mRemoteMediaCache.addOrUpdate(testSiteId, videoMedia1); + mRemoteMediaCache.addOrUpdate(testSiteId, videoMedia2); + mRemoteMediaCache.addOrUpdate(testSiteId, documentMedia); // verify the correct media is returned final List storeVideos = mMediaStore @@ -166,29 +162,27 @@ public void testSearchSiteAudio() { final long testDocumentId = 43; // generate media of different types - MediaModel imageMedia = generateMediaFromPath(testSiteId, testImageId, testImagePath); - imageMedia.setTitle("Title test"); + MediaModel imageMedia = generateMediaFromPath(testSiteId, testImageId, testImagePath, + "Title test", null, null); assertTrue(MediaUtils.isImageMimeType(imageMedia.getMimeType())); - MediaModel audioMedia1 = generateMediaFromPath(testSiteId, testAudioId1, testAudioPath1); - audioMedia1.setTitle("The big one"); - audioMedia1.setDescription("Test for the World"); + MediaModel audioMedia1 = generateMediaFromPath(testSiteId, testAudioId1, testAudioPath1, + "The big one", "Test for the World", null); assertTrue(MediaUtils.isAudioMimeType(audioMedia1.getMimeType())); - MediaModel audioMedia2 = generateMediaFromPath(testSiteId, testAudioId2, testAudioPath2); - audioMedia2.setTitle("The test!"); - audioMedia2.setDescription("Without description"); + MediaModel audioMedia2 = generateMediaFromPath(testSiteId, testAudioId2, testAudioPath2, + "The test!", "Without description", null); assertTrue(MediaUtils.isAudioMimeType(audioMedia2.getMimeType())); - MediaModel documentMedia = generateMediaFromPath(testSiteId, testDocumentId, testDocumentPath); - documentMedia.setTitle("Document with every test of the app"); + MediaModel documentMedia = generateMediaFromPath(testSiteId, testDocumentId, testDocumentPath, + "Document with every test of the app", null, null); assertTrue(MediaUtils.isApplicationMimeType(documentMedia.getMimeType())); // insert media of different types - insertMediaIntoDatabase(imageMedia); - insertMediaIntoDatabase(audioMedia1); - insertMediaIntoDatabase(audioMedia2); - insertMediaIntoDatabase(documentMedia); + mRemoteMediaCache.addOrUpdate(testSiteId, imageMedia); + mRemoteMediaCache.addOrUpdate(testSiteId, audioMedia1); + mRemoteMediaCache.addOrUpdate(testSiteId, audioMedia2); + mRemoteMediaCache.addOrUpdate(testSiteId, documentMedia); // verify the correct media is returned (just audio) final List storeAudio = mMediaStore @@ -221,38 +215,32 @@ public void testSearchSiteDocuments() { final long testDocumentId4 = 543; // generate media of different types - MediaModel audioMedia = generateMediaFromPath(testSiteId, testAudioId, testAudioPath); - audioMedia.setTitle("My first test"); - audioMedia.setDescription("This is a description test"); - audioMedia.setCaption("Caption test"); + MediaModel audioMedia = generateMediaFromPath(testSiteId, testAudioId, testAudioPath, + "My first test", "This is a description test", "Caption test"); assertTrue(MediaUtils.isAudioMimeType(audioMedia.getMimeType())); - MediaModel documentMedia1 = generateMediaFromPath(testSiteId, testDocumentId1, testDocumentPath1); - documentMedia1.setTitle("The Document"); - documentMedia1.setDescription("short description"); + MediaModel documentMedia1 = generateMediaFromPath(testSiteId, testDocumentId1, testDocumentPath1, + "The Document", "short description", null); assertTrue(MediaUtils.isApplicationMimeType(documentMedia1.getMimeType())); - MediaModel documentMedia2 = generateMediaFromPath(testSiteId, testDocumentId2, testDocumentPath2); - documentMedia2.setTitle("Document to Test"); - documentMedia2.setDescription("medium description"); + MediaModel documentMedia2 = generateMediaFromPath(testSiteId, testDocumentId2, testDocumentPath2, + "Document to Test", "medium description", null); assertTrue(MediaUtils.isApplicationMimeType(documentMedia2.getMimeType())); - MediaModel documentMedia3 = generateMediaFromPath(testSiteId, testDocumentId3, testDocumentPath3); - documentMedia3.setTitle("Document"); - documentMedia3.setDescription("Large description with a test"); + MediaModel documentMedia3 = generateMediaFromPath(testSiteId, testDocumentId3, testDocumentPath3, + "Document", "Large description with a test", null); assertTrue(MediaUtils.isApplicationMimeType(documentMedia3.getMimeType())); - MediaModel documentMedia4 = generateMediaFromPath(testSiteId, testDocumentId4, testDocumentPath4); - documentMedia4.setTitle("Document Title"); - documentMedia4.setDescription("description"); + MediaModel documentMedia4 = generateMediaFromPath(testSiteId, testDocumentId4, testDocumentPath4, + "Document Title", "description", null); assertTrue(MediaUtils.isApplicationMimeType(documentMedia4.getMimeType())); // insert media of different types - insertMediaIntoDatabase(audioMedia); - insertMediaIntoDatabase(documentMedia1); - insertMediaIntoDatabase(documentMedia2); - insertMediaIntoDatabase(documentMedia3); - insertMediaIntoDatabase(documentMedia4); + mRemoteMediaCache.addOrUpdate(testSiteId, audioMedia); + mRemoteMediaCache.addOrUpdate(testSiteId, documentMedia1); + mRemoteMediaCache.addOrUpdate(testSiteId, documentMedia2); + mRemoteMediaCache.addOrUpdate(testSiteId, documentMedia3); + mRemoteMediaCache.addOrUpdate(testSiteId, documentMedia4); // verify the correct media is returned (just documents) final List storeDocuments = mMediaStore diff --git a/libs/fluxc/src/test/java/org/wordpress/android/fluxc/store/TimestampMediaIdGeneratorTest.kt b/libs/fluxc/src/test/java/org/wordpress/android/fluxc/store/TimestampMediaIdGeneratorTest.kt new file mode 100644 index 000000000000..ca269e65b76f --- /dev/null +++ b/libs/fluxc/src/test/java/org/wordpress/android/fluxc/store/TimestampMediaIdGeneratorTest.kt @@ -0,0 +1,52 @@ +package org.wordpress.android.fluxc.store + +import org.assertj.core.api.Assertions +import org.junit.Before +import org.junit.Test +import kotlin.time.Clock +import kotlin.time.ExperimentalTime +import kotlin.time.Instant + +@OptIn(ExperimentalTime::class) +internal class TimestampMediaIdGeneratorTest { + + lateinit var sut: TimestampMediaIdGenerator + + var currentMillis = 123L + private val fakeClock = object : Clock { + override fun now(): Instant { + return Instant.fromEpochMilliseconds(currentMillis) + } + } + + @Before + fun setUp() { + sut = TimestampMediaIdGenerator(fakeClock) + } + + @Test + fun `when generating with same inputs, then it returns consistent ID`() { + val id1 = sut.generate("/path/to/file.jpg") + val id2 = sut.generate("/path/to/file.jpg") + + Assertions.assertThat(id1).isEqualTo(id2) + } + + @Test + fun `when generating with different file paths, then it returns different IDs`() { + val id1 = sut.generate("/path/to/file1.jpg") + val id2 = sut.generate("/path/to/file2.jpg") + + Assertions.assertThat(id1).isNotEqualTo(id2) + } + + @Test + fun `when generating with different timestamps, then it returns different IDs`() { + currentMillis = 1000L + val id1 = sut.generate("/path/to/file.jpg") + currentMillis = 2000L + val id2 = sut.generate("/path/to/file.jpg") + + Assertions.assertThat(id1).isNotEqualTo(id2) + } +} diff --git a/libs/fluxc/src/testFixtures/kotlin/org/wordpress/android/fluxc/media/MediaTestUtils.kt b/libs/fluxc/src/testFixtures/kotlin/org/wordpress/android/fluxc/media/MediaTestUtils.kt new file mode 100644 index 000000000000..a4162ccc89d7 --- /dev/null +++ b/libs/fluxc/src/testFixtures/kotlin/org/wordpress/android/fluxc/media/MediaTestUtils.kt @@ -0,0 +1,135 @@ +package org.wordpress.android.fluxc.media + +import org.wordpress.android.fluxc.model.MediaModel +import org.wordpress.android.fluxc.utils.MimeTypes +import java.util.concurrent.atomic.AtomicInteger + +object MediaTestUtils { + private val nextId = AtomicInteger(1) + private val mimeTypes = MimeTypes() + + @JvmStatic + fun createLocalTestMedia(): LocalTestMediaBuilder { + return LocalTestMediaBuilder() + } + + @JvmStatic + fun createRemoteTestMedia(): RemoteTestMediaBuilder { + return RemoteTestMediaBuilder() + } + + class LocalTestMediaBuilder internal constructor() { + private var localSiteId: Int = 1 + private var mediaId: Long = 0L + private var postId: Long = 0L + private var uploadDate: String? = "2024-01-01T00:00:00+00:00" + private var fileName: String? = "test-image.jpg" + private var filePath: String? = "/test/test-image.jpg" + private var mimeType: String? = "image/jpeg" + private var title: String? = "Test Image" + + fun localSiteId(value: Int) = apply { this.localSiteId = value } + fun mediaId(value: Long) = apply { this.mediaId = value } + fun postId(value: Long) = apply { this.postId = value } + fun uploadDate(value: String?) = apply { this.uploadDate = value } + fun fileName(value: String?) = apply { this.fileName = value } + fun filePath(value: String?) = apply { this.filePath = value } + fun mimeType(value: String?) = apply { this.mimeType = value } + fun title(value: String?) = apply { this.title = value } + + fun build(): MediaModel { + val media = MediaModel( + this.localSiteId, + this.uploadDate, + this.fileName, + this.filePath, + this.mimeType, + this.title + ).apply { + setMediaId(this@LocalTestMediaBuilder.mediaId) + setPostId(this@LocalTestMediaBuilder.postId) + } + media.id = nextId.getAndIncrement() + return media + } + } + + class RemoteTestMediaBuilder internal constructor() { + private var localSiteId: Int = 1 + private var mediaId: Long = 1L + private var postId: Long = 0L + private var uploadDate: String? = "2024-01-01T00:00:00+00:00" + private var url: String = "https://example.com/test-image.jpg" + private var fileName: String? = "test-image.jpg" + private var mimeType: String? = "image/jpeg" + private var title: String? = "Test Image" + private var caption: String = "" + private var description: String = "" + private var alt: String = "" + + fun localSiteId(value: Int) = apply { this.localSiteId = value } + fun mediaId(value: Long) = apply { this.mediaId = value } + fun postId(value: Long) = apply { this.postId = value } + fun uploadDate(value: String?) = apply { this.uploadDate = value } + fun url(value: String) = apply { this.url = value } + fun fileName(value: String?) = apply { this.fileName = value } + fun mimeType(value: String?) = apply { this.mimeType = value } + fun title(value: String?) = apply { this.title = value } + fun caption(value: String) = apply { this.caption = value } + fun description(value: String) = apply { this.description = value } + fun alt(value: String) = apply { this.alt = value } + + fun build(): MediaModel { + val media = MediaModel( + this.localSiteId, + this.mediaId, + this.postId, + this.uploadDate, + this.url, + this.fileName, + this.mimeType, + this.title, + this.caption, + this.description, + ) + media.id = nextId.getAndIncrement() + return media + } + } + + @JvmStatic + @JvmOverloads + fun generateMediaFromPath( + localSiteId: Int, + mediaId: Long, + filePath: String, + title: String? = null, + description: String? = null, + caption: String? = null + ): MediaModel { + val fileName = filePath.substringAfterLast('/') + val extension = fileName.substringAfterLast('.', "") + val mimeType = mimeTypes.getMimeTypeForExtension(extension) + + return if (description.isNullOrEmpty() && caption.isNullOrEmpty()) { + createLocalTestMedia() + .localSiteId(localSiteId) + .mediaId(mediaId) + .fileName(fileName) + .filePath(filePath) + .mimeType(mimeType) + .title(title ?: fileName) + .build() + } else { + createRemoteTestMedia() + .localSiteId(localSiteId) + .mediaId(mediaId) + .fileName(fileName) + .mimeType(mimeType) + .title(title ?: fileName) + .description(description ?: "") + .caption(caption ?: "") + .build() + } + } +}