안녕하세요, 음성 녹음 기능을 구현하려고 하는데 도저히 답을 모르겠어서 질문 올립니다.
음성 녹음 버튼을 누르면 해당 오류가 뜹니다.
E/AndroidRuntime: FATAL EXCEPTION: main
Process: org.androidtown.http, PID: 24384
java.lang.RuntimeException: Unable to start activity ComponentInfo{org.androidtown.http/org.androidtown.multimemo.VoiceRecordingActivity}: java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3114)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3257)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1948)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7050)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:494)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:965)
Caused by: java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.
at android.support.v7.app.AppCompatDelegateImpl.createSubDecor(AppCompatDelegateImpl.java:555)
at android.support.v7.app.AppCompatDelegateImpl.ensureSubDecor(AppCompatDelegateImpl.java:518)
at android.support.v7.app.AppCompatDelegateImpl.setContentView(AppCompatDelegateImpl.java:466)
at android.support.v7.app.AppCompatActivity.setContentView(AppCompatActivity.java:140)
at org.androidtown.multimemo.VoiceRecordingActivity.onCreate(VoiceRecordingActivity.java:85)
at android.app.Activity.performCreate(Activity.java:7327)
at android.app.Activity.performCreate(Activity.java:7318)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1271)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3094)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3257)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1948)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7050)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:494)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:965)
Process 24384 terminated.
여기서 제 코드 (VoiceRecordingActivity.java:85)를 가보면..
init(savedInstanceState);
이 친구가 에러났다고 하는데 컴파일 에러는 없습니다.
이거 액티비티랑 xml이랑 미스매칭되서 나는 오류 아닌가요..? 저번에 이런적 있었는데.. 그때는 매칭을 잘못해서 여기서 에러가 났었거든요. 이번엔 아닙니다.
도저히 알 수가 없어서 질문드립니다..
다음은 전체 코드입니다. (아예 액티비티 전환이 안되고 너무 길어서 첨부안할까 했는데 혹시 몰라서 해봅니다.)
VoiceRecordingActivity.java
package org.androidtown.multimemo;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.os.StatFs;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.Window;
import android.widget.TextView;
import org.androidtown.multimemo.VoiceRecorder.OnStateChangedListener;
import org.androidtown.multimemo.common.TitleBitmapButton;
import java.io.File;
/**
* 음성 녹음 액티비티
*/
public class VoiceRecordingActivity extends AppCompatActivity implements OnStateChangedListener{
public static final String TAG = "VoiceRecordingActivity";
TextView mRecordingTimeText;
TitleBitmapButton mStartStopBtn;
TitleBitmapButton mCancelBtn;
boolean isRecording;
RemainingTimeCalculator mRemainingTimeCalculator;
static final String AUDIO_3GPP = "audio/3gpp";
static final String AUDIO_AMR = "audio/amr";
static final String AUDIO_ANY = "audio/*";
static final String ANY_ANY = "*/*";
static final int BITRATE_AMR = 5900;
static final int BITRATE_3GPP = 5900;
String mRequestedType = AUDIO_AMR;
VoiceRecorder mRecorder;
boolean mSampleInterrupted = false;
public static final int WARNING_INSERT_SDCARD = 1011;
public static final int WARNING_DISK_SPACE_FULL = 1012;
public static final int RECORDING_START = 1013;
static final String STATE_FILE_NAME = "soundrecorder.state";
static final String RECORDER_STATE_KEY = "recorder_state";
static final String SAMPLE_INTERRUPTED_KEY = "sample_interrupted";
static final String MAX_FILE_SIZE_KEY = "max_file_size";
long mMaxFileSize = -1;
WakeLock mWakeLock;
long mRecordingTime;
String mTimerFormat;
Handler mHandler = new Handler();
Runnable mUpdateTimer = new Runnable() {
public void run() { updateTimerView(mRecordingTimeText); }
};
public int mRecordingState = 0;
public static final int RECORDING_IDLE = 0;
public static final int RECORDING_RUNNING = 1;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.voice_recording_activity);
setBottomBtns();
init(savedInstanceState);
}
public void setBottomBtns()
{
isRecording = false;
mRecordingTimeText = (TextView)findViewById(R.id.recording_recordingTimeText);
mRecordingTimeText.setText("00:00");
mStartStopBtn = (TitleBitmapButton)findViewById(R.id.recording_startstopBtn);
mStartStopBtn.setCompoundDrawablesWithIntrinsicBounds(0, R.drawable.btn_voice_record, 0, 0);
mCancelBtn = (TitleBitmapButton)findViewById(R.id.recording_cancelBtn);
mStartStopBtn.setText(R.string.audio_recording_start_title);
mStartStopBtn.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
if(isRecording)
{
stopVoiceRecording();
isRecording = false;
}
else
{
startVoiceRecording();
isRecording = true;
}
}
});
mCancelBtn.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
finish();
}
});
}
public void init(Bundle icycle)
{
mRecorder = new VoiceRecorder();
mRecorder.setOnStateChangedListener(this);
mRemainingTimeCalculator = new RemainingTimeCalculator();
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
mWakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, "myapp:MediGalaxy");
if (icycle != null) {
Bundle recorderState = icycle.getBundle(RECORDER_STATE_KEY);
if (recorderState != null) {
mRecorder.restoreState(recorderState);
mSampleInterrupted = recorderState.getBoolean(SAMPLE_INTERRUPTED_KEY, false);
mMaxFileSize = recorderState.getLong(MAX_FILE_SIZE_KEY, -1);
}
}
mTimerFormat = "%02d:%02d";
updateUi(mRecordingTimeText);
}
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
updateUi(mRecordingTimeText);
}
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (mRecorder.sampleLength() == 0)
return;
Bundle recorderState = new Bundle();
mRecorder.saveState(recorderState);
recorderState.putBoolean(SAMPLE_INTERRUPTED_KEY, mSampleInterrupted);
recorderState.putLong(MAX_FILE_SIZE_KEY, mMaxFileSize);
outState.putBundle(RECORDER_STATE_KEY, recorderState);
}
/*
* Called when Recorder changed it's state.
*/
public void onStateChanged(int state) {
if (state == VoiceRecorder.PLAYING_STATE || state == VoiceRecorder.RECORDING_STATE) {
mSampleInterrupted = false;
}
if (state == VoiceRecorder.RECORDING_STATE) {
mWakeLock.acquire();
} else {
if (mWakeLock.isHeld())
mWakeLock.release();
}
updateUi(mRecordingTimeText);
}
/*
* Called when MediaPlayer encounters an error.
*/
public void onError(int error) {
String message = null;
switch (error) {
case VoiceRecorder.SDCARD_ACCESS_ERROR:
message = "SD카드 접근 오류입니다.";
break;
case VoiceRecorder.IN_CALL_RECORD_ERROR:
case VoiceRecorder.INTERNAL_ERROR:
message = "내부 오류입니다.";
break;
}
if (message != null) {
new AlertDialog.Builder(this)
.setTitle(R.string.audio_recording_title)
.setMessage(message)
.setPositiveButton(R.string.confirm_btn, null)
.setCancelable(false)
.show();
}
}
public void updateUi(TextView textView) {
updateTimerView(textView);
}
private void updateTimerView(TextView textView) {
Resources res = getResources();
int state = mRecorder.state();
boolean ongoing = state == VoiceRecorder.RECORDING_STATE || state == VoiceRecorder.PLAYING_STATE;
long time = ongoing ? mRecorder.progress() : mRecorder.sampleLength();
String timeStr = String.format(mTimerFormat, time/60, time%60);
textView.setText(timeStr);
mRecordingTime = time;
if (state == VoiceRecorder.PLAYING_STATE) {
} else if (state == VoiceRecorder.RECORDING_STATE) {
updateTimeRemaining();
}
if (ongoing)
mHandler.postDelayed(mUpdateTimer, 1000);
}
private void updateTimeRemaining() {
long t = mRemainingTimeCalculator.timeRemaining();
if (t <= 0) {
mSampleInterrupted = true;
int limit = mRemainingTimeCalculator.currentLowerLimit();
switch (limit) {
case RemainingTimeCalculator.DISK_SPACE_LIMIT:
break;
case RemainingTimeCalculator.FILE_SIZE_LIMIT:
break;
default:
break;
}
mRecorder.stop();
return;
}
String timeStr = "";
if (t < 60)
timeStr = String.format("%d min available", t);
else if (t < 540)
timeStr = String.format("%d s available", t/60 + 1);
}
public void startVoiceRecording()
{
// start recording
voiceRecordingStart();
mStartStopBtn.setText(R.string.audio_recording_stop_title);
mStartStopBtn.setCompoundDrawablesWithIntrinsicBounds(0, R.drawable.btn_voice_stop, 0, 0);
mRecordingTimeText.setText("00:00");
}
public void voiceRecordingStart() {
mRemainingTimeCalculator.reset();
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
mSampleInterrupted = true;
showDialog(WARNING_INSERT_SDCARD);
stopAudioPlayback();
if (AUDIO_AMR.equals(mRequestedType)) {
mRemainingTimeCalculator.setBitRate(BITRATE_AMR);
mRecorder.startRecording(MediaRecorder.OutputFormat.RAW_AMR, ".amr", this);
} else if (AUDIO_3GPP.equals(mRequestedType)) {
mRemainingTimeCalculator.setBitRate(BITRATE_3GPP);
mRecorder.startRecording(MediaRecorder.OutputFormat.THREE_GPP, ".3gpp", this);
} else {
throw new IllegalArgumentException("Invalid output file type requested");
}
if (mMaxFileSize != -1) {
mRemainingTimeCalculator.setFileSizeLimit(mRecorder.sampleFile(), mMaxFileSize);
}
} else if (!mRemainingTimeCalculator.diskSpaceAvailable()) {
mSampleInterrupted = true;
showDialog(WARNING_DISK_SPACE_FULL);
} else {
stopAudioPlayback();
if (AUDIO_AMR.equals(mRequestedType)) {
mRemainingTimeCalculator.setBitRate(BITRATE_AMR);
mRecorder.startRecording(MediaRecorder.OutputFormat.RAW_AMR, ".amr", this);
} else if (AUDIO_3GPP.equals(mRequestedType)) {
mRemainingTimeCalculator.setBitRate(BITRATE_3GPP);
mRecorder.startRecording(MediaRecorder.OutputFormat.THREE_GPP, ".3gpp", this);
} else {
throw new IllegalArgumentException("Invalid output file type requested");
}
if (mMaxFileSize != -1) {
mRemainingTimeCalculator.setFileSizeLimit(mRecorder.sampleFile(), mMaxFileSize);
}
}
mRecordingState = RECORDING_RUNNING;
}
public void stopVoiceRecording() {
mRecorder.stop();
mStartStopBtn.setText(R.string.audio_recording_start_title);
mStartStopBtn.setCompoundDrawablesWithIntrinsicBounds(0, R.drawable.btn_voice_record, 0, 0);
File tempFile = mRecorder.sampleFile();
saveRecording(tempFile);
mRecordingState = RECORDING_IDLE;
}
private void saveRecording(File tempFile) {
checkVoiceFolder();
String voiceName = "recorded";
try {
tempFile.renameTo(new File(BasicInfo.FOLDER_VOICE + voiceName));
setResult(RESULT_OK);
} catch(Exception ex) {
Log.e(TAG, "Exception in storing recording.", ex);
}
if (voiceName != null) {
Log.d(TAG, "Recording file saved to : " + voiceName);
}
}
public void checkVoiceFolder() {
File voiceFolder = new File(BasicInfo.FOLDER_VOICE);
if(!voiceFolder.isDirectory()){
Log.d(TAG, "creating voice folder : " + voiceFolder);
voiceFolder.mkdirs();
}
}
private void stopAudioPlayback() {
Intent i = new Intent("com.android.music.musicservicecommand");
i.putExtra("command", "pause");
sendBroadcast(i);
}
class RemainingTimeCalculator {
public static final int UNKNOWN_LIMIT = 0;
public static final int FILE_SIZE_LIMIT = 1;
public static final int DISK_SPACE_LIMIT = 2;
private int mCurrentLowerLimit = UNKNOWN_LIMIT;
private File mSDCardDirectory;
private File mRecordingFile;
private long mMaxBytes;
private int mBytesPerSecond;
private long mBlocksChangedTime;
private long mLastBlocks;
private long mFileSizeChangedTime;
private long mLastFileSize;
public RemainingTimeCalculator() {
mSDCardDirectory = Environment.getExternalStorageDirectory();
}
public void setFileSizeLimit(File file, long maxBytes) {
mRecordingFile = file;
mMaxBytes = maxBytes;
}
public void reset() {
mCurrentLowerLimit = UNKNOWN_LIMIT;
mBlocksChangedTime = -1;
mFileSizeChangedTime = -1;
}
public long timeRemaining() {
// Calculate how long we can record based on free disk space
StatFs fs = new StatFs(mSDCardDirectory.getAbsolutePath());
long blocks = fs.getAvailableBlocks();
long blockSize = fs.getBlockSize();
long now = System.currentTimeMillis();
if (mBlocksChangedTime == -1 || blocks != mLastBlocks) {
mBlocksChangedTime = now;
mLastBlocks = blocks;
}
long result = mLastBlocks*blockSize/mBytesPerSecond;
result -= (now - mBlocksChangedTime)/1000;
if (mRecordingFile == null) {
mCurrentLowerLimit = DISK_SPACE_LIMIT;
return result;
}
mRecordingFile = new File(mRecordingFile.getAbsolutePath());
long fileSize = mRecordingFile.length();
if (mFileSizeChangedTime == -1 || fileSize != mLastFileSize) {
mFileSizeChangedTime = now;
mLastFileSize = fileSize;
}
long result2 = (mMaxBytes - fileSize)/mBytesPerSecond;
result2 -= (now - mFileSizeChangedTime)/1000;
result2 -= 1;
mCurrentLowerLimit = result < result2
? DISK_SPACE_LIMIT : FILE_SIZE_LIMIT;
return Math.min(result, result2);
}
public int currentLowerLimit() {
return mCurrentLowerLimit;
}
public boolean diskSpaceAvailable() {
StatFs fs = new StatFs(mSDCardDirectory.getAbsolutePath());
// keep one free block
return fs.getAvailableBlocks() > 1;
}
public void setBitRate(int bitRate) {
mBytesPerSecond = bitRate/8;
}
}
}
voice_recording_activity.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="200dp"
android:layout_height="match_parent"
>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
>
<TextView
android:id="@+id/recording_recordingTimeText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:gravity="center"
android:textSize="30sp"
android:textColor="#ffffffff"
android:text="00:00"
/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
<org.androidtown.multimemo.common.TitleBitmapButton
android:id="@+id/recording_startstopBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="시작/중지"
android:textStyle="bold"
android:textSize="18dp"
/>
<org.androidtown.multimemo.common.TitleBitmapButton
android:id="@+id/recording_cancelBtn"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="닫기"
android:textStyle="bold"
android:textSize="18dp"
/>
</LinearLayout>
</LinearLayout>