티스토리 뷰

후기

SpeakUP 안드로이드 앱 개발기

노이지 2020. 11. 17. 11:02
반응형

이화여자대학교 2020-2 졸업프로젝트 그로쓰3팀 SpeakUP 안드로이드 앱 개발기입니다. 전체 코드는 여기에서 확인할 수 있습니다. 소스코드를 다운받아 안드로이드 스튜디오에서 build하면 어플리케이션을 사용해 볼 수 있습니다.

1학기에는 팀원들 모두 함께 딥러닝 모델을 개발했으며, 2학기에는 모델 정확도 개선, 서버, 안드로이드 개발로 역할을 분담하여 프로젝트를 진행했습니다.

프로젝트 소개

통번역학과 교수님들과의 협업 프로젝트로, '통번역 교수자와 학습자를 위한 스마트러닝 어플리케이션'입니다.

필요성

  • 교수자가 학습자의 통역 평가시 필수적으로 필요한 전사 자료를 만드는 데에 많은 시간 소요
  • 통역 음성 파일로만은 통역 개시 지연 시간을 직관적으로 파악하기 불가능
  • 객관적 평가 지표 확인하기 어려움

전사 자료란 오역, 누락, 불필요한 첨삭 등 내용의 정확성을 파악하기 위해 필요한 통역 음성을 텍스트화한 것을 말합니다.

제안내용

  • 침묵구간의 위치 및 길이, 추임새를 텍스트에 표시해주는 전사 시스템 개발
  • 통역 개시 지연시간 파악 및 가시화 기능 개발
  • 전사파일 채점과 자가평가를 위한 통계분석 제공

기대효과

  • 학습자가 전사파일을 제공하기 위해 반복해서 통역 음성 파일을 들을 필요 없이 바로 전사파일 생성
  • 교수자와 학습자가 전사자료를 만드는데 소요되는 시간 및 노력 감소
  • 교수자에게 학습자의 실질적인 통역 평가를 위한 충분한 시간 제공
  • 통역평가의 중요한 요소 중 하나인 통역 개시 지연 시간을 시스템에서 측정하여 교수자의 원활한 평가 가능
  • 학습자의 통역평가를 위한 통계분석 자료를 제공해 교수자의 객관적 판단 및 학습자의 자가평가 도움

시나리오

어플리케이션 사용자는 로그인을 한 후 자신이 수강 중인 강의 목록을 조회하고 각 강의별 과제 목록을 조회하여 원하는 과제를 수행할 수 있습니다. 과제 수행은 교수자가 서버에 업로드해놓은 원문 음성 파일 재생과 사용자의 통역 음성 녹음이 반복됩니다. 과제 제출이 완료되면 과제 상세 화면에서 제출 상황을 확인할 수 있으며 최종 제출 결과를 확인할 수 있습니다. 제출 결과에서는 번역 음성의 전사 결과와 추임새 횟수, 침묵 구간 비율 통계를 확인할 수 있습니다.

 

지원버전

Android SDK version 21 이상

 

어플리케이션 기본 설정

1. colors.xml

어플리케이션에 사용할 색상들을 정의합니다.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#6200EE</color>
    <color name="colorPrimaryDark">#3700B3</color>
    <color name="colorAccent">#6200EE</color>
    <color name="colorHint">#C3C3C3</color>
    <color name="colorBlack">#000000</color>
    <color name="colorWhite">#FFFFFF</color>
    <color name="colorDarkGray">#89000000</color>
    <color name="bgGray">#F3F3F3</color>
    <color name="bgLightGray">#FAFAFA</color>
</resources>

2. styles.xml

어플리케이션 기본 style을 설정합니다. action bar 유무, 상태바 색상 등을 설정할 수 있습니다.

<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
    <!-- Customize your theme here. -->
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
    <item name="android:colorBackground">@color/colorWhite</item>
    <item name="android:statusBarColor">@color/colorWhite</item>
    <item name="android:windowLightStatusBar" tools:targetApi="m">true</item>
</style>

 

각 화면 설명

1. 로그인 화면

아이디와 비밀번호를 입력하여 로그인을 할 수 있습니다. 아직 계정이 없는 경우 가입하기를 클릭해 회원가입 화면으로 넘어갈 수 있습니다.

 

로그인 버튼을 클릭하면 아이디와 비밀번호를 입력했는지 확인한 후 모두 입력한 경우 로그인 API를 호출합니다.

String id = mEtId.getText().toString().trim();
String pwd = mEtPwd.getText().toString().trim();
if (id.isEmpty()) //아이디 입력 필요
    showCustomToast(getString(R.string.login_id_hint));
else if (pwd.isEmpty()) //비밀번호 입력 필요
    showCustomToast(getString(R.string.login_pwd_hint));
else
    tryPostLogin(id, pwd); //로그인 API 호출

로그인 성공 시 서버로부터 받은 JWT token을 SharedPreferences에 저장하고 main(강의 목록) 화면으로 넘어갑니다. JWT token은 이후 API 호출 시 서버에서 사용자 인증을 할 때 사용합니다.

// 로그인 성공
@Override
public void loginSuccess(String message, LoginResult result) {
    hideProgressDialog();

    SharedPreferences.Editor editor = ApplicationClass.sSharedPreferences.edit();
    editor.putString(ApplicationClass.X_ACCESS_TOKEN, result.getJwt());
    editor.apply();

    Intent intent = new Intent(this, MainActivity.class);
    startActivity(intent);
    finish();
}

 

2. 회원가입 화면

학번, 아이디, 비밀번호를 입력하여 회원가입을 진행합니다. 모든 항목을 입력했는지, 학번을 7자리로 입력했는지, 비밀번호를 제대로 입력했는지 확인 후 회원가입 API를 호출합니다.

String studentId = mEtStudentId.getText().toString().trim(); //학번
String id = mEtId.getText().toString().trim(); //아이디
String pwd = mEtPwd.getText().toString().trim(); //비밀번호
String pwdCheck = mEtPwdCheck.getText().toString().trim(); //비밀번호 재확인

if (studentId.isEmpty()) //학번 입력 필요
    showCustomToast(getString(R.string.signup_student_id_hint));
else if (studentId.length() != 7) //학번 길이 체크
    showCustomToast(getString(R.string.signup_student_id_length));
else if (id.isEmpty()) //아이디 입력 필요
    showCustomToast(getString(R.string.login_id_hint));
else if (pwd.isEmpty()) //비밀번호 입력 필요
    showCustomToast(getString(R.string.login_pwd_hint));
else if (pwdCheck.isEmpty()) //비밀번호 재확인 필요
    showCustomToast(getString(R.string.signup_pwd_check_hint));
else if (!pwd.equals(pwdCheck)) //비밀번호 재확인 필요
    showCustomToast(getString(R.string.signup_pwd_check_wrong));
else //회원가입 API 호출
    tryPostUser(studentId, id, pwd);

회원가입 성공 시 회원가입 성공 토스트 메시지를 나타내고 화면을 종료하여 로그인 화면으로 돌아갑니다.

// 회원가입 성공
@Override
public void signupSuccess(String message) {
    hideProgressDialog();
    showCustomToast(getString(R.string.signup_complete));
    finish();
}

 

3. 강의 목록 화면

수강 중인 강의 목록을 확인할 수 있습니다. 화면 시작 시 강의 목록을 나타내기 위한 recyclerview와 adapter, layoutmanager를 정의하고 강의 목록 조회 API를 호출합니다.

RecyclerView recyclerView = findViewById(R.id.main_recyclerview);
mClProgressBar = findViewById(R.id.main_cl_progressbar);
mTvEmpty = findViewById(R.id.main_tv_empty);

mCourseList = new ArrayList<>();
mCourseAdapter = new CourseAdapter(mCourseList);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(linearLayoutManager);
recyclerView.setAdapter(mCourseAdapter);

//강의 목록 조회 API 호출
tryGetCourse();

강의 목록 조회 성공 시 수강 중인 강의가 없는 경우 '수강 중인 강의가 없습니다' textview를 나타내고 수강 중인 강의가 있는 경우 강의 목록 recyclerview를 update 합니다.

// 강의 목록 조회 성공
@Override
public void getCourseSuccess(String message, MainResult result) {
    mClProgressBar.setVisibility(View.GONE);

    if (result.getCourseList().size() == 0) //수강 중인 강의가 없는 경우
        mTvEmpty.setVisibility(View.VISIBLE);
    else //수강 중인 강의가 있는 경우
        mTvEmpty.setVisibility(View.INVISIBLE);

    mCourseList.addAll(result.getCourseList());
    mCourseAdapter.notifyDataSetChanged(); //recyclerview update
}

강의 목록의 각 강의를 클릭하면 intent에 강의 id와 강의명을 추가하여 과제 목록 화면으로 이동합니다.

class CourseListViewHolder extends RecyclerView.ViewHolder {

    private TextView tvTitle;

    CourseListViewHolder(@NonNull final View itemView) {
        super(itemView);

        tvTitle = itemView.findViewById(R.id.item_course_title);

        itemView.setOnClickListener(v -> {
            int pos = getAdapterPosition();

            Intent intent = new Intent(itemView.getContext(), CourseActivity.class);
            intent.putExtra("courseId", mCourseList.get(pos).getCourseId());
            intent.putExtra("courseName", mCourseList.get(pos).getSubjectName());
            itemView.getContext().startActivity(intent);
        });
    }
}

 

4. 과제 목록 화면

과제 목록 화면 시작 시 이전 화면에서 intent로 받은 강의명을 제목에 표시하고 과제 목록을 나타내기 위한 recyclerview와 adapter, layoutmanager를 정의합니다. 이후 intent로 받은 강의 id를 이용해 과제 목록 조회 API를 호출합니다.

//강의명 설정
String title = getIntent().getStringExtra("courseName");
tvTitle.setText(title);

mAssignmentList = new ArrayList<>();
mAssignmentAdapter = new AssignmentAdapter(mAssignmentList);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(linearLayoutManager);
recyclerView.setAdapter(mAssignmentAdapter);

//과제 목록 조회 API 호출
int courseId = getIntent().getIntExtra("courseId", 0);
tryGetAssignment(courseId);

과제 목록 조회 성공 시 과제가 없는 경우 '과제가 존재하지 않습니다' textview를 나타내고 과제가 있는 경우 과제 목록 recyclerview를 update 합니다.

//과제 목록 조회 성공
@Override
public void getAssignmentSuccess(String message, CourseResult result) {
    mClProgressBar.setVisibility(View.GONE);

    if (result != null &&result.getAssignmentList().size() != 0) {
        mTvEmpty.setVisibility(View.INVISIBLE);
        mAssignmentList.addAll(result.getAssignmentList());
    }
    else {
        mTvEmpty.setVisibility(View.VISIBLE);
    }

    mAssignmentAdapter.notifyDataSetChanged();
}

과제 목록의 각 과제를 클릭하면 intent에 과제 id와 과제명, 과제 제출 여부, 과제 종료 일시를 추가하여 과제 상세 화면으로 이동합니다.

class AssignmentListViewHolder extends RecyclerView.ViewHolder {

    private TextView tvTitle;

    AssignmentListViewHolder(@NonNull final View itemView) {
        super(itemView);

        tvTitle = itemView.findViewById(R.id.item_assignment_title);

        itemView.setOnClickListener(v -> {
            int pos = getAdapterPosition();

            Intent intent = new Intent(itemView.getContext(), AssignmentActivity.class);
            SharedPreferences.Editor editor = ApplicationClass.sSharedPreferences.edit();
            editor.putInt("assignmentId", mAssignmentList.get(pos).getAssignmentId());
            editor.apply();
            intent.putExtra("assignmentName", mAssignmentList.get(pos).getAssignmentName());
            intent.putExtra("assignmentSubmit", mAssignmentList.get(pos).getSubmitCheck());
            intent.putExtra("assignmentDueDate", mAssignmentList.get(pos).getDueDate());
            itemView.getContext().startActivity(intent);
        });
    }
}

5. 과제 상세 화면

과제 상세 화면 시작 시 이전 화면에서 intent로 받은 과제명, 과제 제출 여부, 과제 종료 일시를 표시합니다. 과제 제출 여부를 확인하여 과제를 이미 제출한 경우 '제출 완료' 메시지를 표시하고 아직 제출하지 않은 경우 '제출 안 함' 메시지를 표시하고 글씨색을 빨간색으로 변경합니다.

tvTitle.setText(getIntent().getStringExtra("assignmentName"));
mSubmitStatus = getIntent().getIntExtra("assignmentSubmit", 0);
if (mSubmitStatus == 0) {
    tvSubmitStatus.setText(getString(R.string.assignment_submit_no));
    tvSubmitStatus.setTextColor(Color.parseColor("#ffff0000"));
} else
    tvSubmitStatus.setText(getString(R.string.assignment_submit_yes));
String dueDate = getIntent().getStringExtra("assignmentDueDate");
tvDueDate.setText(dueDate);

마감까지 남은 기한을 표시하기 위해 과제 종료 일시를 이용하여 남은 기한을 계산합니다. 우선 종료 일시 dueDate를 LocalDateTime 변수로 parsing합니다. Duration.between 메서드를 이용하여 parsing한 LocalDateTime 변수 dueDateTime과 현재 시각을 나타내는 LocalDateTime.now()의 차이값을 구합니다. toMintues 메서드를 이용해 두 LocalDateTime 변수의 차이값을 분 단위로 계산합니다. 이 값으로 과제 종료 일시까지 남은 날, 시간, 분을 계산하여 표시합니다. 만약 차이값이 양수가 아닌 경우 즉, 과제 종료 일시가 지난 경우 '제출 마감이 지났습니다' 메시지를 표시하고 글씨색을 빨간색으로 변경합니다.

LocalDateTime dueDateTime = LocalDateTime.parse(dueDate, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));
mRemainMinutes = Duration.between(LocalDateTime.now(), dueDateTime).toMinutes();
int remainDays;
int remainHours;
String remain = "";

if (mRemainMinutes > 0) {
    remainDays = (int)mRemainMinutes/(60*24);
    remainHours = (int)mRemainMinutes%(60*24)/60;
    mRemainMinutes = mRemainMinutes%60;

    if (remainDays != 0)
        remain += remainDays + getString(R.string.day_unit)+" ";
    if (remainHours != 0)
        remain += remainHours + getString(R.string.hour_unit)+" ";
    if (remainDays == 0 && remainHours == 0)
        remain = mRemainMinutes + getString(R.string.minute_unit);
} else {
    remain = getString(R.string.assignment_remain_late);
    tvRemain.setTextColor(Color.parseColor("#ffff0000"));
}

tvRemain.setText(remain);

통역하기 버튼을 눌렀을 때 나타나는 과제 수행 제한시간 선택 다이얼로그를 설정해줍니다. 우선 다이얼로그에 layout을 설정해주고 '취소' 버튼과 '완료' 버튼에 클릭 리스너를 설정합니다. 취소 버튼을 누르면 다이얼로그를 닫고 완료 버튼을 누르면 선택한 radio button에 따라 제한시간을 SharedPreferences에 저장합니다. 이후 과제 수행 화면으로 넘어가고 다이얼로그는 종료합니다.

AlertDialog.Builder builder = new AlertDialog.Builder(this);
View view = LayoutInflater.from(this).inflate(R.layout.dialog_speed, null, false);
builder.setView(view);

mSpeedDialog = builder.create();
mSpeedDialog.setCanceledOnTouchOutside(false); //dialog 밖의 영역 터치했을 때 꺼지지 않도록 설정

TextView tvCancel = view.findViewById(R.id.dialog_speed_tv_cancel);
TextView tvComplete = view.findViewById(R.id.dialog_speed_tv_complete);
final RadioGroup rg = view.findViewById(R.id.dialog_speed_rg);

tvCancel.setOnClickListener(v-> mSpeedDialog.dismiss());

tvComplete.setOnClickListener(v -> {
    SharedPreferences.Editor editor = ApplicationClass.sSharedPreferences.edit();
    switch (rg.getCheckedRadioButtonId()) {
        case R.id.dialog_speed_rb1: editor.putFloat("speed", 1); break;
        case R.id.dialog_speed_rb2: editor.putFloat("speed", 1.2f); break;
        case R.id.dialog_speed_rb3: editor.putFloat("speed", 1.5f); break;
    }
    editor.apply();
    Intent intent = new Intent(getApplicationContext(), RecordActivity.class);
    startActivity(intent);
    mSpeedDialog.dismiss();
});

과제 수행 제한시간 선택 다이얼로그의 layout에서는 여러 개의 radio button 중 하나만을 선택하기 위해 radio group을 사용했습니다.

<RadioGroup
    android:id="@+id/dialog_speed_rg"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="10dp"
    android:orientation="vertical"
    app:layout_constraintTop_toBottomOf="@id/dialog_speed_description">

    <RadioButton
        android:id="@+id/dialog_speed_rb1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="10dp"
        android:text="@string/assignment_speed1" />

    <RadioButton
        android:id="@+id/dialog_speed_rb2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="10dp"
        android:text="@string/assignment_speed2" />

    <RadioButton
        android:id="@+id/dialog_speed_rb3"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="10dp"
        android:text="@string/assignment_speed3" />

</RadioGroup>

결과 확인 버튼을 누르면 제출한 과제가 존재하지 않는 경우 '과제 제출 기록이 존재하지 않습니다.' 메시지를 띄우고 제출한 과제에 대한 결과가 존재하는 경우 과제 결과 화면으로 이동합니다.

if (mSubmitStatus == 0)
    showCustomToast(getString(R.string.assignment_result_no));
else {
    intent = new Intent(this, ResultActivity.class);
    intent.putExtra("assignmentName", getIntent().getStringExtra("assignmentName"));
    startActivity(intent);
}

통역하기 버튼을 누르면 과제 제출 마감이 지난 경우 '제출 마감이 지났습니다.' 메시지를 띄우고 제출 마감이 지나지 않은 경우 위에서 설정한 다이얼로그를 화면에 띄웁니다.

if (mRemainMinutes > 0)
    mSpeedDialog.show();
else
    showCustomToast(getString(R.string.assignment_remain_late));

 

6. 과제 수행 화면

과제 수행 화면 시작 시 녹음 권한을 요청합니다.

private String [] permissions = {Manifest.permission.RECORD_AUDIO, Manifest.permission.WRITE_EXTERNAL_STORAGE};
ActivityCompat.requestPermissions(this, permissions, REQUEST_RECORD_AUDIO_PERMISSION);

녹음 권한이 허락되지 않은 경우 화면을 종료합니다.

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    if (requestCode == REQUEST_RECORD_AUDIO_PERMISSION) {
        permissionToRecordAccepted = grantResults[0] == PackageManager.PERMISSION_GRANTED && grantResults[1] == PackageManager.PERMISSION_GRANTED;
    }
    if (!permissionToRecordAccepted ) finish();
}

권한 설정이 완료된 후 원문 음성 파일 조회 API를 호출합니다. 이 때 녹음 파일을 저장할 base path를 설정합니다.

// 원문 음성 파일 조회
tryGetFile(ApplicationClass.sSharedPreferences.getInt("assignmentId", 0));
mFilepath = Objects.requireNonNull(getExternalCacheDir()).getAbsolutePath() +"/record";

원문 음성 파일 조회 성공 시 음성 파일 리스트를 설정하고 첫 번째 음성 파일을 재생합니다.

// 원문 음성 파일 조회 성공
@Override
public void getFileSuccess(String message, GetFileResult result) {
    mFileList = result.getFilePathList();
    startAudio(mFileIdx);
}

1) 음성파일 재생 (startAudio)

음성파일 재생 시작 시 상태 메시지를 '원문 음성 재생 중'으로 변경하고 녹음 완료 버튼을 보이지 않게 설정합니다. startAudio 메서드가 main thread가 아닌 추가로 생성된 thread에서 실행되므로 ui를 변경하는 작업은 handler를 통해 작업해야 합니다.

handler.post(new Runnable() {
    public void run() {
        mTvStatus.setText(getString(R.string.record_audio_playing));
    }
});
mBtnStop.setVisibility(View.INVISIBLE);

MediaPlayer를 이용해 연사 음성 파일을 재생합니다. mediaplayer에 재생하고자 하는 파일 url을 설정합니다. mediaplayer를 실행할 준비가 완료되면 음성 파일 재생을 시작하고 녹음 제한시간 설정을 위해 음성 파일의 길이를 불러와 progressbar의 최대길이로 설정하고 초기화합니다.

mMediaPlayer.reset();
mMediaPlayer.setDataSource(mFileList.get(index));
mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
    @Override
    public void onPrepared(MediaPlayer mp) {
        mp.start();

        mFileLength = mMediaPlayer.getDuration();
        minutes = (int) (mFileLength / 1000 / 60);
        seconds = (int) ((mFileLength / 1000) % (60));
        mProgressBar.setMax(minutes*60+seconds);
        progressMinutes = 0;
        progressSeconds = -1;
        mProgressBar.setProgress(0);
    }
});
mMediaPlayer.prepareAsync();

음성파일 재생이 완료되면 저장할 녹음 파일 이름을 설정한 후 녹음을 시작합니다.

mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
    @Override
    public void onCompletion(MediaPlayer mp) {
        mFileIdx++;
        String fileName = mFilepath + mFileIdx +".3gp";
        startRecord(fileName);
    }
});

2) 녹음 시작 (startRecord)

MediaRecorder를 이용해 사용자의 음성을 녹음합니다. audio source를 mic로 설정하고 output file format, 파일명, encorder를 설정합니다.

recorder = new MediaRecorder();
recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
recorder.setOutputFile(fileName);
recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);

녹음 제한시간을 표시해주기 위한 thread를 생성합니다. 이 thread에서 progressbar를 1초에 한 칸씩 증가시킵니다. main thread가 아닌 thread에서 ui를 변경시키기 때문에 handler를 이용합니다. 녹음 완료 버튼을 누르거나 제한시간이 끝나면 녹음을 중지합니다.

Thread thread = new Thread(new Runnable() {
    public void run() {
        while (!Thread.currentThread().isInterrupted() && (progressMinutes * 60 + progressSeconds) < (minutes * 60 + seconds)) {
            progressSeconds += 1;
            if (progressSeconds == 60) {
                progressMinutes++;
                progressSeconds = 0;
            }
            handler.post(new Runnable() {
                public void run() {
                    mProgressBar.setProgress(progressMinutes * 60 + progressSeconds);
                }
            });
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        stopRecord();
    }
});

thread 생성 후 recorder로 녹음을 시작하고 thread를 시작합니다. 이 때 상태 메시지를 '녹음 중'으로 변경하고 녹음 완료 버튼을 보이도록 설정합니다.

recorder.prepare();
recorder.start();

mProgressBar.setVisibility(View.VISIBLE);
thread.start();

handler.post(new Runnable() {
    public void run() {
        mTvStatus.setText(getString(R.string.record_audio_recording));
    }
});
mBtnStop.setVisibility(View.VISIBLE);

녹음 제한 시간 표시 progressbar를 사용하기 위해 styles.xml에 progressbar style을 추가하여 사용합니다. progress bar의 배경 색상 등을 설정할 수 있습니다.

<style name="ProgressBar" parent="@style/Widget.AppCompat.ProgressBar.Horizontal">
    <item name="android:progressBackgroundTint">#9c9c9c</item>
    <item name="android:progressTint">@color/colorAccent</item>
    <item name="android:minWidth">200dp</item>
</style>

 

3) 녹음 완료 (stopRecord)

원문 음성 파일이 남아있는 경우 다음 음성을 재생하고 남아있는 음성 파일이 없는 경우 과제 수행을 완료한 것으로, 녹음한 파일들을 base64 string으로 encoding하여 서버로 전송합니다.

private void stopRecord() {
    // 녹음 파일 저장
    mBtnStop.setVisibility(View.INVISIBLE);
    mProgressBar.setVisibility(View.INVISIBLE);

    recorder.stop();
    recorder.release();
    recorder = null;

    // 원문 음성이 남은 경우
    if (mFileIdx < mFileList.size())
        startAudio(mFileIdx);
    else { // 과제 수행이 끝난 경우
        handler.post(new Runnable() {
            public void run() {
                mTvStatus.setText(getString(R.string.record_audio_complete));
            }
        });
        ArrayList<String> encodedFileList = new ArrayList<>();
        for (int i=1;i<=mFileList.size();i++) {
            File file = new File(mFilepath+i+".3gp");
            try {
                byte[] bytes = FileUtils.readFileToByteArray(file);
                String encoded = Base64.encodeToString(bytes, 0);
                Log.d("encoded"," string: "+encoded);
                encodedFileList.add(encoded);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        tryPostFile(ApplicationClass.sSharedPreferences.getInt("assignmentId", 0), encodedFileList);
    }
}

과제 파일 전송 성공 시 main(강의 목록) 화면으로 이동합니다. main 화면 이외의 모든 화면을 종료하기 위해 intent에 FLAG_ACTIVITY_NEW_TASK, FLAG_ACTIVITY_SINGLE_TOP, FLAG_ACTIVITY_CLEAR_TASK flag를 설정합니다.

//과제 파일 전송 성공
@Override
public void postFileSuccess(String message) {
    hideProgressDialog();
    showCustomToast(message == null || message.isEmpty() ? getString(R.string.network_error) : message);
    Intent intent = new Intent(this, MainActivity.class);
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
    intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
    startActivity(intent);
}

 

7. 과제 결과 화면

과제 결과 화면 시작 시 intent로 받은 과제명을 제목에 표시하고 과제 id를 이용하여 과제 결과 API를 호출합니다.

tvTitle.setText(getIntent().getStringExtra("assignmentName"));
tryGetResult(ApplicationClass.sSharedPreferences.getInt("assignmentId", 0));

과제 결과 조회 성공 시 전사자료를 나타내는 webview와 통계자료 chart를 설정합니다.

@Override
public void getResultSuccess(String message, ResultResult result) {
    setHtml(result.getHtml());
    setBarChart(result.getFillerStatistics(), mBarChart);
    setPieChart(result.getSilenceStatistics(), mPieChart);
    hideProgressDialog();
}

 

1) 웹뷰(Web View)

전사결과 html을 화면에 보여주기 위해 웹뷰를 사용했습니다.

우선 xml에 webview를 추가합니다.

<WebView
    android:id="@+id/result_webview"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="10dp"
    android:overScrollMode="never"
    android:layout_marginStart="10dp"
    android:layout_marginEnd="10dp" />

java 파일에서 webview 기본 설정을 합니다. webview 확대가 가능하도록 설정합니다.

private WebView mWebView;
private String result; //전사결과 html
mWebView.getSettings().setBuiltInZoomControls(true);
mWebView.getSettings().setSupportZoom(true);
mWebView.getSettings().setDisplayZoomControls(false);

서버에서 받은 html 데이터를 webview에 불러옵니다. 전사결과 html이 여러개인 경우 사이에 <br></br>을 추가해 줄바꿈해줍니다.

String result = "";
for (int i=0;i<htmlList.size();i++) {
    result = result + htmlList.get(i) + "<br></br>";
}
mWebView.loadData(result, "text/html", "UTF-8");

 

2) MPAndroidChart

통계 그래프 구현을 위해 MPAndroidChart 라이브러리를 사용했습니다. 추임새 회수 통계 그래프는 bar chart로, 침묵 비율 통계 그래프는 pie chart로 표현했습니다.

implementation 'com.github.PhilJay:MPAndroidChart:v2.2.4'
allprojects {
    repositories {
        google()
        jcenter()
        maven {url 'https://jitpack.io'}
    }
}

bar chart를 설정합니다. 각 추임새의 횟수를 이용해 BarEntry 객체를 생성하여 entry list에 추가합니다. chart의 애니메이션, 최소 높이, 색상을 설정하고 데이터를 적용시킵니다.

private void setBarChart(ArrayList<Integer> countList, BarChart barChart) {
    ArrayList<BarEntry> entryList = new ArrayList();
    for (int i=0;i<countList.size();i++) {
        entryList.add(new BarEntry(countList.get(i),i));
    }

    ArrayList<String> filler = new ArrayList<>();
    filler.add("음");
    filler.add("그");
    filler.add("어");

    barChart.animateY(1000);
    barChart.setMinimumHeight(500);
    BarDataSet barDataSet = new BarDataSet(entryList, "filler");
    BarData data = new BarData(filler, barDataSet);
    barDataSet.setColors(ColorTemplate.COLORFUL_COLORS);
    barChart.setData(data);
}

pie chart를 설정합니다. 개시지연시간, 침묵, 발화의 비율값을 이용해 Entry 객체를 생성하여 entry list에 추가합니다. chart의 애니메이션, 최소 높이, 색상을 설정하고 데이터를 적용시킵니다.

private void setPieChart(ArrayList<Integer> countList, PieChart pieChart) {
    ArrayList<Entry> entryList = new ArrayList<>();
    for (int i=0;i<countList.size();i++) {
        entryList.add(new Entry(countList.get(i),i));
    }

    ArrayList<String> type = new ArrayList<>();
    type.add("개시지연시간");
    type.add("침묵");
    type.add("발화");

    pieChart.animateXY(1000,1000);
    pieChart.setMinimumHeight(700);
    PieDataSet pieDatSet = new PieDataSet(entryList, "silence");
    PieData data = new PieData(type, pieDatSet);
    pieDatSet.setColors(ColorTemplate.COLORFUL_COLORS);
    pieChart.setData(data);
}

사용된 기술들

Java

AndroidX

Retrofit, OkHttp: 서버와의 HTTP 통신을 위해 사용합니다.

MPAndroidChart: 통계 그래프

 

딥러닝 모델 소스 코드: github.com/EwhaSpeakUP/SpeakUP

안드로이드 소스 코드: github.com/EwhaSpeakUP/android

 

EwhaSpeakUP/android

이화여자대학교 2020-2 졸업프로젝트 SpeakUP. Contribute to EwhaSpeakUP/android development by creating an account on GitHub.

github.com

반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함