94일차(1)/Android App(58) : mp3 파일 재생 예제 / Metadata 추출
- 안드로이드 화면에서 로그인 처리해서 개인 음악목록 출력하기
- mp3파일의 메타데이터 추출
- Spring의 이 메소드에서 로그인 성공여부가 json문자열로 전달된다.
- json문자열을 map에 담아서 리턴해준다.
- 로그인 정보를 session에 담아서 session에서 읽어온다.
- session이란 서버의 클라이언트 처리 방식이다.
하나의 서버 요청을 100명이 보낸다고 하면, 그 100명을 각각 구분할 수 있어야 한다.
- sessionDB를 톰캣 서버가 알아서 운영하고 있다.
- 이 sessionDB에 클라이언트를 담으면 session id 가 생기고, id라는 키값으로 특정 이름이 저장된다.
- session에다가 담는다는 것은 특정 키값으로 어떤 문자열을 저장하는 것이다.
- 서버가 클라이언트를 식별하려면 sessionID를 읽어내야 한다.
- 다음번 요청을 할때 쿠키로 세션아이디를 응답하고 받아온다.
- 세션아이디가 서버로 넘어오지 않으면 서버는 새로운 sessionID를 계속 발급한다.
- 클라이언트는 서버가 발급받은 세션아이디를 저장해두었다가 다음번 요청에 쿠키에 담아서 같이 전달해주어야만 로그인 처리가 된다.
- 웹브라우저 안에는 이러한 작업이 자동화되어 있다!
- 브라우저-검사 에서 보면 이렇게 되어있다. JSESSIONID라는 값을 가지고 있다.
- 클라이언트가 음악 목록보기 링크를 클릭하면 -> 이 session 값도 같이 전달되고, 서버는 이 값을 읽어들인다.
- 안드로이드 LoginTask
- httpUrlConnection으로 로그인 요청을 보낸다.
- 이렇게 세션아이디가 있다면 직접 쿠키에 넣어준다. 서버가 나를 식별하게 하기 위해!
- key, value 형식의 문자열을 전달하면 서버가 알아서 처리를 해준다.
- 서버가 다시 쿠키를 응답한다.
- 서버가 발급한 세션id 를 SharedPreference를 통해서 앱에서 저장한다.
필드에 저장해놓고 다음번 요청에 다시 전달해준다.
- SharedPreference 는 xml문서를 자동으로 만들어준다. 그 xml 문서에 세선의 아이디를 저장해준다!
- 안드로이드에서는 이렇게 SharedPreference 에 저장하는 작업을 추가로 해줘야한다.
(웹브라우저에서는 자동화되어 있다)
- finish로 LoginActivity가 종료되면 다시 MainActivity가 활성화된다.
- Main에서는 LoginCheckTask에서 로그인했는지 확인해서 로그인하지 않았다면 다시 LoginActivity로 보낸다.
- 현재 로그인 액티비티 밑에 메인액티비티가 깔려있는 상태이다.
- 로그인하면 LoginActivity가 종료되고 MainActivity가 다시 activate 된다.
- 이제 이곳에 재생할 음악의 목록을 받아올 것이다!
- JSON으로 응답해주는 서버 쪽의 메소드를 만들 예정
MusicController
package com.sy.boot07.music.controller;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.ModelAndView;
import com.sy.boot07.music.dao.MusicDao;
import com.sy.boot07.music.dto.MusicDto;
import com.sy.boot07.music.service.MusicService;
import com.sy.boot07.users.dao.UsersDao;
import com.sy.boot07.users.dto.UsersDto;
@Controller
public class MusicController {
@Autowired MusicService service;
@Autowired UsersDao usersDao;
@Autowired MusicDao musicDao;
@RequestMapping("/music/login")
@ResponseBody
public Map<String, Object> login(UsersDto dto, HttpSession session){
Map<String,Object> map=new HashMap<>();
boolean isValid=false;
//입력한 아이디를 이용해서 DB에서 정보를 읽어온다.
UsersDto resultDto=usersDao.getData(dto.getId());
//만일 실제로 존재하는 아이디라면
if(resultDto != null) {
//입력한 비밀번호와 DB에 저장된 암호화된 비밀번호를 비교해서 일치 여부를 얻어낸다.
isValid = BCrypt.checkpw(dto.getPwd(), resultDto.getPwd());
}
//만일 비밀번호도 일치한다면
if(isValid) {
//로그인 처리를 한다.
session.setAttribute("id", dto.getId());
//아이디도 담는다.
map.put("id", dto.getId());
}
//로그인 성공 여부를 담는다.
map.put("isSuccess", isValid);
System.out.println(dto.getId()+"|"+dto.getPwd()+"|"+isValid);
return map;
}
//로그인 체크
@RequestMapping("/music/logincheck")
@ResponseBody
public Map<String, Object> loginCheck(HttpSession session){
String id=(String)session.getAttribute("id");
Map<String, Object> map=new HashMap<>();
if(id==null) {
map.put("isLogin", false);
}else {
map.put("isLogin", true);
map.put("id",id);
}
return map;
}
//음악파일 업로드 폼 요청 처리
@RequestMapping("/music/insertform")
public String insertForm() {
return "music/insertform";
}
//음악파일 업로드 요청처리
@RequestMapping("/music/insert")
public String insert(MultipartFile file, HttpServletRequest request) {
service.saveFile(file, request);
return "redirect:/music/list";
}
//음악파일 목록 요청처리
@RequestMapping("/music/list")
public ModelAndView list(ModelAndView mView, HttpSession session) {
service.getList(mView, session);
mView.setViewName("music/list");
return mView;
}
@RequestMapping("/api/music/list")
@ResponseBody
public List<MusicDto> list2(HttpSession session){
//로그인된 아이디를 읽어온다.
String id=(String)session.getAttribute("id");
//해당 사용자가 업로드한 음악 파일 목록을 읽어와서
List<MusicDto> list=musicDao.getList(id);
//ResponseBody로 응답한다(json 문자열 응답)
return list;
}
/*
* get 방식 파라미터로 전달되는 num에 해당하는 음악 하나의 정보를 json 형식의 문자열로 응답하는 컨트롤러 메소드
* {"writer":"xxx", "title":"xxx", "saveFileName":"xxx", .... }
*/
@RequestMapping("/music/detail")
@ResponseBody
public MusicDto checkDetail(int num, HttpServletRequest request) {
return service.getDetail(num);
}
@GetMapping("/music/delete")
public String checkDelete(int num, HttpServletRequest request) {
service.deleteFile(num, request);
return "redirect:/music/list";
}
}
- MusicDao DI 추가
- 안드로이드용 list2 메소드
- 이 요청도 로그인했을 때만 응답되어야 한다. -> Interceptor 추가!
- 로그인하지 않은 상태에서 음악 목록에 들어가려고 하면 Interceptor가 실행된다.
WebConfig 추가
@Override
public void addInterceptors(InterceptorRegistry registry) {
//웹브라우저의 요청에 대해 개입할 인터셉터 등록
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/users/*","/gallery/*","/cafe/*","/file/*", "/music/*")
.excludePathPatterns("/users/signup_form", "/users/signup", "/users/loginform", "/users/login",
"/gallery/list", "/gallery/detail",
"/cafe/list","cafe/detail","/cafe/ajax_comment_list",
"/file/list","/file/download",
"/music/login");
//모바일 요청에 대해 개입할 인터셉터 등록
registry.addInterceptor(mLoginInterceptor)
.addPathPatterns("/api/gallery/*", "/api/music/*");
}
- webconfig에 /api/music/* 경로를 추가해주었다.
- 이제 로그인을 하지 않은 상태로 Music 페이지에 들어가면 401 에러가 응답된다.
- MobileLoginInterceptor 에서 설정한 에러이다. Unauthorized
- 개인의 음악 목록을 출력하려면 로그인정보를 서버에 쿠키로 같이 보내주어야 한다.
- 로그인하면 개인의 음악 목록이 나오도록 하려면 sessionID를 함께 보내주어야 한다.
- 안드로이드에서 이전에는 Util을 사용해서 목록을 받아왔는데,
여기에는 sessionID에 대한 고려는 없어서 새로 만들어야 한다..
MainActivity
package com.example.step23mp3player;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.MediaMetadataRetriever;
import android.media.MediaPlayer;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class MainActivity extends AppCompatActivity implements AdapterView.OnItemClickListener {
MediaPlayer mp;
//재생 준비가 되었는지 여부
boolean isPrepared=false;
ImageButton playBtn;
ProgressBar progress;
TextView time;
SeekBar seek;
//서비스의 참조값을 저장할 필드
MusicService service;
//서비스에 연결되었는지 여부
boolean isConnected;
//Adapter 에 연결된 모델 (단순 문자열)
List<String> songs;
//Adapter 의 참조값
ArrayAdapter<String> adapter;
SharedPreferences pref;
String sessionId;
String id;
//재생음악 목록(자세한 정보가 들어있는 목록)
List<MusicDto> musicList=new ArrayList<>();
//서비스 연결객체
ServiceConnection sConn=new ServiceConnection() {
//서비스에 연결이 되었을때 호출되는 메소드
@Override
public void onServiceConnected(ComponentName name, IBinder binder) {
//MusicService 객체의 참조값을 얻어와서 필드에 저장
//IBinder 객체를 원래 type 으로 casting
MusicService.LocalBinder lBinder=(MusicService.LocalBinder)binder;
service=lBinder.getService();
//연결되었다고 표시
isConnected=true;
//핸들러에 메세지 보내기
handler.removeMessages(0); //만일 핸들러가 동작중에 있으면 메세지를 제거하고
handler.sendEmptyMessageDelayed(0, 100); //다시 보내기
}
//서비스에 연결이 해제 되었을때 호출되는 메소드
@Override
public void onServiceDisconnected(ComponentName name) {
//연결 해제 되었다고 표시
isConnected=false;
}
};
//UI 를 주기적으로 업데이트 하기 위한 Handler
Handler handler=new Handler(){
/*
이 Handler 에 메세지를 한번만 보내면 아래의 handleMessage() 메소드가
1/10 초 마다 반복적으로 호출된다.
handleMessage() 메소드는 UI 스레드 상에서 실행되기 때문에
마음대로 UI 를 업데이트 할수가 있다.
*/
@Override
public void handleMessage(@NonNull Message msg) {
if(service.isPrepared()){
//전체 재생시간
int maxTime=service.getMp().getDuration();
progress.setMax(maxTime);
seek.setMax(maxTime);
//현재 재생 위치
int currentTime=service.getMp().getCurrentPosition();
//음악 재생이 시작된 이후에 주기적으로 계속 실행이 되어야 한다.
progress.setProgress(currentTime);
seek.setProgress(currentTime);
//현재 재생 시간을 TextView 에 출력하기
String info=String.format("%d min, %d sec",
TimeUnit.MILLISECONDS.toMinutes(currentTime),
TimeUnit.MILLISECONDS.toSeconds(currentTime)
-TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS. toMinutes(currentTime)) );
time.setText(info);
}
//자신의 객체에 다시 빈 메세제를 보내서 handleMessage() 가 일정시간 이후에 호출 되도록 한다.
handler.sendEmptyMessageDelayed(0, 100); // 1/10 초 이후에
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//TextView 의 참조값 얻어와서 필드에 저장
time=findViewById(R.id.time);
// %d 는 숫자, %s 문자
String info=String.format("%d min, %d sec", 0, 0);
time.setText(info);
//ProgressBar 의 참조값 얻어오기
progress=findViewById(R.id.progress);
seek=findViewById(R.id.seek);
//재생 버튼
playBtn=findViewById(R.id.playBtn);
//재생버튼을 눌렀을때
playBtn.setOnClickListener(v->{
//서비스의 playMusic() 메소드를 호출해서 음악이 재생 되도록 한다.
service.playMusic();
});
//일시 중지 버튼
ImageButton pauseBtn=findViewById(R.id.pauseBtn);
pauseBtn.setOnClickListener(v->{
service.pauseMusic();
});
//알림체널만들기
createNotificationChannel();
//ListView 관련 작업
ListView listView=findViewById(R.id.listView);
//셈플 데이터
songs=new ArrayList<>();
//ListView 에 연결할 아답타
adapter=new ArrayAdapter<>(this, android.R.layout.simple_list_item_activated_1, songs);
listView.setAdapter(adapter);
//ListView 에 아이템 클릭 리스너 등록
listView.setOnItemClickListener(this);
}
@Override
protected void onStart() {
super.onStart();
// MusicService 에 연결할 인텐트 객체
Intent intent=new Intent(this, MusicService.class);
//서비스 시작 시키기
//startService(intent);
// 액티비티의 bindService() 메소드를 이용해서 연결한다.
// 만일 서비스가 시작이 되지 않았으면 서비스 객체를 생성해서
// 시작할 준비만 된 서비스에 바인딩이 된다.
bindService(intent, sConn, Context.BIND_AUTO_CREATE);
pref= PreferenceManager.getDefaultSharedPreferences(this);
sessionId=pref.getString("sessionId", "");
//로그인 했는지 체크하기
new LoginCheckTask().execute(AppConstants.BASE_URL+"/music/logincheck");
}
@Override
protected void onStop() {
super.onStop();
if(isConnected){
//서비스 바인딩 해제
unbindService(sConn);
isConnected=false;
}
}
//앱의 사용자가 알림을 직접 관리 할수 있도록 알림 체널을 만들어야한다.
public void createNotificationChannel(){
//알림 체널을 지원하는 기기인지 확인해서
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//알림 체널을 만들기
//셈플 데이터
String name="Music Player";
String text="Control";
//알림체널 객체를 얻어내서
//알림을 1/10 초마다 새로 보낼 예정이기 때문에 진동은 울리지 않도록 IMPORTANCE_LOW 로 설정한다
NotificationChannel channel=
new NotificationChannel(AppConstants.CHANNEL_ID, name, NotificationManager.IMPORTANCE_LOW);
//체널의 설명을 적고
channel.setDescription(text);
//알림 메니저 객체를 얻어내서
NotificationManager notiManager=(NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
//알림 체널을 만든다.
notiManager.createNotificationChannel(channel);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode){
case 0:
//권한을 부여 했다면
if(grantResults[0] == PackageManager.PERMISSION_GRANTED){
}else{//권한을 부여 하지 않았다면
Toast.makeText(this, "알림을 띄울 권한이 필요합니다.",
Toast.LENGTH_SHORT).show();
}
break;
}
}
//ListView 의 cell 을 클릭하면 호출되는 메소드
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
// position 은 클릭한 셀의 인덱스
String fileName=musicList.get(position).getSaveFileName();
service.initMusic(AppConstants.MUSIC_URL+fileName);
}
//로그인 여부를 체크하는 작업을 할 비동기 task
class LoginCheckTask extends AsyncTask<String, Void, Boolean> {
@Override
protected Boolean doInBackground(String... strings) {
//로그인 체크 url
String requestUrl=strings[0];
//서버가 http 요청에 대해서 응답하는 문자열을 누적할 객체
StringBuilder builder=new StringBuilder();
HttpURLConnection conn=null;
InputStreamReader isr=null;
BufferedReader br=null;
boolean isLogin=false;
try{
//URL 객체 생성
URL url=new URL(requestUrl);
//HttpURLConnection 객체의 참조값 얻어오기
conn=(HttpURLConnection)url.openConnection();
if(conn!=null){//연결이 되었다면
conn.setConnectTimeout(20000); //응답을 기다리는 최대 대기 시간
conn.setRequestMethod("GET");//Default 설정
conn.setUseCaches(false);//케쉬 사용 여부
//App 에 저장된 session id 가 있다면 요청할때 쿠키로 같이 보내기
if(!sessionId.equals("")) {
// JSESSIONID=xxx 형식의 문자열을 쿠키로 보내기
conn.setRequestProperty("Cookie", sessionId);
}
//응답 코드를 읽어온다.
int responseCode=conn.getResponseCode();
if(responseCode==200){//정상 응답이라면...
//서버가 출력하는 문자열을 읽어오기 위한 객체
isr=new InputStreamReader(conn.getInputStream());
br=new BufferedReader(isr);
//반복문 돌면서 읽어오기
while(true){
//한줄씩 읽어들인다.
String line=br.readLine();
//더이상 읽어올 문자열이 없으면 반복문 탈출
if(line==null)break;
//읽어온 문자열 누적 시키기
builder.append(line);
}
}
}
//서버가 응답한 쿠키 목록을 읽어온다.
List<String> cookList=conn.getHeaderFields().get("Set-Cookie");
//만일 쿠키가 존대 한다면
if(cookList != null){
//반복문 돌면서
for(String tmp : cookList){
//session id 가 들어 있는 쿠키를 찾아내서
if(tmp.contains("JSESSIONID")){
//session id 만 추출해서
String sessionId=tmp.split(";")[0];
//SharedPreferences 을 편집할수 있는 객체를 활용해서
SharedPreferences.Editor editor=pref.edit();
//sessionId 라는 키값으로 session id 값을 저장한다.
editor.putString("sessionId", sessionId);
editor.apply();//apply() 는 비동기로 저장하기 때문에 실행의 흐름이 잡혀 있지 않다(지연이 없음)
//필드에도 담아둔다.
MainActivity.this.sessionId=sessionId;
}
}
}
//출력받은 문자열 전체 얻어내기
JSONObject obj=new JSONObject(builder.toString());
/*
{"isLogin":false} or {"isLogin":true, "id":"kimgura"}
서버에서 위와 같은 형식의 json 문자열을 응답할 예정이다.
*/
Log.d("서버가 응답한 문자열", builder.toString());
//로그인 여부를 읽어와서
isLogin=obj.getBoolean("isLogin");
//만일 로그인을 했다면
if(isLogin){
//필드에 로그인된 아이디를 담아둔다.
id=obj.getString("id");
}
}catch(Exception e){//예외가 발생하면
Log.e("LoginCheckTask", e.getMessage());
}finally {
try{
if(isr!=null)isr.close();
if(br!=null)br.close();
if(conn!=null)conn.disconnect();
}catch(Exception e){}
}
//로그인 여부를 리턴하면 아래의 onPostExecute() 메소드에 전달된다.
return isLogin;
}
@Override
protected void onPostExecute(Boolean isLogin) {
super.onPostExecute(isLogin);
//여기는 UI 스레드 이기 때문에 UI 와 관련된 작업을 할수 있다.
//TextView 에 로그인 여부를 출력하기
if(isLogin){
TextView infoText=findViewById(R.id.infoText);
infoText.setText(id+" 님 로그인중...");
//재생목록 받아오기
new MusicListTask().execute(AppConstants.BASE_URL+"/api/music/list");
}else{
//로그인 액티비티로 이동
Intent intent=new Intent(MainActivity.this, LoginActivity.class);
startActivity(intent);
}
}
}
//재생목록을 얻어올 작업을 할 비동기 task
class MusicListTask extends AsyncTask<String, Void, String> {
@Override
protected String doInBackground(String... strings) {
//요청 url
String requestUrl=strings[0];
//서버가 http 요청에 대해서 응답하는 문자열을 누적할 객체
StringBuilder builder=new StringBuilder();
HttpURLConnection conn=null;
InputStreamReader isr=null;
BufferedReader br=null;
try{
//URL 객체 생성
URL url=new URL(requestUrl);
//HttpURLConnection 객체의 참조값 얻어오기
conn=(HttpURLConnection)url.openConnection();
if(conn!=null){//연결이 되었다면
conn.setConnectTimeout(20000); //응답을 기다리는 최대 대기 시간
conn.setRequestMethod("GET");//Default 설정
conn.setUseCaches(false);//케쉬 사용 여부
//App 에 저장된 session id 가 있다면 요청할때 쿠키로 같이 보내기
if(!sessionId.equals("")) {
// JSESSIONID=xxx 형식의 문자열을 쿠키로 보내기
conn.setRequestProperty("Cookie", sessionId);
}
//응답 코드를 읽어온다.
int responseCode=conn.getResponseCode();
if(responseCode==200){//정상 응답이라면...
//서버가 출력하는 문자열을 읽어오기 위한 객체
isr=new InputStreamReader(conn.getInputStream());
br=new BufferedReader(isr);
//반복문 돌면서 읽어오기
while(true){
//한줄씩 읽어들인다.
String line=br.readLine();
//더이상 읽어올 문자열이 없으면 반복문 탈출
if(line==null)break;
//읽어온 문자열 누적 시키기
builder.append(line);
}
}
}
//서버가 응답한 쿠키 목록을 읽어온다.
List<String> cookList=conn.getHeaderFields().get("Set-Cookie");
//만일 쿠키가 존대 한다면
if(cookList != null){
//반복문 돌면서
for(String tmp : cookList){
//session id 가 들어 있는 쿠키를 찾아내서
if(tmp.contains("JSESSIONID")){
//session id 만 추출해서
String sessionId=tmp.split(";")[0];
//SharedPreferences 을 편집할수 있는 객체를 활용해서
SharedPreferences.Editor editor=pref.edit();
//sessionId 라는 키값으로 session id 값을 저장한다.
editor.putString("sessionId", sessionId);
editor.apply();//apply() 는 비동기로 저장하기 때문에 실행의 흐름이 잡혀 있지 않다(지연이 없음)
//필드에도 담아둔다.
MainActivity.this.sessionId=sessionId;
}
}
}
}catch(Exception e){//예외가 발생하면
Log.e("MusicListTask", e.getMessage());
}finally {
try{
if(isr!=null)isr.close();
if(br!=null)br.close();
if(conn!=null)conn.disconnect();
}catch(Exception e){}
}
//응답받은 문자열을 리턴한다.
return builder.toString();
}
@Override
protected void onPostExecute(String jsonStr) {
super.onPostExecute(jsonStr);
//여기는 UI 스레드 (자유롭게 UI작업을 할수있다)
//jsonStr 은 [{},{},...] 형식의 문자열이기 때문에 JSONArray 객체를 생성한다.
songs.clear();
musicList.clear();
try {
JSONArray arr=new JSONArray(jsonStr);
for(int i=0; i<arr.length(); i++){
//i번째 JSONObject 객체를 참조
JSONObject tmp=arr.getJSONObject(i);
int num=tmp.getInt("num");
String writer=tmp.getString("writer");
//"title" 이라는 키값으로 저장된 문자열 읽어오기
String title=tmp.getString("title");
String artist=tmp.getString("artist");
String orgFileName=tmp.getString("orgFileName");
String saveFileName=tmp.getString("saveFileName");
String regdate=tmp.getString("regdate");
//ListView에 연결된 모델에 곡의 제목을 담는다.
songs.add(title);
//음악 하나의 자세한 정보를 MusicDto에 담고
MusicDto dto=new MusicDto();
dto.setNum(num);
dto.setWriter(writer);
dto.setTitle(title);
dto.setArtist(artist);
dto.setOrgFileName(orgFileName);
dto.setSaveFileName(saveFileName);
dto.setRegdate(regdate);
//MusicDto를 list에 누적시킨다.
musicList.add(dto);
}
adapter.notifyDataSetChanged();
} catch (JSONException je) {
Log.e("onPstExecute()", je.getMessage());
}
}
}
}
- loginCheckTask 를 MusicListTask로 복사해오고, 결과타입은 String으로 받기
- 이것을 이용해서 리스트를 JSON 문자열로 받아올 것이다.
- 어느 시점에 사용해야 할까?
→ Activity가 onStart 되는 시점에, 로그인된 것을 확인했을때 이 MusicListTask를 통해서 목록을 얻어오면 된다.
- 이렇게 추가해주면 JSON 문자열을 응답받게 되고,
반복문 돌면서 stringBuilder에 누적시켜서 builder.toString() 으로 화면에 읽어낼 수 있다
- 목록 출력시키기
- [ ] 문자열 바깥이 대괄호로 되어있으므로 JSONArray 객체 생성!
- JSONArray 안에 JSONObject가 있는 형태이다.
- jsonStr이 JSON이 아닌 다른 형식의 문자열일 수도 있으므로, 오류가 발생한다.
- try~catch 문으로 묶어주기!
- key가 title이고 String 타입의 값을 읽어온다
- 반복문을 돌면서 songs 리스트에다가 title 값을 추가하고,
완료한 후 notifyDataSetChanged(); 로 아답타에 데이터가 바뀐것을 전달한다.
- 이렇게 업로드한 파일의 title 목록을 받아올 수 있다.(아직 재생은 안된다)
- 지금은 그냥 List<String>으로 사용하고 있는데, 모델의 데이터가 이렇게 되어있으면... 활용하는 데에 한계가 있다.
- 곡 하나가 여러개의 정보로 구성되어 있어서,
안드로이드에서도 이 데이터를 관리하려면 MusicDto 형태로 만드는것이 좋다.
- 응답받은 JSON문자열은 여러 종류의 데이터가 있으므로...
- List<String>은 단순히 아답타에 출력하기 위한 목록이고,
다양한 종류의 데이터를 관리하기 위해서 안드로이드에서도 MusicDto 생성
MusicDto
package com.example.step23mp3player;
public class MusicDto {
private int num;
private String writer;
private String title;
private String artist;
private String orgFileName;
private String saveFileName;
private String regdate;
public MusicDto(){}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public String getWriter() {
return writer;
}
public void setWriter(String writer) {
this.writer = writer;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getArtist() {
return artist;
}
public void setArtist(String artist) {
this.artist = artist;
}
public String getOrgFileName() {
return orgFileName;
}
public void setOrgFileName(String orgFileName) {
this.orgFileName = orgFileName;
}
public String getSaveFileName() {
return saveFileName;
}
public void setSaveFileName(String saveFileName) {
this.saveFileName = saveFileName;
}
public String getRegdate() {
return regdate;
}
public void setRegdate(String regdate) {
this.regdate = regdate;
}
}
- 현재 안드로이드에서 기본으로 제공해주는 ArrayAdapter를 사용하고 있다.
- adapter의 레이아웃이 딱 정해져 있다.
- 저 모델을 쓰려면 Custom Adapter를 생성해야 한다.
- 화면에 출력되는 songs 목록에는 제목 값만 누적되어 있고,
musicList 목록에는 곡 하나하나의 정보가 들어있는 자세한 데이터가 들어있다.
- 곡 재생을 위해서는 실제로 파일 시스템에 저장된 saveFileName이 필요하다.
- songs 대신 musicList를 활용해서 saveFileName을 얻어내기
- 음악 url에 얻어낸 fileName을 붙여서 재생!
- 클릭하면 음악이 잘 재생된다.
- 웹브라우저에서 파일을 업로드하면 바로바로 목록이 잘 갱신된다.
- 커스텀 아답타의 필요성이 느껴진다.
- 현재 아답타에 연결한 모델과 실제 재생하는 음악을 가져오는 작업이 이원화되어 있는데,
직접 커스텀 아답타를 만들면 musicList를 아답타의 데이터로 쓸 수 있다.
- mp3 플레이어 MediaMetadata 클래스에서 음악의 이미지 등 메타데이터를 추출할 수 있고,
그 타이틀 이미지를 아답타에 출력할 수 있다.
(android.media 패키지에 들어있다)
- android image metadata 등으로 활용방법을 검색해보면 된다!
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/infoText"
android:background="#cecece"
android:textSize="20sp"/>
<ListView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:id="@+id/listView"
android:choiceMode="singleChoice"/>
<ImageView
android:layout_width="match_parent"
android:layout_height="200dp"
android:id="@+id/imageView"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@android:drawable/ic_media_play"
android:tooltipText="재생버튼"
android:id="@+id/playBtn"/>
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@android:drawable/ic_media_pause"
android:tooltipText="일시정지"
android:id="@+id/pauseBtn"/>
</LinearLayout>
<ProgressBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:id="@+id/progress"/>
<SeekBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:id="@+id/seek"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20sp"
android:id="@+id/time"/>
</LinearLayout>
- ImageView 요소 추가.
- 이 ImageView 안에 재생하는 곡의 타이틀 이미지를 출력할 예정!
- 재생, 일시정지 ImageButton은 새 리니어 레이아웃의 자식요소로 넣어주기
- 이미지 버튼(재생,일시정지)이 수평으로 배치되었다.
- 음악파일에 이미지 정보가 존재한다면 아래 파란박스 위치에 출력할 것!
- MediaMetadataRetriever 객체를 생성해서 이미지 소스를 얻어낸다.
- getImbeddedPicture 메소드는 바이트 배열 byte[ ] 을 리턴한다!
- 바이트 배열을 사용해서 비트맵 객체를 얻어낼 수 있다.
- byte 배열을 decoding해서 비트맵을 얻어내는 메소드
- 배열의 0~마지막 방까지 돌면서 바이트 알갱이를 읽어낸다.
setImageView()
- ImageView 요소안에 비트맵 이미지를 출력할 수 있는 메소드!
- 이런 식으로 출력할 수 있다.
- 이미지가 없는 경우에는 기본으로 해줄것인지 등을 추가로 해볼 수 있다.
- 바이트배열 데이터를 얻어내서, decodeByteArray 메소드를 사용해서 비트맵 이미지를 얻어내고,
그 데이터를 imageView에 넣으면 된다.
- 원래 mp3파일이 아니었는데 mp3로 변환했다거나 하면 메타데이터가 없다.(이미지, 타이틀정보 등이 null이다)
- 이런 곡을 재생하는 경우 NullPointException이 발생하면서 앱이 종료되어 버린다.
- 곡의 메타데이터가 null일 경우 기본이미지를 출력하는 코드가 들어가야 한다.
'국비교육(22-23)' 카테고리의 다른 글
96일차(1)/Android App(60) : mp3 파일 재생 예제 / 곡 연속 재생 설정 (0) | 2023.02.24 |
---|---|
95일차(1)/Android App(59) : mp3 파일 재생 예제 / 곡 목록 출력 (0) | 2023.02.22 |
93일차(2)/Android App(57) : mp3 파일 재생 예제 / 로그인 기능 구현 (0) | 2023.02.21 |
93일차(1)/Spring Boot(16) : Custom Exception 활용 예제 (0) | 2023.02.20 |
91일차(1)/Spring Boot(15) : mp3 파일 업로드, 재생 / AOP 활용 예제 (1) | 2023.02.19 |