diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/ILogger.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/ILogger.java new file mode 100644 index 000000000..f1d231694 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/ILogger.java @@ -0,0 +1,31 @@ +package com.clevertap.android.sdk; + +public interface ILogger { + void debug(String message); + + void debug(String suffix, String message); + + void debug(String suffix, String message, Throwable t); + + @SuppressWarnings("unused") + void debug(String message, Throwable t); + + @SuppressWarnings("unused") + void info(String message); + + void info(String suffix, String message); + + @SuppressWarnings("unused") + void info(String suffix, String message, Throwable t); + + @SuppressWarnings("unused") + void info(String message, Throwable t); + + void verbose(String message); + + void verbose(String suffix, String message); + + void verbose(String suffix, String message, Throwable t); + + void verbose(String message, Throwable t); +} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/Logger.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/Logger.java index bb6ab32af..e2f065dfd 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/Logger.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/Logger.java @@ -2,7 +2,7 @@ import android.util.Log; -public final class Logger { +public final class Logger implements ILogger { private int debugLevel; @@ -94,12 +94,14 @@ public static void v(String message, Throwable t) { this.debugLevel = level; } + @Override public void debug(String message) { if (getStaticDebugLevel() > CleverTapAPI.LogLevel.INFO.intValue()) { Log.d(Constants.CLEVERTAP_LOG_TAG, message); } } + @Override public void debug(String suffix, String message) { if (getStaticDebugLevel() > CleverTapAPI.LogLevel.INFO.intValue()) { if (message.length() > 4000) { @@ -111,12 +113,14 @@ public void debug(String suffix, String message) { } } + @Override public void debug(String suffix, String message, Throwable t) { if (getStaticDebugLevel() > CleverTapAPI.LogLevel.INFO.intValue()) { Log.d(Constants.CLEVERTAP_LOG_TAG + ":" + suffix, message, t); } } + @Override @SuppressWarnings("unused") public void debug(String message, Throwable t) { if (getStaticDebugLevel() > CleverTapAPI.LogLevel.INFO.intValue()) { @@ -124,6 +128,7 @@ public void debug(String message, Throwable t) { } } + @Override @SuppressWarnings("unused") public void info(String message) { if (getDebugLevel() >= CleverTapAPI.LogLevel.INFO.intValue()) { @@ -131,12 +136,14 @@ public void info(String message) { } } + @Override public void info(String suffix, String message) { if (getDebugLevel() >= CleverTapAPI.LogLevel.INFO.intValue()) { Log.i(Constants.CLEVERTAP_LOG_TAG + ":" + suffix, message); } } + @Override @SuppressWarnings("unused") public void info(String suffix, String message, Throwable t) { if (getDebugLevel() >= CleverTapAPI.LogLevel.INFO.intValue()) { @@ -144,6 +151,7 @@ public void info(String suffix, String message, Throwable t) { } } + @Override @SuppressWarnings("unused") public void info(String message, Throwable t) { if (getDebugLevel() >= CleverTapAPI.LogLevel.INFO.intValue()) { @@ -151,12 +159,14 @@ public void info(String message, Throwable t) { } } + @Override public void verbose(String message) { if (getStaticDebugLevel() > CleverTapAPI.LogLevel.DEBUG.intValue()) { Log.v(Constants.CLEVERTAP_LOG_TAG, message); } } + @Override public void verbose(String suffix, String message) { if (getStaticDebugLevel() > CleverTapAPI.LogLevel.DEBUG.intValue()) { if (message.length() > 4000) { @@ -168,12 +178,14 @@ public void verbose(String suffix, String message) { } } + @Override public void verbose(String suffix, String message, Throwable t) { if (getStaticDebugLevel() > CleverTapAPI.LogLevel.DEBUG.intValue()) { Log.v(Constants.CLEVERTAP_LOG_TAG + ":" + suffix, message, t); } } + @Override public void verbose(String message, Throwable t) { if (getStaticDebugLevel() > CleverTapAPI.LogLevel.DEBUG.intValue()) { Log.v(Constants.CLEVERTAP_LOG_TAG, message, t); diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/Utils.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/Utils.java index 281912981..58007a8ba 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/Utils.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/Utils.java @@ -36,16 +36,11 @@ import com.clevertap.android.sdk.network.DownloadedBitmap; import com.clevertap.android.sdk.network.DownloadedBitmapFactory; import com.google.firebase.messaging.RemoteMessage; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; -import javax.net.ssl.HttpsURLConnection; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -164,37 +159,6 @@ public static long getNowInMillis() { return System.currentTimeMillis(); } - public static byte[] getByteArrayFromImageURL(String srcUrl) { - srcUrl = srcUrl.replace("///", "/"); - srcUrl = srcUrl.replace("//", "/"); - srcUrl = srcUrl.replace("http:/", "http://"); - srcUrl = srcUrl.replace("https:/", "https://"); - HttpsURLConnection connection = null; - try { - URL url = new URL(srcUrl); - connection = (HttpsURLConnection) url.openConnection(); - InputStream is = connection.getInputStream(); - byte[] buffer = new byte[8192]; - int bytesRead; - final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - while ((bytesRead = is.read(buffer)) != -1) { - baos.write(buffer, 0, bytesRead); - } - return baos.toByteArray(); - } catch (IOException e) { - Logger.v("Error processing image bytes from url: " + srcUrl); - return null; - } finally { - try { - if (connection != null) { - connection.disconnect(); - } - } catch (Throwable t) { - Logger.v("Couldn't close connection!", t); - } - } - } - @SuppressLint("MissingPermission") public static String getCurrentNetworkType(final Context context) { try { @@ -329,11 +293,7 @@ public static boolean isActivityDead(Activity activity) { if (activity == null) { return true; } - boolean isActivityDead = activity.isFinishing(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - isActivityDead = isActivityDead || activity.isDestroyed(); - } - return isActivityDead; + return activity.isFinishing() || activity.isDestroyed(); } @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @@ -497,14 +457,14 @@ private static boolean checkForExoPlayer() { if (logo == null) { throw new Exception("Logo is null"); } - return DownloadedBitmapFactory.INSTANCE.successBitmap(drawableToBitmap(logo), 0); + return DownloadedBitmapFactory.INSTANCE.successBitmap(drawableToBitmap(logo), 0, null); } catch (Exception e) { e.printStackTrace(); // Try to get the app icon now // No error handling here - handle upstream return DownloadedBitmapFactory.INSTANCE.successBitmap( drawableToBitmap(context.getPackageManager().getApplicationIcon(context.getApplicationInfo())), - 0); + 0, null); } } diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/bitmap/BitmapDownloader.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/bitmap/BitmapDownloader.kt index f38387126..9dea02e1d 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/bitmap/BitmapDownloader.kt +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/bitmap/BitmapDownloader.kt @@ -47,8 +47,11 @@ class BitmapDownloader( return DownloadedBitmapFactory.nullBitmapWithStatus(SIZE_LIMIT_EXCEEDED) } - return bitmapInputStreamReader.readInputStream(inputStream, this, downloadStartTimeInMilliseconds) - ?: DownloadedBitmapFactory.nullBitmapWithStatus(DOWNLOAD_FAILED) + return bitmapInputStreamReader.readInputStream( + inputStream = inputStream, + connection = this, + downloadStartTimeInMilliseconds = downloadStartTimeInMilliseconds + ) } } catch (e: Throwable) { Logger.v("Couldn't download the notification icon. URL was: $srcUrl") diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/bitmap/BitmapInputStreamDecoder.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/bitmap/BitmapInputStreamDecoder.kt index 466721857..2da038cf9 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/bitmap/BitmapInputStreamDecoder.kt +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/bitmap/BitmapInputStreamDecoder.kt @@ -5,23 +5,58 @@ import com.clevertap.android.sdk.Logger import com.clevertap.android.sdk.Utils import com.clevertap.android.sdk.network.DownloadedBitmap import com.clevertap.android.sdk.network.DownloadedBitmapFactory +import java.io.ByteArrayOutputStream import java.io.InputStream import java.net.HttpURLConnection -class BitmapInputStreamDecoder(private val nextBitmapInputStreamReader: GzipBitmapInputStreamReader? = null) : - IBitmapInputStreamReader { +open class BitmapInputStreamDecoder( + val saveBytes: Boolean = false, + val logger: Logger? = null +) : IBitmapInputStreamReader { override fun readInputStream( inputStream: InputStream, connection: HttpURLConnection, downloadStartTimeInMilliseconds: Long - ): DownloadedBitmap? { - Logger.v("reading bitmap input stream in BitmapInputStreamDecoder....") - - return nextBitmapInputStreamReader?.readInputStream(inputStream,connection, downloadStartTimeInMilliseconds) - ?: DownloadedBitmapFactory.successBitmap( - BitmapFactory.decodeStream(inputStream), - Utils.getNowInMillis() - downloadStartTimeInMilliseconds - ) + ): DownloadedBitmap { + logger?.verbose("reading bitmap input stream in BitmapInputStreamDecoder....") + + val bufferForHttpInputStream = ByteArray(16384) + val finalDataFromHttpInputStream = ByteArrayOutputStream() + + var totalBytesRead = 0 + var bytesRead: Int + + // Read data from input stream + while (inputStream.read(bufferForHttpInputStream).also { bytesRead = it } != -1) { + totalBytesRead += bytesRead + finalDataFromHttpInputStream.write(bufferForHttpInputStream, 0, bytesRead) + logger?.verbose("Downloaded $totalBytesRead bytes") + } + logger?.verbose("Total download size for bitmap = $totalBytesRead") + + val dataReadFromStreamInByteArray = finalDataFromHttpInputStream.toByteArray() + // Decode the bitmap from decompressed data + val bitmap = BitmapFactory.decodeByteArray( + dataReadFromStreamInByteArray, + 0, + dataReadFromStreamInByteArray.size + ) + + val fileLength = connection.contentLength + if (fileLength != -1 && fileLength != totalBytesRead) { + logger?.debug("File not loaded completely not going forward. URL was: ${connection.url}") + return DownloadedBitmapFactory.nullBitmapWithStatus(DownloadedBitmap.Status.DOWNLOAD_FAILED) + } + + return DownloadedBitmapFactory.successBitmap( + bitmap = bitmap, + downloadTime = Utils.getNowInMillis() - downloadStartTimeInMilliseconds, + data = if (saveBytes) { + dataReadFromStreamInByteArray + } else { + null + } + ) } } \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/bitmap/BitmapInputStreamReader.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/bitmap/BitmapInputStreamReader.kt index bab5de0ab..9d109d8dd 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/bitmap/BitmapInputStreamReader.kt +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/bitmap/BitmapInputStreamReader.kt @@ -20,7 +20,7 @@ class BitmapInputStreamReader( inputStream: InputStream, connection: HttpURLConnection, downloadStartTimeInMilliseconds: Long - ): DownloadedBitmap? { + ): DownloadedBitmap { Logger.v("reading bitmap input stream in BitmapInputStreamReader....") val bufferForHttpInputStream = ByteArray(16384) @@ -47,22 +47,31 @@ class BitmapInputStreamReader( } return nextBitmapInputStreamReader?.readInputStream( - ByteArrayInputStream(finalDataFromHttpInputStream.toByteArray()), - connection, - downloadStartTimeInMilliseconds - ) ?: getDownloadedBitmapFromStream(finalDataFromHttpInputStream, downloadStartTimeInMilliseconds) + inputStream = ByteArrayInputStream(finalDataFromHttpInputStream.toByteArray()), + connection = connection, + downloadStartTimeInMilliseconds = downloadStartTimeInMilliseconds + ) ?: getDownloadedBitmapFromStream( + dataReadFromStream = finalDataFromHttpInputStream, + downloadStartTimeInMilliseconds = downloadStartTimeInMilliseconds + ) } private fun getDownloadedBitmapFromStream( - dataReadFromStream: ByteArrayOutputStream, downloadStartTimeInMilliseconds: Long + dataReadFromStream: ByteArrayOutputStream, + downloadStartTimeInMilliseconds: Long ): DownloadedBitmap { val dataReadFromStreamInByteArray = dataReadFromStream.toByteArray() // Decode the bitmap from decompressed data - val bitmap = - BitmapFactory.decodeByteArray(dataReadFromStreamInByteArray, 0, dataReadFromStreamInByteArray.size) + val bitmap = BitmapFactory.decodeByteArray( + dataReadFromStreamInByteArray, + 0, + dataReadFromStreamInByteArray.size + ) + return DownloadedBitmapFactory.successBitmap( - bitmap, Utils.getNowInMillis() - downloadStartTimeInMilliseconds + bitmap = bitmap, + downloadTime = Utils.getNowInMillis() - downloadStartTimeInMilliseconds ) } } \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/bitmap/GzipBitmapInputStreamReader.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/bitmap/GzipBitmapInputStreamReader.kt index ac8b42678..662103122 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/bitmap/GzipBitmapInputStreamReader.kt +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/bitmap/GzipBitmapInputStreamReader.kt @@ -10,13 +10,16 @@ import java.io.InputStream import java.net.HttpURLConnection import java.util.zip.GZIPInputStream -class GzipBitmapInputStreamReader : IBitmapInputStreamReader { +class GzipBitmapInputStreamReader( + saveBytes: Boolean = false, + logger: Logger? = null +) : BitmapInputStreamDecoder(saveBytes, logger) { override fun readInputStream( inputStream: InputStream, connection: HttpURLConnection, downloadStartTimeInMilliseconds: Long - ): DownloadedBitmap? { + ): DownloadedBitmap { Logger.v("reading bitmap input stream in GzipBitmapInputStreamReader....") @@ -37,22 +40,36 @@ class GzipBitmapInputStreamReader : IBitmapInputStreamReader { decompressedFile.write(bufferForGzipInputStream, 0, bytesRead) } - Logger.v("Total decompressed download size for bitmap from output stream = ${decompressedFile.size()}") + logger?.verbose("Total decompressed download size for bitmap from output stream = ${decompressedFile.size()}") - return getDownloadedBitmapFromStream(decompressedFile, downloadStartTimeInMilliseconds) - } else null + return getDownloadedBitmapFromStream( + dataReadFromStream = decompressedFile, + downloadStartTimeInMilliseconds = downloadStartTimeInMilliseconds + ) + } else { + super.readInputStream( + inputStream = inputStream, + connection = connection, + downloadStartTimeInMilliseconds = downloadStartTimeInMilliseconds + ) + } } private fun getDownloadedBitmapFromStream( - dataReadFromStream: ByteArrayOutputStream, downloadStartTimeInMilliseconds: Long + dataReadFromStream: ByteArrayOutputStream, + downloadStartTimeInMilliseconds: Long ): DownloadedBitmap { val dataReadFromStreamInByteArray = dataReadFromStream.toByteArray() // Decode the bitmap from decompressed data - val bitmap = - BitmapFactory.decodeByteArray(dataReadFromStreamInByteArray, 0, dataReadFromStreamInByteArray.size) + val bitmap = BitmapFactory.decodeByteArray( + dataReadFromStreamInByteArray, + 0, + dataReadFromStreamInByteArray.size + ) return DownloadedBitmapFactory.successBitmap( - bitmap, Utils.getNowInMillis() - downloadStartTimeInMilliseconds + bitmap = bitmap, + downloadTime = Utils.getNowInMillis() - downloadStartTimeInMilliseconds ) } } \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/bitmap/HttpBitmapLoader.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/bitmap/HttpBitmapLoader.kt index bb8af892e..2cb7362c3 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/bitmap/HttpBitmapLoader.kt +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/bitmap/HttpBitmapLoader.kt @@ -10,6 +10,9 @@ import com.clevertap.android.sdk.network.DownloadedBitmap object HttpBitmapLoader { + private const val RESOURCE_CONNECTION_TIMEOUT = 5000 + private const val RESOURCE_READ_TIMEOUT = 15000 + private val standardGzipHttpUrlConnectionParams = HttpUrlConnectionParams( connectTimeout = Constants.PN_IMAGE_CONNECTION_TIMEOUT_IN_MILLIS, readTimeout = Constants.PN_IMAGE_READ_TIMEOUT_IN_MILLIS, @@ -18,16 +21,19 @@ object HttpBitmapLoader { requestMap = mapOf("Accept-Encoding" to "gzip, deflate") ) private val inAppStandardHttpUrlConnectionParams = HttpUrlConnectionParams( + connectTimeout = RESOURCE_CONNECTION_TIMEOUT, + readTimeout = RESOURCE_READ_TIMEOUT, useCaches = true, doInput = true ) - enum class HttpBitmapOperation { DOWNLOAD_NOTIFICATION_BITMAP, DOWNLOAD_GZIP_NOTIFICATION_BITMAP_WITH_TIME_LIMIT, DOWNLOAD_SIZE_CONSTRAINED_GZIP_NOTIFICATION_BITMAP, - DOWNLOAD_SIZE_CONSTRAINED_GZIP_NOTIFICATION_BITMAP_WITH_TIME_LIMIT, DOWNLOAD_INAPP_BITMAP + DOWNLOAD_SIZE_CONSTRAINED_GZIP_NOTIFICATION_BITMAP_WITH_TIME_LIMIT, + DOWNLOAD_INAPP_BITMAP, + DOWNLOAD_ANY_BITMAP } @JvmStatic @@ -37,63 +43,85 @@ object HttpBitmapLoader { ): DownloadedBitmap { return when (bitmapOperation) { - DOWNLOAD_NOTIFICATION_BITMAP -> NotificationBitmapDownloadRequestHandler( - BitmapDownloadRequestHandler( - BitmapDownloader(standardGzipHttpUrlConnectionParams, BitmapInputStreamDecoder()) + DOWNLOAD_NOTIFICATION_BITMAP -> { + NotificationBitmapDownloadRequestHandler( + iBitmapDownloadRequestHandler = BitmapDownloadRequestHandler( + bitmapDownloader = BitmapDownloader( + httpUrlConnectionParams = standardGzipHttpUrlConnectionParams, + bitmapInputStreamReader = BitmapInputStreamDecoder() + ) + ) + ).handleRequest( + bitmapDownloadRequest = bitmapDownloadRequest ) - ).handleRequest( - bitmapDownloadRequest - ) + } DOWNLOAD_GZIP_NOTIFICATION_BITMAP_WITH_TIME_LIMIT -> { - val notificationBitmapDownloadRequestHandlerWithTimeLimit = BitmapDownloadRequestHandlerWithTimeLimit( - NotificationBitmapDownloadRequestHandler( - BitmapDownloadRequestHandler( - BitmapDownloader( - standardGzipHttpUrlConnectionParams, BitmapInputStreamDecoder( - GzipBitmapInputStreamReader() - ) + BitmapDownloadRequestHandlerWithTimeLimit( + iBitmapDownloadRequestHandler = NotificationBitmapDownloadRequestHandler( + iBitmapDownloadRequestHandler = BitmapDownloadRequestHandler( + bitmapDownloader = BitmapDownloader( + httpUrlConnectionParams = standardGzipHttpUrlConnectionParams, + bitmapInputStreamReader = GzipBitmapInputStreamReader() ) ) ) + ).handleRequest( + bitmapDownloadRequest = bitmapDownloadRequest ) - notificationBitmapDownloadRequestHandlerWithTimeLimit.handleRequest(bitmapDownloadRequest) } - DOWNLOAD_SIZE_CONSTRAINED_GZIP_NOTIFICATION_BITMAP -> NotificationBitmapDownloadRequestHandler( - BitmapDownloadRequestHandler( - BitmapDownloader( - standardGzipHttpUrlConnectionParams, - BitmapInputStreamDecoder(GzipBitmapInputStreamReader()), - Pair(true, bitmapDownloadRequest.downloadSizeLimitInBytes) + DOWNLOAD_SIZE_CONSTRAINED_GZIP_NOTIFICATION_BITMAP -> { + NotificationBitmapDownloadRequestHandler( + iBitmapDownloadRequestHandler = BitmapDownloadRequestHandler( + bitmapDownloader = BitmapDownloader( + httpUrlConnectionParams = standardGzipHttpUrlConnectionParams, + bitmapInputStreamReader = GzipBitmapInputStreamReader(), + sizeConstrainedPair = Pair(true, bitmapDownloadRequest.downloadSizeLimitInBytes) + ) ) + ).handleRequest( + bitmapDownloadRequest = bitmapDownloadRequest ) - ).handleRequest( - bitmapDownloadRequest - ) + } DOWNLOAD_SIZE_CONSTRAINED_GZIP_NOTIFICATION_BITMAP_WITH_TIME_LIMIT -> { - val notificationBitmapDownloadRequestHandlerWithTimeLimit = BitmapDownloadRequestHandlerWithTimeLimit( - NotificationBitmapDownloadRequestHandler( - BitmapDownloadRequestHandler( - BitmapDownloader( - standardGzipHttpUrlConnectionParams, - BitmapInputStreamDecoder(GzipBitmapInputStreamReader()), - Pair(true, bitmapDownloadRequest.downloadSizeLimitInBytes) + BitmapDownloadRequestHandlerWithTimeLimit( + iBitmapDownloadRequestHandler = NotificationBitmapDownloadRequestHandler( + iBitmapDownloadRequestHandler = BitmapDownloadRequestHandler( + bitmapDownloader = BitmapDownloader( + httpUrlConnectionParams = standardGzipHttpUrlConnectionParams, + bitmapInputStreamReader = GzipBitmapInputStreamReader(), + sizeConstrainedPair = Pair(true, bitmapDownloadRequest.downloadSizeLimitInBytes) ) ) ) + ).handleRequest( + bitmapDownloadRequest = bitmapDownloadRequest ) - notificationBitmapDownloadRequestHandlerWithTimeLimit.handleRequest(bitmapDownloadRequest) } - DOWNLOAD_INAPP_BITMAP -> BitmapDownloadRequestHandler( - BitmapDownloader( - inAppStandardHttpUrlConnectionParams, BitmapInputStreamDecoder() + DOWNLOAD_INAPP_BITMAP -> { + BitmapDownloadRequestHandler( + bitmapDownloader = BitmapDownloader( + httpUrlConnectionParams = inAppStandardHttpUrlConnectionParams, + bitmapInputStreamReader = BitmapInputStreamDecoder(saveBytes = true) + ) + ).handleRequest( + bitmapDownloadRequest = bitmapDownloadRequest + ) + } + + HttpBitmapOperation.DOWNLOAD_ANY_BITMAP -> { + BitmapDownloadRequestHandler( + bitmapDownloader = BitmapDownloader( + httpUrlConnectionParams = inAppStandardHttpUrlConnectionParams, + bitmapInputStreamReader = GzipBitmapInputStreamReader() + ) + ).handleRequest( + bitmapDownloadRequest = bitmapDownloadRequest ) - ).handleRequest( - bitmapDownloadRequest - ) + } } } } \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/bitmap/IBitmapInputStreamReader.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/bitmap/IBitmapInputStreamReader.kt index cc3cd11df..b4aa81ad1 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/bitmap/IBitmapInputStreamReader.kt +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/bitmap/IBitmapInputStreamReader.kt @@ -10,5 +10,5 @@ interface IBitmapInputStreamReader { inputStream: InputStream, connection: HttpURLConnection, downloadStartTimeInMilliseconds: Long - ): DownloadedBitmap? + ): DownloadedBitmap } \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFragment.java index 9ab382451..96532ebf9 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFragment.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFragment.java @@ -6,15 +6,16 @@ import android.os.Bundle; import android.util.TypedValue; import android.view.View; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import com.clevertap.android.sdk.CleverTapInstanceConfig; import com.clevertap.android.sdk.Constants; import com.clevertap.android.sdk.DidClickForHardPermissionListener; -import com.clevertap.android.sdk.InAppNotificationActivity; +import com.clevertap.android.sdk.Logger; import com.clevertap.android.sdk.Utils; import com.clevertap.android.sdk.customviews.CloseImageView; +import com.clevertap.android.sdk.inapp.images.InAppResourceProvider; + import java.lang.ref.WeakReference; import java.util.HashMap; import java.util.Set; @@ -46,6 +47,8 @@ public void onClick(View view) { private DidClickForHardPermissionListener didClickForHardPermissionListener; + private InAppResourceProvider provider; + @Override public void onAttach(Context context) { super.onAttach(context); @@ -54,6 +57,11 @@ public void onAttach(Context context) { if (bundle != null) { inAppNotification = bundle.getParcelable(Constants.INAPP_KEY); config = bundle.getParcelable(Constants.KEY_CONFIG); + Logger logger = null; + if (config != null) { + logger = config.getLogger(); + } + provider = new InAppResourceProvider(context, logger); currentOrientation = getResources().getConfiguration().orientation; generateListener(); /*Initialize the below listener only when in app has InAppNotification activity as their host activity @@ -182,4 +190,8 @@ void handleButtonClickAtIndex(int index) { } } + public InAppResourceProvider resourceProvider() { + return provider; + } + } diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeCoverFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeCoverFragment.java index e68f5d976..ff99d5890 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeCoverFragment.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeCoverFragment.java @@ -2,6 +2,7 @@ import android.annotation.SuppressLint; import android.content.res.Configuration; +import android.graphics.Bitmap; import android.graphics.Color; import android.os.Bundle; import android.view.LayoutInflater; @@ -38,11 +39,11 @@ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, inAppButtons.add(secondaryButton); ImageView imageView = relativeLayout.findViewById(R.id.backgroundImage); - if (inAppNotification.getInAppMediaForOrientation(currentOrientation) != null) { - if (inAppNotification.getImage(inAppNotification.getInAppMediaForOrientation(currentOrientation)) - != null) { - imageView.setImageBitmap(inAppNotification - .getImage(inAppNotification.getInAppMediaForOrientation(currentOrientation))); + CTInAppNotificationMedia mediaForOrientation = inAppNotification.getInAppMediaForOrientation(currentOrientation); + if (mediaForOrientation != null) { + Bitmap bitmap = resourceProvider().cachedImage(mediaForOrientation.getMediaUrl()); + if (bitmap != null) { + imageView.setImageBitmap(bitmap); imageView.setTag(0); imageView.setOnClickListener(new CTInAppNativeButtonClickListener()); } diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeCoverImageFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeCoverImageFragment.java index 4671ce8ee..b1e6e098e 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeCoverImageFragment.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeCoverImageFragment.java @@ -1,6 +1,7 @@ package com.clevertap.android.sdk.inapp; import android.annotation.SuppressLint; +import android.graphics.Bitmap; import android.graphics.Color; import android.os.Bundle; import android.view.LayoutInflater; @@ -26,11 +27,11 @@ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, RelativeLayout relativeLayout = fl.findViewById(R.id.cover_image_relative_layout); ImageView imageView = relativeLayout.findViewById(R.id.cover_image); - if (inAppNotification.getInAppMediaForOrientation(currentOrientation) != null) { - if (inAppNotification.getImage(inAppNotification.getInAppMediaForOrientation(currentOrientation)) - != null) { - imageView.setImageBitmap(inAppNotification - .getImage(inAppNotification.getInAppMediaForOrientation(currentOrientation))); + CTInAppNotificationMedia mediaForOrientation = inAppNotification.getInAppMediaForOrientation(currentOrientation); + if (mediaForOrientation != null) { + Bitmap bitmap = resourceProvider().cachedImage(mediaForOrientation.getMediaUrl()); + if (bitmap != null) { + imageView.setImageBitmap(bitmap); imageView.setTag(0); imageView.setOnClickListener(new CTInAppNativeButtonClickListener()); } diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeFooterFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeFooterFragment.java index 98bf6e2ed..a18cde070 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeFooterFragment.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeFooterFragment.java @@ -48,7 +48,7 @@ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, ImageView imageView = linearLayout1.findViewById(R.id.footer_icon); if (!inAppNotification.getMediaList().isEmpty()) { - Bitmap image = inAppNotification.getImage(inAppNotification.getMediaList().get(0)); + Bitmap image = resourceProvider().cachedImage(inAppNotification.getMediaList().get(0).getMediaUrl()); if (image != null) { imageView.setImageBitmap(image); } else { diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeHalfInterstitialFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeHalfInterstitialFragment.java index 6483ee4ab..bcd8b452e 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeHalfInterstitialFragment.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeHalfInterstitialFragment.java @@ -3,6 +3,7 @@ import android.annotation.SuppressLint; import android.content.Context; import android.content.res.Configuration; +import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.os.Bundle; @@ -140,12 +141,12 @@ public void run() { } - if (inAppNotification.getInAppMediaForOrientation(currentOrientation) != null) { - if (inAppNotification.getImage(inAppNotification.getInAppMediaForOrientation(currentOrientation)) - != null) { + CTInAppNotificationMedia mediaForOrientation = inAppNotification.getInAppMediaForOrientation(currentOrientation); + if (mediaForOrientation != null) { + Bitmap bitmap = resourceProvider().cachedImage(mediaForOrientation.getMediaUrl()); + if (bitmap != null) { ImageView imageView = relativeLayout.findViewById(R.id.backgroundImage); - imageView.setImageBitmap(inAppNotification - .getImage(inAppNotification.getInAppMediaForOrientation(currentOrientation))); + imageView.setImageBitmap(bitmap); } } diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeHalfInterstitialImageFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeHalfInterstitialImageFragment.java index 0da408eff..4a60d67d8 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeHalfInterstitialImageFragment.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeHalfInterstitialImageFragment.java @@ -2,6 +2,7 @@ import android.annotation.SuppressLint; import android.content.res.Configuration; +import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.os.Bundle; @@ -125,11 +126,11 @@ public void run() { break; } - if (inAppNotification.getInAppMediaForOrientation(currentOrientation) != null) { - if (inAppNotification.getImage(inAppNotification.getInAppMediaForOrientation(currentOrientation)) - != null) { - imageView.setImageBitmap(inAppNotification - .getImage(inAppNotification.getInAppMediaForOrientation(currentOrientation))); + CTInAppNotificationMedia mediaForOrientation = inAppNotification.getInAppMediaForOrientation(currentOrientation); + if (mediaForOrientation != null) { + Bitmap bitmap = resourceProvider().cachedImage(mediaForOrientation.getMediaUrl()); + if (bitmap != null) { + imageView.setImageBitmap(bitmap); imageView.setTag(0); imageView.setOnClickListener(new CTInAppNativeButtonClickListener()); } diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeHeaderFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeHeaderFragment.java index 75d9e9a0e..80358dd97 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeHeaderFragment.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeHeaderFragment.java @@ -45,7 +45,7 @@ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, ImageView imageView = linearLayout1.findViewById(R.id.header_icon); if (!inAppNotification.getMediaList().isEmpty()) { - Bitmap image = inAppNotification.getImage(inAppNotification.getMediaList().get(0)); + Bitmap image = resourceProvider().cachedImage(inAppNotification.getMediaList().get(0).getMediaUrl()); if (image != null) { imageView.setImageBitmap(image); } else { diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeInterstitialFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeInterstitialFragment.java index aa6864b5d..68231c085 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeInterstitialFragment.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeInterstitialFragment.java @@ -154,27 +154,27 @@ public void onGlobalLayout() { } if (!inAppNotification.getMediaList().isEmpty()) { - if (inAppNotification.getMediaList().get(0).isImage()) { - Bitmap image = inAppNotification.getImage(inAppNotification.getMediaList().get(0)); + CTInAppNotificationMedia media = inAppNotification.getMediaList().get(0); + if (media.isImage()) { + Bitmap image = resourceProvider().cachedImage(media.getMediaUrl()); if (image != null) { ImageView imageView = relativeLayout.findViewById(R.id.backgroundImage); imageView.setVisibility(View.VISIBLE); - imageView.setImageBitmap( - inAppNotification.getImage(inAppNotification.getMediaList().get(0))); + imageView.setImageBitmap(image); } - } else if (inAppNotification.getMediaList().get(0).isGIF()) { - if (inAppNotification.getGifByteArray(inAppNotification.getMediaList().get(0)) != null) { + } else if (media.isGIF()) { + byte[] gifByteArray = resourceProvider().cachedGif(media.getMediaUrl()); + if (gifByteArray != null) { gifImageView = relativeLayout.findViewById(R.id.gifImage); gifImageView.setVisibility(View.VISIBLE); - gifImageView.setBytes( - inAppNotification.getGifByteArray(inAppNotification.getMediaList().get(0))); + gifImageView.setBytes(gifByteArray); gifImageView.startAnimation(); } - } else if (inAppNotification.getMediaList().get(0).isVideo()) { + } else if (media.isVideo()) { initFullScreenDialog(); prepareMedia(); playMedia(); - } else if (inAppNotification.getMediaList().get(0).isAudio()) { + } else if (media.isAudio()) { prepareMedia(); playMedia(); disableFullScreenButton(); @@ -238,7 +238,8 @@ public void onGlobalLayout() { public void onStart() { super.onStart(); if (gifImageView != null) { - gifImageView.setBytes(inAppNotification.getGifByteArray(inAppNotification.getMediaList().get(0))); + CTInAppNotificationMedia inAppMedia = inAppNotification.getMediaList().get(0); + gifImageView.setBytes(resourceProvider().cachedGif(inAppMedia.getMediaUrl())); gifImageView.startAnimation(); } } diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeInterstitialImageFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeInterstitialImageFragment.java index dea1dd7a3..9ac973ccf 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeInterstitialImageFragment.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeInterstitialImageFragment.java @@ -2,8 +2,10 @@ import android.annotation.SuppressLint; import android.content.res.Configuration; +import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; +import android.net.Uri; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -89,11 +91,11 @@ public void onGlobalLayout() { break; } - if (inAppNotification.getInAppMediaForOrientation(currentOrientation) != null) { - if (inAppNotification.getImage(inAppNotification.getInAppMediaForOrientation(currentOrientation)) - != null) { - imageView.setImageBitmap(inAppNotification - .getImage(inAppNotification.getInAppMediaForOrientation(currentOrientation))); + CTInAppNotificationMedia mediaForOrientation = inAppNotification.getInAppMediaForOrientation(currentOrientation); + if (mediaForOrientation != null) { + Bitmap bitmap = resourceProvider().cachedImage(mediaForOrientation.getMediaUrl()); + if (bitmap != null) { + imageView.setImageBitmap(bitmap); imageView.setTag(0); imageView.setOnClickListener(new CTInAppNativeButtonClickListener()); } diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNotification.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNotification.java index b9233cd08..daba14f6e 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNotification.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNotification.java @@ -7,13 +7,12 @@ import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; -import android.util.LruCache; + import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo.Scope; import com.clevertap.android.sdk.Constants; import com.clevertap.android.sdk.Logger; -import com.clevertap.android.sdk.Utils; -import com.clevertap.android.sdk.utils.ImageCache; +import com.clevertap.android.sdk.inapp.images.InAppResourceProvider; import java.util.ArrayList; import java.util.Iterator; import org.json.JSONArray; @@ -23,108 +22,6 @@ @RestrictTo(Scope.LIBRARY) public class CTInAppNotification implements Parcelable { - // intended to only hold an gif byte array reference for the life of the parent CTInAppNotification, in order to facilitate parceling - private static class GifCache { - - private static final int MIN_CACHE_SIZE = 1024 * 5; // 5mb minimum (in KB) - - private final static int maxMemory = (int) (Runtime.getRuntime().maxMemory()) / 1024; - - private final static int cacheSize = Math.max((maxMemory / 32), MIN_CACHE_SIZE); - - private static LruCache mMemoryCache; - - static boolean addByteArray(String key, byte[] byteArray) { - - if (mMemoryCache == null) { - return false; - } - - if (getByteArray(key) == null) { - synchronized (GifCache.class) { - int arraySize = getByteArraySizeInKB(byteArray); - int available = getAvailableMemory(); - Logger.v( - "CTInAppNotification.GifCache: gif size: " + arraySize + "KB. Available mem: " + available - + "KB."); - if (arraySize > getAvailableMemory()) { - Logger.v("CTInAppNotification.GifCache: insufficient memory to add gif: " + key); - return false; - } - mMemoryCache.put(key, byteArray); - Logger.v("CTInAppNotification.GifCache: added gif for key: " + key); - } - } - return true; - } - - static byte[] getByteArray(String key) { - synchronized (GifCache.class) { - return mMemoryCache == null ? null : mMemoryCache.get(key); - } - } - - static void init() { - synchronized (GifCache.class) { - if (mMemoryCache == null) { - Logger.v("CTInAppNotification.GifCache: init with max device memory: " + maxMemory - + "KB and allocated cache size: " + cacheSize + "KB"); - try { - mMemoryCache = new LruCache(cacheSize) { - @Override - protected int sizeOf(String key, byte[] byteArray) { - // The cache size will be measured in kilobytes rather than - // number of items. - int size = getByteArraySizeInKB(byteArray); - Logger.v("CTInAppNotification.GifCache: have gif of size: " + size + "KB for key: " - + key); - return size; - } - }; - } catch (Throwable t) { - Logger.v("CTInAppNotification.GifCache: unable to initialize cache: ", t.getCause()); - } - } - } - } - - static void removeByteArray(String key) { - synchronized (GifCache.class) { - if (mMemoryCache == null) { - return; - } - mMemoryCache.remove(key); - Logger.v("CTInAppNotification.GifCache: removed gif for key: " + key); - cleanup(); - } - } - - private static void cleanup() { - synchronized (GifCache.class) { - if (isEmpty()) { - Logger.v("CTInAppNotification.GifCache: cache is empty, removing it"); - mMemoryCache = null; - } - } - } - - private static int getAvailableMemory() { - synchronized (GifCache.class) { - return mMemoryCache == null ? 0 : cacheSize - mMemoryCache.size(); - } - } - - private static int getByteArraySizeInKB(byte[] byteArray) { - return byteArray.length / 1024; - } - - private static boolean isEmpty() { - synchronized (GifCache.class) { - return mMemoryCache.size() <= 0; - } - } - } - interface CTInAppNotificationListener { void notificationReady(CTInAppNotification inAppNotification); @@ -369,8 +266,8 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeLong(timeToLive); } - void didDismiss() { - removeImageOrGif(); + void didDismiss(InAppResourceProvider resourceProvider) { + removeImageOrGif(resourceProvider); } String getBackgroundColor() { @@ -409,10 +306,6 @@ public boolean fallBackToNotificationSettings() { return fallBackToNotificationSettings; } - byte[] getGifByteArray(CTInAppNotificationMedia inAppMedia) { - return GifCache.getByteArray(inAppMedia.getCacheKey()); - } - int getHeight() { return height; } @@ -425,10 +318,6 @@ String getHtml() { return html; } - Bitmap getImage(CTInAppNotificationMedia inAppMedia) { - return ImageCache.getBitmap(inAppMedia.getCacheKey()); - } - CTInAppNotificationMedia getInAppMediaForOrientation(int orientation) { CTInAppNotificationMedia returningMedia = null; for (CTInAppNotificationMedia inAppNotificationMedia : this.mediaList) { @@ -541,45 +430,23 @@ boolean isTablet() { return isTablet; } - void prepareForDisplay() { + void prepareForDisplay(InAppResourceProvider inAppResourceProvider) { for (CTInAppNotificationMedia media : this.mediaList) { if (media.isGIF()) { - GifCache.init(); - if (this.getGifByteArray(media) != null) { + byte[] bytes = inAppResourceProvider.fetchInAppGif(media.getMediaUrl()); + if (bytes != null && bytes.length > 0) { listener.notificationReady(this); - return; - } - - if (media.getMediaUrl() != null) { - Logger.v("CTInAppNotification: downloading GIF :" + media.getMediaUrl()); - byte[] gifByteArray = Utils.getByteArrayFromImageURL(media.getMediaUrl()); - if (gifByteArray != null) { - Logger.v("GIF Downloaded from url: " + media.getMediaUrl()); - if (!GifCache.addByteArray(media.getCacheKey(), gifByteArray)) { - this.error = "Error processing GIF"; - } - } + } else { + this.error = "Error processing GIF"; } } else if (media.isImage()) { - ImageCache.init(); - if (this.getImage(media) != null) { - listener.notificationReady(this); - return; - } - if (media.getMediaUrl() != null) { - Logger.v("CTInAppNotification: downloading Image :" + media.getMediaUrl()); - Bitmap imageBitmap = Utils.getBitmapFromURL(media.getMediaUrl()); - if (imageBitmap != null) { - Logger.v("Image Downloaded from url: " + media.getMediaUrl()); - if (!ImageCache.addBitmap(media.getCacheKey(), imageBitmap)) { - this.error = "Error processing image"; - } - } else { - Logger.d("Image Bitmap is null"); - this.error = "Error processing image as bitmap was NULL"; - } + Bitmap bitmap = inAppResourceProvider.fetchInAppImage(media.getMediaUrl(), Bitmap.class); + if (bitmap != null) { + listener.notificationReady(this); + } else { + this.error = "Error processing image as bitmap was NULL"; } } else if (media.isVideo() || media.isAudio()) { if (!this.videoSupported) { @@ -767,15 +634,16 @@ private void legacyConfigureWithJson(JSONObject jsonObject) { } } - private void removeImageOrGif() { + private void removeImageOrGif(InAppResourceProvider resourceProvider) { for (CTInAppNotificationMedia inAppMedia : this.mediaList) { - if (inAppMedia.getMediaUrl() != null && inAppMedia.getCacheKey() != null) { - if (!inAppMedia.getContentType().equals("image/gif")) { - ImageCache.removeBitmap(inAppMedia.getCacheKey(), false); - Logger.v("Deleted image - " + inAppMedia.getCacheKey()); + String mediaUrl = inAppMedia.getMediaUrl(); + if (mediaUrl != null) { + if (inAppMedia.isImage()) { + resourceProvider.deleteImage(mediaUrl); + Logger.v("Deleted image - " + mediaUrl); } else { - GifCache.removeByteArray(inAppMedia.getCacheKey()); - Logger.v("Deleted GIF - " + inAppMedia.getCacheKey()); + resourceProvider.deleteGif(mediaUrl); + Logger.v("Deleted GIF - " + mediaUrl); } } } diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/InAppController.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/InAppController.java index 2b638ffbb..9215b8912 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/InAppController.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/InAppController.java @@ -39,6 +39,7 @@ import com.clevertap.android.sdk.inapp.data.InAppServerSide; import com.clevertap.android.sdk.inapp.evaluation.EvaluationManager; import com.clevertap.android.sdk.inapp.evaluation.LimitAdapter; +import com.clevertap.android.sdk.inapp.images.InAppResourceProvider; import com.clevertap.android.sdk.task.CTExecutorFactory; import com.clevertap.android.sdk.task.MainLooperHandler; import com.clevertap.android.sdk.task.Task; @@ -89,7 +90,7 @@ public void run() { return; } inAppNotification.listener = inAppControllerWeakReference.get(); - inAppNotification.prepareForDisplay(); + inAppNotification.prepareForDisplay(resourceProvider); } } @@ -136,6 +137,8 @@ int intValue() { private final Logger logger; + private InAppResourceProvider resourceProvider; + private final MainLooperHandler mainLooperHandler; private final InAppQueue inAppQueue; @@ -172,6 +175,7 @@ public InAppController(Context context, this.coreMetaData = coreMetaData; this.inAppState = InAppState.RESUMED; this.deviceInfo = deviceInfo; + this.resourceProvider = new InAppResourceProvider(context, logger); this.inAppQueue = inAppQueue; this.evaluationManager = evaluationManager; onAppLaunchEventSent = () -> { @@ -327,7 +331,8 @@ public void inAppNotificationDidClick(CTInAppNotification inAppNotification, Bun @Override public void inAppNotificationDidDismiss(final Context context, final CTInAppNotification inAppNotification, final Bundle formData) { - inAppNotification.didDismiss(); + inAppNotification.didDismiss(resourceProvider); + if (controllerManager.getInAppFCManager() != null) { controllerManager.getInAppFCManager().didDismiss(inAppNotification); logger.verbose(config.getAccountId(), "InApp Dismissed: " + inAppNotification.getCampaignId()); diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/images/Extensions.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/images/Extensions.kt new file mode 100644 index 000000000..56d84d51d --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/images/Extensions.kt @@ -0,0 +1,16 @@ +package com.clevertap.android.sdk.inapp.images + +import android.graphics.BitmapFactory +import java.io.File + +fun File?.hasValidBitmap() : Boolean { + if (this == null || this.exists().not()) { + return false + } + val options = BitmapFactory.Options().also { + + } + options.inJustDecodeBounds = true + BitmapFactory.decodeFile(this.path, options) + return options.outWidth != -1 && options.outHeight != -1 +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/images/InAppImageFetchApi.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/images/InAppImageFetchApi.kt new file mode 100644 index 000000000..1ea9e8d9b --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/images/InAppImageFetchApi.kt @@ -0,0 +1,15 @@ +package com.clevertap.android.sdk.inapp.images + +import com.clevertap.android.sdk.bitmap.BitmapDownloadRequest +import com.clevertap.android.sdk.bitmap.HttpBitmapLoader +import com.clevertap.android.sdk.network.DownloadedBitmap + +object InAppImageFetchApi { + fun makeApiCallForInAppBitmap(url: String): DownloadedBitmap { + val request = BitmapDownloadRequest(url) + return HttpBitmapLoader.getHttpBitmap( + bitmapOperation = HttpBitmapLoader.HttpBitmapOperation.DOWNLOAD_INAPP_BITMAP, + bitmapDownloadRequest = request + ) + } +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/images/InAppImagePreloader.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/images/InAppImagePreloader.kt new file mode 100644 index 000000000..8f10f0fc0 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/images/InAppImagePreloader.kt @@ -0,0 +1,68 @@ +package com.clevertap.android.sdk.inapp.images + +import android.graphics.Bitmap +import com.clevertap.android.sdk.ILogger +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.Deferred +import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.launch + +internal class InAppImagePreloader( + private val inAppImageProvider: InAppResourceProvider, + private val dispatchers: DispatcherProvider = CtDefaultDispatchers(), + private val logger: ILogger? = null, + private val config: InAppImagePreloadConfig = InAppImagePreloadConfig.default() +) { + + private var job: Job? = null + + fun preloadImages(urls: List) { + + val handler = CoroutineExceptionHandler { _, throwable -> + logger?.verbose("Cancelled image pre fetch \n ${throwable.stackTrace}") + } + val scope = CoroutineScope(dispatchers.io()) + job = scope.launch(context = handler) { + val list = mutableListOf>() + urls.chunked( + config.parallelDownloads + ).forEach { chunks -> + logger?.verbose("Downloading image chunk with size ${chunks.size}") + chunks.forEach { url -> + if (inAppImageProvider.isCached(url = url).not()) { + // start async download if not found in cache + val async: Deferred = async { + val bitmap = inAppImageProvider.fetchInAppImage(url, Bitmap::class.java) + bitmap + } + list.add(async) + } else { + logger?.verbose("Found cached image for $url") + } + } + list.awaitAll() + } + } + } + + fun cleanup() { + job?.cancel() + } +} + +internal data class InAppImagePreloadConfig( + val parallelDownloads: Int, +) { + companion object { + private const val DEFAULT_PARALLEL_DOWNLOAD = 4 + + fun default() : InAppImagePreloadConfig = InAppImagePreloadConfig( + parallelDownloads = DEFAULT_PARALLEL_DOWNLOAD + ) + } +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/images/InAppResourceProvider.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/images/InAppResourceProvider.kt new file mode 100644 index 000000000..6f6e47890 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/images/InAppResourceProvider.kt @@ -0,0 +1,181 @@ +package com.clevertap.android.sdk.inapp.images + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import com.clevertap.android.sdk.ILogger +import com.clevertap.android.sdk.inapp.images.InAppImageFetchApi.makeApiCallForInAppBitmap +import com.clevertap.android.sdk.network.DownloadedBitmap +import com.clevertap.android.sdk.utils.CTCaches +import java.io.ByteArrayOutputStream + +internal class InAppResourceProvider( + val context: Context, + val logger: ILogger? = null +) { + + private val ctCaches = CTCaches.instance(logger = logger) + + fun saveImage(cacheKey: String, bitmap: Bitmap, bytes: ByteArray) { + + val imageMemoryCache = ctCaches.imageCache() + imageMemoryCache.add(cacheKey, bitmap) + + val imageDiskCache = ctCaches.imageCacheDisk(context = context) + imageDiskCache.add(cacheKey, bytes) + } + + fun saveGif(cacheKey: String, bytes: ByteArray) { + val gifMemoryCache = ctCaches.gifCache() + gifMemoryCache.add(cacheKey, bytes) + + val gifDiskCache = ctCaches.gifCacheDisk(context = context) + gifDiskCache.add(cacheKey, bytes) + } + + fun isCached(url: String) : Boolean { + val imageMemoryCache = ctCaches.imageCache() + + if (imageMemoryCache.get(url) != null) { + return true + } + + val imageDiskCache = ctCaches.imageCacheDisk(context = context) + val file = imageDiskCache.get(url) + + return (file != null) + } + + fun cachedImage(cacheKey: String?): Bitmap? { + + if (cacheKey == null) { + logger?.verbose("Bitmap for null key requested") + return null + } + + // Try in memory + val imageMemoryCache = ctCaches.imageCache() + val bitmap = imageMemoryCache.get(cacheKey) + + if (bitmap != null) { + return bitmap + } + + // Try disk + val imageDiskCache = ctCaches.imageCacheDisk(context = context) + val file = imageDiskCache.get(cacheKey) + + if (file != null && file.hasValidBitmap()) { + return BitmapFactory.decodeFile(file.absolutePath) + } + logger?.verbose("cached image not present for url : $cacheKey") + return null + } + + fun cachedGif(cacheKey: String?): ByteArray? { + if (cacheKey == null) { + logger?.verbose("GIF for null key requested") + return null + } + // Try in memory + val gifMemoryCache = ctCaches.gifCache() + val gifStream = gifMemoryCache.get(cacheKey) + + if (gifStream != null) { + return gifStream + } + + val gifDiskCache = ctCaches.gifCacheDisk(context = context) + + return gifDiskCache.get(cacheKey)?.readBytes() + } + + /** + * 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. + */ + fun fetchInAppImage(url: String, clazz: Class): T? { + + val cachedImage: Bitmap? = cachedImage(url) + + if (cachedImage != null) { + return if (clazz.isAssignableFrom(Bitmap::class.java)) { + 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 + } + } + + val downloadedBitmap = makeApiCallForInAppBitmap(url = url) + + when (downloadedBitmap.status) { + + DownloadedBitmap.Status.SUCCESS -> { + saveImage( + cacheKey = url, + bitmap = downloadedBitmap.bitmap!!, + bytes = downloadedBitmap.bytes!! + ) + } + else -> { + logger?.verbose("There was a problem fetching data for bitmap") + return null + } + } + + return if (clazz.isAssignableFrom(Bitmap::class.java)) { + downloadedBitmap.bitmap as? T + } else if (clazz.isAssignableFrom(ByteArray::class.java)) { + downloadedBitmap.bytes as? T + } else { + null + } + } + + fun fetchInAppGif(url: String) : ByteArray? { + val cachedGif = cachedGif(url) + + if (cachedGif != null) { + logger?.verbose("Returning requested $url gif from cache with size ${cachedGif.size}") + return cachedGif + } + + val downloadedGif = makeApiCallForInAppBitmap(url = url) + + return when (downloadedGif.status) { + + DownloadedBitmap.Status.SUCCESS -> { + saveGif(cacheKey = url, bytes = downloadedGif.bytes!!) + logger?.verbose("Returning requested $url gif with network, saved in cache") + downloadedGif.bytes + } + + else -> { + logger?.verbose("There was a problem fetching data for bitmap, status:${downloadedGif.status}") + null + } + } + + } + + fun deleteImage(cacheKey: String) { + val imageMemoryCache = ctCaches.imageCache() + imageMemoryCache.remove(cacheKey) + + val imageDiskCache = ctCaches.imageCacheDisk(context = context) + imageDiskCache.remove(cacheKey) + } + + fun deleteGif(cacheKey: String) { + val imageMemoryCache = ctCaches.gifCache() + imageMemoryCache.remove(cacheKey) + + val imageDiskCache = ctCaches.gifCacheDisk(context = context) + imageDiskCache.remove(cacheKey) + } +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/network/DownloadedBitmap.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/network/DownloadedBitmap.kt index 3c308dc67..c89690c43 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/network/DownloadedBitmap.kt +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/network/DownloadedBitmap.kt @@ -9,10 +9,11 @@ import android.graphics.Bitmap * @property status The status of the downloaded bitmap. * @property downloadTime The time taken to download the bitmap, in milliseconds. */ -data class DownloadedBitmap( - var bitmap: Bitmap?, - var status: Status, - var downloadTime: Long +data class DownloadedBitmap constructor( + val bitmap: Bitmap?, + val status: Status, + val downloadTime: Long, + val bytes: ByteArray? = null ) { /** @@ -28,4 +29,26 @@ data class DownloadedBitmap( INIT_ERROR("INIT_ERROR"), SIZE_LIMIT_EXCEEDED("SIZE_LIMIT_EXCEEDED") } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as DownloadedBitmap + + if (bitmap != other.bitmap) return false + if (status != other.status) return false + if (downloadTime != other.downloadTime) return false + if (!bytes.contentEquals(other.bytes)) return false + + return true + } + + override fun hashCode(): Int { + var result = bitmap?.hashCode() ?: 0 + result = 31 * result + status.hashCode() + result = 31 * result + downloadTime.hashCode() + result = 31 * result + bytes.contentHashCode() + return result + } } diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/network/DownloadedBitmapFactory.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/network/DownloadedBitmapFactory.kt index 6988ef65e..bedc7779e 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/network/DownloadedBitmapFactory.kt +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/network/DownloadedBitmapFactory.kt @@ -27,7 +27,16 @@ object DownloadedBitmapFactory { * @param downloadTime The time taken for the download operation in millis. * @return The DownloadedBitmap object with the specified bitmap, success status, and download time. */ - fun successBitmap(bitmap: Bitmap, downloadTime: Long): DownloadedBitmap { - return DownloadedBitmap(bitmap, SUCCESS, downloadTime) + fun successBitmap( + bitmap: Bitmap, + downloadTime: Long, + data: ByteArray? = null + ): DownloadedBitmap { + return DownloadedBitmap( + bitmap = bitmap, + status = SUCCESS, + downloadTime = downloadTime, + bytes = data + ) } } diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/CTCaches.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/CTCaches.kt new file mode 100644 index 000000000..02eb40119 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/CTCaches.kt @@ -0,0 +1,126 @@ +package com.clevertap.android.sdk.utils + +import android.content.Context +import android.graphics.Bitmap +import com.clevertap.android.sdk.ILogger +import kotlin.math.max + +/** + * We have 2 caches in CT, image cache and a gif cache with different size configs + */ +// todo locking should be individual cache based +class CTCaches private constructor( + val config: CTCachesConfig = CTCachesConfig.DEFAULT_CONFIG, + val logger: ILogger? = null +) { + + companion object { + private var ctCaches: CTCaches? = null + + private const val IMAGE_DIRECTORY_NAME = "CleverTap.Images." + private const val GIF_DIRECTORY_NAME = "CleverTap.Gif." + + fun instance( + logger: ILogger? + ) : CTCaches { + synchronized(this) { + if (ctCaches == null) { + ctCaches = CTCaches(logger = logger) + } + return ctCaches!! + } + } + } + + private var imageCache: LruCache? = null + private var gifCache: LruCache? = null + + private var imageFileCache: FileCache? = null + private var gifFileCache: FileCache? = null + + fun imageCache(): LruCache { + synchronized(this) { + if (imageCache == null) { + imageCache = LruCache(maxSize = imageCacheSize()) + } + return imageCache!! + } + } + + fun gifCache(): LruCache { + synchronized(this) { + if (gifCache == null) { + gifCache = LruCache(maxSize = gifCacheSize()) + } + return gifCache!! + } + } + + fun imageCacheDisk(context: Context): FileCache { + synchronized(this) { + if (imageFileCache == null) { + imageFileCache = FileCache( + directory = context.getDir("images", Context.MODE_PRIVATE), + maxFileSizeKb = config.maxImageSizeDiskKb.toInt(), + logger = logger + ) + } + return imageFileCache!! + } + } + + fun gifCacheDisk(context: Context): FileCache { + synchronized(this) { + if (gifFileCache == null) { + gifFileCache = FileCache( + directory = context.getDir("gifs", Context.MODE_PRIVATE), + maxFileSizeKb = config.maxImageSizeDiskKb.toInt(), + logger = logger + ) + } + return gifFileCache!! + } + } + + private fun imageCacheSize(): Int { + val selected = max(config.optimistic, config.minImageCacheKb).toInt() + + logger?.verbose("Image cache:: max-mem/1024 = ${config.optimistic}, minCacheSize = ${config.minImageCacheKb}, selected = $selected") + + return selected + } + + private fun gifCacheSize(): Int { + val selected = max(config.optimistic, config.minImageCacheKb).toInt() + + logger?.verbose(" Gif cache:: max-mem/1024 = ${config.optimistic}, minCacheSize = ${config.minImageCacheKb}, selected = $selected") + + return selected + } + + fun freeMemory() { + synchronized(this) { + imageCache?.empty() + imageCache = null + gifCache?.empty() + gifCache = null + } + } + +} + +data class CTCachesConfig( + val minImageCacheKb: Long, + val minGifCacheKb: Long, + val optimistic: Long, + val maxImageSizeDiskKb: Long +) { + companion object { + val DEFAULT_CONFIG = CTCachesConfig( + minImageCacheKb = 20 * 1024, + minGifCacheKb = 5 * 1024, + optimistic = Runtime.getRuntime().maxMemory() / (1024 * 32), + maxImageSizeDiskKb = 5 * 1024 + ) + } +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/Cache.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/Cache.kt new file mode 100644 index 000000000..a2371d125 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/Cache.kt @@ -0,0 +1,54 @@ +package com.clevertap.android.sdk.utils + +import android.graphics.Bitmap +import android.util.LruCache +import androidx.core.util.lruCache + +class LruCache( + private val maxSize: Int +) { + + companion object { + const val TYPE_LRU = "TYPE_LRU" + + } + + private val memoryCache: LruCache = lruCache( + maxSize = maxSize, + sizeOf = { _, v -> + return@lruCache v.sizeInKb() + } + ) + + fun add(key: String, value: T) : Boolean { + if (value.sizeInKb() > maxSize) { + return false + } + memoryCache.put(key, value) + return true + } + + fun get(key: String): T? { + return memoryCache.get(key) + } + + fun remove(key: String): T? { + return memoryCache.remove(key) + } + + fun empty() { + memoryCache.evictAll() + } +} + +fun Any?.sizeInKb() : Int = when (this) { + is Bitmap -> { + byteCount / 1024 + } + is ByteArray -> { + size / 1024 + } + else -> { + 1 + } +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/CtDefaultDispatchers.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/CtDefaultDispatchers.kt new file mode 100644 index 000000000..759576d6a --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/CtDefaultDispatchers.kt @@ -0,0 +1,12 @@ +package com.clevertap.android.sdk.utils + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers + +class CtDefaultDispatchers: DispatcherProvider { + override fun io(): CoroutineDispatcher = Dispatchers.IO + + override fun main(): CoroutineDispatcher = Dispatchers.Main + + override fun processing(): CoroutineDispatcher = Dispatchers.Unconfined +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/DispatcherProvider.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/DispatcherProvider.kt new file mode 100644 index 000000000..2401232ad --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/DispatcherProvider.kt @@ -0,0 +1,12 @@ +package com.clevertap.android.sdk.utils + +import kotlinx.coroutines.CoroutineDispatcher + +interface DispatcherProvider { + + fun io() : CoroutineDispatcher + + fun main(): CoroutineDispatcher + + fun processing(): CoroutineDispatcher +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/FileCache.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/FileCache.kt new file mode 100644 index 000000000..d7620de66 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/FileCache.kt @@ -0,0 +1,70 @@ +package com.clevertap.android.sdk.utils + +import com.clevertap.android.sdk.ILogger +import java.io.File +import java.io.FileOutputStream +import java.lang.Exception + +class FileCache( + private val directory: File, + private val maxFileSizeKb: Int, + private val logger: ILogger? = null, + private val hashFunction: (key: String) -> String = UrlHashGenerator.hash() +) { + + companion object { + //private const val DIGEST_ALGO = "SHA256" + private const val FILE_PREFIX = "CT_FILE" + } + + fun add(key: String, value: ByteArray) : Boolean { + if (value.sizeInKb() > maxFileSizeKb) { + return false + } + val file = fetchFile(key) + + if (file.exists()) { + file.delete() + } + try { + val newFile = fetchFile(key) + val os = FileOutputStream(newFile) + os.write(value) + os.close() + } catch (e: Exception) { + logger?.verbose("Error in saving data to file", e) + return false + } + + return true + } + + fun get(key: String): File? { + val file = fetchFile(key) + + return if (file.exists()) { + file + } else { + null + } + } + + fun remove(key: String): Boolean { + val file = fetchFile(key) + return if (file.exists()) { + file.delete() + true + } else { + false + } + } + + fun empty() : Boolean { + return directory.deleteRecursively() + } + + private fun fetchFile(key: String) : File { + val filePath = "${directory}/${FILE_PREFIX}_${hashFunction(key)}" + return File(filePath) + } +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/ImageCache.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/ImageCache.java deleted file mode 100644 index 5d2d59491..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/ImageCache.java +++ /dev/null @@ -1,241 +0,0 @@ -package com.clevertap.android.sdk.utils; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.util.Base64; -import android.util.LruCache; -import com.clevertap.android.sdk.Logger; -import com.clevertap.android.sdk.Utils; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -public class ImageCache { - - private static final int MIN_CACHE_SIZE = 1024 * 20; // 20mb minimum (in KB) - - private final static int maxMemory = (int) (Runtime.getRuntime().maxMemory()) / 1024; - - private final static int cacheSize = Math.max((maxMemory / 32), MIN_CACHE_SIZE); - - private static final int MAX_BITMAP_SIZE = 10000000; // 10 MB - - private static final String DIRECTORY_NAME = "CleverTap.Images."; - - private static final String FILE_PREFIX = "CT_IMAGE_"; - - private static LruCache memoryCache; - - private static File imageFileDirectory; - - private static MessageDigest messageDigest; - - @SuppressWarnings("WeakerAccess") - // only adds to mem cache, use getForFetchBitmap for disk cache support - public static boolean addBitmap(String key, Bitmap bitmap) { - if (memoryCache == null) { - return false; - } - if (getBitmapFromMemCache(key) == null) { - synchronized (ImageCache.class) { - int imageSize = getImageSizeInKB(bitmap); - int available = getAvailableMemory(); - Logger.v( - "CleverTap.ImageCache: image size: " + imageSize + "KB. Available mem: " + available + "KB."); - if (imageSize > getAvailableMemory()) { - Logger.v("CleverTap.ImageCache: insufficient memory to add image: " + key); - return false; - } - memoryCache.put(key, bitmap); - Logger.v("CleverTap.ImageCache: added image for key: " + key); - } - } - return true; - } - - // only checks mem cache and will not load a missing image, use getForFetchBitmap for loading and disk cache support - @SuppressWarnings("WeakerAccess") - public static Bitmap getBitmap(String key) { - synchronized (ImageCache.class) { - if (key != null) { - return memoryCache == null ? null : memoryCache.get(key); - } else { - return null; - } - } - } - - // potentially blocking, will always persist to the file system. for mem cache only use addBitmap + getBitmap - public static Bitmap getOrFetchBitmap(String url) { - Bitmap bitmap = getBitmap(url); - if (bitmap == null) { - final File imageFile = getOrFetchAndWriteImageFile(url); - if (imageFile != null) { - bitmap = decodeImageFromFile(imageFile); - addBitmap(url, bitmap); - } else { - return null; - } - } - return bitmap; - } - - public static void init() { - synchronized (ImageCache.class) { - if (memoryCache == null) { - Logger.v("CleverTap.ImageCache: init with max device memory: " + maxMemory - + "KB and allocated cache size: " + cacheSize + "KB"); - try { - memoryCache = new LruCache(cacheSize) { - @Override - protected int sizeOf(String key, Bitmap bitmap) { - // The cache size will be measured in kilobytes rather than - // number of items. - int size = getImageSizeInKB(bitmap); - Logger.v("CleverTap.ImageCache: have image of size: " + size + "KB for key: " + key); - return size; - } - }; - } catch (Throwable t) { - Logger.v("CleverTap.ImageCache: unable to initialize cache: ", t.getCause()); - } - } - } - } - - public static void initWithPersistence(Context context) { - synchronized (ImageCache.class) { - if (imageFileDirectory == null) { - imageFileDirectory = context.getDir(DIRECTORY_NAME, Context.MODE_PRIVATE); - } - if (messageDigest == null) { - try { - messageDigest = MessageDigest.getInstance("SHA256"); - } catch (NoSuchAlgorithmException e) { - Logger.d( - "CleverTap.ImageCache: image file system caching unavailable as SHA1 hash function not available on platform"); - } - } - } - init(); - } - - public static void removeBitmap(String key, boolean isPersisted) { - synchronized (ImageCache.class) { - if (isPersisted) { - removeFromFileSystem(key); - } - if (memoryCache == null) { - return; - } - memoryCache.remove(key); - Logger.v("CleverTap.ImageCache: removed image for key: " + key); - cleanup(); - } - } - - private static void cleanup() { - synchronized (ImageCache.class) { - if (isEmpty()) { - Logger.v("CTInAppNotification.ImageCache: cache is empty, removing it"); - memoryCache = null; - } - } - } - - private static Bitmap decodeImageFromFile(File file) { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = false; - BitmapFactory.decodeFile(file.getAbsolutePath(), options); - float imageSize = (float) options.outHeight * options.outWidth * 4; - float imageSizeKb = imageSize / 1024; - if (imageSizeKb > getAvailableMemory()) { - Logger.v("CleverTap.ImageCache: image too large to decode"); - return null; - } - Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath()); - if (bitmap == null) { - //noinspection ResultOfMethodCallIgnored - file.delete(); - } - return bitmap; - } - - private static int getAvailableMemory() { - synchronized (ImageCache.class) { - return memoryCache == null ? 0 : cacheSize - memoryCache.size(); - } - } - - private static Bitmap getBitmapFromMemCache(String key) { - if (key != null) { - return memoryCache == null ? null : memoryCache.get(key); - } - return null; - } - - private static File getFile(String url) { - if (messageDigest == null) { - return null; - } - final byte[] hashed = messageDigest.digest(url.getBytes()); - final String safeName = FILE_PREFIX + Base64.encodeToString(hashed, Base64.URL_SAFE | Base64.NO_WRAP); - return new File(imageFileDirectory, safeName); - } - - private static int getImageSizeInKB(Bitmap bitmap) { - return bitmap.getByteCount() / 1024; - } - - // will do a blocking network fetch if file does not already exist - private static File getOrFetchAndWriteImageFile(String url) { - final File file = getFile(url); - byte[] bytes; - if (file == null || !file.exists()) { - bytes = Utils.getByteArrayFromImageURL(url); // blocking network operation - if (bytes != null) { - if (file != null && bytes.length < MAX_BITMAP_SIZE) { - OutputStream out = null; - try { - out = new FileOutputStream(file); - out.write(bytes); - } catch (FileNotFoundException e) { - Logger.v("CleverTap.ImageCache: error writing image file", e); - return null; - } catch (IOException e) { - Logger.v("CleverTap.ImageCache: error writing image file", e); - return null; - } finally { - if (out != null) { - try { - out.close(); - } catch (IOException e) { - Logger.v("CleverTap.ImageCache: error closing image output file", e); - } - } - } - } - } - } - return file; - } - - private static boolean isEmpty() { - synchronized (ImageCache.class) { - return memoryCache.size() <= 0; - } - } - - private static void removeFromFileSystem(String url) { - final File file = getFile(url); - if (file != null && file.exists()) { - //noinspection ResultOfMethodCallIgnored - file.delete(); - } - } -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/UrlHashGenerator.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/UrlHashGenerator.kt new file mode 100644 index 000000000..7396b86ff --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/UrlHashGenerator.kt @@ -0,0 +1,20 @@ +package com.clevertap.android.sdk.utils + +import java.util.UUID + +/** + * Returns unique UUID for a url key, if there is failure in generating uuid then we simply call + * hashcode for the url + */ +object UrlHashGenerator { + + fun hash() : (key: String) -> String = { key -> + var nameUUIDFromBytes: UUID? = null + try { + nameUUIDFromBytes = UUID.nameUUIDFromBytes(key.toByteArray()) + } catch (e: InternalError) { + key.hashCode().toString() + } + nameUUIDFromBytes?.toString()?: key.hashCode().toString() + } +} \ No newline at end of file diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/TestLogger.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/TestLogger.kt new file mode 100644 index 000000000..b69f045a1 --- /dev/null +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/TestLogger.kt @@ -0,0 +1,51 @@ +package com.clevertap.android.sdk + +class TestLogger : ILogger { + override fun debug(message: String?) { + println("$message") + } + + override fun debug(suffix: String?, message: String?) { + println("$suffix - $message") + } + + override fun debug(suffix: String?, message: String?, t: Throwable?) { + println("$suffix - $message - ${t?.printStackTrace()}") + } + + override fun debug(message: String?, t: Throwable?) { + println("$message - ${t?.printStackTrace()}") + } + + override fun info(message: String?) { + println("$message") + } + + override fun info(suffix: String?, message: String?) { + println("$suffix - $message") + } + + override fun info(suffix: String?, message: String?, t: Throwable?) { + println("$suffix - $message - ${t?.printStackTrace()}") + } + + override fun info(message: String?, t: Throwable?) { + println("$message - ${t?.printStackTrace()}") + } + + override fun verbose(message: String?) { + println("$message") + } + + override fun verbose(suffix: String?, message: String?) { + println("$suffix - $message") + } + + override fun verbose(suffix: String?, message: String?, t: Throwable?) { + println("$suffix - $message - ${t?.printStackTrace()}") + } + + override fun verbose(message: String?, t: Throwable?) { + println("$message - ${t?.printStackTrace()}") + } +} \ No newline at end of file diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/UtilsTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/UtilsTest.kt index 5543136a9..52ee0ef8e 100644 --- a/clevertap-core/src/test/java/com/clevertap/android/sdk/UtilsTest.kt +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/UtilsTest.kt @@ -281,21 +281,6 @@ class UtilsTest : BaseTestCase() { //------------------------------------------------------------------------------------ - @Test - fun test_getByteArrayFromImageURL_when_CorrectImageLinkArePassed_should_ReturnImageByteArray() { - val url2 = "https://www.freedesktop.org/wiki/logo1.png" - val array2: ByteArray? = Utils.getByteArrayFromImageURL(url2) - println(" downloaded an array2 of size ${array2?.size} bytes ") - assertNull(array2) - - val url = "https://www.freedesktop.org/wiki/logo.png" - val array: ByteArray? = Utils.getByteArrayFromImageURL(url) - println(" downloaded an array of size ${array?.size} bytes ,") - assertNotNull(array) - } - - //------------------------------------------------------------------------------------ - @Test fun test_getCurrentNetworkType_when_FunctionIsCalledWithContext_should_ReturnNetworkType() { // if context is null, network type will be unavailable diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/utils/FileCacheTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/utils/FileCacheTest.kt new file mode 100644 index 000000000..128c36242 --- /dev/null +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/utils/FileCacheTest.kt @@ -0,0 +1,82 @@ +package com.clevertap.android.sdk.utils + +import com.clevertap.android.sdk.TestLogger +import org.junit.Assert.assertEquals +import org.junit.Test +import java.io.File +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class FileCacheTest { + + private val fileCache: FileCache = FileCache( + directory = File("/"), + maxFileSizeKb = 500, + logger = TestLogger() + ) + + @Test + @Throws(Exception::class) + fun testAdd() { + val result = fileCache.add("key", byteArrayOf(0.toByte())) + assertEquals(true, result) + } + + @Test + @Throws(Exception::class) + fun `add method fails if size is greater than allowed size`() { + val largeBytes = ByteArray(1000 * 1024) { index -> + index.toByte() + } + val result = fileCache.add("key", largeBytes) + assertEquals(false, result) + } + + @Test + @Throws(Exception::class) + fun testGet() { + fileCache.add("key", byteArrayOf(0.toByte())) + val result = fileCache.get("key") + assertNotNull(result) + assertEquals(result.exists(), true) + } + + @Test + @Throws(Exception::class) + fun `file cache returns null when there is no file for url`() { + val result = fileCache.get("non-cached-key-res") + assertNull(result) + } + + @Test + @Throws(Exception::class) + fun testRemove() { + fileCache.add("key", byteArrayOf(0.toByte())) + val result = fileCache.remove("key") + assertEquals(true, result) + } + + @Test + @Throws(Exception::class) + fun `remove returns false when we try to remove file which is not there in cache`() { + val result = fileCache.remove("non-cached-key-res") + assertEquals(true, result) + } + + @Test + @Throws(Exception::class) + fun `empty deletes all files`() { + + // warm up cache + fileCache.add("key1", byteArrayOf(0.toByte())) + fileCache.add("key2", byteArrayOf(0.toByte())) + + // check it is warmed up + val get = fileCache.get("key1") + assertNotNull(get) + + // clear cache + val result = fileCache.empty() + assertEquals(true, result) + } +} \ No newline at end of file diff --git a/clevertap-pushtemplates/src/main/java/com/clevertap/android/pushtemplates/Utils.java b/clevertap-pushtemplates/src/main/java/com/clevertap/android/pushtemplates/Utils.java index 1ddb917b0..00fe5d2b6 100644 --- a/clevertap-pushtemplates/src/main/java/com/clevertap/android/pushtemplates/Utils.java +++ b/clevertap-pushtemplates/src/main/java/com/clevertap/android/pushtemplates/Utils.java @@ -12,7 +12,6 @@ import android.content.pm.ApplicationInfo; import android.content.pm.ResolveInfo; import android.graphics.Bitmap; -import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.PorterDuff; @@ -27,33 +26,31 @@ import android.text.format.DateUtils; import android.widget.RemoteViews; import android.widget.Toast; -import androidx.annotation.NonNull; + import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.core.content.ContextCompat; + import com.clevertap.android.sdk.CleverTapAPI; import com.clevertap.android.sdk.CleverTapInstanceConfig; import com.clevertap.android.sdk.Constants; import com.clevertap.android.sdk.Logger; -import com.clevertap.android.sdk.network.NetworkManager; +import com.clevertap.android.sdk.bitmap.BitmapDownloadRequest; +import com.clevertap.android.sdk.bitmap.HttpBitmapLoader; +import com.clevertap.android.sdk.network.DownloadedBitmap; import com.clevertap.android.sdk.task.CTExecutorFactory; import com.clevertap.android.sdk.task.Task; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; + +import org.json.JSONArray; +import org.json.JSONObject; + import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.concurrent.Callable; -import java.util.zip.GZIPInputStream; -import org.json.JSONArray; -import org.json.JSONObject; @SuppressWarnings("WeakerAccess") public class Utils { @@ -104,91 +101,25 @@ static Bitmap drawableToBitmap(Drawable drawable) } private static Bitmap getBitmapFromURL(String srcUrl, @Nullable Context context) { - if (context != null) { - boolean isNetworkOnline = NetworkManager.isNetworkOnline(context); - if (!isNetworkOnline) { - Logger.v("Network connectivity unavailable. Not downloading bitmap. URL was: " + srcUrl); - return null; - } - } - - // Safe bet, won't have more than three /s - srcUrl = srcUrl.replace("///", "/"); - srcUrl = srcUrl.replace("//", "/"); - srcUrl = srcUrl.replace("http:/", "http://"); - srcUrl = srcUrl.replace("https:/", "https://"); - HttpURLConnection connection = null; - try { - URL url = new URL(srcUrl); - connection = (HttpURLConnection) url.openConnection(); - connection.setDoInput(true); - connection.setUseCaches(true); - connection.addRequestProperty("Content-Type", "application/json"); - connection.addRequestProperty("Accept-Encoding", "gzip, deflate"); - connection.setConnectTimeout(Constants.PN_IMAGE_CONNECTION_TIMEOUT_IN_MILLIS); - connection.setReadTimeout(Constants.PN_IMAGE_READ_TIMEOUT_IN_MILLIS); - connection.connect(); - // expect HTTP 200 OK, so we don't mistakenly save error report - // instead of the file - if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { - PTLog.debug("File not loaded completely not going forward. URL was: " + srcUrl); - return null; - } - - // might be -1: server did not report the length - long fileLength = connection.getContentLength(); - boolean isGZipEncoded = (connection.getContentEncoding() != null && - connection.getContentEncoding().contains("gzip")); - - // download the file - InputStream input = connection.getInputStream(); - byte[] data = new byte[16384]; - ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - - long total = 0; - int count; - while ((count = input.read(data)) != -1) { - total += count; - buffer.write(data, 0, count); - } - - byte[] tmpByteArray = new byte[16384]; - long totalDownloaded = total; - - if (isGZipEncoded) { - InputStream is = new ByteArrayInputStream(buffer.toByteArray()); - ByteArrayOutputStream decompressedFile = new ByteArrayOutputStream(); - GZIPInputStream gzipInputStream = new GZIPInputStream(is); - total = 0; - int counter; - while ((counter = gzipInputStream.read(tmpByteArray)) != -1) { - total += counter; - decompressedFile.write(tmpByteArray, 0, counter); - } - if (fileLength != -1 && fileLength != totalDownloaded) { - PTLog.debug("File not loaded completely not going forward. URL was: " + srcUrl); - return null; - } - return BitmapFactory.decodeByteArray(decompressedFile.toByteArray(), 0, (int) total); - } - - if (fileLength != -1 && fileLength != totalDownloaded) { - PTLog.debug("File not loaded completely not going forward. URL was: " + srcUrl); - return null; - } - return BitmapFactory.decodeByteArray(buffer.toByteArray(), 0, (int) totalDownloaded); - } catch (IOException e) { - PTLog.verbose("Couldn't download the file. URL was: " + srcUrl); + BitmapDownloadRequest request = new BitmapDownloadRequest(srcUrl, + false, + context, + null, + -1, + -1 + ); + + DownloadedBitmap db = HttpBitmapLoader.getHttpBitmap( + HttpBitmapLoader.HttpBitmapOperation.DOWNLOAD_ANY_BITMAP, + request + ); + + if (db.getStatus() == DownloadedBitmap.Status.SUCCESS) { + return db.getBitmap(); + } else { + Logger.v("network call for bitmap download failed with url : " + srcUrl + " http status: " + db.getStatus()); return null; - } finally { - try { - if (connection != null) { - connection.disconnect(); - } - } catch (Throwable t) { - PTLog.verbose("Couldn't close connection!", t); - } } }