국비교육(22-23)

94일차(1)/Android App(58) : mp3 파일 재생 예제 / Metadata 추출

서리/Seori 2023. 2. 21. 22:54

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를 통해서 앱에서 저장한다.

  필드에 저장해놓고 다음번 요청에 다시 전달해준다.

SharedPreferencexml문서를 자동으로 만들어준다. 그 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일 경우 기본이미지를 출력하는 코드가 들어가야 한다.