mirror of
https://github.com/apache/cordova-android.git
synced 2025-02-20 23:56:20 +08:00
W3C Media Capture API
An implementation of the W3C Media Capture spec: http://dev.w3.org/2009/dap/camera/Overview-API Capture operations are supported for audio, video, and images. Each capture operation launches the native audio recorder, video recorder, or camera application, respectively.
This commit is contained in:
parent
d74569ffa7
commit
e766188689
187
framework/assets/js/capture.js
Normal file
187
framework/assets/js/capture.js
Normal file
@ -0,0 +1,187 @@
|
||||
/*
|
||||
* 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) 2010-2011, IBM Corporation
|
||||
*/
|
||||
|
||||
/**
|
||||
* The CaptureError interface encapsulates all errors in the Capture API.
|
||||
*/
|
||||
function CaptureError() {
|
||||
this.code = null;
|
||||
};
|
||||
|
||||
// Capture error codes
|
||||
CaptureError.CAPTURE_INTERNAL_ERR = 0;
|
||||
CaptureError.CAPTURE_APPLICATION_BUSY = 1;
|
||||
CaptureError.CAPTURE_INVALID_ARGUMENT = 2;
|
||||
CaptureError.CAPTURE_NO_MEDIA_FILES = 3;
|
||||
CaptureError.CAPTURE_NOT_SUPPORTED = 20;
|
||||
|
||||
/**
|
||||
* The Capture interface exposes an interface to the camera and microphone of the hosting device.
|
||||
*/
|
||||
function Capture() {
|
||||
this.supportedAudioFormats = [];
|
||||
this.supportedImageFormats = [];
|
||||
this.supportedVideoFormats = [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Launch audio recorder application for recording audio clip(s).
|
||||
*
|
||||
* @param {Function} successCB
|
||||
* @param {Function} errorCB
|
||||
* @param {CaptureAudioOptions} options
|
||||
*/
|
||||
Capture.prototype.captureAudio = function(successCallback, errorCallback, options) {
|
||||
PhoneGap.exec(successCallback, errorCallback, "Capture", "captureAudio", [options]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Launch camera application for taking image(s).
|
||||
*
|
||||
* @param {Function} successCB
|
||||
* @param {Function} errorCB
|
||||
* @param {CaptureImageOptions} options
|
||||
*/
|
||||
Capture.prototype.captureImage = function(successCallback, errorCallback, options) {
|
||||
PhoneGap.exec(successCallback, errorCallback, "Capture", "captureImage", [options]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Launch camera application for taking image(s).
|
||||
*
|
||||
* @param {Function} successCB
|
||||
* @param {Function} errorCB
|
||||
* @param {CaptureImageOptions} options
|
||||
*/
|
||||
Capture.prototype._castMediaFile = function(pluginResult) {
|
||||
var mediaFiles = [];
|
||||
var i;
|
||||
for (i=0; i<pluginResult.message.length; i++) {
|
||||
var mediaFile = new MediaFile();
|
||||
mediaFile.name = pluginResult.message[i].name;
|
||||
mediaFile.fullPath = pluginResult.message[i].fullPath;
|
||||
mediaFile.type = pluginResult.message[i].type;
|
||||
mediaFile.lastModifiedDate = pluginResult.message[i].lastModifiedDate;
|
||||
mediaFile.size = pluginResult.message[i].size;
|
||||
mediaFiles.push(mediaFile);
|
||||
}
|
||||
pluginResult.message = mediaFiles;
|
||||
return pluginResult;
|
||||
};
|
||||
|
||||
/**
|
||||
* Launch device camera application for recording video(s).
|
||||
*
|
||||
* @param {Function} successCB
|
||||
* @param {Function} errorCB
|
||||
* @param {CaptureVideoOptions} options
|
||||
*/
|
||||
Capture.prototype.captureVideo = function(successCallback, errorCallback, options) {
|
||||
PhoneGap.exec(successCallback, errorCallback, "Capture", "captureVideo", [options]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Encapsulates a set of parameters that the capture device supports.
|
||||
*/
|
||||
function ConfigurationData() {
|
||||
// The ASCII-encoded string in lower case representing the media type.
|
||||
this.type;
|
||||
// The height attribute represents height of the image or video in pixels.
|
||||
// In the case of a sound clip this attribute has value 0.
|
||||
this.height = 0;
|
||||
// The width attribute represents width of the image or video in pixels.
|
||||
// In the case of a sound clip this attribute has value 0
|
||||
this.width = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Encapsulates all image capture operation configuration options.
|
||||
*/
|
||||
function CaptureImageOptions() {
|
||||
// Upper limit of images user can take. Value must be equal or greater than 1.
|
||||
this.limit = 1;
|
||||
// The selected image mode. Must match with one of the elements in supportedImageModes array.
|
||||
this.mode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Encapsulates all video capture operation configuration options.
|
||||
*/
|
||||
function CaptureVideoOptions() {
|
||||
// Upper limit of videos user can record. Value must be equal or greater than 1.
|
||||
this.limit;
|
||||
// Maximum duration of a single video clip in seconds.
|
||||
this.duration;
|
||||
// The selected video mode. Must match with one of the elements in supportedVideoModes array.
|
||||
this.mode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Encapsulates all audio capture operation configuration options.
|
||||
*/
|
||||
function CaptureAudioOptions() {
|
||||
// Upper limit of sound clips user can record. Value must be equal or greater than 1.
|
||||
this.limit;
|
||||
// Maximum duration of a single sound clip in seconds.
|
||||
this.duration;
|
||||
// The selected audio mode. Must match with one of the elements in supportedAudioModes array.
|
||||
this.mode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a single file.
|
||||
*
|
||||
* name {DOMString} name of the file, without path information
|
||||
* fullPath {DOMString} the full path of the file, including the name
|
||||
* type {DOMString} mime type
|
||||
* lastModifiedDate {Date} last modified date
|
||||
* size {Number} size of the file in bytes
|
||||
*/
|
||||
function MediaFile(name, fullPath, type, lastModifiedDate, size) {
|
||||
this.name = name || null;
|
||||
this.fullPath = fullPath || null;
|
||||
this.type = type || null;
|
||||
this.lastModifiedDate = lastModifiedDate || null;
|
||||
this.size = size || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch device camera application for recording video(s).
|
||||
*
|
||||
* @param {Function} successCB
|
||||
* @param {Function} errorCB
|
||||
*/
|
||||
MediaFile.prototype.getFormatData = function(successCallback, errorCallback) {
|
||||
PhoneGap.exec(successCallback, errorCallback, "Capture", "getFormatData", [this.fullPath, this.type]);
|
||||
};
|
||||
|
||||
/**
|
||||
* MediaFileData encapsulates format information of a media file.
|
||||
*
|
||||
* @param {DOMString} codecs
|
||||
* @param {long} bitrate
|
||||
* @param {long} height
|
||||
* @param {long} width
|
||||
* @param {float} duration
|
||||
*/
|
||||
function MediaFileData(codecs, bitrate, height, width, duration) {
|
||||
this.codecs = codecs || null;
|
||||
this.bitrate = bitrate || 0;
|
||||
this.height = height || 0;
|
||||
this.width = width || 0;
|
||||
this.duration = duration || 0;
|
||||
}
|
||||
|
||||
PhoneGap.addConstructor(function() {
|
||||
if (typeof navigator.device === "undefined") {
|
||||
navigator.device = window.device = new Device();
|
||||
}
|
||||
if (typeof navigator.device.capture === "undefined") {
|
||||
navigator.device.capture = window.device.capture = new Capture();
|
||||
}
|
||||
});
|
@ -98,6 +98,8 @@ Device.prototype.exitApp = function() {
|
||||
};
|
||||
|
||||
PhoneGap.addConstructor(function() {
|
||||
navigator.device = window.device = new Device();
|
||||
if (typeof navigator.device === "undefined") {
|
||||
navigator.device = window.device = new Device();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -9,11 +9,6 @@
|
||||
if (!PhoneGap.hasResource("file")) {
|
||||
PhoneGap.addResource("file");
|
||||
|
||||
/**
|
||||
* This class provides generic read and write access to the mobile device file system.
|
||||
* They are not used to read files from a server.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class provides some useful information about a file.
|
||||
* This is the fields returned when navigator.fileMgr.getFileProperties()
|
||||
|
346
framework/src/com/phonegap/Capture.java
Normal file
346
framework/src/com/phonegap/Capture.java
Normal file
@ -0,0 +1,346 @@
|
||||
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.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.media.MediaPlayer;
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
import android.util.Log;
|
||||
|
||||
import com.phonegap.api.Plugin;
|
||||
import com.phonegap.api.PluginResult;
|
||||
|
||||
public class Capture extends Plugin {
|
||||
|
||||
private static final String _DATA = "_data"; // The column name where the file path is stored
|
||||
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 (mimeType.equals("image/jpeg")) {
|
||||
obj = getImageData(filePath, obj);
|
||||
}
|
||||
else if (filePath.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(Environment.getExternalStorageDirectory(), "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 {
|
||||
// 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();
|
||||
|
||||
// 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
|
||||
*/
|
||||
private JSONObject createMediaFile(Uri data) {
|
||||
File fp = new File(getRealPathFromURI(data));
|
||||
|
||||
JSONObject obj = new JSONObject();
|
||||
|
||||
try {
|
||||
// File properties
|
||||
obj.put("name", fp.getName());
|
||||
obj.put("fullPath", fp.getAbsolutePath());
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries the media store to find out what the file path is for the Uri we supply
|
||||
*
|
||||
* @param contentUri the Uri of the audio/image/video
|
||||
* @return the full path to the file
|
||||
*/
|
||||
private String getRealPathFromURI(Uri contentUri) {
|
||||
String[] proj = { _DATA };
|
||||
Cursor cursor = this.ctx.managedQuery(contentUri, proj, null, null, null);
|
||||
int column_index = cursor.getColumnIndexOrThrow(_DATA);
|
||||
cursor.moveToFirst();
|
||||
return cursor.getString(column_index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send error message to JavaScript.
|
||||
*
|
||||
* @param err
|
||||
*/
|
||||
public void fail(String err) {
|
||||
this.error(new PluginResult(PluginResult.Status.ERROR, err), this.callbackId);
|
||||
}
|
||||
}
|
@ -287,6 +287,7 @@ public class DroidGap extends PhonegapActivity {
|
||||
this.addService("Storage", "com.phonegap.Storage");
|
||||
this.addService("Temperature", "com.phonegap.TempListener");
|
||||
this.addService("FileTransfer", "com.phonegap.FileTransfer");
|
||||
this.addService("Capture", "com.phonegap.Capture");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -3,7 +3,7 @@
|
||||
* MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
|
||||
*
|
||||
* Copyright (c) 2005-2010, Nitobi Software Inc.
|
||||
* Copyright (c) 2010, IBM Corporation
|
||||
* Copyright (c) 2010-2011, IBM Corporation
|
||||
*/
|
||||
package com.phonegap;
|
||||
|
||||
@ -942,7 +942,7 @@ public class FileUtils extends Plugin {
|
||||
* @param filename
|
||||
* @return a mime type
|
||||
*/
|
||||
private String getMimeType(String filename) {
|
||||
public static String getMimeType(String filename) {
|
||||
MimeTypeMap map = MimeTypeMap.getSingleton();
|
||||
return map.getMimeTypeFromExtension(map.getFileExtensionFromUrl(filename));
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user