mirror of
				https://github.com/KokaKiwi/BarInsta
				synced 2025-10-31 03:25:34 +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,16 +461,21 @@ 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) { | ||||
|         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(), | ||||
|                         bitmap, | ||||
|                     bitmapResult.getBitmap(), | ||||
|                     onFilterClickListener | ||||
|             ); | ||||
|             appExecutors.getMainThread().execute(() -> { | ||||
| @ -479,13 +485,7 @@ public class FiltersFragment extends Fragment { | ||||
|                     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,13 +740,12 @@ 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 | ||||
|                 val uploadId = response1.optString("upload_id") | ||||
|         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") | ||||
|                 val response2 = service.broadcastPhoto(clientContext, threadIdOrUserIds, uploadId) | ||||
|                 parseResponse(response2, data, directItem) | ||||
|             } catch (e: Exception) { | ||||
| @ -758,13 +755,6 @@ class ThreadManager private constructor( | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|             override fun onFailure(t: Throwable) { | ||||
|                 data.postValue(error(t.message, directItem)) | ||||
|                 Log.e(TAG, "onFailure: ", t) | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     private fun sendVideo( | ||||
|         data: MutableLiveData<Resource<Any?>>, | ||||
|         uri: Uri, | ||||
| @ -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 | ||||
|     private val octetStreamMediaType: MediaType = requireNotNull(MediaType.parse("application/octet-stream")) { | ||||
|         "No media type found for application/octet-stream" | ||||
|     } | ||||
| 
 | ||||
|     fun uploadPhoto( | ||||
|     suspend 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 | ||||
|     ): MediaUploadResponse { | ||||
|         return withContext(Dispatchers.IO) { | ||||
|             val bitmapResult = BitmapUtils.loadBitmap(contentResolver, uri, 1000f, false) | ||||
|             val bitmap = bitmapResult?.bitmap ?: throw IOException("bitmap is null") | ||||
|             uploadPhoto(bitmap) | ||||
|         } | ||||
|                 uploadPhoto(bitmap, listener) | ||||
|     } | ||||
| 
 | ||||
|             override fun onFailure(t: Throwable) { | ||||
|                 listener.onFailure(t) | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     private fun uploadPhoto( | ||||
|     @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 | ||||
|             } | ||||
|     ): 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 + "/" | ||||
|             appExecutors.networkIO.execute { | ||||
|         try { | ||||
|                     FileInputStream(file).use { input -> upload(input, url, headers, listener) } | ||||
|                 } catch (e: IOException) { | ||||
|                     listener.onFailure(e) | ||||
|             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 { | ||||
|     ): MediaUploadResponse = withContext(Dispatchers.IO) { | ||||
|         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) | ||||
|                 } | ||||
|                 // 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() | ||||
|             return withContext(Dispatchers.IO) { | ||||
|                 val response = client.newCall(request).await() | ||||
|                 val body = response.body() | ||||
|             if (!response.isSuccessful) { | ||||
|                 listener.onFailure(IOException("Unexpected code " + response + if (body != null) ": " + body.string() else "")) | ||||
|                 return | ||||
|                 @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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user