Browse Source
Convert BitmapUtils to kotlin and migrate MediaUploader funcs to suspend
renovate/org.robolectric-robolectric-4.x
Convert BitmapUtils to kotlin and migrate MediaUploader funcs to suspend
renovate/org.robolectric-robolectric-4.x
Ammar Githam
4 years ago
6 changed files with 358 additions and 425 deletions
-
2app/build.gradle
-
48app/src/main/java/awais/instagrabber/fragments/imageedit/FiltersFragment.java
-
70app/src/main/java/awais/instagrabber/managers/ThreadManager.kt
-
280app/src/main/java/awais/instagrabber/utils/BitmapUtils.java
-
255app/src/main/java/awais/instagrabber/utils/BitmapUtils.kt
-
128app/src/main/java/awais/instagrabber/utils/MediaUploader.kt
@ -1,280 +0,0 @@ |
|||
package awais.instagrabber.utils; |
|||
|
|||
import android.content.ContentResolver; |
|||
import android.content.Context; |
|||
import android.graphics.Bitmap; |
|||
import android.graphics.BitmapFactory; |
|||
import android.net.Uri; |
|||
import android.util.Log; |
|||
import android.util.LruCache; |
|||
|
|||
import androidx.annotation.NonNull; |
|||
import androidx.annotation.Nullable; |
|||
import androidx.core.util.Pair; |
|||
|
|||
import com.google.common.util.concurrent.FutureCallback; |
|||
import com.google.common.util.concurrent.Futures; |
|||
import com.google.common.util.concurrent.ListenableFuture; |
|||
|
|||
import java.io.File; |
|||
import java.io.FileOutputStream; |
|||
import java.io.IOException; |
|||
import java.io.InputStream; |
|||
import java.io.OutputStream; |
|||
import java.util.concurrent.ExecutorService; |
|||
import java.util.concurrent.Executors; |
|||
|
|||
public final class BitmapUtils { |
|||
private static final String TAG = BitmapUtils.class.getSimpleName(); |
|||
private static final LruCache<String, Bitmap> bitmapMemoryCache; |
|||
private static final AppExecutors appExecutors = AppExecutors.INSTANCE; |
|||
private static final ExecutorService callbackHandlers = Executors |
|||
.newCachedThreadPool(r -> new Thread(r, "bm-load-callback-handler#" + NumberUtils.random(0, 100))); |
|||
public static final float THUMBNAIL_SIZE = 200f; |
|||
|
|||
static { |
|||
// Get max available VM memory, exceeding this amount will throw an |
|||
// OutOfMemory exception. Stored in kilobytes as LruCache takes an |
|||
// int in its constructor. |
|||
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); |
|||
// Use 1/8th of the available memory for this memory cache. |
|||
final int cacheSize = maxMemory / 8; |
|||
bitmapMemoryCache = new LruCache<String, Bitmap>(cacheSize) { |
|||
@Override |
|||
protected int sizeOf(String key, Bitmap bitmap) { |
|||
// The cache size will be measured in kilobytes rather than |
|||
// number of items. |
|||
return bitmap.getByteCount() / 1024; |
|||
} |
|||
}; |
|||
|
|||
} |
|||
|
|||
public static void addBitmapToMemoryCache(final String key, final Bitmap bitmap, final boolean force) { |
|||
if (force || getBitmapFromMemCache(key) == null) { |
|||
bitmapMemoryCache.put(key, bitmap); |
|||
} |
|||
} |
|||
|
|||
public static Bitmap getBitmapFromMemCache(final String key) { |
|||
return bitmapMemoryCache.get(key); |
|||
} |
|||
|
|||
public static void getThumbnail(final Context context, final Uri uri, final ThumbnailLoadCallback callback) { |
|||
if (context == null || uri == null || callback == null) return; |
|||
final String key = uri.toString(); |
|||
final Bitmap cachedBitmap = getBitmapFromMemCache(key); |
|||
if (cachedBitmap != null) { |
|||
callback.onLoad(cachedBitmap, -1, -1); |
|||
return; |
|||
} |
|||
loadBitmap(context.getContentResolver(), uri, THUMBNAIL_SIZE, THUMBNAIL_SIZE, true, callback); |
|||
} |
|||
|
|||
/** |
|||
* Loads bitmap from given Uri |
|||
* |
|||
* @param contentResolver {@link ContentResolver} to resolve the uri |
|||
* @param uri Uri from where Bitmap will be loaded |
|||
* @param reqWidth Required width |
|||
* @param reqHeight Required height |
|||
* @param addToCache true if the loaded bitmap should be added to the mem cache |
|||
* @param callback Bitmap load callback |
|||
*/ |
|||
public static void loadBitmap(final ContentResolver contentResolver, |
|||
final Uri uri, |
|||
final float reqWidth, |
|||
final float reqHeight, |
|||
final boolean addToCache, |
|||
final ThumbnailLoadCallback callback) { |
|||
loadBitmap(contentResolver, uri, reqWidth, reqHeight, -1, addToCache, callback); |
|||
} |
|||
|
|||
/** |
|||
* Loads bitmap from given Uri |
|||
* |
|||
* @param contentResolver {@link ContentResolver} to resolve the uri |
|||
* @param uri Uri from where Bitmap will be loaded |
|||
* @param maxDimenSize Max size of the largest side of the image |
|||
* @param addToCache true if the loaded bitmap should be added to the mem cache |
|||
* @param callback Bitmap load callback |
|||
*/ |
|||
public static void loadBitmap(final ContentResolver contentResolver, |
|||
final Uri uri, |
|||
final float maxDimenSize, |
|||
final boolean addToCache, |
|||
final ThumbnailLoadCallback callback) { |
|||
loadBitmap(contentResolver, uri, -1, -1, maxDimenSize, addToCache, callback); |
|||
} |
|||
|
|||
/** |
|||
* Loads bitmap from given Uri |
|||
* |
|||
* @param contentResolver {@link ContentResolver} to resolve the uri |
|||
* @param uri Uri from where {@link Bitmap} will be loaded |
|||
* @param reqWidth Required width (set to -1 if maxDimenSize provided) |
|||
* @param reqHeight Required height (set to -1 if maxDimenSize provided) |
|||
* @param maxDimenSize Max size of the largest side of the image (set to -1 if setting reqWidth and reqHeight) |
|||
* @param addToCache true if the loaded bitmap should be added to the mem cache |
|||
* @param callback Bitmap load callback |
|||
*/ |
|||
private static void loadBitmap(final ContentResolver contentResolver, |
|||
final Uri uri, |
|||
final float reqWidth, |
|||
final float reqHeight, |
|||
final float maxDimenSize, |
|||
final boolean addToCache, |
|||
final ThumbnailLoadCallback callback) { |
|||
if (contentResolver == null || uri == null || callback == null) return; |
|||
final ListenableFuture<BitmapResult> future = appExecutors |
|||
.getTasksThread() |
|||
.submit(() -> getBitmapResult(contentResolver, uri, reqWidth, reqHeight, maxDimenSize, addToCache)); |
|||
Futures.addCallback(future, new FutureCallback<BitmapResult>() { |
|||
@Override |
|||
public void onSuccess(@Nullable final BitmapResult result) { |
|||
if (result == null) { |
|||
callback.onLoad(null, -1, -1); |
|||
return; |
|||
} |
|||
callback.onLoad(result.bitmap, result.width, result.height); |
|||
} |
|||
|
|||
@Override |
|||
public void onFailure(@NonNull final Throwable t) { |
|||
callback.onFailure(t); |
|||
} |
|||
}, callbackHandlers); |
|||
} |
|||
|
|||
@Nullable |
|||
public static BitmapResult getBitmapResult(final ContentResolver contentResolver, |
|||
final Uri uri, |
|||
final float reqWidth, |
|||
final float reqHeight, |
|||
final float maxDimenSize, |
|||
final boolean addToCache) { |
|||
BitmapFactory.Options bitmapOptions; |
|||
float actualReqWidth = reqWidth; |
|||
float actualReqHeight = reqHeight; |
|||
try (InputStream input = contentResolver.openInputStream(uri)) { |
|||
BitmapFactory.Options outBounds = new BitmapFactory.Options(); |
|||
outBounds.inJustDecodeBounds = true; |
|||
outBounds.inPreferredConfig = Bitmap.Config.ARGB_8888; |
|||
BitmapFactory.decodeStream(input, null, outBounds); |
|||
if ((outBounds.outWidth == -1) || (outBounds.outHeight == -1)) return null; |
|||
bitmapOptions = new BitmapFactory.Options(); |
|||
if (maxDimenSize > 0) { |
|||
// Raw height and width of image |
|||
final int height = outBounds.outHeight; |
|||
final int width = outBounds.outWidth; |
|||
final float ratio = (float) width / height; |
|||
if (height > width) { |
|||
actualReqHeight = maxDimenSize; |
|||
actualReqWidth = actualReqHeight * ratio; |
|||
} else { |
|||
actualReqWidth = maxDimenSize; |
|||
actualReqHeight = actualReqWidth / ratio; |
|||
} |
|||
} |
|||
bitmapOptions.inSampleSize = calculateInSampleSize(outBounds, actualReqWidth, actualReqHeight); |
|||
} catch (Exception e) { |
|||
Log.e(TAG, "loadBitmap: ", e); |
|||
return null; |
|||
} |
|||
try (InputStream input = contentResolver.openInputStream(uri)) { |
|||
bitmapOptions.inPreferredConfig = Bitmap.Config.ARGB_8888; |
|||
Bitmap bitmap = BitmapFactory.decodeStream(input, null, bitmapOptions); |
|||
if (addToCache) { |
|||
addBitmapToMemoryCache(uri.toString(), bitmap, true); |
|||
} |
|||
return new BitmapResult(bitmap, (int) actualReqWidth, (int) actualReqHeight); |
|||
} catch (Exception e) { |
|||
Log.e(TAG, "loadBitmap: ", e); |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
public static class BitmapResult { |
|||
public Bitmap bitmap; |
|||
int width; |
|||
int height; |
|||
|
|||
public BitmapResult(final Bitmap bitmap, final int width, final int height) { |
|||
this.width = width; |
|||
this.height = height; |
|||
this.bitmap = bitmap; |
|||
} |
|||
} |
|||
|
|||
private static int calculateInSampleSize(final BitmapFactory.Options options, final float reqWidth, final float reqHeight) { |
|||
// Raw height and width of image |
|||
final int height = options.outHeight; |
|||
final int width = options.outWidth; |
|||
int inSampleSize = 1; |
|||
if (height > reqHeight || width > reqWidth) { |
|||
final float halfHeight = height / 2f; |
|||
final float halfWidth = width / 2f; |
|||
// Calculate the largest inSampleSize value that is a power of 2 and keeps both |
|||
// height and width larger than the requested height and width. |
|||
while ((halfHeight / inSampleSize) >= reqHeight |
|||
&& (halfWidth / inSampleSize) >= reqWidth) { |
|||
inSampleSize *= 2; |
|||
} |
|||
} |
|||
return inSampleSize; |
|||
} |
|||
|
|||
public interface ThumbnailLoadCallback { |
|||
/** |
|||
* @param bitmap Resulting bitmap |
|||
* @param width width of the bitmap (Only correct if loadBitmap was called or -1) |
|||
* @param height height of the bitmap (Only correct if loadBitmap was called or -1) |
|||
*/ |
|||
void onLoad(@Nullable Bitmap bitmap, int width, int height); |
|||
|
|||
void onFailure(@NonNull Throwable t); |
|||
} |
|||
|
|||
/** |
|||
* Decodes the bounds of an image from its Uri and returns a pair of the dimensions |
|||
* |
|||
* @param uri the Uri of the image |
|||
* @return dimensions of the image |
|||
*/ |
|||
public static Pair<Integer, Integer> decodeDimensions(@NonNull final ContentResolver contentResolver, |
|||
@NonNull final Uri uri) throws IOException { |
|||
BitmapFactory.Options options = new BitmapFactory.Options(); |
|||
options.inJustDecodeBounds = true; |
|||
try (final InputStream stream = contentResolver.openInputStream(uri)) { |
|||
BitmapFactory.decodeStream(stream, null, options); |
|||
return (options.outWidth == -1 || options.outHeight == -1) |
|||
? null |
|||
: new Pair<>(options.outWidth, options.outHeight); |
|||
} |
|||
} |
|||
|
|||
public static File convertToJpegAndSaveToFile(@NonNull final Bitmap bitmap, @Nullable final File file) throws IOException { |
|||
File tempFile = file; |
|||
if (file == null) { |
|||
tempFile = DownloadUtils.getTempFile(); |
|||
} |
|||
try (OutputStream output = new FileOutputStream(tempFile)) { |
|||
final boolean compressResult = bitmap.compress(Bitmap.CompressFormat.JPEG, 100, output); |
|||
if (!compressResult) { |
|||
throw new RuntimeException("Compression failed!"); |
|||
} |
|||
} |
|||
return tempFile; |
|||
} |
|||
|
|||
public static void convertToJpegAndSaveToUri(@NonNull Context context, |
|||
@NonNull final Bitmap bitmap, |
|||
@NonNull final Uri uri) throws Exception { |
|||
try (OutputStream output = context.getContentResolver().openOutputStream(uri)) { |
|||
final boolean compressResult = bitmap.compress(Bitmap.CompressFormat.JPEG, 100, output); |
|||
if (!compressResult) { |
|||
throw new RuntimeException("Compression failed!"); |
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,255 @@ |
|||
package awais.instagrabber.utils |
|||
|
|||
import android.content.ContentResolver |
|||
import android.content.Context |
|||
import android.graphics.Bitmap |
|||
import android.graphics.BitmapFactory |
|||
import android.net.Uri |
|||
import android.util.Log |
|||
import android.util.LruCache |
|||
import androidx.core.util.Pair |
|||
import awais.instagrabber.utils.extensions.TAG |
|||
import kotlinx.coroutines.Dispatchers |
|||
import kotlinx.coroutines.withContext |
|||
import java.io.File |
|||
import java.io.FileOutputStream |
|||
import java.io.IOException |
|||
|
|||
object BitmapUtils { |
|||
private val bitmapMemoryCache: LruCache<String, Bitmap> |
|||
|
|||
// private val appExecutors = AppExecutors |
|||
// private val callbackHandlers = Executors |
|||
// .newCachedThreadPool { r: Runnable? -> Thread(r, "bm-load-callback-handler#" + random(0, 100)) } |
|||
const val THUMBNAIL_SIZE = 200f |
|||
|
|||
@JvmStatic |
|||
fun addBitmapToMemoryCache(key: String, bitmap: Bitmap, force: Boolean) { |
|||
if (force || getBitmapFromMemCache(key) == null) { |
|||
bitmapMemoryCache.put(key, bitmap) |
|||
} |
|||
} |
|||
|
|||
@JvmStatic |
|||
fun getBitmapFromMemCache(key: String): Bitmap? { |
|||
return bitmapMemoryCache[key] |
|||
} |
|||
|
|||
@JvmStatic |
|||
suspend fun getThumbnail(context: Context, uri: Uri): BitmapResult? { |
|||
val key = uri.toString() |
|||
val cachedBitmap = getBitmapFromMemCache(key) |
|||
if (cachedBitmap != null) { |
|||
return BitmapResult(cachedBitmap, -1, -1) |
|||
} |
|||
return loadBitmap(context.contentResolver, uri, THUMBNAIL_SIZE, THUMBNAIL_SIZE, true) |
|||
} |
|||
|
|||
/** |
|||
* Loads bitmap from given Uri |
|||
* |
|||
* @param contentResolver [ContentResolver] to resolve the uri |
|||
* @param uri Uri from where Bitmap will be loaded |
|||
* @param reqWidth Required width |
|||
* @param reqHeight Required height |
|||
* @param addToCache true if the loaded bitmap should be added to the mem cache |
|||
// * @param callback Bitmap load callback |
|||
*/ |
|||
suspend fun loadBitmap( |
|||
contentResolver: ContentResolver?, |
|||
uri: Uri?, |
|||
reqWidth: Float, |
|||
reqHeight: Float, |
|||
addToCache: Boolean, |
|||
): BitmapResult? = loadBitmap(contentResolver, uri, reqWidth, reqHeight, -1f, addToCache) |
|||
|
|||
/** |
|||
* Loads bitmap from given Uri |
|||
* |
|||
* @param contentResolver [ContentResolver] to resolve the uri |
|||
* @param uri Uri from where Bitmap will be loaded |
|||
* @param maxDimenSize Max size of the largest side of the image |
|||
* @param addToCache true if the loaded bitmap should be added to the mem cache |
|||
// * @param callback Bitmap load callback |
|||
*/ |
|||
suspend fun loadBitmap( |
|||
contentResolver: ContentResolver?, |
|||
uri: Uri?, |
|||
maxDimenSize: Float, |
|||
addToCache: Boolean, |
|||
): BitmapResult? = loadBitmap(contentResolver, uri, -1f, -1f, maxDimenSize, addToCache) |
|||
|
|||
/** |
|||
* Loads bitmap from given Uri |
|||
* |
|||
* @param contentResolver [ContentResolver] to resolve the uri |
|||
* @param uri Uri from where [Bitmap] will be loaded |
|||
* @param reqWidth Required width (set to -1 if maxDimenSize provided) |
|||
* @param reqHeight Required height (set to -1 if maxDimenSize provided) |
|||
* @param maxDimenSize Max size of the largest side of the image (set to -1 if setting reqWidth and reqHeight) |
|||
* @param addToCache true if the loaded bitmap should be added to the mem cache |
|||
// * @param callback Bitmap load callback |
|||
*/ |
|||
private suspend fun loadBitmap( |
|||
contentResolver: ContentResolver?, |
|||
uri: Uri?, |
|||
reqWidth: Float, |
|||
reqHeight: Float, |
|||
maxDimenSize: Float, |
|||
addToCache: Boolean, |
|||
): BitmapResult? = |
|||
if (contentResolver == null || uri == null) null else withContext(Dispatchers.IO) { |
|||
getBitmapResult(contentResolver, |
|||
uri, |
|||
reqWidth, |
|||
reqHeight, |
|||
maxDimenSize, |
|||
addToCache) |
|||
} |
|||
|
|||
fun getBitmapResult( |
|||
contentResolver: ContentResolver, |
|||
uri: Uri, |
|||
reqWidth: Float, |
|||
reqHeight: Float, |
|||
maxDimenSize: Float, |
|||
addToCache: Boolean, |
|||
): BitmapResult? { |
|||
var bitmapOptions: BitmapFactory.Options |
|||
var actualReqWidth = reqWidth |
|||
var actualReqHeight = reqHeight |
|||
try { |
|||
contentResolver.openInputStream(uri).use { input -> |
|||
val outBounds = BitmapFactory.Options() |
|||
outBounds.inJustDecodeBounds = true |
|||
outBounds.inPreferredConfig = Bitmap.Config.ARGB_8888 |
|||
BitmapFactory.decodeStream(input, null, outBounds) |
|||
if (outBounds.outWidth == -1 || outBounds.outHeight == -1) return null |
|||
bitmapOptions = BitmapFactory.Options() |
|||
if (maxDimenSize > 0) { |
|||
// Raw height and width of image |
|||
val height = outBounds.outHeight |
|||
val width = outBounds.outWidth |
|||
val ratio = width.toFloat() / height |
|||
if (height > width) { |
|||
actualReqHeight = maxDimenSize |
|||
actualReqWidth = actualReqHeight * ratio |
|||
} else { |
|||
actualReqWidth = maxDimenSize |
|||
actualReqHeight = actualReqWidth / ratio |
|||
} |
|||
} |
|||
bitmapOptions.inSampleSize = calculateInSampleSize(outBounds, actualReqWidth, actualReqHeight) |
|||
} |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "loadBitmap: ", e) |
|||
return null |
|||
} |
|||
try { |
|||
contentResolver.openInputStream(uri).use { input -> |
|||
bitmapOptions.inPreferredConfig = Bitmap.Config.ARGB_8888 |
|||
val bitmap = BitmapFactory.decodeStream(input, null, bitmapOptions) |
|||
if (addToCache && bitmap != null) { |
|||
addBitmapToMemoryCache(uri.toString(), bitmap, true) |
|||
} |
|||
return BitmapResult(bitmap, actualReqWidth.toInt(), actualReqHeight.toInt()) |
|||
} |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "loadBitmap: ", e) |
|||
} |
|||
return null |
|||
} |
|||
|
|||
private fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Float, reqHeight: Float): Int { |
|||
// Raw height and width of image |
|||
val height = options.outHeight |
|||
val width = options.outWidth |
|||
var inSampleSize = 1 |
|||
if (height > reqHeight || width > reqWidth) { |
|||
val halfHeight = height / 2f |
|||
val halfWidth = width / 2f |
|||
// Calculate the largest inSampleSize value that is a power of 2 and keeps both |
|||
// height and width larger than the requested height and width. |
|||
while (halfHeight / inSampleSize >= reqHeight |
|||
&& halfWidth / inSampleSize >= reqWidth |
|||
) { |
|||
inSampleSize *= 2 |
|||
} |
|||
} |
|||
return inSampleSize |
|||
} |
|||
|
|||
/** |
|||
* Decodes the bounds of an image from its Uri and returns a pair of the dimensions |
|||
* |
|||
* @param uri the Uri of the image |
|||
* @return dimensions of the image |
|||
*/ |
|||
@Throws(IOException::class) |
|||
fun decodeDimensions( |
|||
contentResolver: ContentResolver, |
|||
uri: Uri, |
|||
): Pair<Int, Int>? { |
|||
val options = BitmapFactory.Options() |
|||
options.inJustDecodeBounds = true |
|||
contentResolver.openInputStream(uri).use { stream -> |
|||
BitmapFactory.decodeStream(stream, null, options) |
|||
return if (options.outWidth == -1 || options.outHeight == -1) null else Pair(options.outWidth, options.outHeight) |
|||
} |
|||
} |
|||
|
|||
@Throws(IOException::class) |
|||
fun convertToJpegAndSaveToFile(bitmap: Bitmap, file: File?): File { |
|||
val tempFile = file ?: DownloadUtils.getTempFile() |
|||
FileOutputStream(tempFile).use { output -> |
|||
val compressResult = bitmap.compress(Bitmap.CompressFormat.JPEG, 100, output) |
|||
if (!compressResult) { |
|||
throw RuntimeException("Compression failed!") |
|||
} |
|||
} |
|||
return tempFile |
|||
} |
|||
|
|||
@JvmStatic |
|||
@Throws(Exception::class) |
|||
fun convertToJpegAndSaveToUri( |
|||
context: Context, |
|||
bitmap: Bitmap, |
|||
uri: Uri, |
|||
) { |
|||
context.contentResolver.openOutputStream(uri).use { output -> |
|||
val compressResult = bitmap.compress(Bitmap.CompressFormat.JPEG, 100, output) |
|||
if (!compressResult) { |
|||
throw RuntimeException("Compression failed!") |
|||
} |
|||
} |
|||
} |
|||
|
|||
class BitmapResult(var bitmap: Bitmap?, var width: Int, var height: Int) |
|||
|
|||
interface ThumbnailLoadCallback { |
|||
/** |
|||
* @param bitmap Resulting bitmap |
|||
* @param width width of the bitmap (Only correct if loadBitmap was called or -1) |
|||
* @param height height of the bitmap (Only correct if loadBitmap was called or -1) |
|||
*/ |
|||
fun onLoad(bitmap: Bitmap, width: Int, height: Int) |
|||
fun onFailure(t: Throwable) |
|||
} |
|||
|
|||
init { |
|||
// Get max available VM memory, exceeding this amount will throw an |
|||
// OutOfMemory exception. Stored in kilobytes as LruCache takes an |
|||
// int in its constructor. |
|||
val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt() |
|||
// Use 1/8th of the available memory for this memory cache. |
|||
val cacheSize: Int = maxMemory / 8 |
|||
bitmapMemoryCache = object : LruCache<String, Bitmap>(cacheSize) { |
|||
override fun sizeOf(key: String, bitmap: Bitmap): Int { |
|||
// The cache size will be measured in kilobytes rather than |
|||
// number of items. |
|||
return bitmap.byteCount / 1024 |
|||
} |
|||
} |
|||
} |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue