Skip to content

Commit

Permalink
Lp/image preloading executors (#505)
Browse files Browse the repository at this point in the history
* Prefetch using executors

- we had added code to prefetch using coroutines
- executors support for same task is added
- made method to return resource executor
- need to handle null config as there is tight coupling
- added method in IO executor to have callback on same thread.

* Method overload + dependency

- redendant dependency is removed
- method overload to hide detail about return type

* CtExecutors without config dependency

- we needed to pass config just to have logger in it
- logger static methods can be used if logging is not required on acc level
- created methods to support the same

* Method overload for resurce download executor

- thread pool size defaults to 8

* Strategy pattern used

- Instead of having same file with 2 methods we used strategy pattern.
- Refactored into different files

* Async -> Limited parallelism

- used limited parallelism instead of using async
- this will give desired functionality to limit no of threads

* Impl -> Strategy

- ref correction

* Method invocation

- called overloaded method.

* Preload handling

- preloading using coroutine executors
- parsed inapp media to get urls
- removed inapp dismiss code, we have to figure correct place

* Handled cache parsing failure

- we fallback to api
- this case is unlikely but added as fail safe
  • Loading branch information
CTLalit authored Nov 21, 2023
1 parent 52d661a commit d019b07
Show file tree
Hide file tree
Showing 17 changed files with 276 additions and 111 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,12 @@ fun JSONArray.toList(): List<JSONObject> {
return jsonObjectList
}

fun JSONArray.iterator(foreach: (jsonObject: JSONObject) -> Unit) {
for (index in 0 until length()) {
foreach(getJSONObject(index))
}
}

fun JSONObject.safeGetJSONArray(key: String): Pair<Boolean, JSONArray?> {
val has = has(key)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ public void writeToParcel(Parcel dest, int flags) {
}

void didDismiss(InAppResourceProvider resourceProvider) {
removeImageOrGif(resourceProvider);
//removeImageOrGif(resourceProvider); // todo add coorect removal
}

String getBackgroundColor() {
Expand Down Expand Up @@ -443,7 +443,7 @@ void prepareForDisplay(InAppResourceProvider inAppResourceProvider) {
}
} else if (media.isImage()) {

Bitmap bitmap = inAppResourceProvider.fetchInAppImage(media.getMediaUrl(), Bitmap.class);
Bitmap bitmap = inAppResourceProvider.fetchInAppImage(media.getMediaUrl());
if (bitmap != null) {
listener.notificationReady(this);
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public CTInAppNotificationMedia[] newArray(int size) {

private String mediaUrl;

CTInAppNotificationMedia() {
public CTInAppNotificationMedia() {
}

private CTInAppNotificationMedia(Parcel in) {
Expand Down Expand Up @@ -65,7 +65,7 @@ String getContentType() {
return contentType;
}

String getMediaUrl() {
public String getMediaUrl() {
return mediaUrl;
}

Expand All @@ -74,7 +74,7 @@ void setMediaUrl(String mediaUrl) {
this.mediaUrl = mediaUrl;
}

CTInAppNotificationMedia initWithJSON(JSONObject mediaObject, int orientation) {
public CTInAppNotificationMedia initWithJSON(JSONObject mediaObject, int orientation) {
this.orientation = orientation;
try {
this.contentType = mediaObject.has(Constants.KEY_CONTENT_TYPE) ? mediaObject
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.clevertap.android.sdk.inapp.data

import android.content.res.Configuration
import com.clevertap.android.sdk.Constants
import com.clevertap.android.sdk.inapp.CTInAppNotificationMedia
import com.clevertap.android.sdk.inapp.evaluation.LimitAdapter
import com.clevertap.android.sdk.inapp.evaluation.TriggerAdapter
import com.clevertap.android.sdk.iterator
import com.clevertap.android.sdk.orEmptyArray
import com.clevertap.android.sdk.safeGetJSONArray
import com.clevertap.android.sdk.toList
Expand Down Expand Up @@ -34,7 +37,65 @@ class InAppResponseAdapter(
}
}

val preloadImage: List<String> = emptyList() // todo provide images
val preloadImage: List<String>

val legacyInApps: Pair<Boolean, JSONArray?> = responseJson.safeGetJSONArray(Constants.INAPP_JSON_RESPONSE_KEY)

val clientSideInApps: Pair<Boolean, JSONArray?> = responseJson.safeGetJSONArray(Constants.INAPP_NOTIFS_KEY_CS)

val serverSideInApps: Pair<Boolean, JSONArray?> = responseJson.safeGetJSONArray(Constants.INAPP_NOTIFS_KEY_SS)

init {
val list = mutableListOf<String>()

// do legacy inapps stuff
if (legacyInApps.first) {
legacyInApps.second?.iterator { jsonObject ->
val portrait = jsonObject.optJSONObject(Constants.KEY_MEDIA)

if (portrait != null) {
val portraitMedia = CTInAppNotificationMedia()
.initWithJSON(portrait, Configuration.ORIENTATION_PORTRAIT)

list.add(portraitMedia.mediaUrl)
}
val landscape = jsonObject.optJSONObject(Constants.KEY_MEDIA_LANDSCAPE)
if (landscape != null) {
val landscapeMedia = CTInAppNotificationMedia()
.initWithJSON(landscape, Configuration.ORIENTATION_LANDSCAPE)

list.add(landscapeMedia.mediaUrl)
}
}
}

// do cs inapps stuff
if (clientSideInApps.first) {
clientSideInApps.second?.iterator { jsonObject ->
val portrait = jsonObject.optJSONObject(Constants.KEY_MEDIA)

if (portrait != null) {
val portraitMedia = CTInAppNotificationMedia()
.initWithJSON(portrait, Configuration.ORIENTATION_PORTRAIT)

if (portraitMedia != null && portraitMedia.mediaUrl != null) {
list.add(portraitMedia.mediaUrl)
}
}
val landscape = jsonObject.optJSONObject(Constants.KEY_MEDIA_LANDSCAPE)
if (landscape != null) {
val landscapeMedia = CTInAppNotificationMedia()
.initWithJSON(landscape, Configuration.ORIENTATION_LANDSCAPE)

if (landscapeMedia != null && landscapeMedia.mediaUrl != null) {
list.add(landscapeMedia.mediaUrl)
}
}
}
}

preloadImage = list
}

val inAppsPerSession: Int = responseJson.optInt(IN_APP_SESSION_KEY, IN_APP_DEFAULT_SESSION)

Expand All @@ -44,14 +105,8 @@ class InAppResponseAdapter(

val staleInApps: Pair<Boolean, JSONArray?> = responseJson.safeGetJSONArray(Constants.INAPP_NOTIFS_STALE_KEY)

val legacyInApps: Pair<Boolean, JSONArray?> = responseJson.safeGetJSONArray(Constants.INAPP_JSON_RESPONSE_KEY)

val appLaunchServerSideInApps: Pair<Boolean, JSONArray?> =
responseJson.safeGetJSONArray(Constants.INAPP_NOTIFS_APP_LAUNCHED_KEY)

val clientSideInApps: Pair<Boolean, JSONArray?> = responseJson.safeGetJSONArray(Constants.INAPP_NOTIFS_KEY_CS)

val serverSideInApps: Pair<Boolean, JSONArray?> = responseJson.safeGetJSONArray(Constants.INAPP_NOTIFS_KEY_SS)
}

// Define a common interface for the properties that are common to both data classes
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ internal class InAppResourceProvider constructor(
return fileToBytes(gifDiskCache.get(cacheKey))
}

fun fetchInAppImage(url: String): Bitmap? {
return fetchInAppImage(url = url, clazz = Bitmap::class.java)
}

/**
* Function that would fetch and cache bitmap image into Memory and File cache and return it.
* If image is found in cache, the cached image is returned.
Expand All @@ -134,15 +138,13 @@ internal class InAppResourceProvider constructor(
val cachedImage: Bitmap? = cachedImage(url)

if (cachedImage != null) {
return if (clazz.isAssignableFrom(Bitmap::class.java)) {
cachedImage as? T
if (clazz.isAssignableFrom(Bitmap::class.java)) {
return cachedImage as? T
} else if (clazz.isAssignableFrom(ByteArray::class.java)) {
val stream = ByteArrayOutputStream()
cachedImage.compress(Bitmap.CompressFormat.PNG, 100, stream)
val byteArray = stream.toByteArray()
byteArray as? T
} else {
null
return byteArray as? T
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.clevertap.android.sdk.inapp.images.preload

internal data class InAppImagePreloadConfig(
val parallelDownloads: Int,
) {
companion object {
private const val DEFAULT_PARALLEL_DOWNLOAD = 4

fun default() : InAppImagePreloadConfig = InAppImagePreloadConfig(
parallelDownloads = DEFAULT_PARALLEL_DOWNLOAD
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.clevertap.android.sdk.inapp.images.preload

import com.clevertap.android.sdk.ILogger
import com.clevertap.android.sdk.inapp.images.InAppResourceProvider
import com.clevertap.android.sdk.utils.CtDefaultDispatchers
import com.clevertap.android.sdk.utils.DispatcherProvider
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlin.system.measureTimeMillis

internal class InAppImagePreloaderCoroutine @JvmOverloads constructor(
override val inAppImageProvider: InAppResourceProvider,
override val logger: ILogger? = null,
private val dispatchers: DispatcherProvider = CtDefaultDispatchers(),
override val config: InAppImagePreloadConfig = InAppImagePreloadConfig.default()
) : InAppImagePreloaderStrategy {

private val jobs: MutableList<Job> = mutableListOf()

@OptIn(ExperimentalCoroutinesApi::class)
override fun preloadImages(urls: List<String>) {

val handler = CoroutineExceptionHandler { _, throwable ->
logger?.verbose("Cancelled image pre fetch \n ${throwable.stackTrace}")
}
val scope = CoroutineScope(dispatchers.io().limitedParallelism(config.parallelDownloads))

urls.forEach { url ->
val job = scope.launch(handler) {
logger?.verbose("started image url fetch $url")

val mils = measureTimeMillis {
inAppImageProvider.fetchInAppImage(url)
}

logger?.verbose("finished image url fetch $url in $mils ms")
}
jobs.add(job)
}
}

override fun cleanup() {
jobs.map { job -> job.cancel() }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.clevertap.android.sdk.inapp.images.preload

import com.clevertap.android.sdk.ILogger
import com.clevertap.android.sdk.inapp.images.InAppResourceProvider
import com.clevertap.android.sdk.task.CTExecutors

internal class InAppImagePreloaderExecutors @JvmOverloads constructor(
private val executor: CTExecutors,
override val inAppImageProvider: InAppResourceProvider,
override val logger: ILogger? = null,
override val config: InAppImagePreloadConfig = InAppImagePreloadConfig.default()
) : InAppImagePreloaderStrategy {

override fun preloadImages(urls: List<String>) {

for (url in urls) {
val task = executor.ioTaskNonUi<Void>()

task.execute("tag") {
inAppImageProvider.fetchInAppImage(url)
null
}
}
}

override fun cleanup() {
//executor?.shutdown
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.clevertap.android.sdk.inapp.images.preload

import com.clevertap.android.sdk.ILogger
import com.clevertap.android.sdk.inapp.images.InAppResourceProvider

internal interface InAppImagePreloaderStrategy {

val inAppImageProvider: InAppResourceProvider

val logger: ILogger?

val config: InAppImagePreloadConfig

fun preloadImages(urls: List<String>)
fun cleanup()
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
import com.clevertap.android.sdk.CoreMetaData;
import com.clevertap.android.sdk.Logger;
import com.clevertap.android.sdk.inapp.data.InAppResponseAdapter;
import com.clevertap.android.sdk.inapp.images.InAppImagePreloader;
import com.clevertap.android.sdk.inapp.images.preload.InAppImagePreloaderCoroutine;
import com.clevertap.android.sdk.inapp.images.InAppResourceProvider;
import com.clevertap.android.sdk.inapp.images.preload.InAppImagePreloaderStrategy;
import com.clevertap.android.sdk.inapp.store.preference.ImpressionStore;
import com.clevertap.android.sdk.inapp.store.preference.InAppStore;
import com.clevertap.android.sdk.inapp.store.preference.StoreRegistry;
import com.clevertap.android.sdk.task.CTExecutorFactory;
import com.clevertap.android.sdk.task.CTExecutors;
import com.clevertap.android.sdk.task.Task;
import java.util.concurrent.Callable;
import kotlin.Pair;
Expand Down Expand Up @@ -125,7 +127,10 @@ public void processResponse(
}

InAppResourceProvider inAppResourceProvider = new InAppResourceProvider(context, logger);
InAppImagePreloader preloader = new InAppImagePreloader(inAppResourceProvider, logger);

CTExecutors executor = CTExecutorFactory.executorResourceDownloader();
InAppImagePreloaderStrategy preloader = new InAppImagePreloaderCoroutine(inAppResourceProvider, logger);
//InAppImagePreloaderStrategy preloader = new InAppImagePreloaderExecutors(executor, inAppResourceProvider, logger);

preloader.preloadImages(res.getPreloadImage());

Expand Down
Loading

0 comments on commit d019b07

Please sign in to comment.