수업 정리

87일차 수업정리(Android - 오디오 재생, 맵 데이터 읽어오기)

Vita500cc 2020. 8. 7. 13:23

**오디오 재생

    => MediaPlayer 클래스를 이용하면 오디오 및 비디오 재생이 가능

    => MediaPlayer는 android media 패키지에 존재 : C++ 라이브러리

    => 공식적으로 재생 가능한 포맷

        - Audio : wav, mp3, midi, ogg, 3gp

        - video : H263, H264, Mpeg4

    => 설치된 코덱에 따라서 지원 포맷은 늘어나기도 함

1. 재생할 데이터를 설정

    => 메소드는 setDataSource : Overloading(객체지향)

        - 하나의 클래스에 매개변수의 개수나 자료형을 다르게하여 동일한 이름의 메소드가 2개이상 존재하는 경우

          (동일한 작업을 수행하는 메소드가 매개변수 문제로 다른 이름을 가지면 일관성이 없으므로 동일한 이름을 권장)

    - setDataSource(String path)

    - setDataSource(Context context, Uri uri)

    - setDataSource(FileDescriptor fd[ long offset, long length]) : fd만 주거나, offset, length를 모두 대입해도 됨

 

2. 재생 준비

    => 대용량 스트림인 경우 준비하는데 상당한 시간이 걸릴수 있으므로 오픈 직후 준비상태로 만들어 주어야 함

    => 메소드

        - void prepare() : 이 메소드 호출시 작업이 끝날때까지 다음 작업으로 진행하지 않음

        - void prepareAsync : 이 메소드의 수행이 비동기적이라 다음 작업으로 바로 진행

    => 위 메소드를 한번에 진행하는 정적 메소드 create도 존재

        - 정적 메소드 : static 메소드

 

3. 재생 관련 메소드

    => start, stop, pause, setLooping(boolean looping), isLooping

 

4. 정리

    => release(), reset()

 

**파일서버 만들기

    => Tomcat 기준 : webapps 디렉토리에 있는 디렉토리를 외부에서 접속할 수 있도록 해줌

        - Tomcat의 시작 명령은 bin 디렉토리의 startup.sh(Linux나 Unix) or startup.bar(Windows)

        - 중지 명령은 bin 디렉토리의 stop.sh or stop.bat

    => 외부에서 데이터를 다운받는 파일서버를 만드는 경우 webapps 디렉토리에 디렉토리를 만들고 파일 저장 후 Tomcat 시작

        - 외부에서는 http://IP:포트번호 또는 도메인/디렉토리명/파일명 으로 접속 가능

        - 자체적으로 서버를 구축하는 곳은 웹서버, DB서버, 파일서버등을 별도로 구축

    => 파일을 제공하는 서버를 만들때 파일 뿐 아니라 파일의 목록을 데이터베이스에 만들어 놓거나 아니면 일반 파일로 만들어두어야 함

        - 모바일에 제공하는 서버인 경우 마지막에 업데이트된 날짜도 같이 저장

        - 클라이언트는 서버에 어떤 데이터가 있는지 모르므로 데이터베이스나 파일을 읽어서 제공되는 데이터를 확인 할 수 있어야 함

        - 서버의 데이터를 다운받아 로컬에 저장하는 애플리케이션의 경우 서버의 업데이트 정보를 알아야 하므로 업데이트 날짜를 알아야 함

    => 톰캣의 bin 디렉토리에 있는 startup.sh파일을 실행

        - 터미널을 실행시켜서 startup.sh 파일을 드래그 한 후 엔터

    => 테스트

        - 브라우저에서 http://자신의IP:8080/webapps에 만든 디렉토리명/파일명

        - http://192.168.0.?:8080/song/list.txt :  노래 파일 목록

    => 서버 애플리케이션을 실행하는 것은 톰캣을 시작하고 종료

        - 이클립스는 배포와 실행과는 아무런 상관이 없음

 

**서버의 노래를 재생하는 애플리케이션

1. 모듈을 생성

 

2. 레이아웃을 설정

    => 텍스트뷰 1개, 버튼 4개, SeekBar 1개

        - 텍스트 뷰에는 현재 재생중인 노래 제목을 출력

        - 버튼은 노래 재생, 중지, 이전 노래, 다음 노래 재생

        - SeekBar는 현재 재생 중인 노래의 진행 상황을 표시

<?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"
    android:orientation="vertical"
    tools:context=".MainActivity">
    <!-- 안드로이드에서는 뷰를 xml 파일에 디자인 하는 것이 일반적
    디자인시 크기는 필수 요소, 자바코드로 동적으로 변경하고자 하는 경우 반드시 id를 설정 -->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="노래 제목"
        android:id="@+id/filename"/>
    <!-- 버튼들을 배치 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <Button
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="재생"
            android:id="@+id/btnPlay"/>
        <Button
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="중지"
            android:id="@+id/btnStop"/>
        <Button
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="이전"
            android:id="@+id/btnPrev"/>
        <Button
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="다음"
            android:id="@+id/btnNext"/>
    </LinearLayout>

    <!-- PrograssBar는 진행상황을 표시해 줄수 있으나 사용자의 이벤트 입력 불가
         SeekBar는 thumb을 이용하여 사용자의 이벤트를 받을 수 있음-->
    <SeekBar
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:max="100"
        android:progress="0"
        android:id="@+id/progress"/>

</LinearLayout>

 

3. 레이아웃 파일에 디자인한 내용을 자바 코드에서 찾아오기

  1) 필요한 뷰들을 인스턴스 변수로 선언

    Button btnPlay, btnStop, btnPrev, btnNext;
    TextView filename;
    SeekBar progress;

 

  2) onCreate 메소드에서 찾아오기

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        btnPlay = (Button)findViewById(R.id.btnPlay);
        btnStop = (Button)findViewById(R.id.btnStop);
        btnPrev = (Button)findViewById(R.id.btnPrev);
        btnNext = (Button)findViewById(R.id.btnNext);

        filename = (TextView)findViewById(R.id.filename);
        progress = (SeekBar)findViewById(R.id.progress);
    }

    => 안드로이드에서는 리소스를 R.java 파일에 int 상수로 저장하고 관리

        - 다른 Activity의 id도 보이게 됨

        - 다른 Activity의 id를 실수로 작성하는 경우가 있음

 

4. Activity 클래스에 필요한 인스턴스를 선언

    //노래 제목들을 저장할 List
    //m은 인스턴스 변수를 의미
    ArrayList<String> mSongList;
    
    //현재 재생중인 노래의 인덱스
    int mIdx;
            
    //재생여부를 판단할 변수
    boolean isPlaying;
    
    //음악 재생기 변수
    MediaPlayer mMediaPlayer;

 

5. Activity가 종료될 때 호출되는 메소드를 재정의

    => 멀티미디어, 네트워크, 데이터베이스등을 사용할 경우 종료되기 직전에 모든 자원의 연결을 해제

        - 안드로이드, iOS에서는 외부 데이터베이스에 직접 연결이 안되므로 멀티미디어, 네트워크를 사용하는 경우 정확하게 해제해주면 됨

    //내가 만든 클래스가 아닌 클래스의 메소드를 오버라이딩 하는 경우
    //상위 클래스의 메소드를 호출하는 것이 좋음
    //안드로이드에서는 추상 메소드가 아닌 경우 상위 클래스의 메소드를 미호출시 에러
    //메소드 호출 순서는 리턴이 있거나 종료하는 메소드의 경우
    //마지막에 호출해야 하고 그렇지 않으면 먼저 호출
    @Override
    public void onDestroy() {
        if(mMediaPlayer != null){
            mMediaPlayer.release();
            mMediaPlayer = null;
        }
        super.onDestroy();
    }

 

6. 이벤트 처리하는 메소드에서 코드를 전부 작성하면 코드의 길이가 너무 길어서 코드를 분할하기 위해서 사용자 정의 메소드를 생성

    - 접근 지정자를 private로 설정

    => 이 메소드들은 실제 구현시 처음에는 없고, 리펙토링 단계에서 생성

    //인덱스를 받아서 재생 가능한 노래인지 판단하는 메소드
    private void loadMedia(int idx){
        //핸들러에게 전송할 메시지
        Message message = new Message();
        //메시지를 구분할 번호를 저장
        message.what = idx;
        try{
            mMediaPlayer.setDataSource(this, Uri.parse(mSongList.get(idx)));
        }catch (Exception e){
            Log.e("노래 준비 실패", e.getMessage());
            message.obj = false;
        }

        //노래를 바로 재생할 수 있도록 재생 준비
        try{
            mMediaPlayer.prepare();
            message.obj = true;
        }catch (Exception e){
            Log.e("노래 준비 실패", e.getMessage());
            message.obj = false;
        }

        //핸들러 호출
        mMessageHandler.sendMessage();
    }

 

7. MediaPlayer의 이벤트 처리를 위한 리스너 생성

  //MediaPlayer의 이벤트를 처리할 리스너 생성
    //음원 재생이 끝났을 때 호출되는 리스너
    MediaPlayer.OnCompletionListener mOnComplete =
            new MediaPlayer.OnCompletionListener() {
                @Override
                public void onCompletion(
                        MediaPlayer mediaPlayer) {
                    //현재 노래 재생이 끝나면 다음 노래 재생
                    mIdx =
                            (mIdx == mSongList.size()-1
                                    ?0:mIdx+1);
                    mMediaPlayer.reset();
                    loadMedia(mIdx);
                    mediaPlayer.start();

                }
            };

    //노래 재생에 실패했을 때 호출되는 리스너
    MediaPlayer.OnErrorListener mOnError =
            new MediaPlayer.OnErrorListener() {
                @Override
                public boolean onError(
                        MediaPlayer mediaPlayer,
                        int i, int i1) {
                    Toast.makeText(MainActivity.this,
                            "재생 중 에러 발생",
                            Toast.LENGTH_LONG).show();
                    return false;
                }
            };
    //노래 재생 준비가 완료되었을 때 호출되는 리스너
    MediaPlayer.OnSeekCompleteListener mOnSeekCompleter =
            new MediaPlayer.OnSeekCompleteListener() {
                @Override
                public void onSeekComplete(MediaPlayer mediaPlayer) {
                    if(isPlaying){
                        mMediaPlayer.start();
                    }
                }
            };

 

8. SeekBar를 움직였을 때 호출되는 리스너

 

   //시크바의 위치가 변경되었을 때 호출되는 리스너
    SeekBar.OnSeekBarChangeListener mOnSeek =
            new SeekBar.OnSeekBarChangeListener() {
                //썸을 눌러서 이동하고 값이 변경된 후에 호출되는 메소드
                @Override
                public void onProgressChanged(
                        SeekBar seekBar, int i, boolean b) {
                    //boolean b 가 사람에 의해서 변경이 된것인지
                    //다른 이유로 변경되었는지 알려주는 변수
                    if(b){
                        mMediaPlayer.seekTo(i);
                    }

                }
                //썸을 처음 눌렀을 때 호출되는 메소드
                @Override
                public void onStartTrackingTouch(SeekBar seekBar) {
                    if(mMediaPlayer.isPlaying()){
                        isPlaying = mMediaPlayer.isPlaying();
                        mMediaPlayer.pause();
                    }
                }
                //썸에서 손을 뗐을 때 호출되는 메소드
                @Override
                public void onStopTrackingTouch(SeekBar seekBar) {

                }
            };

 

9. 2개의 핸들러를 생성

    => 첫번째 핸들러는 loadMedia 가 호출하는 핸들러로 노래의 재생 가능 여부를 출력하기 위한 핸들러

    => 두번째 핸들러는 노래가 재생될 때 0.2초마다 노래 재생 위치를 확인해서 SeekBar를 업데이트 해주는 핸들러

    //화면 갱신을 위한 핸들러
    Handler mMessageHandler = new Handler(Looper.getMainLooper()){
      @Override
      public void handleMessage(Message message){
          //넘어온 결과 찾아오기
          boolean result = (Boolean)message.obj;
          String resultMsg = null;
          if(result == true){
              resultMsg = "재생 준비 완료";
              filename.setText(mSongList.get(message.what));
              //재생할 노래의 길이로 Seekbar의 길이 설정
              progress.setMax(mMediaPlayer.getDuration());
          }else{
              resultMsg = "재생 준비 실패";
          }
          Toast.makeText(MainActivity.this, resultMsg, Toast.LENGTH_LONG).show();
      }
    };
    //0.2초마다 시크바의 값을 업데이트하는 핸들러
    Handler mProgressHandler = new Handler(
            Looper.getMainLooper()){
        @Override
        public void handleMessage(Message message){
            if(mMediaPlayer == null){
                return;
            }else if(mMediaPlayer.isPlaying()){
                progress.setProgress(
                        mMediaPlayer.getCurrentPosition());
            }
            //0.2초 다시 자기 자신을 호출
            mProgressHandler.sendEmptyMessageDelayed(
                    0,200);
        }
    };

 

10. onCreate 메소드에서 초기화하는 작업을 수행

        mSongList = new ArrayList<>();
        //스레드를 생성해서 노래 목록을 다운 받기
        new Thread(){
            public void run(){
                try{
                    String addr = "http://192.168.0.200:8080/song/";
                    //노래 목록 파일 주소 생성
                    URL url = new URL(addr + "list.txt");
                    //연결
                    HttpURLConnection con = (HttpURLConnection)url.openConnection();
                    //문자열 받기 위한 스트림 생성
                    BufferedReader br = new BufferedReader(new InputStreamReader(
                            con.getInputStream()));
                    StringBuilder sb = new StringBuilder();
                    while(true){
                        String line = br.readLine();
                        if(line == null){
                            break;
                        }
                        sb.append(line + "\n");
                    }
                    String data = sb.toString();

                    //문자열을 콤마로 분해
                    String[] songList = data.split(",");
                    for(String song : songList){
                        mSongList.add(addr + song + ".mp3");
                    }

                    //음원 재생기 생성
                    mMediaPlayer = new MediaPlayer();
                    mIdx = 0;
                    mMediaPlayer.setOnCompletionListener(mOnComplete);
                    mMediaPlayer.setOnErrorListener(mOnError);
                    mMediaPlayer.setOnSeekCompleteListener(mOnSeekCompleter);
                    progress.setOnSeekBarChangeListener(mOnSeek);
                    //핸들러 호출
                    mProgressHandler.sendEmptyMessageDelayed(0, 1000);

                    //버튼의 이벤트 핸들러
                    btnPlay.setOnClickListener(new Button.OnClickListener() {
                        @Override
                        public void onClick(View view) {
                            if(mMediaPlayer.isPlaying() == false){
                                mMediaPlayer.start();
                                btnPlay.setText("pause");
                            }else{
                                mMediaPlayer.pause();
                                btnPlay.setText("play");
                            }
                        }
                    });
                    btnStop.setOnClickListener(new Button.OnClickListener() {
                        @Override
                        public void onClick(View view) {
                            mMediaPlayer.stop();
                            btnPlay.setText("play");
                            progress.setProgress(0);
                        }
                    });
                    btnPrev.setOnClickListener(new Button.OnClickListener() {
                        @Override
                        public void onClick(View view) {
                            //재생 중인지 여부 저장
                            boolean isPlaying = mMediaPlayer.isPlaying();
                            //이전으로 이동
                            mIdx = (mIdx == 0 ? mSongList.size()-1 : mIdx);
                            //플레이어 초기화
                            mMediaPlayer.reset();
                            //노래 재생 준비
                            loadMedia(mIdx);
                            //이전에 재생중이면 바로 재생
                            if(isPlaying){
                                mMediaPlayer.start();
                                btnPlay.setText("pause");
                            }
                        }
                    });
                    btnNext.setOnClickListener(new Button.OnClickListener() {
                        @Override
                        public void onClick(View view) {
                            //재생 중인지 여부 저장
                            boolean isPlaying = mMediaPlayer.isPlaying();
                            //다음으로 이동(btnPrev과 이부분이 다름)
                            mIdx = (mIdx == mSongList.size()-1 ? 0 : mIdx+1);
                            //플레이어 초기화
                            mMediaPlayer.reset();
                            //노래 재생 준비
                            loadMedia(mIdx);
                            //이전에 재생중이면 바로 재생
                            if(isPlaying){
                                mMediaPlayer.start();
                                btnPlay.setText("pause");
                            }
                        }
                    });
                }catch (Exception e){
                    Log.e("다운로드 예외 발생", e.getMessage());
                }
            }
        }.start();

 

11. AndroidManifest.xml 파일에서 인터넷 권한 설정

    <uses-permission android:name="android.permission.INTERNET"/>
    <application
        android:usesCleartextTraffic="true"

 

**기본 Activity가 없어서 실행이 안되는 경우 발생시

    => 실행가능한 Activity를 생성하고 코드를 복사하여 실행

        - 클래스 명과 layout파일의 이름은 수정해야 함

 

**Surface View

    => 그래픽 가속을 위한 뷰
    => 이전에는 그래픽을 출력할 때는 SurfaceView를 이용해서 좋은 성능을 나타냄
    => 최근의 안드로이드에서는 일반 뷰를 최적화해서 SurfaceView 와 성능 차이가 거의 나지 않음
    => 현재는 SurfaceView는 그래픽 가속이 아니라 카메라로 사진이나 동영상을 촬영할 때 카메라 뷰를 만들기 위한 용도로 주로 이용
        - 카메라는 계속해서 변하는 데이터를 출력하므로 약간의 성능 차이가 크게 느껴질 수 있음


**위치 정보 사용

    => 위치 정보를 여러 가지 방법으로 가져올 수 있습니다.
1. 위치 정보 제공자
    => 전화 기지국: 가입된 통신사의 기지국을 기준으로 위치정보를 제공 - 부정확
    => 무선 네트워크를 제공하는 라우터: 무선 네트워크를 이용하는 경우 가장 가까이에 있는 라우터의 위치
        - 여기까지 2개는 고도 측정 안됨
    => GPS: 50m 정도의 오차 범위를 갖는 미국방성 저궤도 위성을 이용하는 방식
    => 갈릴레오 서비스: 유럽에서 제공하는 위치 정보 제공자로 오차범위가 1m 이내

2. 위치 정보 제공자

    - public List<String> getAllProviders(): 모든 위치 정보 제공자를 리스트를 리턴
    - public String getBestProvider(): 현재 상태에서 가장 성능이 좋은 위치 정보 제공자를 리턴

 

3. 위치 정보 제공자의 옵션 설정
    - setAltitudeRequired(고도)
    - setLatitudeRequired(위도)
    - setLongitudeRequired(경도)
    - setBearingRequired(나침반)
    => 어떤 정보를 사용할 지 boolean 으로 설정


    - setPowerRequiement(int level): 전력 소모량
    - setAccuracy(int accuracy): 정밀도
    => 정밀도가 높아지면 배터리 소모량이 많아지고 정밀도를 낮추면 배터리 소모량이 감소량
    => 배터리 소모량이나 정밀도를 설정
    => gps, 동영상. 블루투스, 와이파이는 배터리 소모량이 심합니다.

 

4. 위치를 조사
    => 위치 갱신 리스너를 이용해야 합니다.
    => 위치 정보 제공자를 가지고 requestLocationUpdates(String 위치정보제공자, long minTime, float minDistance,

         LocationListener listener[, Looper looper]): 마지막 Looper는 화면 갱신을 하고자 할 때 사용

        - listener 안에서 핸들러를 호출해도 됩니다.
        - requestLocationUpdates(String 위치정보제공자, long minTime, float minDistance, PendingIntent intent)

          -> 위치정보가 갱신되면 Listener를 호출하는 것이 아니고 intent를 호출
    => 더이상 필요없을 때는 removeUpdates 메소드에 Listener 나 Intent를 대입

 

5. LocationListener
    => 위치 정보 갱신을 처리할 수 있는 리스너 인터페이스
    => onProviderEnabled, onProviderDisabled, onStatusChanged
        - onLocationChanged(Location location): 위치 정보가 갱신되었을 때 호출되는 메소드
        - 위치 정보를 location에 대입되어 넘어옴
        - getLatitude, getLongitude, getAltitude, getSpeed, getBearing, getAccuracy, getTimestamp
    => 주기적으로 전송되어온 위치정보를 사용할 때 타임스탬프에 유의
        - 스마트 폰의 위치 정보 제공자는 변경될 수 있음
        - 현재는 GPS가 최적의 정보 제공자여서 GPS가 보내온 정보가 전송
        - 이전에는 WIFI가 최적의 정보 제공자여서 정보 전송
        - 나중에 전송한 정보가 먼저 도착하기도 함
        - 이전 정보보다 나중에 생성된 정보는 버려야 함

 

6. 위치 정보도 퍼미션이 있어야 하고 동적으로 권한 설정
    => FINE_LOCATION 과 COASE_LOCATION 이 필요

7.현재 위치를 가져와서 출력하기
  1) AndroidManifest.xml 파일에 권한을 설정
    => ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION

    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

 

  2) 동적인 권한 설정을 쉽게 하기 위해서 build.gradle에 AutoPermission 라이브러리의 의존성 설정

    => build.gradle에 작성

allprojects{
    repositories{
        maven { url 'https://jitpack.io' }
    }
}

dependencies {
    implementation 'com.github.pedroSG94:AutoPermissions:1.0.3'

    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

}

 

  3) 실행 가능한 Activity 추가(LocationActivity)

  4) 동적인 권한 요청이 필요한 부분을 처리하기 위한 코드 작성
    => Activity 클래스에 동적 권한 요청 인터페이스를 implements

implements AutoPermissionsListener


    => onCreate 메소드에서 동적 권한 요청 메소드를 호출

AutoPermissions.Companion.loadAllPermissions(this, 101);

 

    => 3개의 메소드를 오버라이딩

    //Activity의 메소드로 권한 요청을 설정했을 때 호출되는 메소드
    @Override
    //requestCode는 권한 요청 할 때 구분하기 위해서 부여한 번호
    //permissions는 요청한 권한의 배열
    //grantResults는 요청한 권한의 허용 여부 배열 
    public void onRequestPermissionsResult(
            int requestCode, 
            String permissions[], 
            int [] grantResults){
        super.onRequestPermissionsResult(
                requestCode, permissions, grantResults);
        //AutoPermissions의 메소드를 호출하도록 설정
        AutoPermissions.Companion.parsePermissions(
                this, requestCode, 
                permissions, this);
    }
    
    //권한 사용을 거부했을 때 호출되는 메소드
    @Override
    public void onDenied(int requestCode, String[] permissions){
        Toast.makeText(this, "권한 사용을 거부함",
                Toast.LENGTH_LONG).show();
    }
    
    //권한 사용을 허용했을 때 호출되는 메소드
    @Override
    public void onGranted(int requestCode, String[] permissions){
        Toast.makeText(this, "권한 사용을 허용함",
                Toast.LENGTH_LONG).show();
    }

 

  5) 레이아웃 수정

    => 버튼 1개와 텍스트뷰 1개 배치

<?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=".LocationActivity"
    android:orientation="vertical">
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="위치정보 가져오기"
        android:id="@+id/btnLocation"/>
    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/lblLocation"/>

</LinearLayout>

 

  6) Activity 파일에 디자인 뷰를 사용하기 위한 인스턴스 변수를 선언

    private TextView lblLocation;
    private Button btnLocation;

 

  7) Activity 파일의 onCreate 메소드에서 디자인 한 뷰 찾아오기

//뷰 찾아오기
lblLocation = findViewById(R.id.lblLocation);
btnLocation = findViewById(R.id.btnLocation);


  8) 위치정보가 갱신되었을 때 호출될 리스너를 생성
    => LocationListner를 implements 해야 함
    => 현재 위치의 위도와 경도를 받아서 텍스트 뷰에 출력

    //위치정보가 갱신될 때 호출될 리스너 객체
    class GPSListener implements LocationListener {

        //위치정보가 변경되면 호출되는 메소드 
        @Override
        public void onLocationChanged(
                @NonNull Location location) {
            //위도와 경도 가져오기
            double latitude = location.getLatitude();
            double longitude = location.getLongitude();
            
            //출력
            String msg = 
                    String.format("위도:%.6f 경도:%.6f",
                            latitude, longitude);
            lblLocation.setText(msg);
        }

        @Override
        public void onStatusChanged(String provider, int status, Bundle extras) {

        }

        @Override
        public void onProviderEnabled(@NonNull String provider) {

        }

        @Override
        public void onProviderDisabled(@NonNull String provider) {

        }
    }

 

  9) Activity 파일에 버튼을 눌렀을 때 호출될 메소드를 작성
    => 위치 정보 수집을 위한 메소드

    //버튼을 눌렀을 때 호출될 메소드
    private void startLocationService(){
        //위치정보 사용 객체를 생성
        LocationManager manager = 
                (LocationManager)getSystemService(
                        Context.LOCATION_SERVICE);
        try{
            //위치정보 제공자를 설정 : 동적 권한 설정이 되어야 함
            //이 코드를 부르는 곳에서 설정 
            Location location = 
                    manager.getLastKnownLocation(
                            LocationManager.GPS_PROVIDER);
            if(location != null){
                //위도와 경도 가져오기
                double latitude = location.getLatitude();
                double longitude = location.getLongitude();

                //출력
                String msg =
                        String.format("위도:%.6f 경도:%.6f",
                                latitude, longitude);
                lblLocation.setText(msg);
            }
            
            //리스너를 생성
            GPSListener gpsListener = new GPSListener();
            //위치정보가 갱신될 때 gpsListener의 메소드를 호출하도록 설정
            //첫번째 매개변수는 위치 정보 갱신을 위한 정보 제공자를 설정
            //두번째 매개변수는 위치 정보를 측정할 시간 단위
            //세번째 매개변수는 위치 정보를 측정할 거리 단위
            //네번째 매개변수가 호출될 리스너
            manager.requestLocationUpdates(
                    LocationManager.GPS_PROVIDER,
                    10000, 10, gpsListener);
            
        }catch(Exception e){
            Log.e("위치 정보 사용 실패", e.getMessage());
        }
    }

 

  10) Activity 파일의 onCreate 메소드에 버튼을 눌렀을 때 처리를 위한 이벤트 핸들러 작성

btnLocation.setOnClickListener(
                new Button.OnClickListener(){
            public void onClick(View view){
                startLocationService();
            }
        });

 

TIP!

**모든 애플리케이션이 공통으로 사용하는 디렉토리 명

    - bin : 명령어 - 실행파일

    - lib : 실행파일들이 사용할 보조적인 역할의 파일

    - logs : 로그파일

    - temp : 임시파일 저장

 

**프로그램에서 말하는 메시지

    => 명령어를 의미(메소드를 호출하는 것을 메시지를 전송한다고 표현)

 

**모든 GUI 프로그래밍에서는 기본적으로 Main.Thread에서만 UI를 갱신할 수 있음

 

**안드로이드에서는 MainThread에게 작업을 부탁하는 클래스로 Handler와 AsyncTask가 있음

 

**핸들러의 속성은 4개

    => Message.what : 정수

    => Message.arg1 :  정수 - LPARAM

    => Message.arg2 : 정수 - RPARAM

    => Message.obj : Object 타입의 데이터

 

**컴파일러 - 기계어로 번역 X

                 - 어셈블리어로 번역