summaryrefslogtreecommitdiff
path: root/plugins/org.apache.cordova.media/src/android
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/org.apache.cordova.media/src/android')
-rw-r--r--plugins/org.apache.cordova.media/src/android/AudioHandler.java410
-rw-r--r--plugins/org.apache.cordova.media/src/android/AudioPlayer.java587
-rw-r--r--plugins/org.apache.cordova.media/src/android/FileHelper.java38
3 files changed, 1035 insertions, 0 deletions
diff --git a/plugins/org.apache.cordova.media/src/android/AudioHandler.java b/plugins/org.apache.cordova.media/src/android/AudioHandler.java
new file mode 100644
index 00000000..f06b75a4
--- /dev/null
+++ b/plugins/org.apache.cordova.media/src/android/AudioHandler.java
@@ -0,0 +1,410 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+*/
+package org.apache.cordova.media;
+
+import org.apache.cordova.CallbackContext;
+import org.apache.cordova.CordovaPlugin;
+import org.apache.cordova.CordovaResourceApi;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+import org.apache.cordova.PluginResult;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.HashMap;
+
+/**
+ * This class called by CordovaActivity to play and record audio.
+ * The file can be local or over a network using http.
+ *
+ * Audio formats supported (tested):
+ * .mp3, .wav
+ *
+ * Local audio files must reside in one of two places:
+ * android_asset: file name must start with /android_asset/sound.mp3
+ * sdcard: file name is just sound.mp3
+ */
+public class AudioHandler extends CordovaPlugin {
+
+ public static String TAG = "AudioHandler";
+ HashMap<String, AudioPlayer> players; // Audio player object
+ ArrayList<AudioPlayer> pausedForPhone; // Audio players that were paused when phone call came in
+ private int origVolumeStream = -1;
+ private CallbackContext messageChannel;
+
+ /**
+ * Constructor.
+ */
+ public AudioHandler() {
+ this.players = new HashMap<String, AudioPlayer>();
+ this.pausedForPhone = new ArrayList<AudioPlayer>();
+ }
+
+ /**
+ * Executes the request and returns PluginResult.
+ * @param action The action to execute.
+ * @param args JSONArry of arguments for the plugin.
+ * @param callbackContext The callback context used when calling back into JavaScript.
+ * @return A PluginResult object with a status and message.
+ */
+ public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
+ CordovaResourceApi resourceApi = webView.getResourceApi();
+ PluginResult.Status status = PluginResult.Status.OK;
+ String result = "";
+
+ if (action.equals("startRecordingAudio")) {
+ String target = args.getString(1);
+ String fileUriStr;
+ try {
+ Uri targetUri = resourceApi.remapUri(Uri.parse(target));
+ fileUriStr = targetUri.toString();
+ } catch (IllegalArgumentException e) {
+ fileUriStr = target;
+ }
+ this.startRecordingAudio(args.getString(0), FileHelper.stripFileProtocol(fileUriStr));
+ }
+ else if (action.equals("stopRecordingAudio")) {
+ this.stopRecordingAudio(args.getString(0));
+ }
+ else if (action.equals("startPlayingAudio")) {
+ String target = args.getString(1);
+ String fileUriStr;
+ try {
+ Uri targetUri = resourceApi.remapUri(Uri.parse(target));
+ fileUriStr = targetUri.toString();
+ } catch (IllegalArgumentException e) {
+ fileUriStr = target;
+ }
+ this.startPlayingAudio(args.getString(0), FileHelper.stripFileProtocol(fileUriStr));
+ }
+ else if (action.equals("seekToAudio")) {
+ this.seekToAudio(args.getString(0), args.getInt(1));
+ }
+ else if (action.equals("pausePlayingAudio")) {
+ this.pausePlayingAudio(args.getString(0));
+ }
+ else if (action.equals("stopPlayingAudio")) {
+ this.stopPlayingAudio(args.getString(0));
+ } else if (action.equals("setVolume")) {
+ try {
+ this.setVolume(args.getString(0), Float.parseFloat(args.getString(1)));
+ } catch (NumberFormatException nfe) {
+ //no-op
+ }
+ } else if (action.equals("getCurrentPositionAudio")) {
+ float f = this.getCurrentPositionAudio(args.getString(0));
+ callbackContext.sendPluginResult(new PluginResult(status, f));
+ return true;
+ }
+ else if (action.equals("getDurationAudio")) {
+ float f = this.getDurationAudio(args.getString(0), args.getString(1));
+ callbackContext.sendPluginResult(new PluginResult(status, f));
+ return true;
+ }
+ else if (action.equals("create")) {
+ String id = args.getString(0);
+ String src = FileHelper.stripFileProtocol(args.getString(1));
+ getOrCreatePlayer(id, src);
+ }
+ else if (action.equals("release")) {
+ boolean b = this.release(args.getString(0));
+ callbackContext.sendPluginResult(new PluginResult(status, b));
+ return true;
+ }
+ else if (action.equals("messageChannel")) {
+ messageChannel = callbackContext;
+ return true;
+ }
+ else { // Unrecognized action.
+ return false;
+ }
+
+ callbackContext.sendPluginResult(new PluginResult(status, result));
+
+ return true;
+ }
+
+ /**
+ * Stop all audio players and recorders.
+ */
+ public void onDestroy() {
+ if (!players.isEmpty()) {
+ onLastPlayerReleased();
+ }
+ for (AudioPlayer audio : this.players.values()) {
+ audio.destroy();
+ }
+ this.players.clear();
+ }
+
+ /**
+ * Stop all audio players and recorders on navigate.
+ */
+ @Override
+ public void onReset() {
+ onDestroy();
+ }
+
+ /**
+ * Called when a message is sent to plugin.
+ *
+ * @param id The message id
+ * @param data The message data
+ * @return Object to stop propagation or null
+ */
+ public Object onMessage(String id, Object data) {
+
+ // If phone message
+ if (id.equals("telephone")) {
+
+ // If phone ringing, then pause playing
+ if ("ringing".equals(data) || "offhook".equals(data)) {
+
+ // Get all audio players and pause them
+ for (AudioPlayer audio : this.players.values()) {
+ if (audio.getState() == AudioPlayer.STATE.MEDIA_RUNNING.ordinal()) {
+ this.pausedForPhone.add(audio);
+ audio.pausePlaying();
+ }
+ }
+
+ }
+
+ // If phone idle, then resume playing those players we paused
+ else if ("idle".equals(data)) {
+ for (AudioPlayer audio : this.pausedForPhone) {
+ audio.startPlaying(null);
+ }
+ this.pausedForPhone.clear();
+ }
+ }
+ return null;
+ }
+
+ //--------------------------------------------------------------------------
+ // LOCAL METHODS
+ //--------------------------------------------------------------------------
+
+ private AudioPlayer getOrCreatePlayer(String id, String file) {
+ AudioPlayer ret = players.get(id);
+ if (ret == null) {
+ if (players.isEmpty()) {
+ onFirstPlayerCreated();
+ }
+ ret = new AudioPlayer(this, id, file);
+ players.put(id, ret);
+ }
+ return ret;
+ }
+
+ /**
+ * Release the audio player instance to save memory.
+ * @param id The id of the audio player
+ */
+ private boolean release(String id) {
+ AudioPlayer audio = players.remove(id);
+ if (audio == null) {
+ return false;
+ }
+ if (players.isEmpty()) {
+ onLastPlayerReleased();
+ }
+ audio.destroy();
+ return true;
+ }
+
+ /**
+ * Start recording and save the specified file.
+ * @param id The id of the audio player
+ * @param file The name of the file
+ */
+ public void startRecordingAudio(String id, String file) {
+ AudioPlayer audio = getOrCreatePlayer(id, file);
+ audio.startRecording(file);
+ }
+
+ /**
+ * Stop recording and save to the file specified when recording started.
+ * @param id The id of the audio player
+ */
+ public void stopRecordingAudio(String id) {
+ AudioPlayer audio = this.players.get(id);
+ if (audio != null) {
+ audio.stopRecording();
+ }
+ }
+
+ /**
+ * Start or resume playing audio file.
+ * @param id The id of the audio player
+ * @param file The name of the audio file.
+ */
+ public void startPlayingAudio(String id, String file) {
+ AudioPlayer audio = getOrCreatePlayer(id, file);
+ audio.startPlaying(file);
+ }
+
+ /**
+ * Seek to a location.
+ * @param id The id of the audio player
+ * @param milliseconds int: number of milliseconds to skip 1000 = 1 second
+ */
+ public void seekToAudio(String id, int milliseconds) {
+ AudioPlayer audio = this.players.get(id);
+ if (audio != null) {
+ audio.seekToPlaying(milliseconds);
+ }
+ }
+
+ /**
+ * Pause playing.
+ * @param id The id of the audio player
+ */
+ public void pausePlayingAudio(String id) {
+ AudioPlayer audio = this.players.get(id);
+ if (audio != null) {
+ audio.pausePlaying();
+ }
+ }
+
+ /**
+ * Stop playing the audio file.
+ * @param id The id of the audio player
+ */
+ public void stopPlayingAudio(String id) {
+ AudioPlayer audio = this.players.get(id);
+ if (audio != null) {
+ audio.stopPlaying();
+ }
+ }
+
+ /**
+ * Get current position of playback.
+ * @param id The id of the audio player
+ * @return position in msec
+ */
+ public float getCurrentPositionAudio(String id) {
+ AudioPlayer audio = this.players.get(id);
+ if (audio != null) {
+ return (audio.getCurrentPosition() / 1000.0f);
+ }
+ return -1;
+ }
+
+ /**
+ * Get the duration of the audio file.
+ * @param id The id of the audio player
+ * @param file The name of the audio file.
+ * @return The duration in msec.
+ */
+ public float getDurationAudio(String id, String file) {
+ AudioPlayer audio = getOrCreatePlayer(id, file);
+ return audio.getDuration(file);
+ }
+
+ /**
+ * Set the audio device to be used for playback.
+ *
+ * @param output 1=earpiece, 2=speaker
+ */
+ @SuppressWarnings("deprecation")
+ public void setAudioOutputDevice(int output) {
+ AudioManager audiMgr = (AudioManager) this.cordova.getActivity().getSystemService(Context.AUDIO_SERVICE);
+ if (output == 2) {
+ audiMgr.setRouting(AudioManager.MODE_NORMAL, AudioManager.ROUTE_SPEAKER, AudioManager.ROUTE_ALL);
+ }
+ else if (output == 1) {
+ audiMgr.setRouting(AudioManager.MODE_NORMAL, AudioManager.ROUTE_EARPIECE, AudioManager.ROUTE_ALL);
+ }
+ else {
+ System.out.println("AudioHandler.setAudioOutputDevice() Error: Unknown output device.");
+ }
+ }
+
+ /**
+ * Get the audio device to be used for playback.
+ *
+ * @return 1=earpiece, 2=speaker
+ */
+ @SuppressWarnings("deprecation")
+ public int getAudioOutputDevice() {
+ AudioManager audiMgr = (AudioManager) this.cordova.getActivity().getSystemService(Context.AUDIO_SERVICE);
+ if (audiMgr.getRouting(AudioManager.MODE_NORMAL) == AudioManager.ROUTE_EARPIECE) {
+ return 1;
+ }
+ else if (audiMgr.getRouting(AudioManager.MODE_NORMAL) == AudioManager.ROUTE_SPEAKER) {
+ return 2;
+ }
+ else {
+ return -1;
+ }
+ }
+
+ /**
+ * Set the volume for an audio device
+ *
+ * @param id The id of the audio player
+ * @param volume Volume to adjust to 0.0f - 1.0f
+ */
+ public void setVolume(String id, float volume) {
+ AudioPlayer audio = this.players.get(id);
+ if (audio != null) {
+ audio.setVolume(volume);
+ } else {
+ System.out.println("AudioHandler.setVolume() Error: Unknown Audio Player " + id);
+ }
+ }
+
+ private void onFirstPlayerCreated() {
+ origVolumeStream = cordova.getActivity().getVolumeControlStream();
+ cordova.getActivity().setVolumeControlStream(AudioManager.STREAM_MUSIC);
+ }
+
+ private void onLastPlayerReleased() {
+ if (origVolumeStream != -1) {
+ cordova.getActivity().setVolumeControlStream(origVolumeStream);
+ origVolumeStream = -1;
+ }
+ }
+
+ void sendEventMessage(String action, JSONObject actionData) {
+ JSONObject message = new JSONObject();
+ try {
+ message.put("action", action);
+ if (actionData != null) {
+ message.put(action, actionData);
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "Failed to create event message", e);
+ }
+
+ PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, message);
+ pluginResult.setKeepCallback(true);
+ if (messageChannel != null) {
+ messageChannel.sendPluginResult(pluginResult);
+ }
+ }
+}
diff --git a/plugins/org.apache.cordova.media/src/android/AudioPlayer.java b/plugins/org.apache.cordova.media/src/android/AudioPlayer.java
new file mode 100644
index 00000000..7524c5b6
--- /dev/null
+++ b/plugins/org.apache.cordova.media/src/android/AudioPlayer.java
@@ -0,0 +1,587 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+*/
+package org.apache.cordova.media;
+
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.media.MediaPlayer.OnCompletionListener;
+import android.media.MediaPlayer.OnErrorListener;
+import android.media.MediaPlayer.OnPreparedListener;
+import android.media.MediaRecorder;
+import android.os.Environment;
+import android.util.Log;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+
+/**
+ * This class implements the audio playback and recording capabilities used by Cordova.
+ * It is called by the AudioHandler Cordova class.
+ * Only one file can be played or recorded per class instance.
+ *
+ * Local audio files must reside in one of two places:
+ * android_asset: file name must start with /android_asset/sound.mp3
+ * sdcard: file name is just sound.mp3
+ */
+public class AudioPlayer implements OnCompletionListener, OnPreparedListener, OnErrorListener {
+
+ // AudioPlayer modes
+ public enum MODE { NONE, PLAY, RECORD };
+
+ // AudioPlayer states
+ public enum STATE { MEDIA_NONE,
+ MEDIA_STARTING,
+ MEDIA_RUNNING,
+ MEDIA_PAUSED,
+ MEDIA_STOPPED,
+ MEDIA_LOADING
+ };
+
+ private static final String LOG_TAG = "AudioPlayer";
+
+ // AudioPlayer message ids
+ private static int MEDIA_STATE = 1;
+ private static int MEDIA_DURATION = 2;
+ private static int MEDIA_POSITION = 3;
+ private static int MEDIA_ERROR = 9;
+
+ // Media error codes
+ private static int MEDIA_ERR_NONE_ACTIVE = 0;
+ private static int MEDIA_ERR_ABORTED = 1;
+// private static int MEDIA_ERR_NETWORK = 2;
+// private static int MEDIA_ERR_DECODE = 3;
+// private static int MEDIA_ERR_NONE_SUPPORTED = 4;
+
+ private AudioHandler handler; // The AudioHandler object
+ private String id; // The id of this player (used to identify Media object in JavaScript)
+ private MODE mode = MODE.NONE; // Playback or Recording mode
+ private STATE state = STATE.MEDIA_NONE; // State of recording or playback
+
+ private String audioFile = null; // File name to play or record to
+ private float duration = -1; // Duration of audio
+
+ private MediaRecorder recorder = null; // Audio recording object
+ private String tempFile = null; // Temporary recording file name
+
+ private MediaPlayer player = null; // Audio player object
+ private boolean prepareOnly = true; // playback after file prepare flag
+ private int seekOnPrepared = 0; // seek to this location once media is prepared
+
+ /**
+ * Constructor.
+ *
+ * @param handler The audio handler object
+ * @param id The id of this audio player
+ */
+ public AudioPlayer(AudioHandler handler, String id, String file) {
+ this.handler = handler;
+ this.id = id;
+ this.audioFile = file;
+ this.recorder = new MediaRecorder();
+
+ if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ this.tempFile = Environment.getExternalStorageDirectory().getAbsolutePath() + "/tmprecording.3gp";
+ } else {
+ this.tempFile = "/data/data/" + handler.cordova.getActivity().getPackageName() + "/cache/tmprecording.3gp";
+ }
+
+ }
+
+ /**
+ * Destroy player and stop audio playing or recording.
+ */
+ public void destroy() {
+ // Stop any play or record
+ if (this.player != null) {
+ if ((this.state == STATE.MEDIA_RUNNING) || (this.state == STATE.MEDIA_PAUSED)) {
+ this.player.stop();
+ this.setState(STATE.MEDIA_STOPPED);
+ }
+ this.player.release();
+ this.player = null;
+ }
+ if (this.recorder != null) {
+ this.stopRecording();
+ this.recorder.release();
+ this.recorder = null;
+ }
+ }
+
+ /**
+ * Start recording the specified file.
+ *
+ * @param file The name of the file
+ */
+ public void startRecording(String file) {
+ switch (this.mode) {
+ case PLAY:
+ Log.d(LOG_TAG, "AudioPlayer Error: Can't record in play mode.");
+ sendErrorStatus(MEDIA_ERR_ABORTED);
+ break;
+ case NONE:
+ this.audioFile = file;
+ this.recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
+ this.recorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT); // THREE_GPP);
+ this.recorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT); //AMR_NB);
+ this.recorder.setOutputFile(this.tempFile);
+ try {
+ this.recorder.prepare();
+ this.recorder.start();
+ this.setState(STATE.MEDIA_RUNNING);
+ return;
+ } catch (IllegalStateException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ sendErrorStatus(MEDIA_ERR_ABORTED);
+ break;
+ case RECORD:
+ Log.d(LOG_TAG, "AudioPlayer Error: Already recording.");
+ sendErrorStatus(MEDIA_ERR_ABORTED);
+ }
+ }
+
+ /**
+ * Save temporary recorded file to specified name
+ *
+ * @param file
+ */
+ public void moveFile(String file) {
+ /* this is a hack to save the file as the specified name */
+ File f = new File(this.tempFile);
+
+ if (!file.startsWith("/")) {
+ if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ file = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + file;
+ } else {
+ file = "/data/data/" + handler.cordova.getActivity().getPackageName() + "/cache/" + file;
+ }
+ }
+
+ String logMsg = "renaming " + this.tempFile + " to " + file;
+ Log.d(LOG_TAG, logMsg);
+ if (!f.renameTo(new File(file))) Log.e(LOG_TAG, "FAILED " + logMsg);
+ }
+
+ /**
+ * Stop recording and save to the file specified when recording started.
+ */
+ public void stopRecording() {
+ if (this.recorder != null) {
+ try{
+ if (this.state == STATE.MEDIA_RUNNING) {
+ this.recorder.stop();
+ this.setState(STATE.MEDIA_STOPPED);
+ }
+ this.recorder.reset();
+ this.moveFile(this.audioFile);
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ //==========================================================================
+ // Playback
+ //==========================================================================
+
+ /**
+ * Start or resume playing audio file.
+ *
+ * @param file The name of the audio file.
+ */
+ public void startPlaying(String file) {
+ if (this.readyPlayer(file) && this.player != null) {
+ this.player.start();
+ this.setState(STATE.MEDIA_RUNNING);
+ this.seekOnPrepared = 0; //insures this is always reset
+ } else {
+ this.prepareOnly = false;
+ }
+ }
+
+ /**
+ * Seek or jump to a new time in the track.
+ */
+ public void seekToPlaying(int milliseconds) {
+ if (this.readyPlayer(this.audioFile)) {
+ this.player.seekTo(milliseconds);
+ Log.d(LOG_TAG, "Send a onStatus update for the new seek");
+ sendStatusChange(MEDIA_POSITION, null, (milliseconds / 1000.0f));
+ }
+ else {
+ this.seekOnPrepared = milliseconds;
+ }
+ }
+
+ /**
+ * Pause playing.
+ */
+ public void pausePlaying() {
+
+ // If playing, then pause
+ if (this.state == STATE.MEDIA_RUNNING && this.player != null) {
+ this.player.pause();
+ this.setState(STATE.MEDIA_PAUSED);
+ }
+ else {
+ Log.d(LOG_TAG, "AudioPlayer Error: pausePlaying() called during invalid state: " + this.state.ordinal());
+ sendErrorStatus(MEDIA_ERR_NONE_ACTIVE);
+ }
+ }
+
+ /**
+ * Stop playing the audio file.
+ */
+ public void stopPlaying() {
+ if ((this.state == STATE.MEDIA_RUNNING) || (this.state == STATE.MEDIA_PAUSED)) {
+ this.player.pause();
+ this.player.seekTo(0);
+ Log.d(LOG_TAG, "stopPlaying is calling stopped");
+ this.setState(STATE.MEDIA_STOPPED);
+ }
+ else {
+ Log.d(LOG_TAG, "AudioPlayer Error: stopPlaying() called during invalid state: " + this.state.ordinal());
+ sendErrorStatus(MEDIA_ERR_NONE_ACTIVE);
+ }
+ }
+
+ /**
+ * Callback to be invoked when playback of a media source has completed.
+ *
+ * @param player The MediaPlayer that reached the end of the file
+ */
+ public void onCompletion(MediaPlayer player) {
+ Log.d(LOG_TAG, "on completion is calling stopped");
+ this.setState(STATE.MEDIA_STOPPED);
+ }
+
+ /**
+ * Get current position of playback.
+ *
+ * @return position in msec or -1 if not playing
+ */
+ public long getCurrentPosition() {
+ if ((this.state == STATE.MEDIA_RUNNING) || (this.state == STATE.MEDIA_PAUSED)) {
+ int curPos = this.player.getCurrentPosition();
+ sendStatusChange(MEDIA_POSITION, null, (curPos / 1000.0f));
+ return curPos;
+ }
+ else {
+ return -1;
+ }
+ }
+
+ /**
+ * Determine if playback file is streaming or local.
+ * It is streaming if file name starts with "http://"
+ *
+ * @param file The file name
+ * @return T=streaming, F=local
+ */
+ public boolean isStreaming(String file) {
+ if (file.contains("http://") || file.contains("https://")) {
+ return true;
+ }
+ else {
+ return false;
+ }
+ }
+
+ /**
+ * Get the duration of the audio file.
+ *
+ * @param file The name of the audio file.
+ * @return The duration in msec.
+ * -1=can't be determined
+ * -2=not allowed
+ */
+ public float getDuration(String file) {
+
+ // Can't get duration of recording
+ if (this.recorder != null) {
+ return (-2); // not allowed
+ }
+
+ // If audio file already loaded and started, then return duration
+ if (this.player != null) {
+ return this.duration;
+ }
+
+ // If no player yet, then create one
+ else {
+ this.prepareOnly = true;
+ this.startPlaying(file);
+
+ // This will only return value for local, since streaming
+ // file hasn't been read yet.
+ return this.duration;
+ }
+ }
+
+ /**
+ * Callback to be invoked when the media source is ready for playback.
+ *
+ * @param player The MediaPlayer that is ready for playback
+ */
+ public void onPrepared(MediaPlayer player) {
+ // Listen for playback completion
+ this.player.setOnCompletionListener(this);
+ // seek to any location received while not prepared
+ this.seekToPlaying(this.seekOnPrepared);
+ // If start playing after prepared
+ if (!this.prepareOnly) {
+ this.player.start();
+ this.setState(STATE.MEDIA_RUNNING);
+ this.seekOnPrepared = 0; //reset only when played
+ } else {
+ this.setState(STATE.MEDIA_STARTING);
+ }
+ // Save off duration
+ this.duration = getDurationInSeconds();
+ // reset prepare only flag
+ this.prepareOnly = true;
+
+ // Send status notification to JavaScript
+ sendStatusChange(MEDIA_DURATION, null, this.duration);
+ }
+
+ /**
+ * By default Android returns the length of audio in mills but we want seconds
+ *
+ * @return length of clip in seconds
+ */
+ private float getDurationInSeconds() {
+ return (this.player.getDuration() / 1000.0f);
+ }
+
+ /**
+ * Callback to be invoked when there has been an error during an asynchronous operation
+ * (other errors will throw exceptions at method call time).
+ *
+ * @param player the MediaPlayer the error pertains to
+ * @param arg1 the type of error that has occurred: (MEDIA_ERROR_UNKNOWN, MEDIA_ERROR_SERVER_DIED)
+ * @param arg2 an extra code, specific to the error.
+ */
+ public boolean onError(MediaPlayer player, int arg1, int arg2) {
+ Log.d(LOG_TAG, "AudioPlayer.onError(" + arg1 + ", " + arg2 + ")");
+
+ // TODO: Not sure if this needs to be sent?
+ this.player.stop();
+ this.player.release();
+
+ // Send error notification to JavaScript
+ sendErrorStatus(arg1);
+ return false;
+ }
+
+ /**
+ * Set the state and send it to JavaScript.
+ *
+ * @param state
+ */
+ private void setState(STATE state) {
+ if (this.state != state) {
+ sendStatusChange(MEDIA_STATE, null, (float)state.ordinal());
+ }
+ this.state = state;
+ }
+
+ /**
+ * Set the mode and send it to JavaScript.
+ *
+ * @param mode
+ */
+ private void setMode(MODE mode) {
+ if (this.mode != mode) {
+ //mode is not part of the expected behavior, so no notification
+ //this.handler.webView.sendJavascript("cordova.require('org.apache.cordova.media.Media').onStatus('" + this.id + "', " + MEDIA_STATE + ", " + mode + ");");
+ }
+ this.mode = mode;
+ }
+
+ /**
+ * Get the audio state.
+ *
+ * @return int
+ */
+ public int getState() {
+ return this.state.ordinal();
+ }
+
+ /**
+ * Set the volume for audio player
+ *
+ * @param volume
+ */
+ public void setVolume(float volume) {
+ this.player.setVolume(volume, volume);
+ }
+
+ /**
+ * attempts to put the player in play mode
+ * @return true if in playmode, false otherwise
+ */
+ private boolean playMode() {
+ switch(this.mode) {
+ case NONE:
+ this.setMode(MODE.PLAY);
+ break;
+ case PLAY:
+ break;
+ case RECORD:
+ Log.d(LOG_TAG, "AudioPlayer Error: Can't play in record mode.");
+ sendErrorStatus(MEDIA_ERR_ABORTED);
+ return false; //player is not ready
+ }
+ return true;
+ }
+
+ /**
+ * attempts to initialize the media player for playback
+ * @param file the file to play
+ * @return false if player not ready, reports if in wrong mode or state
+ */
+ private boolean readyPlayer(String file) {
+ if (playMode()) {
+ switch (this.state) {
+ case MEDIA_NONE:
+ if (this.player == null) {
+ this.player = new MediaPlayer();
+ }
+ try {
+ this.loadAudioFile(file);
+ } catch (Exception e) {
+ sendErrorStatus(MEDIA_ERR_ABORTED);
+ }
+ return false;
+ case MEDIA_LOADING:
+ //cordova js is not aware of MEDIA_LOADING, so we send MEDIA_STARTING instead
+ Log.d(LOG_TAG, "AudioPlayer Loading: startPlaying() called during media preparation: " + STATE.MEDIA_STARTING.ordinal());
+ this.prepareOnly = false;
+ return false;
+ case MEDIA_STARTING:
+ case MEDIA_RUNNING:
+ case MEDIA_PAUSED:
+ return true;
+ case MEDIA_STOPPED:
+ //if we are readying the same file
+ if (this.audioFile.compareTo(file) == 0) {
+ //reset the audio file
+ player.seekTo(0);
+ player.pause();
+ return true;
+ } else {
+ //reset the player
+ this.player.reset();
+ try {
+ this.loadAudioFile(file);
+ } catch (Exception e) {
+ sendErrorStatus(MEDIA_ERR_ABORTED);
+ }
+ //if we had to prepare the file, we won't be in the correct state for playback
+ return false;
+ }
+ default:
+ Log.d(LOG_TAG, "AudioPlayer Error: startPlaying() called during invalid state: " + this.state);
+ sendErrorStatus(MEDIA_ERR_ABORTED);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * load audio file
+ * @throws IOException
+ * @throws IllegalStateException
+ * @throws SecurityException
+ * @throws IllegalArgumentException
+ */
+ private void loadAudioFile(String file) throws IllegalArgumentException, SecurityException, IllegalStateException, IOException {
+ if (this.isStreaming(file)) {
+ this.player.setDataSource(file);
+ this.player.setAudioStreamType(AudioManager.STREAM_MUSIC);
+ //if it's a streaming file, play mode is implied
+ this.setMode(MODE.PLAY);
+ this.setState(STATE.MEDIA_STARTING);
+ this.player.setOnPreparedListener(this);
+ this.player.prepareAsync();
+ }
+ else {
+ if (file.startsWith("/android_asset/")) {
+ String f = file.substring(15);
+ android.content.res.AssetFileDescriptor fd = this.handler.cordova.getActivity().getAssets().openFd(f);
+ this.player.setDataSource(fd.getFileDescriptor(), fd.getStartOffset(), fd.getLength());
+ }
+ else {
+ File fp = new File(file);
+ if (fp.exists()) {
+ FileInputStream fileInputStream = new FileInputStream(file);
+ this.player.setDataSource(fileInputStream.getFD());
+ fileInputStream.close();
+ }
+ else {
+ this.player.setDataSource(Environment.getExternalStorageDirectory().getPath() + "/" + file);
+ }
+ }
+ this.setState(STATE.MEDIA_STARTING);
+ this.player.setOnPreparedListener(this);
+ this.player.prepare();
+
+ // Get duration
+ this.duration = getDurationInSeconds();
+ }
+ }
+
+ private void sendErrorStatus(int errorCode) {
+ sendStatusChange(MEDIA_ERROR, errorCode, null);
+ }
+
+ private void sendStatusChange(int messageType, Integer additionalCode, Float value) {
+
+ if (additionalCode != null && value != null) {
+ throw new IllegalArgumentException("Only one of additionalCode or value can be specified, not both");
+ }
+
+ JSONObject statusDetails = new JSONObject();
+ try {
+ statusDetails.put("id", this.id);
+ statusDetails.put("msgType", messageType);
+ if (additionalCode != null) {
+ JSONObject code = new JSONObject();
+ code.put("code", additionalCode.intValue());
+ statusDetails.put("value", code);
+ }
+ else if (value != null) {
+ statusDetails.put("value", value.floatValue());
+ }
+ } catch (JSONException e) {
+ Log.e(LOG_TAG, "Failed to create status details", e);
+ }
+
+ this.handler.sendEventMessage("status", statusDetails);
+ }
+}
diff --git a/plugins/org.apache.cordova.media/src/android/FileHelper.java b/plugins/org.apache.cordova.media/src/android/FileHelper.java
new file mode 100644
index 00000000..e20752c6
--- /dev/null
+++ b/plugins/org.apache.cordova.media/src/android/FileHelper.java
@@ -0,0 +1,38 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+ */
+package org.apache.cordova.media;
+
+import android.net.Uri;
+
+public class FileHelper {
+
+ /**
+ * Removes the "file://" prefix from the given URI string, if applicable.
+ * If the given URI string doesn't have a "file://" prefix, it is returned unchanged.
+ *
+ * @param uriString the URI string to operate on
+ * @return a path without the "file://" prefix
+ */
+ public static String stripFileProtocol(String uriString) {
+ if (uriString.startsWith("file://")) {
+ return Uri.parse(uriString).getPath();
+ }
+ return uriString;
+ }
+}