<commit_before>// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0

package com.google.appinventor.components.runtime.util;

import com.google.appinventor.components.runtime.Form;
import com.google.appinventor.components.runtime.ReplForm;

import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.media.MediaPlayer;
import android.media.SoundPool;
import android.net.Uri;
import android.os.Environment;
import android.provider.Contacts;
import android.util.Log;
import android.view.Display;
import android.view.WindowManager;
import android.widget.VideoView;

import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Utilities for loading media.
 *
 * @author <EMAIL> (<NAME>)
 */
public class MediaUtil {

  private enum MediaSource { ASSET, REPL_ASSET, SDCARD, FILE_URL, URL, CONTENT_URI, CONTACT_URI }

  private static final String LOG_TAG = "MediaUtil";
  private static final String REPL_ASSET_DIR = "/sdcard/AppInventor/assets/";

  // tempFileMap maps cached media (assets, etc) to their respective temp files.
  private static final Map<String, File> tempFileMap = new HashMap<String, File>();

  private MediaUtil() {
  }

  private static String replAssetPath(String assetName) {
    return REPL_ASSET_DIR + assetName;
  }

  static String fileUrlToFilePath(String mediaPath) throws IOException {
    try {
      return new File(new URL(mediaPath).toURI()).getAbsolutePath();
    } catch (IllegalArgumentException e) {
      throw new IOException("Unable to determine file path of file url " + mediaPath);
    } catch (Exception e) {
      throw new IOException("Unable to determine file path of file url " + mediaPath);
    }
  }

  /**
   * Determines the appropriate MediaSource for the given mediaPath.
   *
   * <p>If <code>mediaPath</code> begins with "/sdcard/", or begins with
   * the path given by {@link Environment#getExternalStorageDirectory()},
   * it is the name of a file on the SD card.
   * <p>Otherwise, if <code>mediaPath</code> starts with "content://contacts",
   * it is the content URI of a contact.
   * <p>Otherwise, if <code>mediaPath</code> starts with "content://", it is a
   * content URI.
   * <p>Otherwise, if <code>mediaPath</code> is a well-formed URL and it starts
   * with "file:", it is a file URL.
   * <p>Otherwise, if <code>mediaPath</code> is a well-formed URL, it is an
   * URL.
   * <p>Otherwise, if <code>mediaPath</code> it is assumed to be the name of
   * an asset.
   *
   * @param form the Form
   * @param mediaPath the path to the media
   */
  private static MediaSource determineMediaSource(Form form, String mediaPath) {
    if (mediaPath.startsWith("/sdcard/") ||
        mediaPath.startsWith(Environment.getExternalStorageDirectory().getAbsolutePath())) {
      return MediaSource.SDCARD;

    } else if (mediaPath.startsWith("content://contacts/")) {
      return MediaSource.CONTACT_URI;

    } else if (mediaPath.startsWith("content://")) {
      return MediaSource.CONTENT_URI;
    }

    try {
      new URL(mediaPath);
      // It's a well formed URL.
      if (mediaPath.startsWith("file:")) {
        return MediaSource.FILE_URL;
      }

      return MediaSource.URL;

    } catch (MalformedURLException e) {
      // It's not a well formed URL!
    }

    if (form instanceof ReplForm) {
      if (((ReplForm)form).isAssetsLoaded())
        return MediaSource.REPL_ASSET;
      else
        return MediaSource.ASSET;
    }

    return MediaSource.ASSET;
  }

  private static ConcurrentHashMap<String, String> pathCache = new ConcurrentHashMap<String, String>(2);

  private static String findCaseinsensitivePath(Form form, String mediaPath)
      throws IOException{
    if( !pathCache.containsKey(mediaPath) ){
      String newPath = findCaseinsensitivePathWithoutCache(form, mediaPath);
      if( newPath == null){
        return null;
      }
      pathCache.put(mediaPath, newPath);
    }
    return pathCache.get(mediaPath);
  }

  /**
   * Don't use this directly! Use findCaseinsensitivePath. It has caching.
   * This is the original findCaseinsensitivePath, unchanged.
   * @param form the Form
   * @param mediaPath the path to the media to resolve
   * @return the correct path, adjusted for case errors
   * @throws IOException
   */
  private static String findCaseinsensitivePathWithoutCache(Form form, String mediaPath)
      throws IOException{
    String[] mediaPathlist = form.getAssets().list("");
    int l = Array.getLength(mediaPathlist);
    for (int i=0; i<l; i++){
      String temp = mediaPathlist[i];
      if (temp.equalsIgnoreCase(mediaPath)){
        return temp;
      }
    }
    return null;
  }

  /**
   * find path of an asset from a mediaPath using case-insensitive comparison,
   * return type InputStream.
   * Throws IOException if there is no matching path
   * @param form the Form
   * @param mediaPath the path to the media
   */
  private static InputStream getAssetsIgnoreCaseInputStream(Form form, String mediaPath)
      throws IOException{
    try {
      return form.getAssets().open(mediaPath);

    } catch (IOException e) {
      String path = findCaseinsensitivePath(form, mediaPath);
      if (path == null) {
          throw e;
        } else {
          return form.getAssets().open(path);
        }
    }
  }

  private static InputStream openMedia(Form form, String mediaPath, MediaSource mediaSource)
      throws IOException {
    switch (mediaSource) {
      case ASSET:
        return getAssetsIgnoreCaseInputStream(form,mediaPath);

      case REPL_ASSET:
        return new FileInputStream(replAssetPath(mediaPath));

      case SDCARD:
        return new FileInputStream(mediaPath);

      case FILE_URL:
      case URL:
        return new URL(mediaPath).openStream();

      case CONTENT_URI:
        return form.getContentResolver().openInputStream(Uri.parse(mediaPath));

      case CONTACT_URI:
        // Open the photo for the contact.
        InputStream is = null;
        if (SdkLevel.getLevel() >= SdkLevel.LEVEL_HONEYCOMB_MR1) {
          is = HoneycombMR1Util.openContactPhotoInputStreamHelper(form.getContentResolver(),
              Uri.parse(mediaPath));
        } else {
          is = Contacts.People.openContactPhotoInputStream(form.getContentResolver(),
              Uri.parse(mediaPath));
        }
        if (is != null) {
          return is;
        }
        // There's no photo for the contact.
        throw new IOException("Unable to open contact photo " + mediaPath + ".");
    }
    throw new IOException("Unable to open media " + mediaPath + ".");
  }

  public static InputStream openMedia(Form form, String mediaPath) throws IOException {
    return openMedia(form, mediaPath, determineMediaSource(form, mediaPath));
  }

  /**
   * Copies the media specified by mediaPath to a temp file and returns the
   * File.
   *
   * @param form the Form
   * @param mediaPath the path to the media
   */
  public static File copyMediaToTempFile(Form form, String mediaPath)
      throws IOException {
    MediaSource mediaSource = determineMediaSource(form, mediaPath);
    return copyMediaToTempFile(form, mediaPath, mediaSource);
  }

  private static File copyMediaToTempFile(Form form, String mediaPath, MediaSource mediaSource)
      throws IOException {
    InputStream in = openMedia(form, mediaPath, mediaSource);
    File file = null;
    try {
      file = File.createTempFile("AI_Media_", null);
      file.deleteOnExit();
      FileUtil.writeStreamToFile(in, file.getAbsolutePath());
      return file;

    } catch (IOException e) {
      if (file != null) {
        Log.e(LOG_TAG, "Could not copy media " + mediaPath + " to temp file " +
            file.getAbsolutePath());
        file.delete();
      } else {
        Log.e(LOG_TAG, "Could not copy media " + mediaPath + " to temp file.");
      }
      // TODO(lizlooney) - figure out how much space is left on the SD card and log that
      // information.
      throw e;

    } finally {
      in.close();
    }
  }

  private static File cacheMediaTempFile(Form form, String mediaPath, MediaSource mediaSource)
      throws IOException {
    File tempFile = tempFileMap.get(mediaPath);
    // If the map didn't contain an entry for mediaPath, or if the temp file no longer exists,
    // copy the file to a new temp file.
    if (tempFile == null || !tempFile.exists()) {
      Log.i(LOG_TAG, "Copying media " + mediaPath + " to temp file...");
      tempFile = copyMediaToTempFile(form, mediaPath, mediaSource);
      Log.i(LOG_TAG, "Finished copying media " + mediaPath + " to temp file " +
          tempFile.getAbsolutePath());
      tempFileMap.put(mediaPath, tempFile);
    }
    return tempFile;
  }

  // Image related methods

  /**
   * Loads the image specified by mediaPath and returns a Drawable.
   *
   * <p/>If mediaPath is null or empty, null is returned.
   *
   * @param form the Form
   * @param mediaPath the path to the media
   * @return a Drawable or null
   */
  public static BitmapDrawable getBitmapDrawable(Form form, String mediaPath) throws IOException {
    if (mediaPath == null || mediaPath.length() == 0) {
      return null;
    }

    MediaSource mediaSource = determineMediaSource(form, mediaPath);

    // Unlike other types of media, we don't cache image files from the internet to temp files.
    // The image at a particular URL, such as an image from a web cam, may change over time.
    // When the app says to fetch the image, we need to get the latest image, not one that we
    // cached previously.

    InputStream is1;
    try {
      is1 = openMedia(form, mediaPath, mediaSource);
    } catch (IOException e) {
      if (mediaSource == MediaSource.CONTACT_URI) {
        // There's no photo for this contact, return a placeholder image.
        return new BitmapDrawable(BitmapFactory.decodeResource(form.getResources(),
            android.R.drawable.picture_frame, null));
      }
      throw e;
    }

    BitmapFactory.Options options;
    try {
      options = getBitmapOptions(form, is1, mediaPath);
    } finally {
      is1.close();
    }

    InputStream is2 = openMedia(form, mediaPath, mediaSource);
    try {
      Log.d(LOG_TAG, "mediaPath = " + mediaPath);
      BitmapDrawable originalBitmapDrawable = new BitmapDrawable(decodeStream(is2, null, options));
      // If options.inSampleSize == 1, then the image was not unreasonably large and may represent
      // the actual size the user intended for the image. However we still have to scale it by
      // the device density.
      // However if we *did* sample the image to make it smaller, then that means that the image
      // was not sized specifically for the application. In that case it makes no sense to
      // scale it, so we don't.
      // When we scale the image we do the following steps:
      //   1. set the density in the returned bitmap drawable.
      //   2. calculate scaled width and height
      //   3. create a scaled bitmap with the scaled measures
      //   4. create a new bitmap drawable with the scaled bitmap
      //   5. set the density in the scaled bitmap.

      // Note: The log line below requires HoneycombMR1, but we may not keep it, so punting for now
      Log.d(LOG_TAG, "originalBitmap.getByteCount() = " + originalBitmapDrawable.getBitmap().getByteCount());

      originalBitmapDrawable.setTargetDensity(form.getResources().getDisplayMetrics());
      if (options.inSampleSize != 1) {
        return originalBitmapDrawable;
      }
      int scaledWidth = (int) (form.deviceDensity() * originalBitmapDrawable.getIntrinsicWidth());
      int scaledHeight = (int) (form.deviceDensity() * originalBitmapDrawable.getIntrinsicHeight());
      Log.d(LOG_TAG, "form.deviceDensity() = " + form.deviceDensity());<commit_msg>Remove errant logging line that fails on < Gingerbread<commit_after>// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0

package com.google.appinventor.components.runtime.util;

import com.google.appinventor.components.runtime.Form;
import com.google.appinventor.components.runtime.ReplForm;

import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.media.MediaPlayer;
import android.media.SoundPool;
import android.net.Uri;
import android.os.Environment;
import android.provider.Contacts;
import android.util.Log;
import android.view.Display;
import android.view.WindowManager;
import android.widget.VideoView;

import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Utilities for loading media.
 *
 * @author <EMAIL> (<NAME>)
 */
public class MediaUtil {

  private enum MediaSource { ASSET, REPL_ASSET, SDCARD, FILE_URL, URL, CONTENT_URI, CONTACT_URI }

  private static final String LOG_TAG = "MediaUtil";
  private static final String REPL_ASSET_DIR = "/sdcard/AppInventor/assets/";

  // tempFileMap maps cached media (assets, etc) to their respective temp files.
  private static final Map<String, File> tempFileMap = new HashMap<String, File>();

  private MediaUtil() {
  }

  private static String replAssetPath(String assetName) {
    return REPL_ASSET_DIR + assetName;
  }

  static String fileUrlToFilePath(String mediaPath) throws IOException {
    try {
      return new File(new URL(mediaPath).toURI()).getAbsolutePath();
    } catch (IllegalArgumentException e) {
      throw new IOException("Unable to determine file path of file url " + mediaPath);
    } catch (Exception e) {
      throw new IOException("Unable to determine file path of file url " + mediaPath);
    }
  }

  /**
   * Determines the appropriate MediaSource for the given mediaPath.
   *
   * <p>If <code>mediaPath</code> begins with "/sdcard/", or begins with
   * the path given by {@link Environment#getExternalStorageDirectory()},
   * it is the name of a file on the SD card.
   * <p>Otherwise, if <code>mediaPath</code> starts with "content://contacts",
   * it is the content URI of a contact.
   * <p>Otherwise, if <code>mediaPath</code> starts with "content://", it is a
   * content URI.
   * <p>Otherwise, if <code>mediaPath</code> is a well-formed URL and it starts
   * with "file:", it is a file URL.
   * <p>Otherwise, if <code>mediaPath</code> is a well-formed URL, it is an
   * URL.
   * <p>Otherwise, if <code>mediaPath</code> it is assumed to be the name of
   * an asset.
   *
   * @param form the Form
   * @param mediaPath the path to the media
   */
  private static MediaSource determineMediaSource(Form form, String mediaPath) {
    if (mediaPath.startsWith("/sdcard/") ||
        mediaPath.startsWith(Environment.getExternalStorageDirectory().getAbsolutePath())) {
      return MediaSource.SDCARD;

    } else if (mediaPath.startsWith("content://contacts/")) {
      return MediaSource.CONTACT_URI;

    } else if (mediaPath.startsWith("content://")) {
      return MediaSource.CONTENT_URI;
    }

    try {
      new URL(mediaPath);
      // It's a well formed URL.
      if (mediaPath.startsWith("file:")) {
        return MediaSource.FILE_URL;
      }

      return MediaSource.URL;

    } catch (MalformedURLException e) {
      // It's not a well formed URL!
    }

    if (form instanceof ReplForm) {
      if (((ReplForm)form).isAssetsLoaded())
        return MediaSource.REPL_ASSET;
      else
        return MediaSource.ASSET;
    }

    return MediaSource.ASSET;
  }

  private static ConcurrentHashMap<String, String> pathCache = new ConcurrentHashMap<String, String>(2);

  private static String findCaseinsensitivePath(Form form, String mediaPath)
      throws IOException{
    if( !pathCache.containsKey(mediaPath) ){
      String newPath = findCaseinsensitivePathWithoutCache(form, mediaPath);
      if( newPath == null){
        return null;
      }
      pathCache.put(mediaPath, newPath);
    }
    return pathCache.get(mediaPath);
  }

  /**
   * Don't use this directly! Use findCaseinsensitivePath. It has caching.
   * This is the original findCaseinsensitivePath, unchanged.
   * @param form the Form
   * @param mediaPath the path to the media to resolve
   * @return the correct path, adjusted for case errors
   * @throws IOException
   */
  private static String findCaseinsensitivePathWithoutCache(Form form, String mediaPath)
      throws IOException{
    String[] mediaPathlist = form.getAssets().list("");
    int l = Array.getLength(mediaPathlist);
    for (int i=0; i<l; i++){
      String temp = mediaPathlist[i];
      if (temp.equalsIgnoreCase(mediaPath)){
        return temp;
      }
    }
    return null;
  }

  /**
   * find path of an asset from a mediaPath using case-insensitive comparison,
   * return type InputStream.
   * Throws IOException if there is no matching path
   * @param form the Form
   * @param mediaPath the path to the media
   */
  private static InputStream getAssetsIgnoreCaseInputStream(Form form, String mediaPath)
      throws IOException{
    try {
      return form.getAssets().open(mediaPath);

    } catch (IOException e) {
      String path = findCaseinsensitivePath(form, mediaPath);
      if (path == null) {
          throw e;
        } else {
          return form.getAssets().open(path);
        }
    }
  }

  private static InputStream openMedia(Form form, String mediaPath, MediaSource mediaSource)
      throws IOException {
    switch (mediaSource) {
      case ASSET:
        return getAssetsIgnoreCaseInputStream(form,mediaPath);

      case REPL_ASSET:
        return new FileInputStream(replAssetPath(mediaPath));

      case SDCARD:
        return new FileInputStream(mediaPath);

      case FILE_URL:
      case URL:
        return new URL(mediaPath).openStream();

      case CONTENT_URI:
        return form.getContentResolver().openInputStream(Uri.parse(mediaPath));

      case CONTACT_URI:
        // Open the photo for the contact.
        InputStream is = null;
        if (SdkLevel.getLevel() >= SdkLevel.LEVEL_HONEYCOMB_MR1) {
          is = HoneycombMR1Util.openContactPhotoInputStreamHelper(form.getContentResolver(),
              Uri.parse(mediaPath));
        } else {
          is = Contacts.People.openContactPhotoInputStream(form.getContentResolver(),
              Uri.parse(mediaPath));
        }
        if (is != null) {
          return is;
        }
        // There's no photo for the contact.
        throw new IOException("Unable to open contact photo " + mediaPath + ".");
    }
    throw new IOException("Unable to open media " + mediaPath + ".");
  }

  public static InputStream openMedia(Form form, String mediaPath) throws IOException {
    return openMedia(form, mediaPath, determineMediaSource(form, mediaPath));
  }

  /**
   * Copies the media specified by mediaPath to a temp file and returns the
   * File.
   *
   * @param form the Form
   * @param mediaPath the path to the media
   */
  public static File copyMediaToTempFile(Form form, String mediaPath)
      throws IOException {
    MediaSource mediaSource = determineMediaSource(form, mediaPath);
    return copyMediaToTempFile(form, mediaPath, mediaSource);
  }

  private static File copyMediaToTempFile(Form form, String mediaPath, MediaSource mediaSource)
      throws IOException {
    InputStream in = openMedia(form, mediaPath, mediaSource);
    File file = null;
    try {
      file = File.createTempFile("AI_Media_", null);
      file.deleteOnExit();
      FileUtil.writeStreamToFile(in, file.getAbsolutePath());
      return file;

    } catch (IOException e) {
      if (file != null) {
        Log.e(LOG_TAG, "Could not copy media " + mediaPath + " to temp file " +
            file.getAbsolutePath());
        file.delete();
      } else {
        Log.e(LOG_TAG, "Could not copy media " + mediaPath + " to temp file.");
      }
      // TODO(lizlooney) - figure out how much space is left on the SD card and log that
      // information.
      throw e;

    } finally {
      in.close();
    }
  }

  private static File cacheMediaTempFile(Form form, String mediaPath, MediaSource mediaSource)
      throws IOException {
    File tempFile = tempFileMap.get(mediaPath);
    // If the map didn't contain an entry for mediaPath, or if the temp file no longer exists,
    // copy the file to a new temp file.
    if (tempFile == null || !tempFile.exists()) {
      Log.i(LOG_TAG, "Copying media " + mediaPath + " to temp file...");
      tempFile = copyMediaToTempFile(form, mediaPath, mediaSource);
      Log.i(LOG_TAG, "Finished copying media " + mediaPath + " to temp file " +
          tempFile.getAbsolutePath());
      tempFileMap.put(mediaPath, tempFile);
    }
    return tempFile;
  }

  // Image related methods

  /**
   * Loads the image specified by mediaPath and returns a Drawable.
   *
   * <p/>If mediaPath is null or empty, null is returned.
   *
   * @param form the Form
   * @param mediaPath the path to the media
   * @return a Drawable or null
   */
  public static BitmapDrawable getBitmapDrawable(Form form, String mediaPath) throws IOException {
    if (mediaPath == null || mediaPath.length() == 0) {
      return null;
    }

    MediaSource mediaSource = determineMediaSource(form, mediaPath);

    // Unlike other types of media, we don't cache image files from the internet to temp files.
    // The image at a particular URL, such as an image from a web cam, may change over time.
    // When the app says to fetch the image, we need to get the latest image, not one that we
    // cached previously.

    InputStream is1;
    try {
      is1 = openMedia(form, mediaPath, mediaSource);
    } catch (IOException e) {
      if (mediaSource == MediaSource.CONTACT_URI) {
        // There's no photo for this contact, return a placeholder image.
        return new BitmapDrawable(BitmapFactory.decodeResource(form.getResources(),
            android.R.drawable.picture_frame, null));
      }
      throw e;
    }

    BitmapFactory.Options options;
    try {
      options = getBitmapOptions(form, is1, mediaPath);
    } finally {
      is1.close();
    }

    InputStream is2 = openMedia(form, mediaPath, mediaSource);
    try {
      Log.d(LOG_TAG, "mediaPath = " + mediaPath);
      BitmapDrawable originalBitmapDrawable = new BitmapDrawable(decodeStream(is2, null, options));
      // If options.inSampleSize == 1, then the image was not unreasonably large and may represent
      // the actual size the user intended for the image. However we still have to scale it by
      // the device density.
      // However if we *did* sample the image to make it smaller, then that means that the image
      // was not sized specifically for the application. In that case it makes no sense to
      // scale it, so we don't.
      // When we scale the image we do the following steps:
      //   1. set the density in the returned bitmap drawable.
      //   2. calculate scaled width and height
      //   3. create a scaled bitmap with the scaled measures
      //   4. create a new bitmap drawable with the scaled bitmap
      //   5. set the density in the scaled bitmap.

      originalBitmapDrawable.setTargetDensity(form.getResources().getDisplayMetrics());
      if (options.inSampleSize != 1) {
        return originalBitmapDrawable;
      }
      int scaledWidth = (int) (form.deviceDensity() * originalBitmapDrawable.getIntrinsicWidth());
      int scaledHeight = (int) (form.deviceDensity() * originalBitmapDrawable.getIntrinsicHeight());
      Log.d(LOG_TAG, "form.deviceDensity() = " + form.deviceDensity());
{'fraction_non_alphanumeric': 0.09822370021899586, 'fraction_numerical': 0.0032038283721307487, 'mean_word_length': 3.059762923938097, 'pattern_counts': {'":': 0, '<': 56, '<?xml version=': 0, '>': 55, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3755445', 'n_tokens_mistral': 7075, 'n_tokens_neox': 6907, 'n_words': 2234}