[안드로이드] 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이라는 파일로해서 저장해주시면 됩니다.




  

  

[Java] @Override 어노테이션의 사용

자바 언어에서 클래스를 작성할때 그 행위를 파생 클래스에 특화시키기 위해 부모 클래스의 매서드를 오버라이드(Override)하는 경우가 있습니다. 예를 들어서.. 안드로이드라는 모바일 개발 플랫폼에서 모바일 애뮬레이터의 Menu 키를 누르게 되면 Activity라는 클래스의 onCreateOptionsMenu 매서드가 호출됩니다. 즉, Activity 클래스를 상속받아 자신만의 메뉴를 만들기 위해 상속받은 클래스는 onCreateOptionsMenu 매서드를 오버라이드해야 한다는 것입니다. 아래처럼요..

public class MenuTest extends Activity {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    public boolean onCreateOptionMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);

         menu.add(0, 1, 0, "Menu1");
         menu.add(0, 2, 0, "Menu2");

         return true;
     }
}

자.. 이제 실행하고 Menu 키를 누르면 메뉴가 짠~하고 나타나길 기대합니다! 하지만 나타나지 않습니다.. 이유는 MenuTest라는 클래스에 Override했다고 생각하는 onCreateOptionsMenu가 Override되지 않았기 때문입니다. 분명히 위의 코드에서 Override했다고요? 눈을 크게 뜨고 보시기 바랍니다.. 네.. 철자가 틀렸습니다! 우리가 Override 했다고 생각한 것은.. 사실 새로운 onCreateOptionMenu 매서스를 추가한 것입니다.. 즉 철자가 틀렸습니다.. onCreateOptionMenu가 아니고 onCreateOptionsMenu입니다..

의도는 분명했으나 개발자도 사람인지라 철자에 대한 실수를 했습니다.. 바로 이러한 실수를 방지할 목적으로 자바에서는 @Override 어노테이션을 지원하게 되었습니다.. 이제 다시 위의 코드에 이 @Override 어노테이션을 사용해 보면 아래와 같습니다..

public class MenuTest extends Activity {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    public boolean onCreateOptionMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);

         menu.add(0, 1, 0, "Menu1");
         menu.add(0, 2, 0, "Menu2");

         return true;
     }
}

컴파일 해보면 에러가 발생합니다.. 에러 내용은 onCreateOptionMenu는 Override될만한 매서드가 아니라는 내용을 개발자에게 전달합니다. 여기서 개발자는 자신의 실수를 인지하고 해결책을 마련합니다. 즉, 틀린 철자를 고쳐서 말입니다.

자바라는 언어가 앞서 예를 든 개발자의 실수를 방지할 수 없으므로.. 비록 차선책이라고 생각되기는 하지만… 바로 @Override라는 어노테이션을 제공하게 되었습니다. 이 어노테이션을 제대로 활용하기 위해서는.. 일괄적으로 개발자 스스로가 Override 되는 모든 매서드에 대해서 바로 이 @Override를 붙여 놓는 것입니다. 이것은 분명히 명확하고 옳바른 습관입니다.