[안드로이드] 이런…. 무책임한 개발환경 같으니…

하루를 정리하고… 퇴근 직전.. 잠시 안드로이드를 공부하던 차에.. 문제가 발생했습니다.. 물론.. 모든 문제의 책임은 개발자에게 있지만… 어느 정도.. 힌트를 줘야 문제를 잡지… Error Log에 달랑 아래와 같은 에러 로그만 찍어주고.. 어디 소스 파일에 문제인지 알려주지 않으니.. 도통 나같은 안드로이드 입문자에게.. 에러 잡기는 서울서 김서방 찾기로군…

사용자 삽입 이미지

이래.. 저래.. 소스 코드며.. xml  파일이며 하나 하나, 한줄 한줄 다 뒤진 끝에.. xml에 문제가 있다는 것을 알았습니다.. 그 xml은 레이아웃을 정의하는 것이고… 다음과 같습니다..

...

; 
  
;

...

보니.. Button인데.. Buttom이죠.. 프로젝트가 방대해지고.. 정신없이 개발하고 있다면.. 어떤 힌트가 제공하지 않는… 이런 성의없는 에러로그로는 정말 당혹스러워질법도 한데요..

이런 상황을 대비해서.. 다시금 에러로그를 살펴봅니다.. org.eclipse.jdt.ui에 Internal Error는.. ui를.. 즉, 레이아웃을 정의하는 기능 내부의 에러라는 의미이고… 그 하단에 딸린 추가 로그 정보인 gen [in tstAndroid] does not exist는.. 무언가 존재하지 않는다는 의미라고 생각할 수 있겠는데요.. 바로 실수로 입력한 Buttom에 대한 ui가 존재하지 않는다는 생각할 수 있겠습니다..

이런 경우에는….. 레이아웃을 정의하는 xml에 태그명이 잘못되었으니.. 이곳에서서 문제를 찾아볼것! 이라고 정리해봅니다.. 아이코…… 벌써.. 11시가 넘었습니다.. 퇴근해야죠…… 내일을 위해서…

[안드로이드] SD 카드에 파일 생성

안드로이드는 보안상의 이유로 모바일 디바이스의 내장 메모리를 통한 파일을 여러개의 어플리케이션에서 공유하는 것이 여러모로 불편하게 되어 있습니다. 하지만 외장 메모리인 SD 카드의 경우는 이러한 제약이 존재하지 않습니다. 이 글은 안드로이드에서 SD 카드에 파일을 생성하는 것에 대한 정리입니다.

먼저 안드로이드에 SD 카드의 사용 가능 여부를 판단하기 위한 코드는 아래와 같습니다.

String ess = Environment.getExternalStorageState();
String sdCardPath = null;
if(ess.equals(Environment.MEDIA_MOUNTED)) {
    sdCardPath = Environment.getExternalStorageDirectory().getAbsolutePath();
    showMsg("SD Card stored in " + sdCardPath);
} else {
    showMsg("SD Card not ready!");
}

showMsg는 안드로이드의 Toast 기능을 좀더 쉽게 사용하기 위한 사용자 정의 매서드로써 아래와 같습니다.

private void showMsg(String msg)
{
    Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
}

Toast는 디바이스 화면에 일정 시간 동안 메세지를 띠워주는 기능입니다. 다시 본론으로 돌아가서.. 정적 클래스인 Environment의 getExternalStorageState 매서드의 반환값이 Environment.MEDIA_MOUNTED 인 경우가 SD 카드를 사용할 수 있다는 의미이고 사용 가능하다면 SD 카드가 마운트된 디렉토리 명을 얻기 위해 다시 Environment의 getExternalStorageDirectory 매서드를 사용합니다. 이렇게 얻은 디렉토리에는 보안상의 제약 없이 자유롭게 파일을 읽고 쓸 수 있습니다. 여기서는 파일을 쓰는 코드에 대해 살펴보겠습니다.

File file = new File(sdCardPath + "/test.txt");
try {
    FileOutputStream fos = new FileOutputStream(file);
    String msg = "이 파일은 SD 카드에 저장된 메세지입니다.";
    fos.write(msg.getBytes());
    fos.close();
} catch(FileNotFoundException fnfe) {
    showMsg("지정된 파일을 생성할 수 없습니다.");
} catch(IOException ioe) {
    showMsg("파일에 데이터를 쓸 수 없습니다.");
}

안드로이드는 자바 언어를 통해 플랫폼의 API를 사용할 수 있습니다. 이런 맥락으로 SD 카드에 파일을 생성하고 읽고, 쓰기는 자바의 API를 그대로 사용할 수 있습니다.

끝으로 SD 카드를 사용하기 위해서는 매니페스트 파일에 다음과 같은 설정값을 추가해줘야 합니다.


이 설정값이 없다고 해도 SD 카드를 사용할 수 있다고 나오지만 실제로 파일을 쓰거나 읽을 수 없으니.. 반드시 이 설정값을 추가하기 바랍니다.

[안드로이드] 사용자 정의 Adapter 만들기

Adapter는 데이터 테이블을 목록 형태로 보여주기 위해 사용되는 것으로 데이터를 다양한 형식의 리스트 형식으로 보여주기 위해서 데이터와 리스트 뷰 사이에 존재하는 객체입니다. 즉, 간단히 말해 데이터와 리스트 뷰 사이의 통신을 위한 다리 역활을 합니다.

안드로이드를 살펴보면서.. 데이터를 사용자 정의 리스트뷰 형식으로 표현하는데 있어서 매우 유연한 방식을 제공한다는 것을 알게 되었고… 이런 유연함을 위해 다소 복잡한 구조에 익숙해져 볼 생각으로 이 글을 작성하게 되었습니다.

안드로이드를 처음 접하시는 분들에게 상당히 불친절한 내용이라고 생각됩니다. 제 개인적으로는 정리 차원의 글이라는 점을 다시금 언급해 드립니다.

먼저 아래와 같은 결과를 목표로 해서 사용자 정의 Adapter 만들기에 대한 핵심 내용을 정리해 보겠습니다.


사용자 삽입 이미지
인물에 대한 사진과 이름 그리고 생일 정보를 리스트 형식으로 보여주고 있는 화면입니다. 위의 화면에 대한 레이아웃은 다음과 같습니다.

사용자 삽입 이미지
즉.. Activity의 View Content가 profilelistview.xml에 해당되며 리스트뷰를 채우는 각 항목은 profileview.xml로 정의된다는 내용입니다. 먼저 큰 레이아웃인 profilelistview.xml에 대한 코드는 다음과 같습니다.



  

단순히 레이아웃 안에 list라는 id의 ListView 위젯만을 가지고 있습니다. 그리고 다음 코드는 이 ListView 위젯 안에 담을 항목에 대한 뷰에 해당하는 profileview.xml입니다.



  
  
    
    
  

위의 코드를 도식화 하면 아래와 같습니다.

사용자 삽입 이미지
수평 정렬로 지정된 레이아웃 안에 ImageView 위젯과 TextView 위젯 2개를 가지고 있는 수직 정렬로 지정된 레이아웃에 대한 내용입니다. 이제 이렇게 UI가 정해졌으니 코드에 대해서 정리해 보겠습니다.

먼저 리스트 뷰를 채울 데이터를 나타낼 클래스인 Profile에 대한 코드입니다.

class Profile {
    private int _photo;
    private String _name;
    private String _telephone;

    public int getPhoto() {
        return _photo;
    }

    public String getName() {
        return _name;
    }

    public String getTelephone() {
        return _telephone;
    }

    public Profile(int photo, String name, String telephone) {
        _photo = photo;
        _name = name;
        _telephone = telephone;
    }
}

사진에 대한 리소스 ID값과 이름 그리고 전화번호에 대한 값을 저장하고 얻기 위한 단순한 클래스입니다.

다음으로 뷰를 화면에 표시하기 위한 Activity 클래스를 상속받은 ProfileList 클래스 입니다.

public class ProfileList extends Activity {
    private ArrayList _profiles = null;

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.profilelistview);
  
        _profiles = new ArrayList();
  
        Profile p1 = new Profile(R.drawable.boa, "보아", "1986년 11월 5일");
        Profile p2 = new Profile(R.drawable.kaj, "김아중", "1982년 10월 16일");
        Profile p3 = new Profile(R.drawable.nrs, "나르샤", "1981년 12월 28일");
        Profile p4 = new Profile(R.drawable.lsy, "이수영", "1979년 4월 12일");
        Profile p5 = new Profile(R.drawable.jyj, "장윤정", "1980년 2월 16");
        Profile p6 = new Profile(R.drawable.nhh, "노현희", "1972년 8월 23일");  

        _profiles.add(p1);
        _profiles.add(p2);
        _profiles.add(p3);
        _profiles.add(p4);
        _profiles.add(p5);
        _profiles.add(p6);  

        ProfileListAdapter adapter = 
            new ProfileListAdapter(this, R.layout.profileview, _profiles);

        ListView listView = (ListView)findViewById(R.id.list);
        listView.setAdapter(adapter);
    }
}

2번 코드의 _profiles는 데이터 목록을 담고 있는 컨테이너입니다. 이 컨테이너에 데이터를 추가하는 코드가 8~22번입니다. 이 컨테이너의 데이터가 바로 리스트 뷰에 표시될 정보입니다. 이 데이터와 리스트 뷰를 연결해주기 위한 것이 바로… 24번 코드, 즉.. 이 글의 주제인 사용자 정의 Adapter인 ProfileListAdapter 입니다. 이 사용자 정의 Adapter 클래스는 BaseAdapter를 상속받습니다. 코드는 아래와 같습니다.

class ProfileListAdapter extends BaseAdapter {
    private LayoutInflater _inflater;
    private ArrayList _profiles;
    private int _layout;

    public ProfileListAdapter(Context context, int layout, 
                                        ArrayList profiles) {
        _inflater = (LayoutInflater)context.getSystemService(
                                  Context.LAYOUT_INFLATER_SERVICE);
        _profiles = profiles;
        _layout = layout;
    }

    @Override
    public int getCount() {
        return _profiles.size();
    }

    @Override
    public String getItem(int pos) {
        return _profiles.get(pos).getName();
    }
 
    @Override
    public long getItemId(int pos) {
        return pos;
    }
 
    @Override 
    public View getView(int pos, View convertView, ViewGroup parent) {
        if(convertView == null) {
            convertView = _inflater.inflate(_layout, parent, false);
        }
  
        Profile profile = _profiles.get(pos);
  
        ImageView photo = (ImageView)convertView.findViewById(R.id.photo);
        photo.setImageResource(profile.getPhoto());
  
        TextView name = (TextView)convertView.findViewById(R.id.name);
        name.setText(profile.getName());
  
        TextView telephone = (TextView)convertView.findViewById(R.id.telephone);
        telephone.setText(profile.getTelephone());
  
        return convertView;
    }
}

중요한 부분만 언급하면… Override해야할 매서드는 모두 4개로써 getCount, getItem, getItemId, getView입니다. 그리고 리스트 뷰의 항목에 대한 뷰를 생성하는 getView는 처음 호출될때 두번째 인자인 convertView가 null이며 이후 개발자가 직접 인스턴스를 생성해주면 이후 호출될때는 처음 호출될때 생성된 인스턴스가 전달되는 구조로써 인스턴스의 생성에 대한 부담을 최소화하기 위한 방안입니다.

이미 안드로이드의 사용자 정의 Adapter 만들기에 대한 내용은 여타의 다양한 개발 플랫폼에서도 사용되고 있는 구조이지만… 안드로이드를 통해 다시금 접해 봄으로써.. 이러한 구조에 익숙해져서 자신이 개발하고 있는 소프트웨어에 그 설계 구조 자체를 적용해 볼 수 있는 발전으로까지 이어가길 스스로에게 다짐해 봅니다.

[안드로이드] 타이머 기능

처음 Windows 운영체제에서 개발언어를 익혔을때 가장 매력적인 기능이 바로 타이머(Timer) 였습니다. 개발자가 지정한 시간 간격으로 자동으로 알아서 어떤 로직을 호출해서 실행시켜 주는 것이 마치… 컴퓨터에게 일을 맡겨 놓고 나는 신경끄고 놀수있다라는 가능성이 매력적이였나 봅니다.

그러나….. 실제 지금까지 개발 현장에서 단한번도 이 타이머를 사용해 본적은 없습니다. 이유는.. 정확도가 떨어지기 때문입니다. 그러니깐… 예를 들어 1초 간격으로 실행해라고 지정해 놓지만.. 정확히 1초 마다가 아니라 경우에 따라 큰 오차가 발생하기 때문입니다..

여하튼… 이런 저런 사정을 떠나… 안드로이드에도 타이머 기능이 존재하는데.. 이 타이머 기능에 대해 정리를 해 보았습니다. 매우 정확한 시간으로 어떤 일을 반복적으로 수행해야할 경우에는 사용하기에는 부적합하지만… 그래도 어떤 일을 주기적으로 반복해서 수행해야할 경우에 매우 요긴하게 사용할 수 있는.. 매우 손쉬운 기능이 바로 이 타이머이기 때문입니다..

정리하는 수준으로 글을 전개해 나갈 것이며 타이머에 대한 예제 코드가 매우 단순하기 때문에 바로 코드 나갑니다!

package mobile.geoservice;

import android.app.*;
import android.os.*;
import android.view.*;
import android.widget.*;

public class Timer extends Activity {
    private TextView _text;
    private CountDownTimer _timer;
 
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.timerlayout);
  
        _text = (TextView)findViewById(R.id.tvMsg);
  
        _timer = new CountDownTimer(10 * 1000, 1000) {
            public void onTick(long millisUntilFinished) {
                _text.setText("value = " + millisUntilFinished);
            }
   
            public void onFinish() {
                _text.setText("finshed");
            }
        };

        Button btnStart = (Button)findViewById(R.id.btnStart);
        btnStart.setOnClickListener(new Button.OnClickListener() {
            public void onClick(View v) {
                _timer.start();
            }
        });
  
        Button btnEnd = (Button)findViewById(R.id.btnStop);
        btnEnd.setOnClickListener(new Button.OnClickListener() {
            public void onClick(View v) {
                _timer.cancel();
            }
        });  
    }
}

10번 코드가 바로 타이머를 위한 클래스로 CountDownTimer입니다. 18번 코드에서 생성하고 있는데… 다른 여느 타이머와는 다르게 반복될 시간 간격뿐만 아니라 작동될 시간까지도 지정합니다. 즉, 생성자에게 2개의 인자를 받는데 첫번째가 작동될 시간이며 두번째가 시간 간격입니다. 위의 경우 첫번째 인자값이 10*1000이므로 10초동안 수행되며, 두번째 인자가 1000이므로 1초 간격으로 수행됩니다. 19번 코드와 23번 코드에 나타난 onTick과 onFinish는 각각 타이머 수행 코드에 대핸 매서드와 타이머가 지정된 시간(여기서는 10초)이 되었을때 발생되는 매서드입니다.

이렇게 만든 타이머의 start 매서드와 cancel 매서드를 통해 시작시키거나 작동을 중지시킬 수 있습니다. cancel 매서드의 경우 여타 다른 환경의 타이머와 다르게 취소만 될뿐.. 중지하여 중지된 시점으로부터 재개할 수는 없습니다. 또한 확인해 본바로는 cancel 매서드를 onTick 매서드 안에서 호출할 경우 의도와 다르게 타이머가 중지하지 않습니다..

이해를 돕고자 위의 코드를 실행했을 경우 애뮬레이터에서 나타나는 UI는 아래 그림과 같습니다.

사용자 삽입 이미지


모바일에서 타이머의 기능을 어디에 활용할 수 있을까… 한번 생각을 해보면… 일정한 시간 간격으로 자신의 위치를 얻어오거나… 일정한 시간 간격으로 메일서버로부터 메일을 확인한다거나… 오히려 일반 데스크탑 환경에서보다 모바일에서 타이머의 기능은 매우 중요할듯합니다..

아뿔싸~ 레이아웃 리소스가 빠졌군요~! 실습을 하시는 분이라면 timerlayout.xml이라는 파일로해서 저장해주시면 됩니다.