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의 데이터 모델은 데이터메이스 모델 상의 간단한 테이블처럼 보여준다.
- 각 행은 레코드, 각 열은 특정 타입과 데이터
(예시)
- 각 행은 레코드, 각 열은 특정 타입과 데이터
| _ID | NUMBER | NUMBER_KEY | LABEL | NAME | TYPE |
|---|---|---|---|---|---|
| 13 | (425)555-6677 | 425 555 6677 | Kirkland office | Alan Vain | TYPE_WORK |
| 44 | (02)123-4231 | 02 123 4231 | Home | Mr.Kim | TYPE_HOME |
| 55 | (031)784-2247 | 031 784 2247 | NHN Office | Lee Jaeyeon | TYPE_WORK |
| 71 | (02)784-1000 | 031 784 1000 | Office | ABCD | TYPE_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);
- ContentRseolver를 이용하는 방법
- 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 질의전화번호 예제 소스
importandroid.provider.Contacts.People;importandroid.database.Cursor;//Form an array specifying which columns to return.String[] projection =newString[]{ 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 returnnull,//Which rows to return (all rows)null,//Selection arguments (none)//Put the results in ascending order by namePeople.NAME +"ASC"); - Query에 의해 반한되는 Cursor의 객체는 결과 set에 접근할 수 있다.
만약, ID로 특정 레코드를 쿼리하면 결과 set은 하나, 그렇지않으면 다수의 값, 일치하는 데이터가 없으면 empty임. - ContentPRovider에서 Cursor 이용하기
privatevoidgetColumnData(Cursor cur){if(cur.moveToFirst()){String name;String phoneNumber;intnameColumn = cur.getColumnIndex(People.NAME);intphoneColumn = cur.getColumnIndex(People.NUMBER);String imagePath;do{//Get the field valuesname = 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 == 23Uri 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 =newContentValues();//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 favoritesvalues.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 =newContentValues();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
- 요청한 레코드의 아이디.
만약 아이디가 없다면 요청한 레코드 전체 데이터를 의미한다.
- 요청한 레코드의 아이디.
- Prefix
- 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 queryString [] projection =newString[] {PhotoInfoColumns._ID,//row IDPhotoInfoColumns.PHOTO_URI};//URI to this row, was insertedString selection ="("+ PhotoInfoColumns.PHOTO+" ='T' )";Cursor c = managedQuery(PhotoInfoColumns.CONTENT_URI, projection, selection,null,null);//Grab first photoslongid = -1;Uri uri =null;if(c.moveToFirst()){id = c.getLong(0);uri = Uri.parse(c.getString(1));}//make the bitmapif(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 bitmaptry{is.close();}catch(IOException e) {...}// use the bitmapmImageView.setImagebitmap(bm);
- Modifying data
- 기존 레코드에 값을 추가할 때
Binary data를 추가할 때는 테이블에 URI를 작성하고, 그 URI를 가지고 ContentResolver.openOutputStream() 을 호출한다.ContentValues values =newContentValue(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를 등록하지 않고 코드상에서 활용
- 정적인 Receiver : Receiver를 고정해서 등록해 놓고 원하는 action에 반응하는 Receiver
- BroadcastReceiver 클래스를 상속받아 onReceive() 메소드를 재정의함
onReceive(Context context, Intent intent)- BroadcastReceiver는 onReceive()메소드를 실행하는 동안만 활성화 되는 것으로 간주하며
- onReceive()메소드가 리턴되면 비활성화된 것으로 간주한다.
(알기 쉽게 그림 예제)
2.2 정적인 Broadcast Receiver
- Braodcast 측
publicclassBroadcastActivityextendsActivity {@OverridepublicvoidonCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.main);}publicvoidonClick(View v){Intent intent =newIntent("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 코드publicclassReceiverextendsBroadcastReceiver {@OverridepublicvoidonReceive(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 실행(웹페이지, 구글맵 띄우기)
- 실행할 타겟이 특정하거나 class, activity로 전달될 때
- 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로 옮겨서 수행해야 한다.