본문 바로가기

IT_Story/안드로이드

Android App Components -2(Content Provider, Broadcast Receiver)

Android App Components - 2

1. Content Provider

1.1 소개

  • 안드로이드 어플리케이션간의 데이터 공유를 하기 위해 사용하는 컴포넌트
  • 모든 어플리케이션이 Content Provider를 통해서 특정 데이터에 접근 하여 저장 및 검색 이 가능하다.
  • 데이터를 읽고 쓰기 위해서는 적절한 permission을 adroidmanifest.xml에 등록해야한다.
  • ContentProvider로 데이터를 접근하기 위해서는 ContentReseolver라는 객체가 필요하다.
  • ContentPRovider는 기본적으로 "CRUD"메소드를 제공한다.
  • activity클래스 내의 getContentResolver()메소드를 통해 ContentResolver를 얻을 수 있다.
    ContentResolver cr = getContentRseolver();

    (알기 쉽게 그림 예제)

  • ContentPRovider의 데이터 모델은 데이터메이스 모델 상의 간단한 테이블처럼 보여준다.
    • 각 행은 레코드, 각 열은 특정 타입과 데이터
      (예시)
_IDNUMBERNUMBER_KEYLABELNAMETYPE
13(425)555-6677425 555 6677Kirkland officeAlan VainTYPE_WORK
44(02)123-423102 123 4231HomeMr.KimTYPE_HOME
55(031)784-2247031 784 2247NHN OfficeLee JaeyeonTYPE_WORK
71(02)784-1000031 784 1000OfficeABCDTYPE_WORK

1.2 Creating a Content Provider

  • 데이터저장을 위해 파일저장 메소드나 SQLite DB를 사용한다.
  • 안드로이드는 DB를 생성할 때 유용한 SQLiteOpenHelper 클래스와 관리를 쉽게 하기 위해 SQLiteDatabase를 제공한다.
  • DB에 접근을 제공하기 위해 ContentProvider 클래스를 상속받고 데이터를 관리하고 제공하는 메서드들을 재정의해야한다.
    • query() , insert(), update(), delete(), getType(), onCreate()
  • ContentProvider.query() 메소드는 Cursor 객체를 반환한다.
  • ContentProvider클래스에는 MINE type을 반환받는 두가지 메소드가 있다.
    • getType() : database를 제공할 때 사용하는 메소드
    • getStreamTypes() : provider에서 file data를 제공할 때 구현해야한다.

1.3 Querying a Content Provider

  • Content Provider를 쿼리하기 위해서는 3가지 정보가 필요하다.
    • Provider를 식별하는 URI
    • 받고자 하는 데이터 필드의 이름들
    • 그 필드의 데이터 타입
    • (옵션) 레코드 ID *특정 레코드를 쿼리할 때
  • ContentResolver.querh() 또는 +Acitivty.managedQuery()+를 사용한다.
     managedQuery()는 Activity LifeCycle에 따라 Cursor의 LifeCycle을 관리할 수 있다.
    • ContentRseolver를 이용하는 방법
      ContentResolver cr = getContentResolver();
      Cursor allRows = cr.query(myPerson, null, null, null);
    • CmangedQueryf를 이용하는 방법
      ursor cur = managedQuery(myPerson, null, null, null);
  • URI 만드는 방법
    //ContentUris.withAppendedID()는 ID의 raw를 URI로 만든다. (ID가 23번 째의 raw를 URI로 가져온다.)
    Uri myPerson = ContentUris.withAppendedID(People.CONTENT_URI, 23);
    //  Uri.withAppendedPath()는 명시한 data(string으로)의 raw를 URI로 만들 수 있다. (“abc”가 있는 raw를 URI로 가져온다.)
    Uri myPerson = Uri.withAppendedPath(People.CONTENT_URI, "abc");
  • managedQuery 질의
    전화번호 예제 소스
    import android.provider.Contacts.People;
    import android.database.Cursor;
    //Form an array specifying which columns to return.
    String[] projection = new String[]{ People._ID, people._COUNT, people.NAME, people.NUMBER};
    //Get the base URI for the People table
    // in the Contacts content provider.
    Uri contacts = People.CONTENT_URI;
    //Make the query.
    Cursor managedCursor = managedquery(contacts,
                             projection, //Which columns to return
                             null, //Which rows to return (all rows)
                             null, //Selection arguments (none)
                             //Put the results in ascending order by name
                             People.NAME + "ASC");
  • Query에 의해 반한되는 Cursor의 객체는 결과 set에 접근할 수 있다.
    만약, ID로 특정 레코드를 쿼리하면 결과 set은 하나, 그렇지않으면 다수의 값, 일치하는 데이터가 없으면 empty임.
  • ContentPRovider에서 Cursor 이용하기
    private void getColumnData(Cursor cur){
       if(cur.moveToFirst()){
         String name;
         String phoneNumber;
         int nameColumn = cur.getColumnIndex(People.NAME);
         int phoneColumn = cur.getColumnIndex(People.NUMBER);
         String imagePath;
         do{
             //Get the field values
             name = cur.getStirng(nameColumn);
             phoneNumber = cur.getString(phoneColumn);
            //Do something with the values.
            ...
          } while (cur.moveToNext());
       }
    }
  • ID를 이용해 특정 Row에 접근하기
    //use the ContentUris method to produce the base URI
    //for the contact with_ID == 23
    Uri myPerson = ContentUris.withAppendedId(People.CONTEXT_URI, 23);
     
    //Altematively, use the Uri method to produce the base URI
    //It takes a string rather than an integer.
    Uri myPerson = Uri.withAppendedPath(People.CONTENT_URI, "23");
     
    //Then query for this specific record.
    Cursor cur = managedQuery(myPerson, null, null, null, null);

1.4 Modifying Data

  • ContentProvider에 의해 유지되는 데이터는 ContentResolver 메소드를 사용해서 아래와 같은 작업들을 수행할 수 있다.
    • 새로운 레코드 추가
    • 기존 레코드에 새로운 값 추가
    • 기존 레코드를 재배치
    • 레코드 삭제
  • 새로운 레코드 추가하기
    ContentValues values = new ContentValues();
     
    //Add Lee Jaeyoen to contacts and make her a favorite.
    values.put(People.NAME, "Lee Jaeyeon");
    //1 = the new contact is added to favorites
    //0 = the new contact is not added to favorites
    values.put(People.STARRED, 1);
     
    Uri. uri = getContentResolver().insert(People.CONTENT_URI, values);
  • 기존 레코드에 새로운 값 추가하기
    전화번호 추가
    phoneUri = Uri.withAppendedPath(uri, People.Phones.CONTENT_DIRECTORY);
     
    values.clear();
    values.put(People.Phones.TYPE, People.Phones.TYPE_MOBILE);
    values.put(People.Phones.NUMBER, "1234531");
    getContentResolver().insert(phoneUri, values);
     
    //Now add an email address in the same way.
    emailUri = Uri.withAppendedPath(uri, People.ContactMethods.CONTENT_DIRECTORY);
    values.clear();
     
    //Contact Methods.KIND is used to distinguish different kinds of
    // contact methods, such as email, IM, etc.
    values.put(People.ContactMethods.KIND, Contacts.KIND_EMAIL);
    values.put(People.ContactMethods.DATA, "test@example.com");
    values.put(People.ContactMethods.TYPE, People.ContactMethods.TYPE_HOME);
    getContentResolver().insert(emailUri, values);
  • 기존 레코드를 재배치로 업데이트 하기
    ContentValues newValues = new ContentValues();
    newValues.put(COLUMN_NAME, "값");
    String where = "_id<5";
    getContentResolver().update(MyProvider.CONTENT_URI, newValues, where, null);
  • 레코드 삭제하기
    //특정 행 삭제
    getContentResolver().delete(myRowUri, null, null);
    //조건을 주고 삭제
    String where = "_id<5";
    getContentResolver().delete(MyProvider.CONTENT_URI, where, null);

1.5 Content URIs

  • 각 ContentProvider는 +데이터 집합을 식별하기 위해서 공용 URI+를 가지며 다수의 데이터 집합을 제어하는 ContentProvider는 각각에 대한 서로 다른 URI를 가진다.
  • 모든 URI는 "content://"로 시작하며, "content:"는 ContentProvider에 의해 통제되는 데이터라는 것을 의미한다.
  • URI 구조
    • Prefix
      • 컨텐트 프로바이더를 사용한다는 고정적인 스키마이다.
    • Authority
      • 컨텐트 프로바이더를 구분하기 위한 고유 이름이다.
        사용하기 위해서 <provider> 엘리멘트에 authority 속성을 정의해야 한다.
    • Path
      • 프로바이더가 제공할 데이터의 타입을 정한다.
        만약 프로바이더가 제공하는 데이터 타입이 하나라면 데이터 유형을 비워도 된다.
        또 "/" 기호를 사용하여 여러개를 연결하여 사용할 수 있다.
    • ID
      • 요청한 레코드의 아이디.
        만약 아이디가 없다면 요청한 레코드 전체 데이터를 의미한다.
  • provider 클래스의 주요 URI(android.provider.*)
    통화로그CallLog.Calls.CONTENT_URI
    전화번호부Contacts.People.COTENT_URI
    내장 미디어 내의 영상MediaStore.Video.Media.INTERNAL_CONTENT_URI
    외부 미디어 내의 영상MEediaStore.Video.Media.EXTERNAL_CONTENT_URI
    내장 미디어의 오디오MediaStore.Audio.Media.INTERNAL_CONTENT_URI
    외장 미디어의 오디오MediaStore.Audio.Media.EXTERNAL_CONTENT_URI

1.6 AndroidManifest.xml에 등록하기

  • android:protectionLevel value 4가지
    • normal : default 값, 최소한의 위험 요소를 가진 어플리케이션이 다른 어플리케이션 레벨의 기능에 대해 접근할 수 있다.
    • dangerous : 사용자의 사적 데이터 접근이나 디바이스에 대한 제어를 허용한다.
    • signature : 어플리케이션이 다른 어플리케이션과 같은 signature를 가지고 있을 때만 시스템이 permission을 부여한다.
    • signatureOrSystem : 시스템이 안드로이드 시스템 이미지 안에 있거나 시스템 이미지 안에 있는 것과 같은 signature로 된 어플리케이션에 한해서만 permission을 부여한다.

 Content Provider를 이용하여 binary 파일에 접근을 제공하는 방법

  • Retrieved Data
    • 일반적으로 50K 정도의 작은 양의 binary 파일은 직접적으로 테이블에 들어갈 수 있고, 데이터를 읽을 때는 Cursor.getBlob() 를 사용한다.
    • 사진이나 전체 노래일 경우에는 직접적으로 테이블에 접근하지 않고 URI를 사용한다. 이 때는 ContentResolver.openInputStream() 메소드를 사용하여 binary파일을 InputStream으로 리턴한다.
      //make the query
      String [] projection = new String[] {PhotoInfoColumns._ID, //row ID
                      PhotoInfoColumns.PHOTO_URI}; //URI to this row, was inserted
      String selection = "(" + PhotoInfoColumns.PHOTO+" ='T' )";
      Cursor c = managedQuery(PhotoInfoColumns.CONTENT_URI, projection, selection, null,null);
       
      //Grab first photos
      long id = -1;
      Uri uri = null;
      if(c.moveToFirst()){
          id = c.getLong(0);
          uri = Uri.parse(c.getString(1));
      }
      //make the bitmap
      if(id > 0) //the query found photos
         //Another way to make the URI from the id
         //Uri uri = Uri.pase(PhotoInfoColumns.CONTENT_URI_ID_BASE+Long.toString(id));
         InputStream is = null;
         try{
            is = getContentResolver().openInputStrem(uri);
         } catch (FileNotFoundException e) {...}
         if(is != null){
              Bitmap bm = BitmapFactory.decodeStream(is); //got the bitmap
              try{
                 is.close();
              } catch (IOException e) {...}
              // use the bitmap
              mImageView.setImagebitmap(bm);
  • Modifying data
    • 기존 레코드에 값을 추가할 때
      Binary data를 추가할 때는 테이블에 URI를 작성하고, 그 URI를 가지고 ContentResolver.openOutputStream() 을 호출한다.
      ContentValues values = new ContentValue(3);
      values.put(Media.DISPLAY_NAME, "road_trip_1");
      values.put(Media.DESCRIPTION, "Day 1, trip to Los Angeles");
      values.put(Media.MINE_TYPE, "image/jpeg");
       
      Uri uri = getContentResolver().insert(Media.EXTERNAL_CONTENT_URI, values);
       
      try{
          OutputSteram outStrem = getContentResolver().openOutputStream(uri);
          sourceBitmap.compress(Bitmap.CompressFormat.JPEG, 50, outStream);
          outStream.close();
      } catch (Exception e){
          Log.e(TAG, "exception while writing image", e);
      }

 Content Provider 사용시 주의점

  • Content Provider는 여러 content Resolver로부터 호출이 될 수 있기에 onCreate()메소드를 제외한 나머지 메소들은 thread-safe방식으로 구현이 되어야 한다.
  • ContentResolver.notifchange()를 통해서 수정된 데이터가 있다는 것을 알려줄 수도 있다.
  • Cursor.close()
    • Android 2.2부터 Cursor 객체에 대한 GC를 보장하지 않는다. 그렇기 때문에 해당 activity(Content Resolver)를 직접 삭제해야 한다.

2. Broadcast Receiver

2.1 소개

  • 안드로이드 응용 프로그램을 구성하는 4개의 컴포넌트 중 하나
  • Intent를 이용하여 전송할 경우나 action이 여러 activity에 전송되어야할 경우 사용하는 컴포넌트
  • 수신받을 수 있는 두 가지 broadcast class
    • Normal broadcasts(Context.sendBroadcast) : 비동기식, broadcast의 모든 receiver는 정의되지 않은 순서로 실행되고 종종 동시에 실행될 수도 있다.
    • Ordered broadcasts(Context.sendOrderedBroadcast) : 한번에 하나의 receiver가 전달된다. receiver가 실행되는 순서는 매칭되는 IntentFilter의 android:priority 속성을 이용해서 컨트롤할 수 있다.
  • 두 가지 Receiver 형태가 있다.
    • 정적인 Receiver : Receiver를 고정해서 등록해 놓고 원하는 action에 반응하는 Receiver
      AndroidManifest.xml에 고정해서 Receiver 등록
    • 동적인 Receiver : AndroidManifest.xml에 Receiver를 등록하지 않고 코드상에서 활용
  • BroadcastReceiver 클래스를 상속받아 onReceive() 메소드를 재정의함
    onReceive(Context context, Intent intent)
    • BroadcastReceiver는 onReceive()메소드를 실행하는 동안만 활성화 되는 것으로 간주하며
    • onReceive()메소드가 리턴되면 비활성화된 것으로 간주한다.

(알기 쉽게 그림 예제)

2.2 정적인 Broadcast Receiver

  • Braodcast 측
    public class BroadcastActivity extends Activity {
        @Override
        public void onCreate(Bundle savedInstanceState) {
             super.onCreate(savedInstanceState);
             setContentView(R.layout.main);
      }
     
      public void onClick(View v)
      {
            Intent intent = new Intent("android.intent.action.SUPERSK");
            intent.setData(Uri.parse("sample:"));
            sendBroadcast(intent);
      }
    }
  • BroadcastReceiver 측
    //androidmanifest.xml 부분
    <application android:icon = "@drawable/icon" android:label="@string/app_name">
     
      <receiver andrdoi:name=".Receiver">
          <intent-filter>
             <action android:name="android.intent.action.SUPERSK" />
             <data android:scheme="sample" />
          </intent-filter>
      </receiver>
    </application>
     
    //.java 코드
    public class Receiver extends BroadcastReceiver {
       @Override
       public void onReceive(Context context, Intent intent)
       {
            Toas.makeText(context, "android.intent.action.SUPERSK", Toas.LENGTH_LONG).show();
      }
    }

2.3 동적인 Broadcast Receiver

public class ReceiverActivity extends Activity{
    BroadcastReceiver mBroadcastReceiver = null;
 
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
          super.onCreate(savedInstanceState);
          setContentView(R.layout.main);
 
          IntentFilter pkgFilter = new IntentFilter();
          pkgFilter.addAction("android.intent.action.SUPERSK");
          pkFilter.addDataScheme("sample");
 
          mBoradcastReceiver = new BroadcastReceiver()
          {
                Taost.makeText(context, "Dynamic android.intent.action.SUPERSK", Toas.LENGTH_LONG).show();
          }
  };
 
   registerReceiver(mBroadcastReceiver, pkgFilter);
}
  • Receiver를 해제할 때 :unregisterReceiver(mBroadadcastReceiver);

 정적 혹은 동적인 Broadcast Receiver를 사용해야하는 경우

  • 정적인 Broadcast Receiver
    • 어플리케이션 실행여부와 관계없이 Broadcast가 manifest에 등록이 되어있으면 onReceive()가 호출된다.
    • 한번 등록을 하면 계속 유지할 수 있지만 해제가 어렵고 무의미한 상황에서도 CPU와 베터리 소모가 있다.
    • EX) Notification, 시간변경 및 사용자 언어 설정 변경
  • 정적인 Broadcast Receiver
    • Broadcast Receiver를 등록한 이후에 발생하는 intent만을 receive해서 수행한다.
    • 등록한 Component Lifecycle 주기를 따라간다. (Lifecycle이 끝나면 BR도 끝내게 된다.)
    • EX) 블루투스

 Intent와 Broadcast Receiver. 언제 사용하는 것일까?

  • Intent
    • 실행할 타겟이 특정하거나 class, activity로 전달될 때
      • Activity, service 실행(웹페이지, 구글맵 띄우기)
  • Broadcast Receiver
    • 시스템이 전체적으로 알림을 줘야할 때
    • 실행할 타겟(component)이 정확하지 않을 때
      • 상태바, 알림(예, 충전이 필요하다 or 3G접속이 되어있다)
      • 불루투스(예, 불루투스 장치들을 찾을 때)

 와이파이가 중간에 끊어지면 이 상황을 Intent로 전달을 하면 특정한 어플리케이션만 그 상황에 반응하겠지만
Broadcast로 날려주면 Receiver를 등록한 모든 어플리케이션이 반응을 한다.

 Broadcast Receiver의 onReceive() 주의점

  • onReceive () 메소드는 5초 이내에 종료가 되어야 한다. 그렇지 않으면 “Application Not Responsive(ANR)” 다이얼로그가 표시된다.
  • 통상적으로 Broadcast Receiver는 콘텐츠 업데이트, 서비스 가동, Activity UI변경 또는 Notification Manger를 이용하여 사용자에게 알림을 통보할 때 사용한다.
  • 5초 이내에 완료하지 않는 Broadcast Receiver일 경우에는 background thread 를 활용한다.
    • 네트워크 조회
    • 파일 작업
    • 데이터베이스 트랜잭션
       모든 components는 하나의 main thread 위에 동작한다. 시간이 많이 걸리는 작업을 하면 화면에 보이는 Activity 뿐 아니라 다른 모든 components까지 block시키는 현상이 있기에 main thread에서 child thread로 옮겨서 수행해야 한다.