hunk.code_window:   return ABI12_0_0EXVersionedOnceTokens;
   }
   void ABI12_0_0EXSetInstanceMethod(Class cls, SEL original, SEL replacement)
   {
     Method originalMethod = class_getInstanceMethod(cls, original);
     Method replacementMethod = class_getInstanceMethod(cls, replacement);
     IMP replacementImplementation = method_getImplementation(replacementMethod);
     const char *replacementArgTypes = method_getTypeEncoding(replacementMethod);
     if (!class_addMethod(cls, original, replacementImplementation, replacementArgTypes)) {
       method_setImplementation(originalMethod, replacementImplementation);
     }
   }
   @interface ABI12_0_0EXVersionManager ()
   // is this the first time this ABI has been touched at runtime?
  
hunk.labels: keep keep keep replace replace replace replace replace replace replace replace replace replace replace replace replace keep keep keep
hunk.file_path: ios/versioned-react-native/ABI12_0_0/Exponent/ABI12_0_0EXVersionManager.m
hunk.type: replace
file: /**
   * Copyright (c) 2015-present, Facebook, Inc.
   * All rights reserved.
   *
   * This source code is licensed under the BSD-style license found in the
   * LICENSE file in the root directory of this source tree. An additional grant
   * of patent rights can be found in the PATENTS file in the same directory.
   */
  
  package com.facebook.react.modules.camera;
  
  import javax.annotation.Nullable;
  
  import java.io.File;
  import java.io.FileOutputStream;
  import java.io.FilenameFilter;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.OutputStream;
  import java.net.URL;
  import java.net.URLConnection;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.List;
  import java.util.Map;
  
  import android.annotation.SuppressLint;
  import android.content.Context;
  import android.database.Cursor;
  import android.graphics.Bitmap;
  import android.graphics.BitmapFactory;
  import android.graphics.Matrix;
  import android.media.ExifInterface;
  import android.net.Uri;
  import android.os.AsyncTask;
  import android.provider.MediaStore;
  import android.text.TextUtils;
  
  import com.facebook.common.logging.FLog;
  import com.facebook.react.bridge.Callback;
  import com.facebook.react.bridge.GuardedAsyncTask;
  import com.facebook.react.bridge.ReactApplicationContext;
  import com.facebook.react.bridge.ReactContext;
  import com.facebook.react.bridge.ReactContextBaseJavaModule;
  import com.facebook.react.bridge.ReactMethod;
  import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
  import com.facebook.react.bridge.ReadableMap;
  import com.facebook.infer.annotation.Assertions;
  import com.facebook.react.common.ReactConstants;
  import com.facebook.react.module.annotations.ReactModule;
  
  /**
   * Native module that provides image cropping functionality.
   */
  @ReactModule(name = ImageEditingManager.NAME)
  public class ImageEditingManager extends ReactContextBaseJavaModule {
  
    protected static final String NAME = "ImageEditingManager";
  
    private static final List<String> LOCAL_URI_PREFIXES = Arrays.asList(
        "file://", "content://");
  
    private static final String TEMP_FILE_PREFIX = "ReactNative_cropped_image_";
  
    /** Compress quality of the output file. */
    private static final int COMPRESS_QUALITY = 90;
  
    @SuppressLint("InlinedApi") private static final String[] EXIF_ATTRIBUTES = new String[] {
      ExifInterface.TAG_APERTURE,
      ExifInterface.TAG_DATETIME,
      ExifInterface.TAG_DATETIME_DIGITIZED,
      ExifInterface.TAG_EXPOSURE_TIME,
      ExifInterface.TAG_FLASH,
      ExifInterface.TAG_FOCAL_LENGTH,
      ExifInterface.TAG_GPS_ALTITUDE,
      ExifInterface.TAG_GPS_ALTITUDE_REF,
      ExifInterface.TAG_GPS_DATESTAMP,
      ExifInterface.TAG_GPS_LATITUDE,
      ExifInterface.TAG_GPS_LATITUDE_REF,
      ExifInterface.TAG_GPS_LONGITUDE,
      ExifInterface.TAG_GPS_LONGITUDE_REF,
      ExifInterface.TAG_GPS_PROCESSING_METHOD,
      ExifInterface.TAG_GPS_TIMESTAMP,
      ExifInterface.TAG_IMAGE_LENGTH,
      ExifInterface.TAG_IMAGE_WIDTH,
      ExifInterface.TAG_ISO,
      ExifInterface.TAG_MAKE,
      ExifInterface.TAG_MODEL,
      ExifInterface.TAG_ORIENTATION,
      ExifInterface.TAG_SUBSEC_TIME,
      ExifInterface.TAG_SUBSEC_TIME_DIG,
      ExifInterface.TAG_SUBSEC_TIME_ORIG,
      ExifInterface.TAG_WHITE_BALANCE
    };
  
    public ImageEditingManager(ReactApplicationContext reactContext) {
      super(reactContext);
      new CleanTask(getReactApplicationContext()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }
  
    @Override
    public String getName() {
      return NAME;
    }
  
    @Override
    public Map<String, Object> getConstants() {
      return Collections.emptyMap();
    }
  
    @Override
    public void onCatalystInstanceDestroy() {
      new CleanTask(getReactApplicationContext()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }
  
    /**
     * Asynchronous task that cleans up cache dirs (internal and, if available, external) of cropped
     * image files. This is run when the catalyst instance is being destroyed (i.e. app is shutting
     * down) and when the module is instantiated, to handle the case where the app crashed.
     */
    private static class CleanTask extends GuardedAsyncTask<Void, Void> {
      private final Context mContext;
  
      private CleanTask(ReactContext context) {
        super(context);
        mContext = context;
      }
  
      @Override
      protected void doInBackgroundGuarded(Void... params) {
        cleanDirectory(mContext.getCacheDir());
        File externalCacheDir = mContext.getExternalCacheDir();
        if (externalCacheDir != null) {
          cleanDirectory(externalCacheDir);
        }
      }
  
      private void cleanDirectory(File directory) {
        File[] toDelete = directory.listFiles(
            new FilenameFilter() {
              @Override
              public boolean accept(File dir, String filename) {
                return filename.startsWith(TEMP_FILE_PREFIX);
              }
            });
        if (toDelete != null) {
          for (File file: toDelete) {
            file.delete();
          }
        }
      }
    }
  
    /**
     * Crop an image. If all goes well, the success callback will be called with the file:// URI of
     * the new image as the only argument. This is a temporary file - consider using
     * CameraRollManager.saveImageWithTag to save it in the gallery.
     *
     * @param uri the MediaStore URI of the image to crop
     * @param options crop parameters specified as {@code {offset: {x, y}, size: {width, height}}}.
     *        Optionally this also contains  {@code {targetSize: {width, height}}}. If this is
     *        specified, the cropped image will be resized to that size.
     *        All units are in pixels (not DPs).
     * @param success callback to be invoked when the image has been cropped; the only argument that
     *        is passed to this callback is the file:// URI of the new image
     * @param error callback to be invoked when an error occurs (e.g. can't create file etc.)
     */
    @ReactMethod
    public void cropImage(
        String uri,
        ReadableMap options,
        final Callback success,
        final Callback error) {
      ReadableMap offset = options.hasKey("offset") ? options.getMap("offset") : null;
      ReadableMap size = options.hasKey("size") ? options.getMap("size") : null;
      if (offset == null || size == null ||
          !offset.hasKey("x") || !offset.hasKey("y") ||
          !size.hasKey("width") || !size.hasKey("height")) {
        throw new JSApplicationIllegalArgumentException("Please specify offset and size");
      }
      if (uri == null || uri.isEmpty()) {
        throw new JSApplicationIllegalArgumentException("Please specify a URI");
      }
  
      CropTask cropTask = new CropTask(
          getReactApplicationContext(),
          uri,
          (int) offset.getDouble("x"),
          (int) offset.getDouble("y"),
          (int) size.getDouble("width"),
          (int) size.getDouble("height"),
          success,
          error);
      if (options.hasKey("displaySize")) {
        ReadableMap targetSize = options.getMap("displaySize");
        cropTask.setTargetSize(targetSize.getInt("width"), targetSize.getInt("height"));
      }
      cropTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }
  
    private static class CropTask extends GuardedAsyncTask<Void, Void> {
      final Context mContext;
      final String mUri;
      final int mX;
      final int mY;
      final int mWidth;
      final int mHeight;
      int mTargetWidth = 0;
      int mTargetHeight = 0;
      final Callback mSuccess;
      final Callback mError;
  
      private CropTask(
          ReactContext context,
          String uri,
          int x,
          int y,
          int width,
          int height,
          Callback success,
          Callback error) {
        super(context);
        if (x < 0 || y < 0 || width <= 0 || height <= 0) {
          throw new JSApplicationIllegalArgumentException(String.format(
              "Invalid crop rectangle: [%d, %d, %d, %d]", x, y, width, height));
        }
        mContext = context;
        mUri = uri;
        mX = x;
        mY = y;
        mWidth = width;
        mHeight = height;
        mSuccess = success;
        mError = error;
      }
  
      public void setTargetSize(int width, int height) {
        if (width <= 0 || height <= 0) {
          throw new JSApplicationIllegalArgumentException(String.format(
              "Invalid target size: [%d, %d]", width, height));
        }
        mTargetWidth = width;
        mTargetHeight = height;
      }
  
      private InputStream openBitmapInputStream() throws IOException {
        InputStream stream;
        if (isLocalUri(mUri)) {
          stream = mContext.getContentResolver().openInputStream(Uri.parse(mUri));
        } else {
          URLConnection connection = new URL(mUri).openConnection();
          stream = connection.getInputStream();
        }
        if (stream == null) {
          throw new IOException("Cannot open bitmap: " + mUri);
        }
        return stream;
      }
  
      @Override
      protected void doInBackgroundGuarded(Void... params) {
        try {
          BitmapFactory.Options outOptions = new BitmapFactory.Options();
  
          // If we're downscaling, we can decode the bitmap more efficiently, using less memory
          boolean hasTargetSize = (mTargetWidth > 0) && (mTargetHeight > 0);
  
          Bitmap cropped;
          if (hasTargetSize) {
            cropped = cropAndResize(mTargetWidth, mTargetHeight, outOptions);
          } else {
            cropped = crop(outOptions);
          }
  
          String mimeType = outOptions.outMimeType;
          if (mimeType == null || mimeType.isEmpty()) {
            throw new IOException("Could not determine MIME type");
          }
  
          File tempFile = createTempFile(mContext, mimeType);
          writeCompressedBitmapToFile(cropped, mimeType, tempFile);
  
          if (mimeType.equals("image/jpeg")) {
            copyExif(mContext, Uri.parse(mUri), tempFile);
          }
  
          mSuccess.invoke(Uri.fromFile(tempFile).toString());
        } catch (Exception e) {
          mError.invoke(e.getMessage());
        }
      }
  
      /**
       * Reads and crops the bitmap.
       * @param outOptions Bitmap options, useful to determine {@code outMimeType}.
       */
      private Bitmap crop(BitmapFactory.Options outOptions) throws IOException {
        InputStream inputStream = openBitmapInputStream();
        try {
          // This can use a lot of memory
          Bitmap fullResolutionBitmap = BitmapFactory.decodeStream(inputStream, null, outOptions);
          if (fullResolutionBitmap == null) {
            throw new IOException("Cannot decode bitmap: " + mUri);
          }
          return Bitmap.createBitmap(fullResolutionBitmap, mX, mY, mWidth, mHeight);
        } finally {
          if (inputStream != null) {
            inputStream.close();
          }
        }
      }
  
      /**
       * Crop the rectangle given by {@code mX, mY, mWidth, mHeight} within the source bitmap
       * and scale the result to {@code targetWidth, targetHeight}.
       * @param outOptions Bitmap options, useful to determine {@code outMimeType}.
       */
      private Bitmap cropAndResize(
          int targetWidth,
          int targetHeight,
          BitmapFactory.Options outOptions)
          throws IOException {
        Assertions.assertNotNull(outOptions);
  
        // Loading large bitmaps efficiently:
        // http://developer.android.com/training/displaying-bitmaps/load-bitmap.html
  
        // Just decode the dimensions
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        InputStream inputStream = openBitmapInputStream();
        try {
          BitmapFactory.decodeStream(inputStream, null, options);
        } finally {
          if (inputStream != null) {
            inputStream.close();
          }
        }
  
        // This uses scaling mode COVER
  
        // Where would the crop rect end up within the scaled bitmap?
        float newWidth, newHeight, newX, newY, scale;
        float cropRectRatio = mWidth / (float) mHeight;
        float targetRatio = targetWidth / (float) targetHeight;
        if (cropRectRatio > targetRatio) {
          // e.g. source is landscape, target is portrait
          newWidth = mHeight * targetRatio;
          newHeight = mHeight;
          newX = mX + (mWidth - newWidth) / 2;
          newY = mY;
          scale = targetHeight / (float) mHeight;
        } else {
          // e.g. source is landscape, target is portrait
          newWidth = mWidth;
          newHeight = mWidth / targetRatio;
          newX = mX;
          newY = mY + (mHeight - newHeight) / 2;
          scale = targetWidth / (float) mWidth;
        }
  
        // Decode the bitmap. We have to open the stream again, like in the example linked above.
        // Is there a way to just continue reading from the stream?
        outOptions.inSampleSize = getDecodeSampleSize(mWidth, mHeight, targetWidth, targetHeight);
        options.inJustDecodeBounds = false;
        inputStream = openBitmapInputStream();
  
        Bitmap bitmap;
        try {
          // This can use significantly less memory than decoding the full-resolution bitmap
          bitmap = BitmapFactory.decodeStream(inputStream, null, outOptions);
          if (bitmap == null) {
            throw new IOException("Cannot decode bitmap: " + mUri);
          }
        } finally {
          if (inputStream != null) {
            inputStream.close();
          }
        }
  
        int cropX = (int) Math.floor(newX / (float) outOptions.inSampleSize);
        int cropY = (int) Math.floor(newY / (float) outOptions.inSampleSize);
        int cropWidth = (int) Math.floor(newWidth / (float) outOptions.inSampleSize);
        int cropHeight = (int) Math.floor(newHeight / (float) outOptions.inSampleSize);
        float cropScale = scale * outOptions.inSampleSize;
  
        Matrix scaleMatrix = new Matrix();
        scaleMatrix.setScale(cropScale, cropScale);
        boolean filter = true;
  
        return Bitmap.createBitmap(bitmap, cropX, cropY, cropWidth, cropHeight, scaleMatrix, filter);
      }
    }
  
    // Utils
  
    private static void copyExif(Context context, Uri oldImage, File newFile) throws IOException {
      File oldFile = getFileFromUri(context, oldImage);
      if (oldFile == null) {
        FLog.w(ReactConstants.TAG, "Couldn't get real path for uri: " + oldImage);
        return;
      }
  
      ExifInterface oldExif = new ExifInterface(oldFile.getAbsolutePath());
      ExifInterface newExif = new ExifInterface(newFile.getAbsolutePath());
      for (String attribute : EXIF_ATTRIBUTES) {
        String value = oldExif.getAttribute(attribute);
        if (value != null) {
          newExif.setAttribute(attribute, value);
        }
      }
      newExif.saveAttributes();
    }
  
    private static @Nullable File getFileFromUri(Context context, Uri uri) {
      if (uri.getScheme().equals("file")) {
        return new File(uri.getPath());
      } else if (uri.getScheme().equals("content")) {
        Cursor cursor = context.getContentResolver()
          .query(uri, new String[] { MediaStore.MediaColumns.DATA }, null, null, null);
        if (cursor != null) {
          try {
            if (cursor.moveToFirst()) {
              String path = cursor.getString(0);
              if (!TextUtils.isEmpty(path)) {
                return new File(path);
              }
            }
          } finally {
            cursor.close();
          }
        }
      }
  
      return null;
    }
  
    private static boolean isLocalUri(String uri) {
      for (String localPrefix : LOCAL_URI_PREFIXES) {
        if (uri.startsWith(localPrefix)) {
          return true;
        }
      }
      return false;
    }
  
    private static String getFileExtensionForType(@Nullable String mimeType) {
      if ("image/png".equals(mimeType)) {
        return ".png";
      }
      if ("image/webp".equals(mimeType)) {
        return ".webp";
      }
      return ".jpg";
    }
  
    private static Bitmap.CompressFormat getCompressFormatForType(String type) {
      if ("image/png".equals(type)) {
        return Bitmap.CompressFormat.PNG;
      }
      if ("image/webp".equals(type)) {
        return Bitmap.CompressFormat.WEBP;
      }
      return Bitmap.CompressFormat.JPEG;
    }
  
    private static void writeCompressedBitmapToFile(Bitmap cropped, String mimeType, File tempFile)
        throws IOException {
      OutputStream out = new FileOutputStream(tempFile);
      try {
        cropped.compress(getCompressFormatForType(mimeType), COMPRESS_QUALITY, out);
      } finally {
        if (out != null) {
          out.close();
        }
      }
    }
  
    /**
     * Create a temporary file in the cache directory on either internal or external storage,
     * whichever is available and has more free space.
     *
     * @param mimeType the MIME type of the file to create (image/*)
     */
    private static File createTempFile(Context context, @Nullable String mimeType)
        throws IOException {
      File externalCacheDir = context.getExternalCacheDir();
      File internalCacheDir = context.getCacheDir();
      File cacheDir;
      if (externalCacheDir == null && internalCacheDir == null) {
        throw new IOException("No cache directory available");
      }
      if (externalCacheDir == null) {
        cacheDir = internalCacheDir;
      }
      else if (internalCacheDir == null) {
        cacheDir = externalCacheDir;
      } else {
        cacheDir = externalCacheDir.getFreeSpace() > internalCacheDir.getFreeSpace() ?
            externalCacheDir : internalCacheDir;
      }
      return File.createTempFile(TEMP_FILE_PREFIX, getFileExtensionForType(mimeType), cacheDir);
    }
  
    /**
     * When scaling down the bitmap, decode only every n-th pixel in each dimension.
     * Calculate the largest {@code inSampleSize} value that is a power of 2 and keeps both
     * {@code width, height} larger or equal to {@code targetWidth, targetHeight}.
     * This can significantly reduce memory usage.
     */
    private static int getDecodeSampleSize(int width, int height, int targetWidth, int targetHeight) {
      int inSampleSize = 1;
      if (height > targetWidth || width > targetHeight) {
        int halfHeight = height / 2;
        int halfWidth = width / 2;
        while ((halfWidth / inSampleSize) >= targetWidth
            && (halfHeight / inSampleSize) >= targetHeight) {
          inSampleSize *= 2;
        }
      }
      return inSampleSize;
    }
  }
  