cordova-android/framework/src/com/phonegap/Capture.java
macdonst 70de3d49a2 Fix for Issue #218: audio capture audio/3gpp mimetype getting set to video/3gpp
Works around an issue where MimeTypeMap.getMimeTypeFromExtension() always returns video/3gpp when the file extension is .3gp or .3gpp.
2011-09-06 14:15:45 -07:00

369 lines
15 KiB
Java

/*
* PhoneGap is available under *either* the terms of the modified BSD license *or* the
* MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
*
* Copyright (c) 2005-2010, Nitobi Software Inc.
* Copyright (c) 2011, IBM Corporation
*/
package com.phonegap;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.app.Activity;
import android.content.ContentValues;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.MediaPlayer;
import android.net.Uri;
import android.util.Log;
import com.phonegap.api.Plugin;
import com.phonegap.api.PluginResult;
public class Capture extends Plugin {
private static final String VIDEO_3GPP = "video/3gpp";
private static final String AUDIO_3GPP = "audio/3gpp";
private static final String IMAGE_JPEG = "image/jpeg";
private static final int CAPTURE_AUDIO = 0; // Constant for capture audio
private static final int CAPTURE_IMAGE = 1; // Constant for capture image
private static final int CAPTURE_VIDEO = 2; // Constant for capture video
private static final String LOG_TAG = "Capture";
private String callbackId; // The ID of the callback to be invoked with our result
private long limit; // the number of pics/vids/clips to take
private double duration; // optional duration parameter for video recording
private JSONArray results; // The array of results to be returned to the user
private Uri imageUri; // Uri of captured image
@Override
public PluginResult execute(String action, JSONArray args, String callbackId) {
this.callbackId = callbackId;
this.limit = 1;
this.duration = 0.0f;
this.results = new JSONArray();
JSONObject options = args.optJSONObject(0);
if (options != null) {
limit = options.optLong("limit", 1);
duration = options.optDouble("duration", 0.0f);
}
if (action.equals("getFormatData")) {
try {
JSONObject obj = getFormatData(args.getString(0), args.getString(1));
return new PluginResult(PluginResult.Status.OK, obj);
} catch (JSONException e) {
return new PluginResult(PluginResult.Status.ERROR);
}
}
else if (action.equals("captureAudio")) {
this.captureAudio();
}
else if (action.equals("captureImage")) {
this.captureImage();
}
else if (action.equals("captureVideo")) {
this.captureVideo(duration);
}
PluginResult r = new PluginResult(PluginResult.Status.NO_RESULT);
r.setKeepCallback(true);
return r;
}
/**
* Provides the media data file data depending on it's mime type
*
* @param filePath path to the file
* @param mimeType of the file
* @return a MediaFileData object
*/
private JSONObject getFormatData(String filePath, String mimeType) {
JSONObject obj = new JSONObject();
try {
// setup defaults
obj.put("height", 0);
obj.put("width", 0);
obj.put("bitrate", 0);
obj.put("duration", 0);
obj.put("codecs", "");
// If the mimeType isn't set the rest will fail
// so let's see if we can determine it.
if (mimeType == null || mimeType.equals("")) {
mimeType = FileUtils.getMimeType(filePath);
}
Log.d(LOG_TAG, "Mime type = " + mimeType);
if (mimeType.equals(IMAGE_JPEG) || filePath.endsWith(".jpg")) {
obj = getImageData(filePath, obj);
}
else if (mimeType.endsWith(AUDIO_3GPP)) {
obj = getAudioVideoData(filePath, obj, false);
}
else if (mimeType.equals(VIDEO_3GPP)) {
obj = getAudioVideoData(filePath, obj, true);
}
}
catch (JSONException e) {
Log.d(LOG_TAG, "Error: setting media file data object");
}
return obj;
}
/**
* Get the Image specific attributes
*
* @param filePath path to the file
* @param obj represents the Media File Data
* @return a JSONObject that represents the Media File Data
* @throws JSONException
*/
private JSONObject getImageData(String filePath, JSONObject obj) throws JSONException {
Bitmap bitmap = BitmapFactory.decodeFile(filePath);
obj.put("height", bitmap.getHeight());
obj.put("width", bitmap.getWidth());
return obj;
}
/**
* Get the Image specific attributes
*
* @param filePath path to the file
* @param obj represents the Media File Data
* @param video if true get video attributes as well
* @return a JSONObject that represents the Media File Data
* @throws JSONException
*/
private JSONObject getAudioVideoData(String filePath, JSONObject obj, boolean video) throws JSONException {
MediaPlayer player = new MediaPlayer();
try {
player.setDataSource(filePath);
player.prepare();
obj.put("duration", player.getDuration());
if (video) {
obj.put("height", player.getVideoHeight());
obj.put("width", player.getVideoWidth());
}
}
catch (IOException e) {
Log.d(LOG_TAG, "Error: loading video file");
}
return obj;
}
/**
* Sets up an intent to capture audio. Result handled by onActivityResult()
*/
private void captureAudio() {
Intent intent = new Intent(android.provider.MediaStore.Audio.Media.RECORD_SOUND_ACTION);
this.ctx.startActivityForResult((Plugin) this, intent, CAPTURE_AUDIO);
}
/**
* Sets up an intent to capture images. Result handled by onActivityResult()
*/
private void captureImage() {
Intent intent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);
// Specify file so that large image is captured and returned
File photo = new File(DirectoryManager.getTempDirectoryPath(ctx), "Capture.jpg");
intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, Uri.fromFile(photo));
this.imageUri = Uri.fromFile(photo);
this.ctx.startActivityForResult((Plugin) this, intent, CAPTURE_IMAGE);
}
/**
* Sets up an intent to capture video. Result handled by onActivityResult()
*/
private void captureVideo(double duration) {
Intent intent = new Intent(android.provider.MediaStore.ACTION_VIDEO_CAPTURE);
// Introduced in API 8
//intent.putExtra(android.provider.MediaStore.EXTRA_DURATION_LIMIT, duration);
this.ctx.startActivityForResult((Plugin) this, intent, CAPTURE_VIDEO);
}
/**
* Called when the video view exits.
*
* @param requestCode The request code originally supplied to startActivityForResult(),
* allowing you to identify who this result came from.
* @param resultCode The integer result code returned by the child activity through its setResult().
* @param intent An Intent, which can return result data to the caller (various data can be attached to Intent "extras").
* @throws JSONException
*/
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
// Result received okay
if (resultCode == Activity.RESULT_OK) {
// An audio clip was requested
if (requestCode == CAPTURE_AUDIO) {
// Get the uri of the audio clip
Uri data = intent.getData();
// create a file object from the uri
results.put(createMediaFile(data));
if (results.length() >= limit) {
// Send Uri back to JavaScript for listening to audio
this.success(new PluginResult(PluginResult.Status.OK, results, "navigator.device.capture._castMediaFile"), this.callbackId);
} else {
// still need to capture more audio clips
captureAudio();
}
} else if (requestCode == CAPTURE_IMAGE) {
// For some reason if I try to do:
// Uri data = intent.getData();
// It crashes in the emulator and on my phone with a null pointer exception
// To work around it I had to grab the code from CameraLauncher.java
try {
// Create an ExifHelper to save the exif data that is lost during compression
ExifHelper exif = new ExifHelper();
exif.createInFile(DirectoryManager.getTempDirectoryPath(ctx) + "/Capture.jpg");
exif.readExifData();
// Read in bitmap of captured image
Bitmap bitmap = android.provider.MediaStore.Images.Media.getBitmap(this.ctx.getContentResolver(), imageUri);
// Create entry in media store for image
// (Don't use insertImage() because it uses default compression setting of 50 - no way to change it)
ContentValues values = new ContentValues();
values.put(android.provider.MediaStore.Images.Media.MIME_TYPE, IMAGE_JPEG);
Uri uri = null;
try {
uri = this.ctx.getContentResolver().insert(android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
} catch (UnsupportedOperationException e) {
System.out.println("Can't write to external media storage.");
try {
uri = this.ctx.getContentResolver().insert(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, values);
} catch (UnsupportedOperationException ex) {
System.out.println("Can't write to internal media storage.");
this.fail("Error capturing image - no media storage found.");
return;
}
}
// Add compressed version of captured image to returned media store Uri
OutputStream os = this.ctx.getContentResolver().openOutputStream(uri);
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os);
os.close();
bitmap.recycle();
bitmap = null;
System.gc();
// Restore exif data to file
exif.createOutFile(FileUtils.getRealPathFromURI(uri, this.ctx));
exif.writeExifData();
// Add image to results
results.put(createMediaFile(uri));
if (results.length() >= limit) {
// Send Uri back to JavaScript for viewing image
this.success(new PluginResult(PluginResult.Status.OK, results, "navigator.device.capture._castMediaFile"), this.callbackId);
} else {
// still need to capture more images
captureImage();
}
} catch (IOException e) {
e.printStackTrace();
this.fail("Error capturing image.");
}
} else if (requestCode == CAPTURE_VIDEO) {
// Get the uri of the video clip
Uri data = intent.getData();
// create a file object from the uri
results.put(createMediaFile(data));
if (results.length() >= limit) {
// Send Uri back to JavaScript for viewing video
this.success(new PluginResult(PluginResult.Status.OK, results, "navigator.device.capture._castMediaFile"), this.callbackId);
} else {
// still need to capture more video clips
captureVideo(duration);
}
}
}
// If canceled
else if (resultCode == Activity.RESULT_CANCELED) {
// If we have partial results send them back to the user
if (results.length() > 0) {
this.success(new PluginResult(PluginResult.Status.OK, results, "navigator.device.capture._castMediaFile"), this.callbackId);
}
// user canceled the action
else {
this.fail("Canceled.");
}
}
// If something else
else {
// If we have partial results send them back to the user
if (results.length() > 0) {
this.success(new PluginResult(PluginResult.Status.OK, results, "navigator.device.capture._castMediaFile"), this.callbackId);
}
// something bad happened
else {
this.fail("Did not complete!");
}
}
}
/**
* Creates a JSONObject that represents a File from the Uri
*
* @param data the Uri of the audio/image/video
* @return a JSONObject that represents a File
* @throws IOException
*/
private JSONObject createMediaFile(Uri data){
File fp = new File(FileUtils.getRealPathFromURI(data, this.ctx));
JSONObject obj = new JSONObject();
try {
// File properties
obj.put("name", fp.getName());
obj.put("fullPath", fp.getAbsolutePath());
// Because of an issue with MimeTypeMap.getMimeTypeFromExtension() all .3gpp files
// are reported as video/3gpp. I'm doing this hacky check of the URI to see if it
// is stored in the audio or video content store.
if (fp.getAbsoluteFile().toString().endsWith(".3gp") || fp.getAbsoluteFile().toString().endsWith(".3gpp")) {
if (data.toString().contains("/audio/")) {
obj.put("type", AUDIO_3GPP);
} else {
obj.put("type", VIDEO_3GPP);
}
} else {
obj.put("type", FileUtils.getMimeType(fp.getAbsolutePath()));
}
obj.put("lastModifiedDate", fp.lastModified());
obj.put("size", fp.length());
} catch (JSONException e) {
// this will never happen
e.printStackTrace();
}
return obj;
}
/**
* Send error message to JavaScript.
*
* @param err
*/
public void fail(String err) {
this.error(new PluginResult(PluginResult.Status.ERROR, err), this.callbackId);
}
}