국비교육(22-23)

90일차(1)/Android App(56) : mp3 파일 재생 예제 / 재생목록 출력(ListView)

서리/Seori 2023. 2. 16. 22:45

90일차(1)/Android App(56) : mp3 파일 재생 예제 / 재생목록 출력(ListView)

 

 

- mp3 파일 여러개를 안드로이드 ListView에 넣어서 보여주기

- 음악파일에는 앨범,타이틀 이미지 등 여러 정보가 들어있다. 이 정보를 읽어오기!

 

- Spring 의 자원을 넣는 폴더 안에 넣어주기

 

- 서버에서는 json으로 출력

→ 안드로이드가 받아서 listview에 출력

→ 그 listview 셀을 클릭하면 재생할 준비를 하고 재생하도록!

 

- 안드로이드 리스트뷰 만들기

 

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">

    <ListView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:id="@+id/listView"
        android:choiceMode="singleChoice"/>
    <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"/>
    <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>

 

android:choiceMode="singleChoice"

- 선택 모드. 하나씩 선택할 수 있는 레이아웃이다.

 

- 이런 형태로 레이아웃을 잡아준다!

 

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.pm.PackageManager;
import android.media.MediaPlayer;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ImageButton;
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.JSONObject;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

public class MainActivity extends AppCompatActivity implements AdapterView.OnItemClickListener , Util.RequestListener {

    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;

    //서비스 연결 객체
    ServiceConnection sConn=new ServiceConnection() {
        //서비스에 연결이 되었을 때 호출되는 메소드
        @Override
        public void onServiceConnected(ComponentName name, IBinder binder) {
            //MusicService 객체의 참조값을 얻어와서 필드에 저장
            //IBinder 객체를 원래 type으로 캐스팅
            MusicService.LocalBinder IBinder=(MusicService.LocalBinder)binder;
            service=IBinder.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);
        //재생할 곡 목록을 서버로부터 받아온다.
        Util.sendGetRequest(1, "http://192.168.0.34:9000/boot07/api/music/list", null, this);
    }

    @Override
    protected void onStart() {
        super.onStart();
        //MusicService 에 연결할 인텐트 객체
        Intent intent=new Intent(this, MusicService.class);
        //서비스 시작시키기
        //startService(intent);
        //Activity의 bindService() 메소드를 이용해서 연결한다.
        //만일 서비스가 시작이 되지 않았으면 서비스 객체를 생성해서
        //시작할 준비만 된 서비스에 바인딩이 된다.
        bindService(intent, sConn, Context.BIND_AUTO_CREATE);
    }

    @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 초마다 새로 보낼 예정이기 때문에 진동은 울리지 않도록 IMPORTANT_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은 클릭한 셀의 인덱스, id는 클릭할 셀의 id, view는 클릭한 cell view
        String fileName=songs.get(position);
        service.initMusic(AppConstants.MUSIC_URL+fileName);
    }

    @Override
    public void onSuccess(int requestId, Map<String, Object> result) {
        if(requestId == 1){
            //[{"fileName":"mp3piano.mp3","title":"쇼팽 녹턴"},{"fileName":"Over_the_Horizon.mp3","title":"Over The Horizon"}]
            String jsonStr=(String) result.get("data");
            try {
                JSONArray arr=new JSONArray(jsonStr);
                //반복문 돌면서
                for(int i=0; i<arr.length(); i++){
                    //JSONObject 객체를 얻어내서 재생목록(음악 목록)에 추가한다.
                    JSONObject obj=arr.getJSONObject(i);
                    String fileName=obj.getString("fileName");
                    songs.add(fileName);
                }
                //listView에 연결된 adapter에 모델이 바뀌었다고 알려서 listView에 목록이 출력되도록 한다.
                adapter.notifyDataSetChanged();
            } catch (Exception e) {

                e.printStackTrace();
            }
        }
    }

    @Override
    public void onFail(int requestId, Map<String, Object> result) {

    }
}

 

- ListView 초기화 작업

 

 

- 일단 샘플데이터를 넣어서 간단히 표시해준다.

- ListView를 쓰려면 아답타가 필요하다.

- 지금은 그냥 ArrayAdapter를 쓰는데, 아답타를 직접 만들고 싶다면 만들어도 된다.

 

- run 해보면 클릭한 셀의 색상이 다르게 나온다.

- simple_list_item_activated_1 와 singlechoice로 지정해서!

- 클릭하면 플레이되도록 준비할 수 있다.

 


 

- 음악1, 음악2 부분에는 재생할 음악의 제목이 출력되어야 한다.

- Activity에 onItemClickListener 구현

 

- songs 리스트를 필드로 옮기고 메소드안에서 songs에 값을 넣어준다.

 

- filneName이라는 변수를 설정하고, 이 파일명을 사용해서 요청 url을 구성한다.

 

- initMusic을 지우고 playMusic으로 수정

 

- initMusic은 여기에 넣어준다.

- 음악도 재생할 음악의 제목과 저장된 파일 경로를 따로 관리할 것이다.

 

- 이런 값은 상수로 관리하면 좋다.

 

 

AppConstants

package com.example.step23mp3player;

public class AppConstants {
    public static final String ACTION_PLAY="action_start";
    public static final String ACTION_PAUSE="action_pause";
    public static final String ACTION_STOP="action_stop";
    //채널의 아이디
    public static final String CHANNEL_ID="my_channel";
    //알림의 아이디
    public static final int NOTI_ID=999;

    public static final String MUSIC_URL="http://192.168.0.34:9000/boot07/resources/upload/";
}

 

- 위와 같이 상수화하여 관리하면 코드를 깔끔하게 만들 수 있다.

 

- 이제 각각의 셀을 클릭하면 음악이 나온다.

 


 

- STS의 안드로이드 컨트롤러 작성

 

AndroidContoller.java

package com.sy.boot07.api;


import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpSession;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;


import com.sy.boot07.gallery.dao.GalleryDao;
import com.sy.boot07.gallery.dto.GalleryDto;

/*
 * 안드로이드의 App요청을 처리할 컨트롤러
 */

@Controller
public class AndroidController {
	
	@RequestMapping("/api/music/list")
	@ResponseBody
	public List<Map<String,Object>> getMusicList(){
		Map<String,Object> song1=new HashMap<>();
		song1.put("title", "쇼팽 녹턴");
		song1.put("fileName", "mp3piano.mp3");
		
		Map<String,Object> song2=new HashMap<>();
		song2.put("title", "Over The Horizon");
		song2.put("fileName", "Over_the_Horizon.mp3");
		
		List<Map<String,Object>> list = new ArrayList<>();
		list.add(song1);
		list.add(song2);
		
		return list;
	}
	
	
	/*
	 * JSON 문자열 
	 * 1. @ResponseBody 어노테이션
     * 2. Map 혹은 List 혹은 Dto 를 리턴하면 자동으로 JSON 문자열로 변환 되어서 응답된다.
	 */
	@RequestMapping("/api/send")
	@ResponseBody
	public Map<String, Object> send(String msg){
		
		System.out.println(msg);
		Map<String, Object> map=new HashMap<>();
		map.put("isSuccess", true);
		map.put("response", "hello client!");
		map.put("num", 1);
		
		return map;
	}
	
	@RequestMapping("/api/list")
	@ResponseBody
	public List<String> list(int pageNum){
		List<String> names=new ArrayList<>();
		names.add("바나나");
		names.add("딸기");
		names.add("복숭아");
		return names;
	}
	
	//로그인 여부를 json으로 응답하는 메소드
	   @RequestMapping("/api/logincheck")
	   @ResponseBody
	   public Map<String, Object> logincheck(HttpSession session){
		  //테스트로 session의 아이디를 출력해보기
	      System.out.println("세션 아이디:"+session.getId());
	      Map<String, Object> map=new HashMap<>();
	      //세션 영역에 id라는 키값으로 저장된 값이 있는지 읽어와 본다.
	      String id=(String)session.getAttribute("id");
	      //만일 로그인을 하지 않았다면
	      if(id == null) {
	         map.put("isLogin", false);
	         System.out.println("로그인중이 아님요");
	      }else {
	         map.put("isLogin", true);
	         map.put("id", id);
	         System.out.println(id+" 로그인중...");
	      }
	      return map;
	   }
	   @RequestMapping("/api/login")
	   @ResponseBody
	   public Map<String, Object> login(String id, String pwd, HttpSession session){
	      //session.setMaxInactiveInterval(60*60*24); //로그인 유지시간
	      System.out.println(id+"|"+pwd);
	      Map<String, Object> map=new HashMap<>();
	      if(id.equals("gura") && pwd.equals("1234")) {
	         map.put("isSuccess", true);
	         map.put("id", id);
	         session.setAttribute("id", id);
	      }else {
	         map.put("isSuccess", false);
	      }
	      return map;
	   }
	   @RequestMapping("/api/logout")
	   @ResponseBody
	   public Map<String, Object> logout(HttpSession session){
	      session.invalidate();
	      Map<String, Object> map=new HashMap<>();
	      map.put("isSuccess", true);
	      return map;
	   }
	   
	   @Autowired GalleryDao dao;
	   
	   @RequestMapping("/api/gallery/list")
	   @ResponseBody
	   public List<GalleryDto> galleryList(){
		   //최신 갤러리 데이터 10개만 가져오기
		   GalleryDto dto=new GalleryDto();
		   dto.setStartRowNum(1);
		   dto.setEndRowNum(10);
		   
		   return dao.getList(dto);
	   }
}

 

- 재생할 음악을 안드로이드에 JSON문자열로 전달하기

 

- spring에서 왼쪽과 같이 작성하면 이런 형식의 JSON을 응답받을 수 있다.

 

 

- map 에 곡 정보를 담고 list에 담았더니 이렇게 응답된다.

- 이 데이터를 안드로이드에서 요청해서 받아갈 수 있다.

 


 

- 이전에 사용했던 Util 클래스 복사해서 가져오기 

 

Util 

package com.example.step23mp3player;

import android.app.Activity;
import android.content.Context;
import android.os.AsyncTask;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.inputmethod.InputMethodManager;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

public class Util {

    //키보드 숨기는 메소드
    public static void hideKeyboard(Activity activity){

        InputMethodManager iManager=(InputMethodManager)
                activity.getSystemService(Context.INPUT_METHOD_SERVICE);
        if(activity.getCurrentFocus()==null)return;
        iManager.hideSoftInputFromWindow(activity.getCurrentFocus().getWindowToken(), 0);
    }
    //메소드의 인자로 전달되는 View 객체의 포커스 해제하는 메소드
    public static void releaseFocus(View view) {
        ViewParent parent = view.getParent();
        ViewGroup group = null;
        View child = null;
        while (parent != null) {
            if (parent instanceof ViewGroup) {
                group = (ViewGroup) parent;
                for (int i = 0; i < group.getChildCount(); i++) {
                    child = group.getChildAt(i);
                    if(child != view && child.isFocusable())
                        child.requestFocus();
                }
            }
            parent = parent.getParent();
        }
    }

    public static interface RequestListener{
        public void onSuccess(int requestId, Map<String, Object> result);
        public void onFail(int requestId, Map<String, Object> result);
    }
    /*
        1. 사용할때 RequestListener 인터페이스 Type 을 전달한다.
        2. 결과는 RequestListener 객체에 전달된다.
        3. Map<String,Object>  에서 응답 코드는
            "code" 라는 키값으로 200, 404, 500, -1 중에 하나가 리턴되고
             -1 이 리턴되면 Exception 발생으로 실패이다. onFail() 호출
     */
    public static void sendGetRequest(int requestId,
                                      String requestUrl,
                                      Map<String,String> params,
                                      RequestListener listener){
        RequestTask task=new RequestTask();
        task.setRequestId(requestId);
        task.setRequestUrl(requestUrl);
        task.setListener(listener);
        task.execute(params);
    }
    private static class RequestTask extends AsyncTask<Map<String,String>, Void, Map<String,Object>> {
        private int requestId;
        private String requestUrl;
        private RequestListener listener;

        public void setListener(RequestListener listener) {
            this.listener = listener;
        }

        public void setRequestId(int requestId) {
            this.requestId = requestId;
        }
        public void setRequestUrl(String requestUrl) {
            this.requestUrl = requestUrl;
        }
        @Override
        protected Map<String, Object> doInBackground(Map<String, String>... params) {
            String requestUrl=this.requestUrl;
            Map<String, String> param=params[0];
            if(param!=null){//요청 파리미터가 존재 한다면
                //서버에 전송할 데이터를 문자열로 구성하기
                StringBuffer buffer=new StringBuffer();
                Set<String> keySet=param.keySet();
                Iterator<String> it=keySet.iterator();
                boolean isFirst=true;
                //반복문 돌면서 map 에 담긴 모든 요소를 전송할수 있도록 구성한다.
                while(it.hasNext()){
                    String key=it.next();
                    String arg=null;
                    //파라미터가 한글일 경우 깨지지 않도록 하기 위해.
                    String encodedValue=null;
                    try {
                        encodedValue= URLEncoder.encode(param.get(key), "utf-8");
                    } catch (UnsupportedEncodingException e) {}
                    if(isFirst){
                        arg="?"+key+"="+encodedValue;
                        isFirst=false;
                    }else{
                        arg="&"+key+"="+encodedValue;
                    }
                    buffer.append(arg);
                }
                String data=buffer.toString();
                requestUrl +=data;
            }
            //서버가 http 요청에 대해서 응답하는 문자열을 누적할 객체
            StringBuilder builder=new StringBuilder();
            HttpURLConnection conn=null;
            InputStreamReader isr=null;
            BufferedReader br=null;
            //결과값을 담을 Map Type 객체
            Map<String,Object> map=new HashMap<String,Object>();
            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);//케쉬 사용 여부
                    //응답 코드를 읽어온다.
                    int responseCode=conn.getResponseCode();
                    //Map 객체에 응답 코드를 담는다.
                    map.put("code", responseCode);
                    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);
                        }
                        //출력받은 문자열 전체 얻어내기
                        String str=builder.toString();
                        //아래 코드는 수행 불가
                        //console.setText(str);
                        //Map 객체에 결과 문자열을 담는다.
                        map.put("data", str);
                    }
                }
            }catch(Exception e){//예외가 발생하면
                //에러 정보를 담는다.
                map.put("code",-1);
                map.put("data", e.getMessage());
            }finally {
                try{
                    if(isr!=null)isr.close();
                    if(br!=null)br.close();
                    if(conn!=null)conn.disconnect();
                }catch(Exception e){}
            }
            //결과를 담고 있는 Map 객체를 리턴해준다.
            return map;
        }

        @Override
        protected void onPostExecute(Map<String, Object> map) {
            super.onPostExecute(map);
            int code=(int)map.get("code");
            if(code!=-1){//성공이라면
                listener.onSuccess(requestId, map);
            }else{//실패 (예외발생)
                listener.onFail(requestId, map);
            }
        }
    }
    //POST 방식 REQUEST
    public static void sendPostRequest(int requestId, String requestUrl, Map<String, String> params, RequestListener listener){
        PostRequestTask task=new PostRequestTask();
        task.setRequestId(requestId);
        task.setRequestUrl(requestUrl);
        task.setListener(listener);
        task.execute(params);
    }

    public static class PostRequestTask extends AsyncTask<Map<String, String>, Void, Map<String, Object>>{
        private int requestId;
        private String requestUrl;
        private RequestListener listener;

        public void setListener(RequestListener listener) {
            this.listener = listener;
        }
        public void setRequestId(int requestId) {
            this.requestId = requestId;
        }
        public void setRequestUrl(String requestUrl) {
            this.requestUrl = requestUrl;
        }
        @Override
        protected Map<String, Object> doInBackground(Map<String, String>... params) {
            Map<String, String> param=params[0];
            String queryString="";
            if(param!=null){//요청 파리미터가 존재 한다면
                //서버에 전송할 데이터를 문자열로 구성하기
                StringBuffer buffer=new StringBuffer();
                Set<String> keySet=param.keySet();
                Iterator<String> it=keySet.iterator();
                boolean isFirst=true;
                //반복문 돌면서 map 에 담긴 모든 요소를 전송할수 있도록 구성한다.
                while(it.hasNext()){
                    String key=it.next();
                    String arg=null;
                    if(isFirst){
                        arg=key+"="+param.get(key);
                        isFirst=false;
                    }else{
                        arg="&"+key+"="+param.get(key);
                    }
                    buffer.append(arg);
                }
                queryString=buffer.toString();
            }
            //서버가 http 요청에 대해서 응답하는 문자열을 누적할 객체
            StringBuilder builder=new StringBuilder();
            HttpURLConnection conn=null;
            InputStreamReader isr=null;
            BufferedReader br=null;
            PrintWriter pw=null;
            //결과값을 담을 Map Type 객체
            Map<String,Object> map=new HashMap<String,Object>();
            try{
                //URL 객체 생성
                URL url=new URL(requestUrl);
                //HttpURLConnection 객체의 참조값 얻어오기
                conn=(HttpURLConnection)url.openConnection();
                if(conn!=null){//연결이 되었다면
                    conn.setConnectTimeout(20000); //응답을 기다리는 최대 대기 시간
                    conn.setDoOutput(true);
                    conn.setRequestMethod("POST");
                    conn.setUseCaches(false);//케쉬 사용 여부
                    //전송하는 데이터에 맞게 값 설정하기
                    conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); //폼전송과 동일
                    //출력할 스트림 객체 얻어오기
                    OutputStreamWriter osw=
                            new OutputStreamWriter(conn.getOutputStream());
                    //문자열을 바로 출력하기 위해 osw 객체를 PrintWriter 객체로 감싼다
                    pw=new PrintWriter(osw);
                    //서버로 출력하기
                    pw.write(queryString);
                    pw.flush();

                    //응답 코드를 읽어온다.
                    int responseCode=conn.getResponseCode();
                    //Map 객체에 응답 코드를 담는다.
                    map.put("code", responseCode);
                    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);
                        }
                        //출력받은 문자열 전체 얻어내기
                        String str=builder.toString();
                        //아래 코드는 수행 불가
                        //console.setText(str);
                        //Map 객체에 결과 문자열을 담는다.
                        map.put("data", str);
                    }
                }
            }catch(Exception e){//예외가 발생하면
                //에러 정보를 담는다.
                map.put("code",-1);
                map.put("data", e.getMessage());
            }finally {
                try{
                    if(pw!=null)pw.close();
                    if(isr!=null)isr.close();
                    if(br!=null)br.close();
                    if(conn!=null)conn.disconnect();
                }catch(Exception e){}
            }
            //결과를 담고 있는 Map 객체를 리턴해준다.
            return map;
        }

        @Override
        protected void onPostExecute(Map<String, Object> map) {
            super.onPostExecute(map);
            int code=(int)map.get("code");
            if(code!=-1){//성공이라면
                listener.onSuccess(requestId, map);
            }else{//실패 (예외발생)
                listener.onFail(requestId, map);
            }
        }
    }
}

 

- Util 클래스를 패키지에 복사해온다.

- 그려면 메인 액티비티에서 Util.sendGetRequest(); 으로 가져와서 사용할 수 있다.

 

- 리스너 역할을 하도록 Activity에 리스너 구현 추가!

 

 

- 요청 id, 경로, 패러미터, 리스너 를 인자로 넣어준다.

- 이것에 대한 응답은 onSuccess() 메소드로 들어온다.

 

 

- [ ]JSONArray

 { } JSONObject 와 대응된다.

 

 

- exception이 발생한다. jsonStr 문자열이 타입에 맞지 않을 수도 있으므로!

- 해당 부분을 try~catch문으로 묶어주기

- for문으로 반복문 돌면서 listView에 출력해주기!

 

- adapter의 참조값은 여기저기에서 필요하므로.. 필드로 빼준다.

- 곡 목록은 서버로부터 받아온다.

 

 

- 서버로부터 받아온 데이터가 ListView에 이렇게 출력된다.

- 셀 클릭시 각각의 음악이 재생된다.

 

- 이렇게 안드로이드+서버 프로그래밍을 결합해서 연습해볼 수 있다.

- 추가로 연습해보고 싶다면 코틀린으로도 바꿔보기!