mirror of
https://github.com/KokaKiwi/BarInsta
synced 2024-12-25 22:26:59 +00:00
Convert BitmapUtils to kotlin and migrate MediaUploader funcs to suspend
This commit is contained in:
parent
5756f055d9
commit
8491d1aac7
@ -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'
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
255
app/src/main/java/awais/instagrabber/utils/BitmapUtils.kt
Normal file
255
app/src/main/java/awais/instagrabber/utils/BitmapUtils.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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?)
|
||||
}
|
Loading…
Reference in New Issue
Block a user