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 질의전화번호 예제 소스
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
- 요청한 레코드의 아이디.
만약 아이디가 없다면 요청한 레코드 전체 데이터를 의미한다.
- 요청한 레코드의 아이디.
- 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 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를 등록하지 않고 코드상에서 활용
- 정적인 Receiver : Receiver를 고정해서 등록해 놓고 원하는 action에 반응하는 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 실행(웹페이지, 구글맵 띄우기)
- 실행할 타겟이 특정하거나 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로 옮겨서 수행해야 한다.