10장 자료 관리

2011. 2. 23. 18:012010년/완전정복

콘텐트 프로바이더

콘텐트 프로바이더는 데이터를 감싸는 래퍼(wrapper)를 일컫는 용어다.
자바에서 wrapper 클래스를 볼경우  문자열로 입력받은 100을  정수로 변경할때
사용한다.. 물론.. 문자열을 -46을 통해서 바꿀수도 있지만.

String str = "100";
int a =parseInt(str);
통해서 a에 정수가 들어간다.

Content Provide같은경우 만들어진 어플리케이션을  끼리의 연결해주는거라고 생각하면된다.
다른곳에서 만든 어플리케이션도 여기서 만든것도 모양은 틀리지만.. 서로 연결가능한 wrapper기능이랑 비슷하다. 약간 공유와 권한 그쪽으로 비슷하다. 어떻게 보면 보안적 측면도 있다.

Content Provide에 있는 데이터를 가져오거나 데이터를 저장하려면  REST다운과 URI를 사용해야한다.
예를 들어 개발자가 책 데이터베이스의 캡슐인 콘텐트 프로바이더를 부터 책을 가져오려면 다음과의 형식의 URI를 사용해야한다.

content://comandroid.book.BookProvider/books
그리고 특정 책(book 23)을 가져오려면
content://comandroid.book.BookProvider/books/23
으로 지정해야줘야한다.

콘텐트 프로바이더의 역할중 데이터 접근보다 캡슐화에 더욱 비중이 있다.
캡슐화를 간단하게 예를 들면..  게임 스타에서 우리가 게임을 즐기지만 실질적으로 안에 소스코드 구현을 잘모른다. 마찬가지로 어플리케이션 데이터에 대해서 안에 소스값을 모르지만 동작하는 인터페이스를 가져다 사용할수 있게하는것이라고 보면 된다.
그래서 이런 내부 데이터를 가져오기위해서 모든 데이터의 저장/접근 이용할수 있다.

그아래로 밑에 4가지가 있다.

Files : 제거 가능한 저장매체에 저장할 수 있는 애플리케이션 내부 파일들
Preferences : 애플리케이션 설정을 저장하도록 지정할 수 있는 키/값 쌍
SQLite : 자신을 생성하는 패키지에만 공개되는 SQLite 데이터베이스들
Network :인터넷을 통해 외부적으로 데이터를 가져오거나 저장할 수 있게 하는 매커니즘




어느 환경에서 정보를 영구적으로 저장하는 방법은 단하나 파일밖에 없다.

안드로이드 자체에서는 파일 관리 기능이 없다.. 그래서 파일 시스템은 리눅스파일시스템을 사용한다. 

여기서 파일 시스템이란  파일을 저장하고 불러오고 검색하고 수정하는 것이다.  그리고 파일을 입출력

하는 라이브러리를 자바를 사용한다. 그래서 리눅스 파일시스템을 대략적으로 이해하고 있어야하며

자바 입출력 스트림은 자유자재로 사용 할수 있어야 한다.

파일 관리 매서드
package kr.co.nsakorea.FileIO;

import java.io.*;

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

public class FileIO extends Activity {
  EditText mEdit;
  
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.fileio);
    
    mEdit = (EditText)findViewById(R.id.edittext);
    findViewById(R.id.save).setOnClickListener(mClickListener);
    findViewById(R.id.load).setOnClickListener(mClickListener);
    findViewById(R.id.loadres).setOnClickListener(mClickListener);
    findViewById(R.id.delete).setOnClickListener(mClickListener);
    findViewById(R.id.clear).setOnClickListener(mClickListener);
  }
  
  Button.OnClickListener mClickListener = new View.OnClickListener() {
    public void onClick(View v) {
      switch (v.getId()) {
      case R.id.clear:
        mEdit.setText("");
        break;
      case R.id.save:
        try {
          FileOutputStream fos = openFileOutput("test.txt"
              Context.MODE_APPEND);
          Editable str =mEdit.getText();//EditText의  getText 반환값이 EditTable임
          fos.write(str.toString().getBytes()); // Editable의 toString()을 가져와서 저장함 
          fos.write("\n".getBytes()); //한줄 띄우는 개행문자
          fos.close();
          mEdit.setText("write success");
        } catch (Exception e) {;}
        
        break;
      case R.id.load:
        try {
          FileInputStream fis = openFileInput("test.txt");
          byte[] data = new byte[fis.available()];
          while (fis.read(data) != -1) {;}
          fis.close();
          mEdit.setText(new String(data));
        } catch (FileNotFoundException e) {
          mEdit.setText("File Not Found");
        }
        catch (Exception e) {;}
        break;
      case R.id.loadres:
        try {
          InputStream fres = getResources().openRawResource(R.raw.restext);
          byte[] data = new byte[fres.available()];
          while (fres.read(data) != -1) {;}
          fres.close();
          mEdit.setText(new String(data));
        } catch (Exception e) {;}
        break;
      case R.id.delete:
        if (deleteFile("test.txt")) {
          mEdit.setText("delete success");
        } else {
          mEdit.setText("delete failed");
        }
        break;
      }
    }
  };
}




위소스는 책이 있는것인데 약간 번형을 한것이다.. 기존에 Button을 이용해서 Save를 하게 되면.
파일 위치는  /data/data/패키지명/files에 저장이 된다. 임의 경로의 파일을 마음대로 열 수는 없다.

adb devices를 통해 연결된 안드로이드 에뮬이나 기기를 확인할수 있다.
들어가는방법은  adb shell 로 인해

cd /data/data/패키지명/files에 가면 test.txt가 보이며
cat test.txt를 통해서 입력한 파일을 볼수 있다.

또는 이클립스에서 DDMS에서 File Explorer를 통해서 파일이 생성 된것을 알수 있다.

하지만 입력된 파일은 따로 로드도 가능하다.


이렇게 test.txt 파일을 확인도 가능하다.


SD 카드
안드로이드 파일 시스템은 OS에 의해  강력하게 보호가 되어있다.
응용프로그램 끼리 서로 침범할 수 없도록 하려는 보안상의 합당한 이유가 있기는 하지만 MP3나 비디오 파일 그림파일처럼 공동으로 사용할 필요가 있는 파일도 있다.
이런 단순 데이터 파일을 저장하기 위한 장소로 SD카드가 사용된다.

그리고 SD 카드가 마운트되는공간은  리눅스 파일 시스템의  /mnt/sdcard 경로로 마운팅 된다.



혹시 sd카드의 공유권한이 읽기 쓰기 아닌경우도 있다.  그래서 이경우 파일을 액세스 하려면
매니페스트에 다음 퍼미션을 지정해야 한다.
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
해야한다.

Uses Permission를 간단하게 예를 들자면  Application에서  이러한 Permission을 사용한다고 알려주는 것이다.  보기에 아무것도 없어 보이지만 어떤 어플리케이션을 만들때 permission에서 통해서 다른 어플리케이션을 접근으로 부터 보호하는 역할을 한다.
즉 내가 만든 A라는 어플에서  컴포넌트로  android:permission='a'를 추가했을경우 반드시
uses-permission = 'a'가 있어야 하는것이라고 생각하면 쉽다.

Permission을 대한 내용은
http://developer.android.com/reference/android/Manifest.permission.html 에서 자세히 설명이 나와 있다
아니면

등이 있다.

SD카드를 사용해서 파일 입출력이다.
package kr.co.nsakorea.SDCard;

import java.io.*;

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

public class SDCard extends Activity {
  EditText mEdit;
  String mSdPath;
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.sdcard);

    mEdit = (EditText)findViewById(R.id.edittext);
    findViewById(R.id.test).setOnClickListener(mClickListener);
    findViewById(R.id.save).setOnClickListener(mClickListener);
    findViewById(R.id.load).setOnClickListener(mClickListener);
    findViewById(R.id.clear).setOnClickListener(mClickListener);
    findViewById(R.id.delete).setOnClickListener(mClickListener);


    String ext = Environment.getExternalStorageState();
    if (ext.equals(Environment.MEDIA_MOUNTED)) {
      mSdPath = Environment.getExternalStorageDirectory().getAbsolutePath();
    } else {
      mSdPath = Environment.MEDIA_UNMOUNTED;
    }
  }

  Button.OnClickListener mClickListener = new View.OnClickListener() {
    public void onClick(View v) {
      switch (v.getId()) {
      case R.id.clear:
        mEdit.setText("");
        break;
      case R.id.test:
        String rootdir = Environment.getRootDirectory().getAbsolutePath();
        String datadir = Environment.getDataDirectory().getAbsolutePath();
        String cachedir = Environment.getDownloadCacheDirectory().getAbsolutePath();
        mEdit.setText(String.format("ext = %s\nroot=%s\ndata=%s\ncache=%s"
            mSdPath, rootdir, datadir, cachedir));
        break;
      case R.id.delete:
        File dird = new File(mSdPath + "/dir");
        dird.mkdir();
        File filed = new File(mSdPath + "/dir/file.txt");
        try {
          FileOutputStream fosdel = new FileOutputStream(filed); //FileOutputStream(file,true)  
                                      //두번째 인자는 append 확인 
          String str = " ";
          fosdel.write(str.getBytes());
          
          
          fosdel.close();
          
          mEdit.setText("Delete success");
        } catch (FileNotFoundException e) {
          mEdit.setText("File Not Found." + e.getMessage());
        }
        catch (SecurityException e) {
          mEdit.setText("Security Exception");
        }
        catch (Exception e) {
          mEdit.setText(e.getMessage());
        }
        break;
      case R.id.save:
        File dir = new File(mSdPath + "/dir");
        dir.mkdir();
        File file = new File(mSdPath + "/dir/file.txt");
        try {
          FileOutputStream fos = new FileOutputStream(file,true); //FileOutputStream(file,true)  
                                      //두번째 인자는 append 확인 
          
/*String str = "This file exists in SDcard";
          fos.write(str.getBytes());
          fos.close();
          */

          Editable str =mEdit.getText();//EditText의  getText 반환값이 EditTable임
          fos.write(str.toString().getBytes());// Editable의 toString()을 가져와서 저장함 
          fos.write("\n".getBytes()); //한줄 띄우는 개행문자
          
          fos.close();
          
          mEdit.setText("write success");
        } catch (FileNotFoundException e) {
          mEdit.setText("File Not Found." + e.getMessage());
        }
        catch (SecurityException e) {
          mEdit.setText("Security Exception");
        }
        catch (Exception e) {
          mEdit.setText(e.getMessage());
        }
        break;
      case R.id.load:
        try {
          FileInputStream fis = new FileInputStream(mSdPath + "/dir/file.txt");
          byte[] data = new byte[fis.available()];
          while (fis.read(data) != -1) {;}
          fis.close();
          mEdit.setText(new String(data));
        } catch (FileNotFoundException e) {
          mEdit.setText("File Not Found");
        }
        catch (Exception e) {;}
        break;
      }
    }
  };
}

크게 틀린점은 없지만.  FileOutputStream 에서 인자의 두번째 값이 Append 확인유무인것과
onCreate에서 이미 SD카드의 엑세스 상태값을 받아오는것이다.
그리고 file의 path로 mSdPaht+"\dir"로  dir.mkdir(); 우잉 mkdir도 보인다.

결과 화면이닷~!

TextLog
이클립스 디버깅 지원하는데  다음소스는 그냥 실무에서 사용한 소스에서 분석 해본다.
================================================================================
TextLogTest.java이다.
package exam.Data;

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

public class TextLogTest extends Activity {
  LinearLayout mLinear;

  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.textlogtest);

    // onCreate에서 로그 유틸리티 초기화
    TextLog.init(this);

    mLinear = (LinearLayout)findViewById(R.id.linear);
    mLinear.setOnTouchListener(new View.OnTouchListener() {
      public boolean onTouch(View v, MotionEvent event) {
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
          // 필요할 때 로그 기록
          lg.o("down. x = " + (int)event.getX() + 
              ", y = " + (int)event.getY());
          return true;
        case MotionEvent.ACTION_MOVE:
          lg.o("move. x = " + (int)event.getX() + 
              ", y = " + (int)event.getY());
          return true;
        }
        return false;
      }
    });
  }

  // 다음 두 메서드를 디버깅 프로젝트의 엑티비티에 추가한다.
  public boolean onCreateOptionsMenu(Menu menu) {
    super.onCreateOptionsMenu(menu);
    TextLog.addMenu(menu);
    return true;
  }

  public boolean onOptionsItemSelected(MenuItem item) {
    if (TextLog.execMenu(item) == true) {
      return true;
    }
    return false;
  }
}

이소스에서 중요한 부부은 그다지 없다. TextLog.init(this); 변수들의 초기화 해주는것
그리고 일반적으로 화면을 터치했을때 그 X값과 Y값을  lg.o를 통해서 클래스로 전달해주고 옵션메뉴를 추가해주는것이다.
=====================================================================
TextLog.java 파일이다.

package exam.Data;

import java.io.*;

import android.app.*;
import android.content.*;
import android.os.*;
import android.util.*;
import android.view.*;

//Android log write utility class.
//author : winapi@winapi.co.kr
//usage : 1.call init(this) at onCreate of main activity
//      2.call TextLog.o(some string) whenever you need
//      3.call TextLog.ViewLog() to inspect log
//      you can change mFile, mDir, mTag by direct assigning if need.
public class TextLog {
  static Context mMain;
  static String mFile;
  static String mTag;
  static int mDir;
  static final int DIR_SELF = 1;
  static final int LOG_SDCARD = 2;
  static final int LOG_SYSTEM = 4;
  static boolean mHaveSD;
  static String mSDPath;

  // init default setting.
  public static void init(Context main) {
    mMain = main;
    mFile = "andlog.txt";
    mTag = "textlog";
    mDir = DIR_SELF | LOG_SDCARD;
    mHaveSD = Environment.getExternalStorageState()
    .equals(Environment.MEDIA_MOUNTED);
    if (mHaveSD) {
      mSDPath = Environment.getExternalStorageDirectory()
      .getAbsolutePath();
    }
    o("start time:" + System.currentTimeMillis());
  }

  // delete log file neatly.
  public static void reset() {
    if ((mDir & DIR_SELF) != 0) {
      mMain.deleteFile(mFile);
    }
    if ((mDir & LOG_SDCARD) != 0 && mHaveSD) {
      File file = new File(mSDPath + "/" + mFile);
      file.delete();
    }
    o("reset time:" + System.currentTimeMillis());
  }

  // write string to log.
  public static void o(String text) {
    // null test is needed because some exception's getMessage() return null
    if (text == null) {
      return;
    }

    if ((mDir & DIR_SELF) != 0) {
      FileOutputStream fout = null;
      try {
        fout = mMain.openFileOutput(mFile, Context.MODE_APPEND);
        if (fout != null) {
          fout.write(text.getBytes());
          fout.write("\n".getBytes());
        }
      } 
      catch (Exception ex) {
        // silent fail
      }
      finally {
        try {
          if(fout != null) fout.close();
        }
        catch (Exception e) { ; }
      }
    }

    if ((mDir & LOG_SDCARD) != 0 && mHaveSD) {
      File file = new File(mSDPath + "/" + mFile);
      FileOutputStream fos = null;
      try {
        fos = new FileOutputStream(file, true);
        if (fos != null) {
          fos.write(text.getBytes());
          fos.write("\n".getBytes());
        }
      } 
      catch (Exception e) {
        // silent fail
      }
      finally {
        try {
          if(fos != null) fos.close();
        }
        catch (Exception e) { ; }
      }
    }

    if ((mDir & LOG_SYSTEM) != 0) {
      Log.d(mTag, text);
    }

  }

  // always view self log file only. you should inspect log in SDcard 
  // using external program. for example file explorer or terminal
  public static void ViewLog() {
    String path;
    int ch;

    path = mMain.getFileStreamPath(mFile).toString();

    StringBuilder Result = new StringBuilder();
    BufferedReader in = null;
    try {
      in = new BufferedReader(new FileReader(path));
      if (in != null) {
        for (;;) {
          ch = in.read();
          if (ch == -1break;
          Result.append((char)ch);
        }
      }
    }
    catch (Exception e) {
      Result.append("log file not found");
    }
    finally {
      try {
        if(in != null) in.close();
      }
      catch (Exception e) { ; }
    }

    new AlertDialog.Builder(mMain) //대화상자 만들기 
    .setMessage(Result.toString())  //결과값을 메세지로 찍
    .setTitle("Log")        //대화상자 제목
    .setPositiveButton("OK"null// 확인버튼 
    .show();        
  }

  public static void addMenu(Menu menu) {
    menu.add(0,101092+1,0,"ViewLog");  //Menuitem add(int groupid,int itemId,int order,CarSequence title)
    menu.add(0,101092+2,0,"ResetLog");
  }

  public static boolean execMenu(MenuItem item) { 
    switch (item.getItemId()) {
    case 101092+1:
      ViewLog();
      return true;
    case 101092+2:
      reset();
      return true;
    }
    return false;
  }
}

// wrapper class for internal package. 
// call shortly lg.o() instead of TextLog.o() 
class lg {
  public static void o(String text) {
    TextLog.o(text);
  }
}

이제 여기서 자세히 봐야할곳이 많다.
TextLogTest.java에서 넘겨받는 여러값이 있지만 거의 모든 함수가 전부 static으로 전역화 되어 있다.

mHaveSD = Environment.getExternalStorageState()
    .equals(Environment.MEDIA_MOUNTED);
일단 여기서 SD카드의 존재 유무를 확인한다. 
 if (mHaveSD) {
      mSDPath = Environment.getExternalStorageDirectory()
      .getAbsolutePath();
    }
그리고 SD카드가 장착되어 있을때 SD카드가 저장되는공간의 위치를 mSDPath에 저장한다.

o("start time:" + System.currentTimeMillis());를
통해서   처음시작하는 값을 가장먼저 나타나게된다.  TextLogTest.java에  TextLog.init(this)로인해서
바로 처음값이 저걸로 선택이 된다.
public static void o(String text)가. 거의 모든 정보를 저장하게 된다.
저장한 공간이 /data/data/패키지/files/로 값이 저장될지
SD카드로 인대 /mnt/sdcard/에 저장되는거는 if(mdir&DIR_SELF!=0)일지  확인하는데 보통 파일은 그대로 저장하고 SD카드유무가 제일 중요하다. if ((mDir & LOG_SDCARD) != 0 && mHaveSD)
public static void ViewLog()를 통해서 화면에 뿌려주는데
new AlertDialog.Builder(mMain) //대화상자 만들기
  .setMessage(Result.toString()) //결과값을 메세지로 찍
  .setTitle("Log")    //대화상자 제목
  .setPositiveButton("OK", null) // 확인버튼
  .show();
대화상자를 만들어주며 결과값을 나타냅니다.
public static void addMenu(Menu menu) 로 메뉴의 옵션을 통해 결과값을 찍을지 지울지 선택한다.

결과값이다.



Preferences(환경설정)
어플리케이션 설정정보를 영구적으로 저장하는 장치이다.  즉 어플리케이션에 만들때 그에 관한 설정을 (사용자설정)만들수있고 안드로이드에서 제공하는 디폴트 설정 둘다 그냥 구분없이 어플리케이션 환경설정(Application preference)이라고 명한다.
이런 Preferences의 데이터를 관리하는 클래스를 SharedPerferences이다.

SharedPerferences getSharedPreferences(String name, int mode)
첫번째 인수는 저장할 XML이름이다. 아무래도 영구적으로 사용하려면 환경설정으로 따로 저장해둬야한다.
모드는 0이면 읽고 쓰기  MODE_WORLD_READALBE이면 읽기 공유  MODE_WORLD_WRITEALBE로 쓰기 공유로 열수 도있다.

package exam.Data;

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

public class PrefTest extends Activity {
  TextView textName;
  TextView textStNum;
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.preftest);

    textName = (TextView)findViewById(R.id.name);
    textStNum = (TextView)findViewById(R.id.stnum);

    SharedPreferences pref = getSharedPreferences("PrefTest",0);
    String Name = pref.getString("Name""이름없음");
    textName.setText(Name);

    int StNum = pref.getInt("StNum",20101234);
    textStNum.setText("" + StNum);
  }

  public void onPause() {
    super.onPause();

    SharedPreferences pref = getSharedPreferences("PrefTest",0);
    SharedPreferences.Editor edit = pref.edit();

    String Name = textName.getText().toString();
    int StNum = 0;
    try {
      StNum = Integer.parseInt(textStNum.getText().toString());
    }
    catch (Exception e) {}

    edit.putString("Name", Name);
    edit.putInt("StNum", StNum);

    edit.commit();
  }
}



/data/data/패키지명/Shared_prefs/안에  저장이 된다 .

xml이라서 shell통해서 확인 할수가 없다  하지만 무적 이클립스는 다해준다.

밖으로 꺼내면 된다.

DDMS에서  File Explorer를 통해서 보면
오른쪽 상단에 Pull a fale from the device라고 하면 밖으로 빼진다.
사실 아마 shell에서 하는 방법이 있을꺼지만.... 아직은 모르겠다.



통해서 xml파일을 볼수 있다.
  <?xml version="1.0" encoding="utf-8" standalone="yes" ?>
- <map>
  <int name="StNum" value="20021550" />
  <string name="Name">leessang</string>
  </map>

그값이다.
위소스에서 가장 중요한점은

public void onPause()이다. 원래 onStop()이나 onDestroy()에서 프레프런스를 만들어야하지만  이는 중간에 메모리가 없을때..  onStop() onDestroy()이가  발생하지 않기때문에 onPause()에서 만든다.

그리고  getSharedPreferences를 통해  프레프런스를 읽어드리고  그에따른 여러가지 타입중에 문자열 정수타입을 지정했다. getString과 getInt로  여기서 첫번째 인수값을 KEY값으로 가져오고 뒤에 인수값을 만약 없다면 기본값으로 지정해주는것이다.

그리고 프레프런스를 저장하는것은 바로  이너클래스인 SharedPreferences.Editor가 제공한다.  프레프런스의 edit메서드를 호출하고 Editor객체를 얻은수 사용하게 된다.

그에 대한값으로는 putInt(),putString() putBoolean() remove() clear()가 있다. 그리고 마지막으로 commit메서드를 반드시 호출해서 파일에 기록한다.


다음은 PreferenceActivity에 대한내용이다
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
   xmlns:android="http://schemas.android.com/apk/res/android">
   <EditTextPreference
      android:key="age"
      android:title="나이"
      android:summary="너 도대체 몇 살이니?"
      android:defaultValue="19" 
   />
   <CheckBoxPreference
      android:key="male"
      android:title="성별"
      android:summary="남자면 체크"
      android:defaultValue="true" 
   />
</PreferenceScreen>


이는 액티비티 레이아웃 루트에 반드시 PreferenceScreen이어야하고 그안 옵션에따라가 값을 바꿀수 있다.
EditText CheckBox 그리고 List Ringtone 이 있으며
그의 속석으로

android:key  옵션의 이름 또는 키
android:title  옵션 제목
android:summary  옵션 용도 요약
android:entries  옵션 목록 항목 문구
android:entryValues 각 항목모다 키, 값을 정의함
android:dialogTitle 대화창 제목
android:defaultValue 목록 항목들중 옵션의 기본값

첫번째 소스처럼 JAVA에서 구현이 가능하고 두번째 소스처럼 res로 Activity로 인해 layout으로 구현이 가능하다. 마찬가지로 환경설정에 관한 XML코드는 역시 /data/data/패키지/shared_prefs/안에 있다.


SQLite
도우미클래스
데이터베이스라고 생각하면 간단하다
세상에 모든 정보를 관리하기위해서 기존에서 파일구조를 따라서 잡아넣게 되었다.  하지만 그렇게 되면 정보의 관리의 생성저장 을 보다 효율적으로 하기위해서 나온것을 데이터베이스라고생각하면 된다. 그런 DB가 강력해진 이유가 바로 SQL이다. 구조적 질의 언어... structured query language 또하나는 DBMS이다 하지만 상용화된 거대한 DBMS와 달리 안정적이고 용량이 적은 데이터베이스에 적합한 SQLite는 바로 무료다.

안드로이드에서 DB를 관리하기위해사 SQLite를 채택했고 SQLiteOpenHelper라는 도우미 클래스를 제공했다.
SQLiteOpenHelper(Copntext context, String name, SQLiteDatabase.CursorFactory factory, int version)

SQL 기초 문법
select * from table1;
테이블의 모든 행 선택

select count(*) from table1;
테이블의 행의 수를 셈

selsct col1, col2 from table1;
특정 열들의 선택해서 보여줌

select distinct col1 form table1;
열에서 무중복 값들을 선택

select count(col1) from( select distinct co1 from table1);
무종복 값들을 셈

select count(*), col1 from table1 group by col1;
그룹으로 묶음

select * from table t1, table t2 where t1.col1=t2.col1;
일반적인 내부 결합

CREATE TABLE tbl2 (
 f1 varchar(30) primary key,
 f2 text,
 f3 real
 );
테이블 tbl2를 만들고 그 속성값을 f1 , f2 ,f3 이며 f1이 기본키 가변길이 30이며
insert into tbl1 values('hello!',10);
tbl1이라는 테이블에 첫번째 속성로 hello!를 넣었고 두번째 10을 넣었다.
delete from tbl1 where two<20;
테이블 tbl1에서 two값이 20보다 작을경우 삭제한다.
여기서 where을 넣지 않을경우 테이블안에 모든 값을 다지운다
대신 테이블은 그대로 남아있다.
Update  tbl1 set han ='소년' where eng='boy';
tbl1에서 eng속성값이 boy인경우 han에 '소년'를 추가시킨다.
CREATE TABLE dic(_id INTEGER PRIMARY KEY AUTOINCREMEN ,end TEXT, han TEXT);
dic 테이블을 만들고 _id 정수형 기본키를 가지며 end의 TEXT han TEXT 를 만들었다.
DROP TABLE IF EXISTS dic
dic 테이블 지운다.

다음 간단한 SQLite를 이용한 소스코드이다.

package exam.Data;

import android.app.*;
import android.content.*;
import android.database.*;
import android.database.sqlite.*;
import android.os.*;
import android.view.*;
import android.widget.*;

public class EnglishWord extends Activity {
  WordDBHelper mHelper; // SQLiteOpenHelper를 상속받는 클래스 
  EditText mText;
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.englishword);

    mHelper = new WordDBHelper(this);
    mText = (EditText)findViewById(R.id.edittext);

    findViewById(R.id.insert).setOnClickListener(mClickListener);
    findViewById(R.id.delete).setOnClickListener(mClickListener);
    findViewById(R.id.update).setOnClickListener(mClickListener);
    findViewById(R.id.select).setOnClickListener(mClickListener);
  }
  
  Button.OnClickListener mClickListener = new View.OnClickListener() {
    public void onClick(View v) {
      SQLiteDatabase db;
      ContentValues row;
      switch (v.getId()) {
      case R.id.insert:
        db = mHelper.getWritableDatabase();
        // insert 메서드로 삽입
        row = new ContentValues();
        row.put("eng""boy");
        row.put("han""머스마");
        db.insert("dic"null, row);
        // SQL 명령으로 삽입
        db.execSQL("INSERT INTO dic VALUES (null, 'girl', '가시나');");
        mHelper.close();
        mText.setText("Insert Success");
        break;
      case R.id.delete:
        db = mHelper.getWritableDatabase();
        // delete 메서드로 삭제
        db.delete("dic"nullnull);
        // SQL 명령으로 삭제
        //db.execSQL("DELETE FROM dic;");
        mHelper.close();
        mText.setText("Delete Success");
        break;
      case R.id.update:
        db = mHelper.getWritableDatabase();
        // update 메서드로 갱신
        row = new ContentValues();
        row.put("han""소년");
        db.update("dic", row, "eng = 'boy'"null);
        // SQL 명령으로 갱신
        //db.execSQL("UPDATE dic SET han = '소년' WHERE eng = 'boy';");
        mHelper.close();
        mText.setText("Update Success");
        break;
      case R.id.select:
        db = mHelper.getReadableDatabase();
        Cursor cursor;
        // query 메서드로 읽기
        //cursor = db.query("dic", new String[] {"eng", "han"}, null, 
        //    null, null, null, null);
        // SQL 명령으로 읽기
        cursor = db.rawQuery("SELECT han,eng FROM dic"null);
      
        String Result = "";
        while (cursor.moveToNext()) {
          String eng = cursor.getString(0);
          String han = cursor.getString(1);
          Result += (eng + = " + han + "\n");
        }

        if (Result.length() == 0) {
          mText.setText("Empyt Set");
        } else {
          mText.setText(Result);
        }
        cursor.close();
        mHelper.close();
        break;
      }
    }
  };
}

class WordDBHelper extends SQLiteOpenHelper {
  public WordDBHelper(Context context) {
    super(context, "EngWord.db"null1);
  }

  public void onCreate(SQLiteDatabase db) {
    db.execSQL("CREATE TABLE dic ( _id INTEGER PRIMARY KEY AUTOINCREMENT, " +
    "eng TEXT, han TEXT);");
  }

  public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    db.execSQL("DROP TABLE IF EXISTS dic");
    onCreate(db);
  }
}
여기소스에서 중요하게 잡아야할 포인트가 몇가지가 있다.
ContentValues와 SQLiteDatabase 가있다.
ContentValues 클래스는 테이터의 추가와 갱신에 SQL문을 사용하는 방법 외에 개별메소를 준비 하는것이다.
SQL문을 이용하기 위해서는 SQLiteDatabase이다.
row = new ContentValues();
    row.put("eng", "boy");
    row.put("han", "머스마");
    db.insert("dic", null, row);
통해서  row.put을 통해서 메소드 정의와 정의값을 적을수 있다.
그리고 같은 방법으로
db.execSQL("INSERT INTO dic VALIES (null,'girl','가시나');");
이렇게도 넣을수 있다.
하지만 이같은경우는 강제 이값을 넣은경우이고
만약에 전화번호북을 만들경우 row.put부분에 EditText에서 가져온값을
넣어서 저장하게 되면 만들수 있게 도니다.
그저 사용방법중에 어느게 쉽나하면 사실 DB를 잘한다면 execSQL를 선택하는 경우가 좋지만 그게 아니라면 ContentValues를 이용해서 사용하면 좀더 쉽다. 
SQL의 기본문법에서 조금 숙지한다면 많이 도움 된다.
그리고 getWritableDatabase()로 열어버린 경우 꼭 close()를 시켜줘야한다.
이하 delete와 insert와 update 다 같다.
그리고 select도 마찬가지며
사실 DB에서 가장중요한것은 select라고 생각한다. 데이터를 저장하는것도 지우는것도 쉽지만 저장한후 정보를 가져와 다시 볼때가 많기 때문이다.
cursor = db.rawQuery("SELECT han,eng FROM dic", null)
cursor를 통해서 쿼리의대한 결과를 알수있다.  그에 대한 메서드는
책 페이지 521이 있다. 그중 우리가 사용한것은 moveToNext를 사용하여 마지막 레코드 일때 false를 수행한다.
그래서 while()을 통해서 마지막까지 돌면서 그에 대한 값을 하나씩 넣는다.

다음소스는 앞에 생성된 쿼리가 딱 2개 지만 너무 많을때 사용하는 방법이다
package exam.Data;

import android.app.*;
import android.content.*;
import android.database.*;
import android.database.sqlite.*;
import android.os.*;
import android.widget.*;

public class ProductList extends Activity {
  ProductDBHelper mHelper;
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.productlist);
    
    mHelper = new ProductDBHelper(this);
    Cursor cursor;
    SQLiteDatabase db = mHelper.getWritableDatabase();

    cursor = db.rawQuery("SELECT * FROM product"null);
    startManagingCursor(cursor);

    SimpleCursorAdapter Adapter = null;
    Adapter = new SimpleCursorAdapter(this
        android.R.layout.simple_list_item_2,
        cursor, new String[] { "name""price" }, 
        new int[] { android.R.id.text1, android.R.id.text2});
    ListView list = (ListView)findViewById(R.id.list);
        list.setAdapter(Adapter);
  }
}

class ProductDBHelper extends SQLiteOpenHelper {
  public ProductDBHelper(Context context) {
    super(context, "Product.db"null1);
  }

  public void onCreate(SQLiteDatabase db) {
    db.execSQL("CREATE TABLE product ( _id INTEGER PRIMARY KEY AUTOINCREMENT, " +
      "name TEXT, price INTEGER);");
    db.execSQL("INSERT INTO product VALUES (null, '오징어 땅콩', 900);");
    db.execSQL("INSERT INTO product VALUES (null, '농심 포테이토 칩', 2000);");
    db.execSQL("INSERT INTO product VALUES (null, '로보트 태권 V', 1000);");
    db.execSQL("INSERT INTO product VALUES (null, '꼬마 자동차 붕붕', 1500);");
    db.execSQL("INSERT INTO product VALUES (null, '윈도우즈 API 정복', 32000);");
    db.execSQL("INSERT INTO product VALUES (null, '롯데 인벤스 아파트', 190000000);");
    db.execSQL("INSERT INTO product VALUES (null, '88 라이트', 1900);");
    db.execSQL("INSERT INTO product VALUES (null, '프라이드 1.6 CVVT 골드', 8900000);");
    db.execSQL("INSERT INTO product VALUES (null, '캐리비안 베이 입장권', 25000);");
  }

  public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    db.execSQL("DROP TABLE IF EXISTS product");
    onCreate(db);
  }
}

어탭터에 커서를 바인딩해서 어댑터 뷰로 출력하는것이다.
 SimpleCursorAdapter Adapter = null;
  Adapter = new SimpleCursorAdapter(this,
    android.R.layout.simple_list_item_2,
    cursor, new String[] { "name", "price" },
    new int[] { android.R.id.text1, android.R.id.text2});
  ListView list = (ListView)findViewById(R.id.list);
        list.setAdapter(Adapter);
딱 핵심 이다.  simpleCursorAdapter를 통해서 가져오는데
일단 DB의 값을 XML로 정의후  다시 가져온다.
sql의 name을 R.id.text1에 price를 text2로 정의한다.
출력값


Content Provider
URI 정보의 고유 명칭으로 Uniform Resource Identifier
웹상에서 URL보다 더상위의 개념이다.
어떤 정볼르 제공하는지 어떤정보를 원하는지 대한정보가 URI에 작성된다.
사용방법은 국제 표준(RFC 2396)에 URI를 작성하는 방식은 다음과 같다.
content://authority/path/id

content는 URI임을 나타내는 접두이고

authority는 보통 패키지명으로 사용된다.

path는 정보의 종류를 지정하는 가상의 경로이며

id는 구체적은 정보이다.

일단 CP는 간단하게 자료공유파일 그리고 사용파일 2가지로 나누어진다.

어느 어플리케이션에 관해서 자료를 공유해주고 그 어플리케이션을 가져와서 사용할

수 있는 메소드를 제공받는다.

그리고 여기서 공유와 사용 둘다 보겠지만 Android에서 제공하는 경우  Phonebook

이나 People 같이 기본으로 제공하는 어플같은경우

http://developer.android.com/reference/android/provider 에 SDK문서가

제공되기때문에 한번 봐야한다.

우리가 만든 어플 파일을 공유하는 법 그리고 사용법 둘다 보겠다.

사실 소스코드를 분석결과 사실 제공하는  Content Provider 메소드 재정의 밖에

없다.

공유부분에서는 Matcher.match와 addURI 부분이 가장중요하다. 여기서 보통 자료

는 일반 파일일경우도 있지만 데이터를 쉽게 관리하기 위해서 DB로 된경우가  거의

전부 다이기때문에 당연히 cursor와 Query를 사용한다.

앞장에서 만든 EnglishWord를 공유한다.

그소스는


package exam.Data;

import android.content.*;
import android.database.*;
import android.database.sqlite.*;
import android.net.*;
import android.text.*;

public class EWProvider extends ContentProvider {
  static final Uri CONTENT_URI = Uri.parse("content://exam.Data.EnglishWord/word");
  static final int ALLWORD = 1;
  static final int ONEWORD = 2;
  
  static final UriMatcher Matcher;
  static {
    Matcher = new UriMatcher(UriMatcher.NO_MATCH);
    Matcher.addURI("exam.Data.EnglishWord""word", ALLWORD);
    Matcher.addURI("exam.Data.EnglishWord""word/*", ONEWORD);
  }
  
  SQLiteDatabase mDB;

  public boolean onCreate() {
    WordDBHelper helper = new WordDBHelper(getContext());
    mDB = helper.getWritableDatabase();
    return true;
  }

  public String getType(Uri uri) {
    if (Matcher.match(uri) == ALLWORD) {
      return "vnd.EnglishWord.Data.cursor.item/word";
    }
    if (Matcher.match(uri) == ONEWORD) {
      return "vnd.EnglishWord.Data.cursor.dir/words";
    }
    return null;
  }

  public Cursor query(Uri uri, String[] projection, String selection,
      String[] selectionArgs, String sortOrder) {
    String sql;
    
    // 전체에 대한 쿼리 명령
    sql = "SELECT eng, han FROM dic";
    
    // 단어 선택 where절 추가
    if (Matcher.match(uri) == ONEWORD) {
      sql += " where eng = '" + uri.getPathSegments().get(1) + "'";
    }

    Cursor cursor = mDB.rawQuery(sql, null);
    return cursor;
  }

  public Uri insert(Uri uri, ContentValues values) {
    long row = mDB.insert("dic"null, values);
    if (row > 0) {
      Uri notiuri = ContentUris.withAppendedId(CONTENT_URI, row);
      getContext().getContentResolver().notifyChange(notiuri, null);
      return notiuri;
    }
    return null;
  }

  public int delete(Uri uri, String selection, String[] selectionArgs) {
    int count = 0;
    
    //*
    switch (Matcher.match(uri)) {
    case ALLWORD:
      count = mDB.delete("dic", selection, selectionArgs);
      break;
    case ONEWORD:
      String where;
      where = "eng = '" + uri.getPathSegments().get(1) + "'";
      if (TextUtils.isEmpty(selection) == false) {
        where += " AND" + selection;
      }
      count = mDB.delete("dic", where, selectionArgs);
      break;
    }
    
    getContext().getContentResolver().notifyChange(uri, null);
    return count;
    //*/
    
    /*
    String sql;
    
    // 전체에 대한 쿼리 명령
    sql = "DELETE FROM dic";
    
    // 단어 선택 where절 추가
    if (Matcher.match(uri) == ONEWORD) {
      sql += " where eng = '" + uri.getPathSegments().get(1) + "'";
    }
    mDB.execSQL(sql);
    return 1;
    //*/

  }

  public int update(Uri uri, ContentValues values, String selection,
      String[] selectionArgs) {
    int count = 0;
    
    switch (Matcher.match(uri)) {
    case ALLWORD:
      count = mDB.update("dic", values, selection, selectionArgs);
      break;
    case ONEWORD:
      String where;
      where = "eng = '" + uri.getPathSegments().get(1) + "'";
      if (TextUtils.isEmpty(selection) == false) {
        where += " AND " + selection;
      }
      count = mDB.update("dic", values, where, selectionArgs);
      break;
    }
    
    getContext().getContentResolver().notifyChange(uri, null);
    return count;
  }
}


===================================================================
Matcher = new UriMatcher(UriMatcher.NO_MATCH);
    Matcher.addURI("exam.Data.EnglishWord""word", ALLWORD);
    Matcher.addURI("exam.Data.EnglishWord""word/*", ONEWORD);

일단 Matcher를 생성해주고 가져올 URI 패키지 그리고 PATH  code로 전체 다가져올지 하나만 가져올지 정한다.  당연히 패키지는 exam.Data.EnglishWord로 우리
기존것을 가져온다.

====================================================================
public boolean onCreate() {
    WordDBHelper helper = new WordDBHelper(getContext());
    mDB = helper.getWritableDatabase();
    return true;
  }

onCreate에서 DB을 열어준다.
====================================================================

  public Cursor query(Uri uri, String[] projection, String selection,
      String[] selectionArgs, String sortOrder) {
    String sql;
    
    // 전체에 대한 쿼리 명령
    sql = "SELECT eng, han FROM dic";
    
    // 단어 선택 where절 추가
    if (Matcher.match(uri) == ONEWORD) {
      sql += " where eng = '" + uri.getPathSegments().get(1) + "'";
    }

    Cursor cursor = mDB.rawQuery(sql, null);
    return cursor;
  }

DATA를 가져올때 Cursor를 사용한다.  그리고 query에서 첫번째 인자는 URI 패키이며 두번째인자는 반환할 행이다. DB에서 열이름을 나타내는 문자열 배열에 불가하다. 만약 이 DB내용을 모른다면 projection을 생성하기가 힘들다. 하지만 책예제는 간단한 DB이기때문에 전부다 가져오게 되어있다.
selection은 가져올 DB에서 테이블의 특정 조건값이다. 예를 들면 where절이라고 보면된다. 이후 selectionArgs,sortOrder같은경우는 SQL문에서 where절이 ?일때 제공하는 특정 조건 그리고 sortOrder order by나타낸다.
==================================================================
 Uri notiuri = ContentUris.withAppendedId(CONTENT_URI, row);
      getContext().getContentResolver().notifyChange(notiuri, null);

insert같은경우 마지막줄에 두가지가 있다 여기서 특징은  ContentUris.withAppendedId를 통해서  URI에 ID를 추가하는것을 지원해주며

getContentResolver()는 Resolve는 Query를 통해서 CP와 간접적으로 통신하는 표준 인터페이스이다.  그래서 그 값이 insert를 호출하면서 변경이 되므로 변경ㄴ된 값을 notiuri로 반환해준다.
===================================================================
혹시나 이 DB에대한정보를 읽기전용으로할경우  insert와 update delete는 생략이 가능하다.

공유하는 cp를 매니페스트에 등록한다.
<provider android:name="EWProvider"
           android:authorities="exam.Data.EnglishWord"
           />

CP의 사용
이렇게 공유한 CP를 사용를 해야한다.

package exam.Data;

import android.app.*;
import android.content.*;
import android.database.*;
import android.net.*;
import android.os.*;
import android.view.*;
import android.widget.*;

public class CallWordCP extends Activity {
  static final String WORDURI = "content://exam.Data.EnglishWord/word";
  EditText mText;
  public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.callwordcp);

    mText = (EditText)findViewById(R.id.edittext);

    findViewById(R.id.readall).setOnClickListener(mClickListener);
    findViewById(R.id.readone).setOnClickListener(mClickListener);
    findViewById(R.id.insert).setOnClickListener(mClickListener);
    findViewById(R.id.delete).setOnClickListener(mClickListener);
    findViewById(R.id.update).setOnClickListener(mClickListener);
    }
    
  Button.OnClickListener mClickListener = new Button.OnClickListener() {
    public void onClick(View v) {
      ContentResolver cr = getContentResolver();
      switch (v.getId()) {
      // 전부 읽기
      case R.id.readall:
        Cursor cursor = cr.query(Uri.parse(WORDURI), nullnullnullnull);//SQL 쿼리통해 커서위치
      
        String Result = "";
        while (cursor.moveToNext()) {
          String eng = cursor.getString(0);
          String han = cursor.getString(1);
          Result += (eng + = " + han + "\n");
        }

        if (Result.length() == 0) {
          mText.setText("Empyt Set");
        } else {
          mText.setText(Result);
        }
        cursor.close();
        break;
      // 하나만 읽기
      case R.id.readone:
        Cursor cursor2 = cr.query(Uri.parse(WORDURI + "/boy"), 
            nullnullnullnull);

        String Result2 = "";
        if (cursor2.moveToFirst()) {
          String eng = cursor2.getString(0);
          String han = cursor2.getString(1);
          Result2 += (eng + = " + han + "\n");
        }
        
        if (Result2.length() == 0) {
          mText.setText("Empyt Set");
        } else {
          mText.setText(Result2);
        }
        cursor2.close();
        break;
      // 삽입
      case R.id.insert:
        ContentValues row = new ContentValues();
        row.put("eng""school");
        row.put("han""학교");

        cr.insert(Uri.parse(WORDURI), row);
        mText.setText("Insert Success");
        break;
      // 삭제
      case R.id.delete:
        cr.delete(Uri.parse(WORDURI), nullnull);
        mText.setText("Delete Success");
        break;
      // 수정
      case R.id.update:
        ContentValues row2 = new ContentValues();
        row2.put("han""핵교");
        cr.update(Uri.parse(WORDURI + "/school"), row2, nullnull);
        mText.setText("Update Success");
        break;
      }
    }
  };
 }
====================================================================


이미 CP를 공유하는곳에서 메소드를 정의를 하였고 만약 외부에 어플을 지원할때는 사용하는 방법을 메뉴얼로 해서 주기만하면 사용이 가능하다. 
이렇게 공유한 DATA를 통해서 사용하면 보안적으로 약해보이지만 사실 permission을 통해서 제한하면 특정 어플만 접근할수 있게 한다.

추가 Tip
Content Provider Query방법

컨텐트 프로바이더에 쿼리하기 위해서는 우선
ContentResolver.query() 메소드 또는 Activity.managedQuery() 메소드를 사용할 수 있습니다.
여기의 두 메소드 모두는 동일한 매개변수 집합을 가지며, 둘다 커서 객체를 리턴해 줍니다.

managedQuery() 메소드는 액티비티로 하여금 커서의 생명주기를 관리할 수 있게 합니다. 매니지드 커서는 액티비티가 멈출 때 스스로를 제거하고, 액티비티가 다시 시작될 때 스스로를 재쿼리 하는것과 같이 모든것을 관리해 줍니다.

Activity.startManagingCursor() 메소드를 호출함으로써, 관리되지 않는 커서 객체를 액티비티가 관리하도록 할 수도 있습니다. query() 메소드나 managedQuery() 메소드에 대한 첫번째 메개변수는 프로바이더의 URI 입니다.

책에서는 ContentResolver.query() 를 사용했다.



-CP사용할때 중요한 5가지-
1. 공유할 패키지의 DATA에 대한 클래스를 생성하고 Content Provider를 상속받는다.
2.URI를 기반으로하는(Matcher.match(uri))DATA와 cursor그리고 return하는 fonction(사용)만들어줌
3.Manifest에 Content Provider대한 내용 기재한다.
4.Content Resolver객체 구하기
5.Manifest에 등록된 authority정보를 URI로 하여 Content Resolver를 통해 기능수행
























'2010년 > 완전정복' 카테고리의 다른 글

8장 어댑터 뷰  (0) 2011.03.03
7장 위젯  (0) 2011.03.03
6장 메뉴  (0) 2011.02.28
5장 입력  (0) 2011.02.22
3장 레이아웃 4장 캔버스  (0) 2011.02.21