<commit_before>package org.commcare.utils;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.Pair;
import android.view.WindowManager;

import org.commcare.engine.references.JavaFileReference;
import org.commcare.logging.AndroidLogger;
import org.commcare.logging.analytics.GoogleAnalyticsFields;
import org.commcare.logging.analytics.GoogleAnalyticsUtils;
import org.commcare.preferences.CommCarePreferences;
import org.javarosa.core.reference.InvalidReferenceException;
import org.javarosa.core.reference.Reference;
import org.javarosa.core.reference.ReferenceManager;
import org.javarosa.core.services.Logger;

import java.io.File;
import java.io.IOException;

/**
 * @author ctsims
 */
public class MediaUtil {

    public static final String FORM_VIDEO = "video";
    public static final String FORM_AUDIO = "audio";
    public static final String FORM_IMAGE = "image";

    /**
     * Attempts to inflate an image from a CommCare UI definition source.
     *
     * @param jrUri          The image to inflate
     * @param boundingWidth  the width of the container this image is being inflated into, to serve
     *                       as a max width. If passed in as -1, gets set to screen width
     * @param boundingHeight the height fo the container this image is being inflated into, to
     *                       serve as a max height. If passed in as -1, gets set to screen height
     * @return A bitmap if one could be created. Null if error occurs or the image is unavailable.
     */
    public static Bitmap inflateDisplayImage(Context context, String jrUri,
                                             int boundingWidth, int boundingHeight) {
        if (jrUri == null || jrUri.equals("")) {
            return null;
        }
        try {
            Reference ref = ReferenceManager._().DeriveReference(jrUri);
            try {
                if (!ref.doesBinaryExist()) {
                    return null;
                }
                if (!(ref instanceof JavaFileReference)) {
                    return BitmapFactory.decodeStream(ref.getStream());
                }
            } catch (IOException e) {
                Logger.log(AndroidLogger.TYPE_ERROR_ASSERTION, "IO Exception loading reference: " + jrUri);
                return null;
            }

            String imageFilename = ref.getLocalURI();
            final File imageFile = new File(imageFilename);
            if (!imageFile.exists()) {
                return null;
            }

            DisplayMetrics displayMetrics = new DisplayMetrics();
            ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE))
                    .getDefaultDisplay()
                    .getMetrics(displayMetrics);

            if (boundingHeight == -1) {
                boundingHeight = displayMetrics.heightPixels;
            }
            if (boundingWidth == -1) {
                boundingWidth = displayMetrics.widthPixels;
            }

            if (CommCarePreferences.isSmartInflationEnabled()) {
                GoogleAnalyticsUtils.reportFeatureUsage(GoogleAnalyticsFields.ACTION_USING_SMART_IMAGE_INFLATION);
                // scale based on native density AND bounding dimens
                return getBitmapScaledForNativeDensity(
                        context.getResources().getDisplayMetrics(), imageFile.getAbsolutePath(),
                        boundingHeight, boundingWidth,
                        CommCarePreferences.getTargetInflationDensity());
            } else {
                // just scaling down if the original image is too big for its container
                return getBitmapScaledToContainer(imageFile.getAbsolutePath(), boundingHeight,
                        boundingWidth);
            }
        } catch (InvalidReferenceException e) {
            Log.e("ImageInflater", "image invalid reference exception for " + e.getReferenceString());
            e.printStackTrace();
        }
        return null;
    }

    public static Bitmap inflateDisplayImage(Context context, String jrUri) {
        return inflateDisplayImage(context, jrUri, -1, -1);
    }

    /**
     * Attempt to inflate an image source into a bitmap whose final dimensions are based upon
     * 2 factors:
     *
     * 1) The application of a scaling factor, which is derived from the relative values of the
     * target density declared by the app and the current device's actual density
     * 2) The absolute dimensions of the bounding container into which this image is being inflated
     * (may just be the screen dimens)
     *
     * @return the bitmap, or null if none could be created from the source
     */
    public static Bitmap getBitmapScaledForNativeDensity(DisplayMetrics metrics, String imageFilepath,
                                                         int containerHeight, int containerWidth,
                                                         int targetDensity) {
        BitmapFactory.Options o = new BitmapFactory.Options();
        o.inJustDecodeBounds = true;
        o.inScaled = false;
        BitmapFactory.decodeFile(imageFilepath, o);
        int imageHeight = o.outHeight;
        int imageWidth = o.outWidth;

        double scaleFactor = computeInflationScaleFactor(metrics, targetDensity);
        int calculatedHeight = Math.round((float)(imageHeight * scaleFactor));
        int calculatedWidth = Math.round((float)(imageWidth * scaleFactor));

        if (containerHeight < imageHeight || containerWidth < imageWidth || calculatedHeight < imageHeight) {
            // If either the container dimens or calculated dimens impose a smaller dimension,
            // scale down
            return getBitmapScaledByTargetOrContainer(imageFilepath, imageHeight, imageWidth,
                    calculatedHeight, calculatedWidth, containerHeight, containerWidth, false);
        } else {
            return attemptBoundedScaleUp(imageFilepath, calculatedHeight, calculatedWidth,
                    containerHeight, containerWidth);
        }
    }

    public static double computeInflationScaleFactor(DisplayMetrics metrics, int targetDensity) {
        final int SCREEN_DENSITY = metrics.densityDpi;

        double actualNativeScaleFactor = metrics.density;
        // The formula below is what Android *usually* uses to compute the value of metrics.density
        // for a device. If this is in fact the value being used, we are not interested in it.
        // However, if the actual value differs at all from the standard calculation, it means
        // Android is taking other factors into consideration (such as straight up screen size),
        // and we want to incorporate that proportionally into our own version of the scale factor
        double standardNativeScaleFactor = (double)SCREEN_DENSITY / DisplayMetrics.DENSITY_DEFAULT;
        double proportionalAdjustmentFactor = 1;
        if (actualNativeScaleFactor > standardNativeScaleFactor) {
            proportionalAdjustmentFactor = 1 +
                    ((actualNativeScaleFactor - standardNativeScaleFactor) / standardNativeScaleFactor);
        } else if (actualNativeScaleFactor < standardNativeScaleFactor) {
            proportionalAdjustmentFactor = actualNativeScaleFactor / standardNativeScaleFactor;
        }

        // Get our custom scale factor, based on this device's density and what the image's target
        // density was
        double customDpiScaleFactor = (double)SCREEN_DENSITY / targetDensity;

        return customDpiScaleFactor * proportionalAdjustmentFactor;
    }

    /**
     * @return A bitmap representation of the given image file, scaled down to the smallest
     * size that still fills the container
     *
     * More precisely, preserves the following 2 conditions:
     * 1. The larger of the 2 sides takes on the size of the corresponding container dimension<commit_msg>refactor<commit_after>package org.commcare.utils;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.Pair;
import android.view.WindowManager;

import org.commcare.engine.references.JavaFileReference;
import org.commcare.logging.AndroidLogger;
import org.commcare.logging.analytics.GoogleAnalyticsFields;
import org.commcare.logging.analytics.GoogleAnalyticsUtils;
import org.commcare.preferences.CommCarePreferences;
import org.javarosa.core.reference.InvalidReferenceException;
import org.javarosa.core.reference.Reference;
import org.javarosa.core.reference.ReferenceManager;
import org.javarosa.core.services.Logger;

import java.io.File;
import java.io.IOException;

/**
 * @author ctsims
 */
public class MediaUtil {

    public static final String FORM_VIDEO = "video";
    public static final String FORM_AUDIO = "audio";
    public static final String FORM_IMAGE = "image";

    /**
     * Attempts to inflate an image from a CommCare UI definition source.
     *
     * @param jrUri          The image to inflate
     * @param boundingWidth  the width of the container this image is being inflated into, to serve
     *                       as a max width. If passed in as -1, gets set to screen width
     * @param boundingHeight the height fo the container this image is being inflated into, to
     *                       serve as a max height. If passed in as -1, gets set to screen height
     * @return A bitmap if one could be created. Null if error occurs or the image is unavailable.
     */
    public static Bitmap inflateDisplayImage(Context context, String jrUri,
                                             int boundingWidth, int boundingHeight) {
        if (jrUri == null || jrUri.equals("")) {
            return null;
        }
        try {
            Reference ref = ReferenceManager._().DeriveReference(jrUri);
            try {
                if (!ref.doesBinaryExist()) {
                    return null;
                }
                if (!(ref instanceof JavaFileReference)) {
                    return BitmapFactory.decodeStream(ref.getStream());
                }
            } catch (IOException e) {
                Logger.log(AndroidLogger.TYPE_ERROR_ASSERTION, "IO Exception loading reference: " + jrUri);
                return null;
            }

            String imageFilename = ref.getLocalURI();
            final File imageFile = new File(imageFilename);
            if (!imageFile.exists()) {
                return null;
            }

            DisplayMetrics displayMetrics = new DisplayMetrics();
            ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE))
                    .getDefaultDisplay()
                    .getMetrics(displayMetrics);

            if (boundingHeight == -1) {
                boundingHeight = displayMetrics.heightPixels;
            }
            if (boundingWidth == -1) {
                boundingWidth = displayMetrics.widthPixels;
            }

            if (CommCarePreferences.isSmartInflationEnabled()) {
                GoogleAnalyticsUtils.reportFeatureUsage(GoogleAnalyticsFields.ACTION_USING_SMART_IMAGE_INFLATION);
                // scale based on native density AND bounding dimens
                return getBitmapScaledForNativeDensity(
                        context.getResources().getDisplayMetrics(), imageFile.getAbsolutePath(),
                        boundingHeight, boundingWidth);
            } else {
                // just scaling down if the original image is too big for its container
                return getBitmapScaledToContainer(imageFile.getAbsolutePath(), boundingHeight,
                        boundingWidth);
            }
        } catch (InvalidReferenceException e) {
            Log.e("ImageInflater", "image invalid reference exception for " + e.getReferenceString());
            e.printStackTrace();
        }
        return null;
    }

    public static Bitmap inflateDisplayImage(Context context, String jrUri) {
        return inflateDisplayImage(context, jrUri, -1, -1);
    }

    /**
     * Attempt to inflate an image source into a bitmap whose final dimensions are based upon
     * 2 factors:
     *
     * 1) The application of a scaling factor, which is derived from the relative values of the
     * target density declared by the app and the current device's actual density
     * 2) The absolute dimensions of the bounding container into which this image is being inflated
     * (may just be the screen dimens)
     *
     * @return the bitmap, or null if none could be created from the source
     */
    public static Bitmap getBitmapScaledForNativeDensity(DisplayMetrics metrics, String imageFilepath,
                                                         int containerHeight, int containerWidth) {
        BitmapFactory.Options o = new BitmapFactory.Options();
        o.inJustDecodeBounds = true;
        o.inScaled = false;
        BitmapFactory.decodeFile(imageFilepath, o);
        int imageHeight = o.outHeight;
        int imageWidth = o.outWidth;

        double scaleFactor = computeInflationScaleFactor(metrics);
        int calculatedHeight = Math.round((float)(imageHeight * scaleFactor));
        int calculatedWidth = Math.round((float)(imageWidth * scaleFactor));

        if (containerHeight < imageHeight || containerWidth < imageWidth || calculatedHeight < imageHeight) {
            // If either the container dimens or calculated dimens impose a smaller dimension,
            // scale down
            return getBitmapScaledByTargetOrContainer(imageFilepath, imageHeight, imageWidth,
                    calculatedHeight, calculatedWidth, containerHeight, containerWidth, false);
        } else {
            return attemptBoundedScaleUp(imageFilepath, calculatedHeight, calculatedWidth,
                    containerHeight, containerWidth);
        }
    }

    /**
     * @return Our custom scale factor, based on this device's density and what the image's target
     * density was
     */
    public static double computeInflationScaleFactor(DisplayMetrics metrics) {
        final int SCREEN_DENSITY = metrics.densityDpi;
        double customDpiScaleFactor = (double)SCREEN_DENSITY /
                CommCarePreferences.getTargetInflationDensity();
        double proportionalAdjustmentFactor = getCustomAndroidAdjustmentFactor(metrics);
        return customDpiScaleFactor * proportionalAdjustmentFactor;
    }

    private static double getCustomAndroidAdjustmentFactor(DisplayMetrics metrics) {
        // This is the formula Android *usually* uses to compute the value of metrics.density
        // for a device. If this is in fact the value being used, we are not interested in it.
        // However, if the actual value differs at all from the standard calculation, it means
        // Android is taking other factors into consideration (such as straight up screen size)
        // when it re-sizes an image for this device, so we want to incorporate that proportionally
        // into our own version of the scale factor
        double standardNativeScaleFactor = (double)metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT;

        double actualNativeScaleFactor = metrics.density;
        if (actualNativeScaleFactor > standardNativeScaleFactor) {
            return 1 +
                    ((actualNativeScaleFactor - standardNativeScaleFactor) / standardNativeScaleFactor);
        } else if (actualNativeScaleFactor < standardNativeScaleFactor) {
            return actualNativeScaleFactor / standardNativeScaleFactor;
        } else {
            return 1;
        }
    }

    /**
     * @return A bitmap representation of the given image file, scaled down to the smallest
     * size that still fills the container
     *
     * More precisely, preserves the following 2 conditions:
     * 1. The larger of the 2 sides takes on the size of the corresponding container dimension
{'fraction_non_alphanumeric': 0.06742343915673922, 'fraction_numerical': 0.001746398054013597, 'mean_word_length': 2.414395229982964, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 5, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 10, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3093605', 'n_tokens_mistral': 3853, 'n_tokens_neox': 3680, 'n_words': 1248}