content: /**
   * Created by G-Canvas Open Source Team.
   * Copyright (c) 2017, Alibaba, Inc. All rights reserved.
   *
   * This source code is licensed under the Apache Licence 2.0.
   * For the full copyright and license information, please view
   * the LICENSE file in the root directory of this source tree.
   */
  package com.taobao.gcanvas.audio;
  
  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 com.taobao.gcanvas.GCanvasResult;
  import com.taobao.gcanvas.util.GLog;
  
  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.
   * <p>
   * 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 GAudioPlayer implements OnCompletionListener, OnPreparedListener, OnErrorListener {
  
      private static final String LOG_TAG = "GAudioPlayer";
  
      // 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_READY = 4;
      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 GAudioHandler 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
      private int recorderCount = 0;
      private GCanvasResult resultContext;
  
  
      /**
       * Constructor.
       *
       * @param handler       The audio handler object
       * @param id            The id of this audio player
       * @param file          The audio file path
       * @param resultContext
       */
      public GAudioPlayer(GAudioHandler handler, String id, String file, GCanvasResult resultContext) {
          this.handler = handler;
          this.id = id;
          this.audioFile = file;
          this.recorder = new MediaRecorder();
          this.resultContext = resultContext;
  
          if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
              this.tempFile = Environment.getExternalStorageDirectory().getAbsolutePath() + "/tmprecording.3gp";
          } else {
              this.tempFile = "/data/data/" + handler.getActivity().getPackageName() + "/cache/tmprecording.3gp";
          }
      }
  
      /**
       * @param file The audio file path
       */
      public void setSourceAudio(String file) {
          this.audioFile = file;
      }
  
      private void sendJavascript(String js) {
          if (this.resultContext != null) {
              this.resultContext.calljs(js);
          }
      }
  
      /**
       * Start recording the specified file.
       *
       * @param file The name of the file
       */
      public void startRecording(String file) {
          switch (this.mode) {
              case PLAY:
                  this.sendJavascript("Media.onStatus('" + this.id + "', " + MEDIA_ERROR + ", { \"code\":" + MEDIA_ERR_ABORTED + "});");
                  break;
              case NONE:
                  this.audioFile = file;
                  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);
                      this.recorderCount++;
                      return;
                  } catch (IllegalStateException e) {
                      e.printStackTrace();
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
  
                  this.sendJavascript("Media.onStatus('" + this.id + "', " + MEDIA_ERROR + ", { \"code\":" + MEDIA_ERR_ABORTED + "});");
                  break;
              case RECORD:
                  this.sendJavascript("Media.onStatus('" + this.id + "', " + MEDIA_ERROR + ", { \"code\":" + 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.getActivity().getPackageName() + "/cache/" + file;
              }
          }
  
          if (!f.renameTo(new File(file))){
              String logMsg = "renaming " + this.tempFile + " to " + file;
              GLog.e(LOG_TAG, "FAILED " + logMsg);
          }
      }
  
      /**
       * Stop recording and save to the file specified when recording started.
       */
      public void stopRecording() {
          if ((this.recorder != null) && (this.recorderCount > 0)) {
              try {
                  if (this.state == STATE.MEDIA_RUNNING) {
                      this.recorder.stop();
                      this.setState(STATE.MEDIA_STOPPED);
                  }
                  this.recorderCount--;
                  this.recorder.reset();
                  this.moveFile(this.audioFile);
              } catch (Exception e) {
                  e.printStackTrace();
              }
          }
      }
  
      /**
       * 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);
              this.sendJavascript("Media.onStatus('" + this.id + "', " + MEDIA_POSITION + ", " + milliseconds / 1000.0f + ");");
          } else {
              this.seekOnPrepared = milliseconds;
          }
      }
  
      public void loadingAudio() {
          if (this.readyPlayer(this.audioFile)) {
              this.sendJavascript("Media.onStatus('" + this.id + "', " + MEDIA_READY + ");");
          }
      }
  
      /**
       * 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 {
              this.sendJavascript("Media.onStatus('" + this.id + "', " + MEDIA_ERROR + ", { \"code\":" + 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);
              this.setState(STATE.MEDIA_STOPPED);
          } else {
              this.sendJavascript("Media.onStatus('" + this.id + "', " + MEDIA_ERROR + ", { \"code\":" + 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) {
          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();
              this.sendJavascript("Media.onStatus('" + this.id + "', " + MEDIA_POSITION + ", " + 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
          this.sendJavascript("Media.onStatus('" + this.id + "', " + MEDIA_DURATION + "," + 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) {
          this.player.stop();
          this.player.release();
  
          // Send error notification to JavaScript
          this.sendJavascript("Media.onStatus('" + this.id + "', { \"code\":" + arg1 + "});");
          return false;
      }
  
      /**
       * Set the mode
       *
       * @param mode
       */
      private void setMode(MODE mode) {
          this.mode = mode;
      }
  
      /**
       * Get the audio state.
       *
       * @return int
       */
      public int getState() {
          return this.state.ordinal();
      }
  
      /**
       * Set the state and send it to JavaScript.
       *
       * @param state
       */
      private void setState(STATE state) {
          if (this.state != state) {
              this.sendJavascript("Media.onStatus('" + this.id + "', " + MEDIA_STATE + ", " + state.ordinal() + ");");
          }
          this.state = state;
      }
  
      /**
       * Set the volume for audio player
       *
       * @param volume
       */
      public void setVolume(float volume) {
          if (this.player != null) {
              this.player.setVolume(volume, volume);
          }
      }
  
      /**
       * 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;
          }
      }
  
      /**
       * 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:
                  this.sendJavascript("Media.onStatus('" + this.id + "', " + MEDIA_ERROR + ", { \"code\":" + 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) {
                          this.sendJavascript("Media.onStatus('" + this.id + "', " + MEDIA_ERROR + ", { \"code\":" + MEDIA_ERR_ABORTED + "});");
                      }
                      return false;
                  case MEDIA_LOADING:
                      //cordova js is not aware of MEDIA_LOADING, so we send MEDIA_STARTING instead
                      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) {
                              this.sendJavascript("Media.onStatus('" + this.id + "', " + MEDIA_ERROR + ", { \"code\":" + MEDIA_ERR_ABORTED + "});");
                          }
                          //if we had to prepare= the file, we won't be in the correct state for playback
                          return false;
                      }
                  default:
                      this.sendJavascript("Media.onStatus('" + this.id + "', " + MEDIA_ERROR + ", { \"code\":" + 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.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 {
                      //file not exists should throw exception!
                      throw new IOException();
                  }
              }
  
              this.setState(STATE.MEDIA_STARTING);
              this.player.setOnPreparedListener(this);
              this.player.prepare();
  
              // Get duration
              this.duration = getDurationInSeconds();
          }
      }
  
      // 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
      }
  }
  