diff --git a/app/build.gradle b/app/build.gradle index 9a9a3c15..815ff708 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -208,6 +208,8 @@ dependencies { implementation "androidx.work:work-runtime:$work_version" implementation "androidx.work:work-runtime-ktx:$work_version" + implementation "ru.gildor.coroutines:kotlin-coroutines-okhttp:1.0" + implementation 'com.facebook.fresco:fresco:2.3.0' implementation 'com.facebook.fresco:animated-webp:2.3.0' implementation 'com.facebook.fresco:webpsupport:2.3.0' diff --git a/app/src/main/java/awais/instagrabber/fragments/imageedit/FiltersFragment.java b/app/src/main/java/awais/instagrabber/fragments/imageedit/FiltersFragment.java index 6cd0de96..fd29212f 100644 --- a/app/src/main/java/awais/instagrabber/fragments/imageedit/FiltersFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/imageedit/FiltersFragment.java @@ -47,6 +47,7 @@ import awais.instagrabber.fragments.imageedit.filters.properties.FloatProperty; import awais.instagrabber.fragments.imageedit.filters.properties.Property; import awais.instagrabber.utils.AppExecutors; import awais.instagrabber.utils.BitmapUtils; +import awais.instagrabber.utils.CoroutineUtilsKt; import awais.instagrabber.utils.SerializablePair; import awais.instagrabber.utils.Utils; import awais.instagrabber.viewmodels.FiltersFragmentViewModel; @@ -460,32 +461,31 @@ public class FiltersFragment extends Fragment { filtersAdapter.setSelected(position); appliedFilter = filter; }; - BitmapUtils.getThumbnail(context, sourceUri, new BitmapUtils.ThumbnailLoadCallback() { - @Override - public void onLoad(@Nullable final Bitmap bitmap, final int width, final int height) { - filtersAdapter = new FiltersAdapter( - tuningFilters.values() - .stream() - .map(Filter::getInstance) - .collect(Collectors.toList()), - sourceUri.toString(), - bitmap, - onFilterClickListener - ); - appExecutors.getMainThread().execute(() -> { - binding.filters.setAdapter(filtersAdapter); - filtersAdapter.submitList(FiltersHelper.getFilters(), () -> { - if (appliedFilter == null) return; - filtersAdapter.setSelectedFilter(appliedFilter.getInstance()); - }); + BitmapUtils.getThumbnail(context, sourceUri, CoroutineUtilsKt.getContinuation((bitmapResult, throwable) -> { + if (throwable != null) { + Log.e(TAG, "setupFilters: ", throwable); + return; + } + if (bitmapResult == null || bitmapResult.getBitmap() == null) { + return; + } + filtersAdapter = new FiltersAdapter( + tuningFilters.values() + .stream() + .map(Filter::getInstance) + .collect(Collectors.toList()), + sourceUri.toString(), + bitmapResult.getBitmap(), + onFilterClickListener + ); + appExecutors.getMainThread().execute(() -> { + binding.filters.setAdapter(filtersAdapter); + filtersAdapter.submitList(FiltersHelper.getFilters(), () -> { + if (appliedFilter == null) return; + filtersAdapter.setSelectedFilter(appliedFilter.getInstance()); }); - } - - @Override - public void onFailure(@NonNull final Throwable t) { - Log.e(TAG, "onFailure: ", t); - } - }); + }); + })); addInitialFilter(); binding.preview.setFilter(filterGroup); } diff --git a/app/src/main/java/awais/instagrabber/managers/ThreadManager.kt b/app/src/main/java/awais/instagrabber/managers/ThreadManager.kt index 11c51abd..8a1953db 100644 --- a/app/src/main/java/awais/instagrabber/managers/ThreadManager.kt +++ b/app/src/main/java/awais/instagrabber/managers/ThreadManager.kt @@ -26,7 +26,6 @@ import awais.instagrabber.repositories.responses.directmessages.* import awais.instagrabber.repositories.responses.giphy.GiphyGif import awais.instagrabber.utils.* import awais.instagrabber.utils.MediaUploader.MediaUploadResponse -import awais.instagrabber.utils.MediaUploader.OnMediaUploadCompleteListener import awais.instagrabber.utils.MediaUploader.uploadPhoto import awais.instagrabber.utils.MediaUploader.uploadVideo import awais.instagrabber.utils.MediaUtils.OnInfoLoadListener @@ -448,10 +447,11 @@ class ThreadManager private constructor( addItems(0, listOf(directItem)) data.postValue(loading(directItem)) val uploadDmVoiceOptions = createUploadDmVoiceOptions(byteLength, duration) - uploadVideo(uri, contentResolver, uploadDmVoiceOptions, object : OnMediaUploadCompleteListener { - override fun onUploadComplete(response: MediaUploadResponse) { + scope.launch(Dispatchers.IO) { + try { + val response = uploadVideo(uri, contentResolver, uploadDmVoiceOptions) // Log.d(TAG, "onUploadComplete: " + response); - if (handleInvalidResponse(data, response)) return + if (handleInvalidResponse(data, response)) return@launch val uploadFinishOptions = UploadFinishOptions( uploadDmVoiceOptions.uploadId, "4", @@ -488,16 +488,14 @@ class ThreadManager private constructor( override fun onFailure(call: Call, t: Throwable) { data.postValue(error(t.message, directItem)) - Log.e(TAG, "onFailure: ", t) + Log.e(TAG, "sendVoice: ", t) } }) + } catch (e: Exception) { + data.postValue(error(e.message, directItem)) + Log.e(TAG, "sendVoice: ", e) } - - override fun onFailure(t: Throwable) { - data.postValue(error(t.message, directItem)) - Log.e(TAG, "onFailure: ", t) - } - }) + } } fun sendReaction( @@ -742,27 +740,19 @@ class ThreadManager private constructor( directItem.isPending = true addItems(0, listOf(directItem)) data.postValue(loading(directItem)) - uploadPhoto(uri, contentResolver, object : OnMediaUploadCompleteListener { - override fun onUploadComplete(response: MediaUploadResponse) { - if (handleInvalidResponse(data, response)) return - val response1 = response.response ?: return + scope.launch(Dispatchers.IO) { + try { + val response = uploadPhoto(uri, contentResolver) + if (handleInvalidResponse(data, response)) return@launch + val response1 = response.response ?: return@launch val uploadId = response1.optString("upload_id") - scope.launch(Dispatchers.IO) { - try { - val response2 = service.broadcastPhoto(clientContext, threadIdOrUserIds, uploadId) - parseResponse(response2, data, directItem) - } catch (e: Exception) { - data.postValue(error(e.message, null)) - Log.e(TAG, "sendPhoto: ", e) - } - } + val response2 = service.broadcastPhoto(clientContext, threadIdOrUserIds, uploadId) + parseResponse(response2, data, directItem) + } catch (e: Exception) { + data.postValue(error(e.message, null)) + Log.e(TAG, "sendPhoto: ", e) } - - override fun onFailure(t: Throwable) { - data.postValue(error(t.message, directItem)) - Log.e(TAG, "onFailure: ", t) - } - }) + } } private fun sendVideo( @@ -806,10 +796,11 @@ class ThreadManager private constructor( addItems(0, listOf(directItem)) data.postValue(loading(directItem)) val uploadDmVideoOptions = createUploadDmVideoOptions(byteLength, duration, width, height) - uploadVideo(uri, contentResolver, uploadDmVideoOptions, object : OnMediaUploadCompleteListener { - override fun onUploadComplete(response: MediaUploadResponse) { + scope.launch(Dispatchers.IO) { + try { + val response = uploadVideo(uri, contentResolver, uploadDmVideoOptions) // Log.d(TAG, "onUploadComplete: " + response); - if (handleInvalidResponse(data, response)) return + if (handleInvalidResponse(data, response)) return@launch val uploadFinishOptions = UploadFinishOptions( uploadDmVideoOptions.uploadId, "2", @@ -843,19 +834,16 @@ class ThreadManager private constructor( data.postValue(error("uploadFinishRequest was not successful and response error body was null", directItem)) Log.e(TAG, "uploadFinishRequest was not successful and response error body was null") } - override fun onFailure(call: Call, t: Throwable) { data.postValue(error(t.message, directItem)) - Log.e(TAG, "onFailure: ", t) + Log.e(TAG, "sendVideo: ", t) } }) + } catch (e: Exception) { + data.postValue(error(e.message, directItem)) + Log.e(TAG, "sendVideo: ", e) } - - override fun onFailure(t: Throwable) { - data.postValue(error(t.message, directItem)) - Log.e(TAG, "onFailure: ", t) - } - }) + } } private fun parseResponse( diff --git a/app/src/main/java/awais/instagrabber/utils/BitmapUtils.java b/app/src/main/java/awais/instagrabber/utils/BitmapUtils.java deleted file mode 100644 index 49137b71..00000000 --- a/app/src/main/java/awais/instagrabber/utils/BitmapUtils.java +++ /dev/null @@ -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 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(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 future = appExecutors - .getTasksThread() - .submit(() -> getBitmapResult(contentResolver, uri, reqWidth, reqHeight, maxDimenSize, addToCache)); - Futures.addCallback(future, new FutureCallback() { - @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 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!"); - } - } - } -} diff --git a/app/src/main/java/awais/instagrabber/utils/BitmapUtils.kt b/app/src/main/java/awais/instagrabber/utils/BitmapUtils.kt new file mode 100644 index 00000000..4cbba93b --- /dev/null +++ b/app/src/main/java/awais/instagrabber/utils/BitmapUtils.kt @@ -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 + + // 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? { + 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(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 + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/MediaUploader.kt b/app/src/main/java/awais/instagrabber/utils/MediaUploader.kt index 25c1d43a..6ae575a7 100644 --- a/app/src/main/java/awais/instagrabber/utils/MediaUploader.kt +++ b/app/src/main/java/awais/instagrabber/utils/MediaUploader.kt @@ -4,12 +4,14 @@ import android.content.ContentResolver import android.graphics.Bitmap import android.net.Uri import awais.instagrabber.models.UploadVideoOptions -import awais.instagrabber.utils.BitmapUtils.ThumbnailLoadCallback import awais.instagrabber.webservices.interceptors.AddCookiesInterceptor +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import okhttp3.* import okio.BufferedSink import okio.Okio import org.json.JSONObject +import ru.gildor.coroutines.okhttp.await import java.io.File import java.io.FileInputStream import java.io.IOException @@ -17,89 +19,61 @@ import java.io.InputStream object MediaUploader { private const val HOST = "https://i.instagram.com" - private val appExecutors = AppExecutors - - fun uploadPhoto( - uri: Uri, - contentResolver: ContentResolver, - listener: OnMediaUploadCompleteListener, - ) { - BitmapUtils.loadBitmap(contentResolver, uri, 1000f, false, object : ThumbnailLoadCallback { - override fun onLoad(bitmap: Bitmap?, width: Int, height: Int) { - if (bitmap == null) { - listener.onFailure(RuntimeException("Bitmap result was null")) - return - } - uploadPhoto(bitmap, listener) - } - - override fun onFailure(t: Throwable) { - listener.onFailure(t) - } - }) + private val octetStreamMediaType: MediaType = requireNotNull(MediaType.parse("application/octet-stream")) { + "No media type found for application/octet-stream" } - private fun uploadPhoto( + suspend fun uploadPhoto( + uri: Uri, + contentResolver: ContentResolver, + ): MediaUploadResponse { + return withContext(Dispatchers.IO) { + val bitmapResult = BitmapUtils.loadBitmap(contentResolver, uri, 1000f, false) + val bitmap = bitmapResult?.bitmap ?: throw IOException("bitmap is null") + uploadPhoto(bitmap) + } + } + + @Suppress("BlockingMethodInNonBlockingContext") + private suspend fun uploadPhoto( bitmap: Bitmap, - listener: OnMediaUploadCompleteListener, - ) { - appExecutors.tasksThread.submit { - val file: File - val byteLength: Long - try { - file = BitmapUtils.convertToJpegAndSaveToFile(bitmap, null) - byteLength = file.length() - } catch (e: Exception) { - listener.onFailure(e) - return@submit - } - val options = createUploadPhotoOptions(byteLength) - val headers = getUploadPhotoHeaders(options) - val url = HOST + "/rupload_igphoto/" + options.name + "/" - appExecutors.networkIO.execute { - try { - FileInputStream(file).use { input -> upload(input, url, headers, listener) } - } catch (e: IOException) { - listener.onFailure(e) - } finally { - file.delete() - } - } + ): MediaUploadResponse = withContext(Dispatchers.IO) { + val file: File = BitmapUtils.convertToJpegAndSaveToFile(bitmap, null) + val byteLength: Long = file.length() + val options = createUploadPhotoOptions(byteLength) + val headers = getUploadPhotoHeaders(options) + val url = HOST + "/rupload_igphoto/" + options.name + "/" + try { + FileInputStream(file).use { input -> upload(input, url, headers) } + } finally { + file.delete() } } @JvmStatic - fun uploadVideo( + @Suppress("BlockingMethodInNonBlockingContext") // See https://youtrack.jetbrains.com/issue/KTIJ-838 + suspend fun uploadVideo( uri: Uri, contentResolver: ContentResolver, options: UploadVideoOptions, - listener: OnMediaUploadCompleteListener, - ) { - appExecutors.tasksThread.submit { - val headers = getUploadVideoHeaders(options) - val url = HOST + "/rupload_igvideo/" + options.name + "/" - appExecutors.networkIO.execute { - try { - contentResolver.openInputStream(uri).use { input -> - if (input == null) { - listener.onFailure(RuntimeException("InputStream was null")) - return@execute - } - upload(input, url, headers, listener) - } - } catch (e: IOException) { - listener.onFailure(e) - } + ): MediaUploadResponse = withContext(Dispatchers.IO) { + val headers = getUploadVideoHeaders(options) + val url = HOST + "/rupload_igvideo/" + options.name + "/" + contentResolver.openInputStream(uri).use { input -> + if (input == null) { + // listener.onFailure(RuntimeException("InputStream was null")) + throw IllegalStateException("InputStream was null") } + upload(input, url, headers) } } - private fun upload( + @Throws(IOException::class) + private suspend fun upload( input: InputStream, url: String, headers: Map, - listener: OnMediaUploadCompleteListener, - ) { + ): MediaUploadResponse { try { val client = OkHttpClient.Builder() // .addInterceptor(new LoggingInterceptor()) @@ -110,24 +84,23 @@ object MediaUploader { val request = Request.Builder() .headers(Headers.of(headers)) .url(url) - .post(create(MediaType.parse("application/octet-stream"), input)) + .post(create(octetStreamMediaType, input)) .build() - val call = client.newCall(request) - val response = call.execute() - val body = response.body() - if (!response.isSuccessful) { - listener.onFailure(IOException("Unexpected code " + response + if (body != null) ": " + body.string() else "")) - return + return withContext(Dispatchers.IO) { + val response = client.newCall(request).await() + val body = response.body() + @Suppress("BlockingMethodInNonBlockingContext") // Blocked by https://github.com/square/okio/issues/501 + MediaUploadResponse(response.code(), if (body != null) JSONObject(body.string()) else null) } - listener.onUploadComplete(MediaUploadResponse(response.code(), if (body != null) JSONObject(body.string()) else null)) } catch (e: Exception) { - listener.onFailure(e) + // rethrow for proper stacktrace. See https://github.com/gildor/kotlin-coroutines-okhttp/tree/master#wrap-exception-manually + throw IOException(e) } } - private fun create(mediaType: MediaType?, inputStream: InputStream): RequestBody { + private fun create(mediaType: MediaType, inputStream: InputStream): RequestBody { return object : RequestBody() { - override fun contentType(): MediaType? { + override fun contentType(): MediaType { return mediaType } @@ -147,10 +120,5 @@ object MediaUploader { } } - interface OnMediaUploadCompleteListener { - fun onUploadComplete(response: MediaUploadResponse) - fun onFailure(t: Throwable) - } - data class MediaUploadResponse(val responseCode: Int, val response: JSONObject?) } \ No newline at end of file