Convert BitmapUtils to kotlin and migrate MediaUploader funcs to suspend

This commit is contained in:
Ammar Githam 2021-06-03 20:40:13 +09:00
parent 5756f055d9
commit 8491d1aac7
6 changed files with 362 additions and 429 deletions

View File

@ -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'

View File

@ -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);
}

View File

@ -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<String?>, 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<String?>, 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(

View File

@ -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!");
}
}
}
}

View File

@ -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
}
}
}
}

View File

@ -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<String, String>,
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?)
}