/**************************************************************************************** * Copyright (c) 2013 Bibek Shrestha <bibekshrestha@gmail.com> * * Copyright (c) 2013 Zaur Molotnikov <qutorial@gmail.com> * * Copyright (c) 2013 Nicolas Raoul <nicolas.raoul@gmail.com> * * Copyright (c) 2013 Flavio Lerda <flerda@gmail.com> * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 3 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see <http://www.gnu.org/licenses/>. * ****************************************************************************************/ package com.ichi2.anki.multimediacard; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; import android.media.MediaPlayer; import android.media.MediaPlayer.OnCompletionListener; import android.media.MediaRecorder; import android.os.Build; import android.view.View; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.Toast; import com.ichi2.anki.R; import timber.log.Timber; // Not designed for visual editing @SuppressLint("ViewConstructor") public class AudioView extends LinearLayout { protected String mAudioPath; protected PlayPauseButton mPlayPause = null; protected StopButton mStop = null; protected RecordButton mRecord = null; private MediaRecorder mRecorder = null; private MediaPlayer mPlayer = null; OnRecordingFinishEventListener mOnRecordingFinishEventListener = null; private Status mStatus = Status.IDLE; int mResPlayImage; int mResPauseImage; int mResStopImage; int mResRecordImage; int mResRecordStopImage; private Context mContext; enum Status { IDLE, // Default initial state INITIALIZED, // When datasource has been set PLAYING, PAUSED, STOPPED, // The different possible states once playing // has started RECORDING // The recorder being played status } /** * @param context Resources for images * @param resPlay * @param resPause * @param resStop * @param audioPath * @return */ public static AudioView createPlayerInstance(Context context, int resPlay, int resPause, int resStop, String audioPath) { return new AudioView(context, resPlay, resPause, resStop, audioPath); } public static AudioView createRecorderInstance(Context context, int resPlay, int resPause, int resStop, int resRecord, int resRecordStop, String audioPath) { return new AudioView(context, resPlay, resPause, resStop, resRecord, resRecordStop, audioPath); } private AudioView(Context context, int resPlay, int resPause, int resStop, String audioPath) { super(context); mContext = context; mResPlayImage = resPlay; mResPauseImage = resPause; mResStopImage = resStop; mAudioPath = audioPath; this.setOrientation(HORIZONTAL); mPlayPause = new PlayPauseButton(context); addView(mPlayPause, new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); mStop = new StopButton(context); addView(mStop, new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); } private void showToast(String msg) { int duration = Toast.LENGTH_SHORT; Toast toast = Toast.makeText(mContext, msg, duration); toast.show(); } private String gtxt(int id) { return mContext.getText(id).toString(); } private AudioView(Context context, int resPlay, int resPause, int resStop, int resRecord, int resRecordStop, String audioPath) { this(context, resPlay, resPause, resStop, audioPath); mResRecordImage = resRecord; mResRecordStopImage = resRecordStop; this.setOrientation(HORIZONTAL); mRecord = new RecordButton(context); addView(mRecord, new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); } public String getAudioPath() { return mAudioPath; } public void setRecordButtonVisible(boolean isVisible) { if (isVisible) { mRecord.setVisibility(VISIBLE); } else { mRecord.setVisibility(INVISIBLE); } } public void setOnRecordingFinishEventListener(OnRecordingFinishEventListener listener) { mOnRecordingFinishEventListener = listener; } public void notifyPlay() { mPlayPause.update(); mStop.update(); if (mRecord != null) { mRecord.update(); } } public void notifyStop() { // Send state change signal to all buttons mPlayPause.update(); mStop.update(); if (mRecord != null) { mRecord.update(); } } public void notifyPause() { mPlayPause.update(); mStop.update(); if (mRecord != null) { mRecord.update(); } } public void notifyRecord() { mPlayPause.update(); mStop.update(); if (mRecord != null) { mRecord.update(); } } public void notifyStopRecord() { if (mRecorder != null && mStatus == Status.RECORDING) { mRecorder.stop(); mStatus = Status.IDLE; if (mOnRecordingFinishEventListener != null) { mOnRecordingFinishEventListener.onRecordingFinish(AudioView.this); } } mPlayPause.update(); mStop.update(); if (mRecord != null) { mRecord.update(); } } public void notifyReleaseRecorder() { if (mRecorder != null) { mRecorder.release(); } } protected class PlayPauseButton extends ImageButton { OnClickListener onClickListener = new View.OnClickListener() { @Override public void onClick(View v) { if (mAudioPath == null) { return; } switch (mStatus) { case IDLE: try { mPlayer = new MediaPlayer(); mPlayer.setDataSource(getAudioPath()); mPlayer.setOnCompletionListener(new OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { mStatus = Status.STOPPED; mPlayer.stop(); notifyStop(); } }); mPlayer.prepare(); mPlayer.start(); setImageResource(mResPauseImage); mStatus = Status.PLAYING; notifyPlay(); } catch (Exception e) { Timber.e(e.getMessage()); showToast(gtxt(R.string.multimedia_editor_audio_view_playing_failed)); mStatus = Status.IDLE; } break; case PAUSED: // -> Play, continue playing mStatus = Status.PLAYING; setImageResource(mResPauseImage); mPlayer.start(); notifyPlay(); break; case STOPPED: // -> Play, start from beginning mStatus = Status.PLAYING; setImageResource(mResPauseImage); try { mPlayer.prepare(); mPlayer.seekTo(0); } catch (Exception e) { Timber.e(e.getMessage()); } mPlayer.start(); notifyPlay(); break; case PLAYING: setImageResource(mResPlayImage); mPlayer.pause(); mStatus = Status.PAUSED; notifyPause(); break; case RECORDING: // this button should be disabled break; default: break; } } }; public PlayPauseButton(Context context) { super(context); setImageResource(mResPlayImage); setOnClickListener(onClickListener); } public void update() { switch (mStatus) { case IDLE: case STOPPED: setImageResource(mResPlayImage); setEnabled(true); break; case RECORDING: setEnabled(false); break; default: setEnabled(true); break; } } } protected class StopButton extends ImageButton { OnClickListener onClickListener = new View.OnClickListener() { @Override public void onClick(View v) { switch (mStatus) { case PAUSED: case PLAYING: mPlayer.stop(); mStatus = Status.STOPPED; notifyStop(); break; case IDLE: case STOPPED: case RECORDING: case INITIALIZED: } } }; public StopButton(Context context) { super(context); setImageResource(mResStopImage); setOnClickListener(onClickListener); } public void update() { switch (mStatus) { case RECORDING: setEnabled(false); break; default: setEnabled(true); } // It doesn't need to update itself on any other state changes } } protected class RecordButton extends ImageButton { OnClickListener onClickListener = new View.OnClickListener() { @TargetApi(Build.VERSION_CODES.GINGERBREAD_MR1) @Override public void onClick(View v) { // Since mAudioPath is not compulsory, we check if it exists if (mAudioPath == null) { return; } switch (mStatus) { case IDLE: // If not already recorded or not already played case STOPPED: // if already recorded or played boolean highSampling = false; int currentapiVersion = android.os.Build.VERSION.SDK_INT; if (currentapiVersion >= android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { try { // try high quality AAC @ 44.1kHz / 192kbps first mRecorder = initMediaRecorder(); mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); mRecorder.setAudioChannels(2); mRecorder.setAudioSamplingRate(44100); mRecorder.setAudioEncodingBitRate(192000); mRecorder.prepare(); mRecorder.start(); highSampling = true; } catch (Exception e) { } } if (!highSampling) { // fall back on default try { mRecorder = initMediaRecorder(); mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); mRecorder.prepare(); mRecorder.start(); } catch (Exception e) { Timber.e("RecordButton.onClick() :: error recording to " + mAudioPath + "\n" +e.getMessage()); showToast(gtxt(R.string.multimedia_editor_audio_view_recording_failed)); mStatus = Status.STOPPED; break; } } mStatus = Status.RECORDING; setImageResource(mResRecordImage); notifyRecord(); break; case RECORDING: setImageResource(mResRecordStopImage); notifyStopRecord(); break; default: // do nothing } } private MediaRecorder initMediaRecorder() { MediaRecorder mr = new MediaRecorder(); mr.setAudioSource(MediaRecorder.AudioSource.MIC); mr.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); mStatus = Status.INITIALIZED; mr.setOutputFile(mAudioPath); // audioPath // could // change return mr; } }; public RecordButton(Context context) { super(context); setImageResource(mResRecordStopImage); setOnClickListener(onClickListener); } public void update() { switch (mStatus) { case PLAYING: case PAUSED: setEnabled(false); break; default: setEnabled(true); break; } } } public interface OnRecordingFinishEventListener { public void onRecordingFinish(View v); } }