From ff6d1b524147403ae41a5c54ffbf1c3e13ebde50 Mon Sep 17 00:00:00 2001 From: Ishita Gambhir Date: Thu, 9 Jan 2025 22:56:49 +0530 Subject: [PATCH] Add image caching to SmallImageCard (#319) * cache content card images * integrate caching in smallImageCard * add unit tests * spotless apply * address PR comments * fix failing tests * run spotless apply * add additional UTs * spotless apply * convert contentCardImageManager to object --- .../aepcomposeui/components/SmallImageCard.kt | 7 +- .../messaging/ContentCardImageManager.kt | 156 ++++++++++++++ .../mobile/messaging/MessagingConstants.java | 2 + .../components/SmallImageCardTests.kt | 41 ++++ .../messaging/ContentCardImageManagerTests.kt | 198 ++++++++++++++++++ .../imagecaching/MockCacheService.kt | 66 ++++++ .../messaging/MessagingTestConstants.java | 1 + 7 files changed, 468 insertions(+), 3 deletions(-) create mode 100644 code/messaging/src/main/java/com/adobe/marketing/mobile/messaging/ContentCardImageManager.kt create mode 100644 code/messaging/src/test/java/com/adobe/marketing/mobile/messaging/ContentCardImageManagerTests.kt create mode 100644 code/messaging/src/test/java/com/adobe/marketing/mobile/messaging/imagecaching/MockCacheService.kt diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/components/SmallImageCard.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/components/SmallImageCard.kt index 8bc864ce..26648ac3 100644 --- a/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/components/SmallImageCard.kt +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/aepcomposeui/components/SmallImageCard.kt @@ -33,7 +33,7 @@ import com.adobe.marketing.mobile.aepcomposeui.UIAction import com.adobe.marketing.mobile.aepcomposeui.UIEvent import com.adobe.marketing.mobile.aepcomposeui.observers.AepUIEventObserver import com.adobe.marketing.mobile.aepcomposeui.style.SmallImageUIStyle -import com.adobe.marketing.mobile.aepcomposeui.utils.UIUtils +import com.adobe.marketing.mobile.messaging.ContentCardImageManager /** * Composable function that renders a small image card UI. @@ -58,13 +58,14 @@ fun SmallImageCard( if (imageUrl.isNullOrBlank()) { isLoading = false } else { - UIUtils.downloadImage(imageUrl) { + ContentCardImageManager.getContentCardImageBitmap(imageUrl) { it.onSuccess { bitmap -> imageBitmap = bitmap isLoading = false } it.onFailure { - // TODO once we have a default image, we can use that here + // todo - confirm default image bitmap to be used here + // imageBitmap = contentCardManager.getDefaultImageBitmap() isLoading = false } } diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/messaging/ContentCardImageManager.kt b/code/messaging/src/main/java/com/adobe/marketing/mobile/messaging/ContentCardImageManager.kt new file mode 100644 index 00000000..bb742d21 --- /dev/null +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/messaging/ContentCardImageManager.kt @@ -0,0 +1,156 @@ +/* + Copyright 2024 Adobe. All rights reserved. + This file is licensed to you under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. You may obtain a copy + of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under + the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + OF ANY KIND, either express or implied. See the License for the specific language + governing permissions and limitations under the License. +*/ + +package com.adobe.marketing.mobile.messaging + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import com.adobe.marketing.mobile.aepcomposeui.utils.UIUtils +import com.adobe.marketing.mobile.messaging.MessagingConstants.CACHE_EXPIRY_TIME +import com.adobe.marketing.mobile.messaging.MessagingConstants.CONTENT_CARD_CACHE_SUBDIRECTORY +import com.adobe.marketing.mobile.services.Log +import com.adobe.marketing.mobile.services.ServiceProvider +import com.adobe.marketing.mobile.services.caching.CacheEntry +import com.adobe.marketing.mobile.services.caching.CacheExpiry +import com.adobe.marketing.mobile.services.caching.CacheResult +import com.adobe.marketing.mobile.services.caching.CacheService +import java.io.InputStream +import java.nio.ByteBuffer + +object ContentCardImageManager { + private val SELF_TAG: String = "ContentCardManager" + private val cacheService: CacheService? = ServiceProvider.getInstance().cacheService + private val defaultCacheName: String = CONTENT_CARD_CACHE_SUBDIRECTORY + + /** + * Fetches the image from cache if present in cache, else downloads the image from the given URL and caches it for future calls. + * + * @param imageUrl the url of the image to be fetched + * @param cacheName(optional) the name of the cache for fetching or caching the image, default value used if cache name is not provided + * @param completion is a completion callback. Result.success() method is invoked with the image bitmap fetched. In case of any failure, Result.failure() method is invoked with a throwable + * */ + fun getContentCardImageBitmap(imageUrl: String, cacheName: String? = defaultCacheName, completion: (Result) -> Unit) { + val resolvedCacheName: String = cacheName ?: defaultCacheName + if (isImageCached(imageUrl, resolvedCacheName)) { + getImageBitmapFromCache(imageUrl, resolvedCacheName, completion) + } else { + downloadAndCacheImageBitmap(imageUrl, resolvedCacheName, completion) + } + } + + /** + * Checks whether the image at given url is present in the cache or not. + * + * @param imageUrl the url of the image + * @param cacheName the name of the cache for fetching or caching the image + * @return `True` if the image is found in cache, `False` otherwise + * */ + private fun isImageCached(imageUrl: String, cacheName: String): Boolean { + val cacheValue = cacheService?.get(cacheName, imageUrl) + return cacheValue != null + } + + /** + * Fetches the image from the cache. + * + * @param imageUrl the url of the image to be fetched + * @param cacheName the name of the cache for fetching the image + * @param completion is a completion callback. Result.success() method is invoked with the image bitmap fetched. In case of any failure, Result.failure() method is invoked with a throwable + * */ + private fun getImageBitmapFromCache(imageUrl: String, cacheName: String, completion: (Result) -> Unit) { + val cachedImageBitmap: CacheResult? = cacheService?.get(cacheName, imageUrl) + val inputStream = cachedImageBitmap?.data + + // Convert the InputStream to a Bitmap + if (inputStream != null) { + try { + completion(Result.success(BitmapFactory.decodeStream(inputStream))) + } catch (e: Exception) { + Log.warning( + MessagingConstants.LOG_TAG, + SELF_TAG, + "getImageBitmapFromCache - Unable to read cached data into a bitmap due to error: $e" + ) + completion(Result.failure(e)) + } + } else { + Log.warning( + MessagingConstants.LOG_TAG, + SELF_TAG, + "getImageBitmapFromCache - Unable to read cached data as the inputStream is null" + ) + completion(Result.failure(Exception("Unable to read cached bitmap data as the inputStream is null for the url: $imageUrl, cacheName: $cacheName"))) + } + } + + /** + * Downloads the image from the given url and caches it. + * + * @param imageUrl the url of the image to be downloaded + * @param completion is a completion callback. Result.success() method is invoked with the image bitmap downloaded. In case of any failure, Result.failure() method is invoked with a throwable + * */ + private fun downloadAndCacheImageBitmap(imageUrl: String, cacheName: String, completion: (Result) -> Unit) { + UIUtils.downloadImage(imageUrl) { + it.onSuccess { bitmap -> + val isImageCacheSuccessful = cacheImage(bitmap, imageUrl, cacheName) + if (!isImageCacheSuccessful) { + Log.warning( + MessagingConstants.LOG_TAG, + SELF_TAG, + "downloadAndCacheImageBitmap - Image downloaded but failed to cache the image from url: $imageUrl" + ) + } + completion(Result.success(bitmap)) + } + it.onFailure { failure -> + Log.warning( + MessagingConstants.LOG_TAG, + SELF_TAG, + "downloadAndCacheImageBitmap - Unable to download image from url: $imageUrl" + ) + completion(Result.failure(failure)) + } + } + } + + /** + * Caches the given image. + * + * @param imageBitmap image to be cached + * @param imageName the unique `key` for storing the image in cache + * @param cacheName name of the cache where cache entry is to be created + * + * @return `True` if image is caches successfully, `False` otherwise + * */ + private fun cacheImage(imageBitmap: Bitmap, imageName: String, cacheName: String): Boolean { + try { + val imageInputStream: InputStream = imageBitmap.let { bitmap -> + val byteArray = ByteArray(bitmap.byteCount) + val buffer = ByteBuffer.wrap(byteArray) + bitmap.copyPixelsToBuffer(buffer) + buffer.rewind() // Reset the buffer position to the beginning + byteArray.inputStream() // Create InputStream from byte array + } + + val cacheEntry = CacheEntry(imageInputStream, CacheExpiry.after(CACHE_EXPIRY_TIME), null) + cacheService?.set(cacheName, imageName, cacheEntry) + + return true + } catch (e: Exception) { + Log.warning( + MessagingConstants.LOG_TAG, + SELF_TAG, + "cacheImage - An unexpected error occurred while caching the downloaded image: \n ${e.localizedMessage}" + ) + return false + } + } +} diff --git a/code/messaging/src/main/java/com/adobe/marketing/mobile/messaging/MessagingConstants.java b/code/messaging/src/main/java/com/adobe/marketing/mobile/messaging/MessagingConstants.java index ca473283..6bd91194 100644 --- a/code/messaging/src/main/java/com/adobe/marketing/mobile/messaging/MessagingConstants.java +++ b/code/messaging/src/main/java/com/adobe/marketing/mobile/messaging/MessagingConstants.java @@ -21,12 +21,14 @@ public final class MessagingConstants { static final String CACHE_BASE_DIR = "messaging"; static final String PROPOSITIONS_CACHE_SUBDIRECTORY = "propositions"; static final String IMAGES_CACHE_SUBDIRECTORY = "images"; + static final String CONTENT_CARD_CACHE_SUBDIRECTORY = "contentCardImages"; static final String HTTP_HEADER_IF_MODIFIED_SINCE = "If-Modified-Since"; static final String HTTP_HEADER_LAST_MODIFIED = "Last-Modified"; static final String HTTP_HEADER_IF_NONE_MATCH = "If-None-Match"; static final String HTTP_HEADER_ETAG = "Etag"; static final int DEFAULT_TIMEOUT = 5; static final long RESPONSE_CALLBACK_TIMEOUT = 10000; // 10 seconds + static final long CACHE_EXPIRY_TIME = 604800000; // 7 days in milliseconds private MessagingConstants() {} diff --git a/code/messaging/src/test/java/com/adobe/marketing/mobile/aepcomposeui/components/SmallImageCardTests.kt b/code/messaging/src/test/java/com/adobe/marketing/mobile/aepcomposeui/components/SmallImageCardTests.kt index f6bf14cf..80578fce 100644 --- a/code/messaging/src/test/java/com/adobe/marketing/mobile/aepcomposeui/components/SmallImageCardTests.kt +++ b/code/messaging/src/test/java/com/adobe/marketing/mobile/aepcomposeui/components/SmallImageCardTests.kt @@ -65,6 +65,7 @@ import com.adobe.marketing.mobile.messaging.R import com.adobe.marketing.mobile.services.NetworkCallback import com.adobe.marketing.mobile.services.Networking import com.adobe.marketing.mobile.services.ServiceProvider +import com.adobe.marketing.mobile.services.caching.CacheService import com.example.compose.TestTheme import com.github.takahirom.roborazzi.captureRoboImage import org.junit.After @@ -74,6 +75,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.MockedStatic +import org.mockito.Mockito import org.mockito.Mockito.any import org.mockito.Mockito.mockStatic import org.mockito.Mockito.times @@ -81,6 +83,7 @@ import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.whenever import org.robolectric.ParameterizedRobolectricTestRunner import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment @@ -433,11 +436,49 @@ class SmallImageCardBehaviorTests { @Mock private lateinit var mockAepUIEventObserver: AepUIEventObserver + @Mock + private lateinit var mockCacheService: CacheService + @Mock + private lateinit var mockServiceProvider: ServiceProvider + private lateinit var mockedStaticServiceProvider: MockedStatic + + @Mock + private lateinit var mockNetworkService: Networking + @Before fun setUp() { + + MockitoAnnotations.openMocks(this) + mockedStaticServiceProvider = mockStatic(ServiceProvider::class.java) + mockedStaticServiceProvider.`when` { ServiceProvider.getInstance() }.thenReturn(mockServiceProvider) + + whenever( + mockCacheService.set( + org.mockito.kotlin.any(), + org.mockito.kotlin.any(), + org.mockito.kotlin.any() + ) + ).thenReturn(true) + + // Mocking Cache to bypass cache check + whenever( + mockCacheService.get( + org.mockito.kotlin.any(), + org.mockito.kotlin.any() + ) + ).thenReturn(null) + + `when`(mockServiceProvider.networkService).thenReturn(mockNetworkService) + MockitoAnnotations.openMocks(this) } + @After + fun tearDown() { + mockedStaticServiceProvider.close() + Mockito.validateMockitoUsage() + } + @Test fun `Test SmallImageCard card click behavior`() { // setup diff --git a/code/messaging/src/test/java/com/adobe/marketing/mobile/messaging/ContentCardImageManagerTests.kt b/code/messaging/src/test/java/com/adobe/marketing/mobile/messaging/ContentCardImageManagerTests.kt new file mode 100644 index 00000000..daa3d6f5 --- /dev/null +++ b/code/messaging/src/test/java/com/adobe/marketing/mobile/messaging/ContentCardImageManagerTests.kt @@ -0,0 +1,198 @@ +/* + Copyright 2025 Adobe. All rights reserved. + This file is licensed to you under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. You may obtain a copy + of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under + the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + OF ANY KIND, either express or implied. See the License for the specific language + governing permissions and limitations under the License. +*/ + +package com.adobe.marketing.mobile.messaging + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import com.adobe.marketing.mobile.messaging.MessagingTestConstants.CONTENT_CARD_TEST_CACHE_SUBDIRECTORY +import com.adobe.marketing.mobile.messaging.imagecaching.MockCacheService +import com.adobe.marketing.mobile.services.HttpConnecting +import com.adobe.marketing.mobile.services.NetworkCallback +import com.adobe.marketing.mobile.services.Networking +import com.adobe.marketing.mobile.services.ServiceProvider +import com.adobe.marketing.mobile.services.caching.CacheEntry +import com.adobe.marketing.mobile.services.caching.CacheExpiry +import org.junit.After +import org.junit.Before +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockedStatic +import org.mockito.Mockito +import org.mockito.Mockito.mock +import org.mockito.Mockito.mockStatic +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.net.HttpURLConnection +import kotlin.test.Test +import kotlin.test.assertNotNull +import kotlin.test.fail + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [33]) +class ContentCardImageManagerTests { + + private var mockCacheService = MockCacheService() + + @Mock + private lateinit var mockServiceProvider: ServiceProvider + private lateinit var mockedStaticServiceProvider: MockedStatic + + @Mock + private lateinit var mockNetworkService: Networking + + private lateinit var testCachePath: String + private val imageUrl = "https://fastly.picsum.photos/id/43/400/300.jpg?hmac=fAPJ5p1wbFahFpnqtg004Nny-vTEADhmMxMkwLUSfw0" + + @Before + fun setup() { + MockitoAnnotations.openMocks(this) + mockedStaticServiceProvider = mockStatic(ServiceProvider::class.java) + mockedStaticServiceProvider.`when` { ServiceProvider.getInstance() }.thenReturn(mockServiceProvider) + `when`(mockServiceProvider.networkService).thenReturn(mockNetworkService) + `when`(mockServiceProvider.cacheService).thenReturn(mockCacheService) + testCachePath = CONTENT_CARD_TEST_CACHE_SUBDIRECTORY + } + + @After + fun tearDown() { + mockedStaticServiceProvider.close() + Mockito.validateMockitoUsage() + } + + @Test + fun `Get image for the first time when it is not in cache, download and cache is successful`() { + + // setup for bitmap download simulation + val mockBitmap: Bitmap = mock(Bitmap::class.java) + `when`(mockBitmap.width).thenReturn(100) + `when`(mockBitmap.height).thenReturn(100) + + val mockedStaticBitmapFactory = mockStatic(BitmapFactory::class.java) + mockedStaticBitmapFactory.`when` { BitmapFactory.decodeStream(Mockito.any()) } + .thenReturn(mockBitmap) + + val simulatedResponse = simulateNetworkResponse(HttpURLConnection.HTTP_OK, bitmapToInputStream(mockBitmap), emptyMap()) + `when`(mockNetworkService.connectAsync(Mockito.any(), Mockito.any())).thenAnswer { + val callback = it.getArgument(1) + callback.call(simulatedResponse) + } + + ContentCardImageManager.getContentCardImageBitmap( + imageUrl, testCachePath, + { + it.onSuccess { bitmap -> + assertNotNull(bitmap) + } + it.onFailure { + fail("Test failed as unable to fetch image from cache") + } + } + ) + + mockedStaticBitmapFactory.close() + } + + @Test + fun `Get image for the first time when it is not in cache, download fails`() { + + // setup for bitmap download simulation + val mockBitmap: Bitmap = mock(Bitmap::class.java) + `when`(mockBitmap.width).thenReturn(100) + `when`(mockBitmap.height).thenReturn(100) + + val mockedStaticBitmapFactory = mockStatic(BitmapFactory::class.java) + mockedStaticBitmapFactory.`when` { BitmapFactory.decodeStream(Mockito.any()) } + .thenReturn(mockBitmap) + + val simulatedResponse = simulateNetworkResponse(HttpURLConnection.HTTP_OK, bitmapToInputStream(mockBitmap), emptyMap()) + `when`(mockNetworkService.connectAsync(Mockito.any(), Mockito.any())).thenAnswer { + val callback = it.getArgument(1) + callback.call(simulatedResponse) + } + + ContentCardImageManager.getContentCardImageBitmap( + "invalidUrl", testCachePath, + { + it.onSuccess { bitmap -> + fail("Test failed as download should have failed for invalid url") + } + it.onFailure { failure -> + assertNotNull(failure) + } + } + ) + + mockedStaticBitmapFactory.close() + } + + @Test + fun `Get image from cache`() { + + // setup for bitmap decoding simulation + val mockBitmap: Bitmap = mock(Bitmap::class.java) + `when`(mockBitmap.width).thenReturn(100) + `when`(mockBitmap.height).thenReturn(100) + + val mockedStaticBitmapFactory = mockStatic(BitmapFactory::class.java) + mockedStaticBitmapFactory.`when` { BitmapFactory.decodeStream(Mockito.any()) } + .thenReturn(mockBitmap) + + mockCacheService.set( + name = testCachePath, + key = imageUrl, + value = CacheEntry( + bitmapToInputStream(mockBitmap), CacheExpiry.never(), emptyMap() + ) + ) + + ContentCardImageManager.getContentCardImageBitmap( + imageUrl, testCachePath, + { + it.onSuccess { bitmap -> + assertNotNull(bitmap) + } + it.onFailure { + fail("Test failed as unable to fetch image from cache") + } + } + ) + + mockedStaticBitmapFactory.close() + } + + private fun bitmapToInputStream(bitmap: Bitmap): ByteArrayInputStream { + val outputStream = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream) + val byteArray = outputStream.toByteArray() + return ByteArrayInputStream(byteArray) + } + + private fun simulateNetworkResponse( + responseCode: Int, + responseStream: InputStream?, + metadata: Map + ): HttpConnecting { + val mockResponse = mock(HttpConnecting::class.java) + `when`(mockResponse.responseCode).thenReturn(responseCode) + `when`(mockResponse.inputStream).thenReturn(responseStream) + `when`(mockResponse.getResponsePropertyValue(org.mockito.kotlin.any())).then { + return@then metadata[it.getArgument(0)] + } + return mockResponse + } +} diff --git a/code/messaging/src/test/java/com/adobe/marketing/mobile/messaging/imagecaching/MockCacheService.kt b/code/messaging/src/test/java/com/adobe/marketing/mobile/messaging/imagecaching/MockCacheService.kt new file mode 100644 index 00000000..dbc2b4ee --- /dev/null +++ b/code/messaging/src/test/java/com/adobe/marketing/mobile/messaging/imagecaching/MockCacheService.kt @@ -0,0 +1,66 @@ +/* + Copyright 2025 Adobe. All rights reserved. + This file is licensed to you under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. You may obtain a copy + of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under + the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + OF ANY KIND, either express or implied. See the License for the specific language + governing permissions and limitations under the License. +*/ + +package com.adobe.marketing.mobile.messaging.imagecaching + +import android.graphics.Bitmap +import com.adobe.marketing.mobile.services.caching.CacheEntry +import com.adobe.marketing.mobile.services.caching.CacheExpiry +import com.adobe.marketing.mobile.services.caching.CacheResult +import com.adobe.marketing.mobile.services.caching.CacheService +import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.InputStream + +class MockCacheService : CacheService { + + private val entrySet = mutableMapOf() + + override fun set(name: String, key: String, value: CacheEntry): Boolean { + entrySet[name + key] = value + return true + } + + override fun get(name: String, key: String): CacheResult? { + return if (entrySet.containsKey(name + key)) { + object : CacheResult { + override fun getData(): InputStream { + val mockBitmap: Bitmap = mock(Bitmap::class.java) + `when`(mockBitmap.width).thenReturn(100) + `when`(mockBitmap.height).thenReturn(100) + return bitmapToInputStream(mockBitmap) + } + override fun getExpiry() = CacheExpiry.never() + override fun getMetadata() = null + } + } else { + null + } + } + + override fun remove(name: String, key: String): Boolean { + if (entrySet.containsKey(name + key)) { + entrySet.remove(name + key) + return true + } else { + return false + } + } + + private fun bitmapToInputStream(bitmap: Bitmap): ByteArrayInputStream { + val outputStream = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream) + val byteArray = outputStream.toByteArray() + return ByteArrayInputStream(byteArray) + } +} diff --git a/code/messagingtestutils/src/main/java/com/adobe/marketing/mobile/messaging/MessagingTestConstants.java b/code/messagingtestutils/src/main/java/com/adobe/marketing/mobile/messaging/MessagingTestConstants.java index ab73e3a5..40713ace 100644 --- a/code/messagingtestutils/src/main/java/com/adobe/marketing/mobile/messaging/MessagingTestConstants.java +++ b/code/messagingtestutils/src/main/java/com/adobe/marketing/mobile/messaging/MessagingTestConstants.java @@ -20,6 +20,7 @@ public class MessagingTestConstants { static final String FRIENDLY_EXTENSION_NAME = "Messaging"; static final String CACHE_NAME = "com.adobe.messaging.test.cache"; static final String PROPOSITIONS_CACHE_SUBDIRECTORY = "propositions"; + static final String CONTENT_CARD_TEST_CACHE_SUBDIRECTORY = "contentCardTestImages"; static final String IMAGES_CACHE_SUBDIRECTORY = "images"; static final String CACHE_BASE_DIR = "messaging"; static final String EXTENSION_NAME = "com.adobe.messaging";