[Android] Getting Started (5)

Android developer site study

  • Getting Started
    • Building Your First App
    • Supporting Different Devices
    • uilding a Dynamic UI with Fragments
    • Saving Data
    • Interacting with Other Apps
    • Working with System Permissions
https://developer.android.com/training/index.html

Android App은 보통 여러 개의 Activity를 가지고 있다. 사용자가 하나의 Activity에서 다른 Activity로 전환하기 위해서는 Intent를 사용하여 App의 intent가 어떠한 작업을 하도록 정의해야 한다.

startActivity()와 같은 메서드로 시스템에 Intent를 전달하면 시스템은 Intent를 사용하여 적절한 App 구성 요소를 식별하고 시작한다.

Intent는 크게 두 가지로 나눌 수 있다.

  1. 특정 구성 요소 (특정 Activity 인스턴스)를 시작하기 위한 명시적 Intent
  2. “사진 캡쳐”와 같은 의도된 작업을 처리할 수 있는 임의의 구성 요소를 시작하기 위한 암시적 Intent

다른 App으로 사용자 이동시키기

암시적 Intent 만들기

암시적 Intent는 시작할 구성 요소의 class 이름을 선언하지 않고 그 대신 수행할 작업을 선언한다.

작업은 보기, 편집, 보내기, 가져오기와 같이 수행하고자 하는 동작을 지정한다.

또한 보기 원하는 주소 또는 전송하기 원하는 이메일 등과 같이 작업과 연결된 데이터를 함께 포함하는 경우가 많다.

생성하고자 하는 Intent에 따라 데이터는 Uri이거나 다른 여러가지 데이터 유형 중 하나일 수 있으며, intent가 데이터를 전혀 필요로 하지 않을 수도 있다.

데이터가 Uri인 경우 간단한 Intent() 생성자를 이용하여 작업 및 데이터를 정의할 수 있다.

아래 코드는 Uri 데이터르 사용하여 전화번호를 지정하여 전화 걸기를 시작하는 Intent를 생성한다.

Uri number = Uri.parse("tel:5551234");
Intent callIntent = new Intent(Intent.ACTION_DIAL, number);

App이 startActivity()를 호출하여 이 intent를 호출하면 전화 app이 주어진 전화번호로 전화를 건다.

아래는 몇 가지 다른 intent와 해당 작업 및 Uri 데이터 쌍이다.

  • 지도 보기
// 주소 기반의 지도 지점
Uri location = Uri.parse("geo:0,0?q=1600+Amphitheatre+Parkway,+Mountain+View,+California");
// 또는 위도/경도 기반의 지도 지점
// Uri location = Uri.parse("geo:37.422219,-122.08364?z=14"); // z param is zoom level
Intent mapIntent = new Intent(Intent.ACTION_VIEW, location);
  • 웹 페이지 보기
Uri webpage = Uri.parse("http://www.android.com");
Intent webIntent = new Intent(Intent.ACTION_VIEW, webpage);

다른 종류의 암시적인 intent는 문자열과 같이 여러 데이터 유형을 제공하는 “Extra” 데이터를 필요로 한다. 다양한 putExtra() 메서드를 사용하여 하나 이상의 Extra 데이터를 추가할 수 있다.

기본적으로 시스템은 포함된 Uri 데이터를 바탕으로 intent가 필요로 하느 ㄴ적절한 MIME 유형을 결정한다.

Uri를 intent에 포함하지 않을 경우, 일반적으로 setType()을 사용하여 intent와 관련된 데이터의 유형을 지정해야 한다.

MIME 유형을 설정하면 intent를 수신할 activity의 종류도 지정된다.

  • 첨부 파일과 함께 이메일 보내기
Intent emailIntet = new Intent(Intent.ACTION_SEND);
// 이 Intent는 URI를 가지고 있지 않다. 때문에 "text/plain" MIME type을 선언한다.
emailIntent.setType(HTTP.PLAIN_TEXT_TYPE);
emailIntent.putExtra(Intent.EXTRA_EMAIL, new String[] {"jon@example.com"}); // 수신자
emailIntent.putExtra(Intent.EXTRA_SUBJECT, "Email subject");
emailIntent.putExtra(Intent.EXTRA_TEXT, "Email message text");
emailIntent.putExtra(Intent.EXTRA_STREAM, Uri.parse(content://path/to/email/attachment"));
// Uri들의 arrayList를 여러 개 보내는 것도 가능하다.
  • 캘린더 이벤트 생성

캘린더 이벤트에 대한 아래 intent는 API Level 14 이상에서만 지원한다.

Intent calendarIntent = new Intent(Intent.ACTION_INSERT, Events.CONTENT_URI);
Calendar beginTime = Calendar.getInstance().set(2017, 0, 19, 7, 30);
Calendar endTime = Calendar.getInstance()set(2017, 0, 19, 10, 30);
calendarIntent.putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, beginTime.getTimeInMillis());
calendarIntent.putExtra(CalendarContract.EXTRA_EVENT_END_TIME, endTime.getTimeInMillis());
calendarIntent.putExtra(Events.TITLE, "Ninja class");
calendarIntent.putExtra(Events.EVENT_LOCATION, "Secret dojo");

Intent는 되도록 구체적으로 정의해야 한다.

예를 들어, ACTION_VIEW intent를 사용하여 이미지를 표시하고자 할 경우, image/*의 MIME 유형을 지정해야 한다.

그렇게 하면 지도 app과 같은 다른 유형의 데이터를 볼 수 있는 app이 intent에 의해 실행되는 것이 방지된다.

Intent를 수신할 app이 있는지 확인하기

Intent를 호출한 후 해당 intent를 처리할 수 있는 app이 기기에 없을 경우 app은 작동을 중단하기 때문에 intent를 호출하기 전에 항상 확인 단계를 포함하는 것이 좋다.

이를 위해 queryIntentActivities()를 호출하여 Intent를 처리할 수 있는 activity 목록을 가져와야 한다.

얻어온 List가 비어있지 않을 경우 intent를 안심하고 사용할 수 있다.

PackageManager packageManager = getPackageManager();
List activities = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
boolean isIntentSafe = activities.size() > 0;

위의 코드에서 isIntentSafetrue일 경우 하나 이상의 intent가 응답하고 있는 것이므로 안심하고 intent를 호출한다.

사용자가 intent를 사용하려고 하기 전에 이 intent를 사용하는 기능을 해제해야 하는 경우, activity가 처음 시작될 때 이 확인 작업을 수행해야 한다.

Intent를 처리할 수 있는 app을 알고 있을 경우, 사용자가 app을 다운로드 할 수 있도록 link를 제공할 수도 있다.

Intent를 사용하여 Activity 시작하기

Intent를 생성하고 EXTRA 정보를 설정한 후에는 startActivity()를 호출하여 시스템에 보내야 한다.

시스템이 Intent를 처리할 수 있는 activity를 둘 이상 식별하는 경우, 아래 그림과 같이 사용자가 사용할 app을 선택할 수 있는 대화상자를 표시한다.

Intent를 처리할 수 있는 activity가 하나밖에 없을 경우, 시스템이 해당 activity를 바로 시작한다.

startActivity(intent);

아래 코드는 지도 보기 intent를 생성하고 intent를 처리할 수 있는 app의 존재 여부를 확인한 후 app을 시작하는 방법에 대한 완벽한 예시이다.

// intent 생성하기
Uri location = Uri.parse("geo:0,0?q=1600+Amphitheatre+Parkway,+Mountain+View,+California");
Intent mapIntent = new Intent(Intent.ACTION_VIEW, location);
// intent 호출 가능여부 확인
PackageManager packageManager = getPackageManager();
List<ResolveInfo> activities = packageManager.queryIntentActivities(mapIntent, 0);
boolean isIntentSafe = activities.size() > 0;
// 안전하다면, activity 시작
if (isIntentSafe) {
startActivity(mapIntent);
}

App 선택기 표시하기

Intent에 응답하는 app이 두 개 이상인 경우 사용자는 어떤 app을 default로 사용할지 선택할 수 있다.

이 방법은 사용자가 일반적으로 항상 동일한 app을 사용하기 원하는 작업을 수행할 때 유용하게 사용된다.

예를 들어, 웹페이지를 열 때 브라우저를 선택하거나 사진을 찍을 때 카메라의 종류를 선택할 때 사용된다.

하지만 “공유” 작업 등과 같이 항목을 공유할 app을 여러 개 보유하고 있는 경우처럼 수행할 작업을 처리할 수 있는 app이 여러 개 있고 사용자가 매번 다른 app을 원한다면, 아래 그림과 같이 선택기 대화상자를 명시적으로 표시할 수 있다.

선택기를 명시적으로 표시하려면, createChooser()를 사용하여 Intent를 만들고 startActivity()에 전달한다.

아래의 예시는 createChooser() 메서드에 전달된 intent에 응답하는 app들의 목록을 대화상자에 표시하며, 제공된 text를 대화상자 제목으로 사용한다.

Intent intent = new Intent(Intent.ACTION_SEND);
...
// UI text는 항상 string 리소스를 사용한다.
// "Share this photo with" 등...
String title = getResource().getString(R.string.chooser_title);
// 선택기에서 보여줄 intent 생성하기
Intent chooser = Intent.createChooser(intent, title);
// intent를 수신할 수 있는 activity 존재 확인
if (intent.resolveActivity(getaPackageManager()) != null) {
startActivity(chooser);
}

Activity로부터 결과 가져오기

다른 Activity를 시작시킨 후 그 activity로부터 결과를 수신할 수도 있다.

결과를 수신하고 싶을 때는 startActivity() 대신 startActivityForResult()를 호출한다.

예를 들어, 자신의 App에서 카메라 app을 실행시킨 후 그 결과로 캡쳐된 사진을 수신할 수 있다.

연락처의 선택 후 그 상세 정보를 수신하는 등의 동작에도 사용 가능하다.

응답하는 activity는 반드시 결과를 반환하도록 설계되어 있어야 하며 해당 activity는 또 다른 Intent 객체의 형태로 결과를 전송한다.

그러면 최초 호출한 activity는 onActivityResult() 콜백으로 결과를 수신한다.

startActivityForResult()를 호출할 때 명시적 또는 암시적 intent를 사용할 수 있다.

자신이 정의한 activity 중 하나를 시작하여 결과를 수신하는 경우 예상하는 결과를 수신하도록 보장하려면 명시적인 intent를 사용해야 한다.

Activity의 시작

결과를 수신하기 위해서 startActivityForResult() 메서드에 추가적인 정수 인수를 전달해야 한다.

이 정수 인수는 요청을 식별하는 요청 코드이다. 결과 intent를 수신하는 경우 app이 결과를 올바르게 식별하여 이를 처리할 방법을 결정할 수 있도록 콜백이 이와 똑같은 요청 코드를 제공한다.

아래는 사용자가 연락처를 선택할 수 있게 하는 activity를 시작하는 방법이다.

static final int PICK_CONTACT_REQUEST = 1; // 요청 코드
...
private void pickContact() {
Intent pickContactIntent = new Intent(Intent.ACTION_PICK, Uri.parse("content://contacts"));
pickContactIntent.setType(Phone.CONTENT_TYPE); // 주소록에 있는 사용자와 폰 번호를 보여줌
startActivityForResult(pickContactIntent, PICK_CONTACT_REQUEST);
}

결과 수신하기

사용자가 후속 activity 작업을 마치고 돌아오면 시스템은 개발자가 정의한 원래 activity의 onActivityResult() 메서드를 호출한다.

이 메서드는 아래의 세 가지 인수를 포함한다.

  1. startActivityForResult()에 전달한 요청 코드
  2. 두 번째 activity가 지정한 결과 코드. 이 코드는 작업이 성공한 경우 RESULT_OK이고 사용자가 작업을 취소하였거나 어떠한 이유로 실패한 경우 `RESULT_CANCELED’가 된다.
  3. 결과 데이터를 전달하는 Intent

아래는 “연락처 선택하기” intent에 대한 결과를 처리하는 방법이다.

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// 요청했던 내용에 대한 응답인지 확인
if (requestCode == PICK_CONTACT_REQUEST) {
// 요청이 성공했는지 확인
if (resultCode == RESULT_OK) {
// 사용자가 주소록을 선택함.
// Intent는 사용자가 선택한 주소록의 Uri 값을 가지고 있음
// 주소록을 가지고 여기서 필요한 작업을 수행함. (다음 코드 예제를 참조)
}
}
}

위의 예시에서는 Android 주소록에서 반환되는 결과 intent가 사용자가 선택한 연락처를 식별하는 Uri 콘텐츠를 제공한다.

결과를 성공적으로 처리하기 위해서는 결과 intent의 형식이 무엇인지 이해하고 있어야 한다. 결과를 반환하는 activity가 자신이 정의한 activity 중 하나인 경우에는 그 결과를 이해하기가 쉽다.

Android 플랫폼에 포함된 app은 특정한 결과 데이터를 기대할 수 있는 고유한 API를 제공한다.

예를 들어, PEOPLE app은 선택된 연락처를 식별하는 콘텐츠 URI와 함께 항상 결과를 반환한다. 또한 카메라 app은 “data” EXTRA에 Bitmap을 반환한다.

보너스: 연락처 데이터 읽기

아래 코드는 데이터를 쿼리하여 선택된 연락처에서 전화번호를 가져오는 방법을 나타낸다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// 요청했던 내용에 대한 응답인지 확인
if (requestCode == PICK_CONTACT_REQUEST) {
// 요청이 성공했는지 확인
if (resultCode == RESULT_OK) {
// 사용자가 주소록을 선택함.
Uri contactUri = data.getData();
// NUMBER column만을 필요로 한다고 가정한다. 왜냐하면 결과로 하나의 row만 얻었을 것이기 때문이다.
String[] projection = {Phone.NUMBER};
// NUMBER column을 주소록으로부터 얻어오기 위한 쿼리를 실행한다.
// 결과 값이 URI로 넘어오기 때문에 별도의 정렬을 할 필요는 없다.
// 주의: query() 메서드는 blocking을 피하기 위해 UI thread와 다른 별도의 thread에서 수행되어야 한다.
// 이 코드에서는 간단한 수행만 할 것이라 굳이 그렇게 코드를 짜지는 않았다.
// query를 수행 할 때 CursorLoader를 사용하는 것을 고려하라.
Cursor cursor = getContentResolver()
.query(contactUri, projection, null, null, null);
cursor.moveToFirst();
// NUMBER column으로부터 폰 번호를 검색한다.
int column = cursor.getColumnIndex(Phone.NUMBER);
String number = cursor.getString(column);
}
}
}

Android 2.3 (API level 9) 이전에서는 위의 코드와 같이 Contacts Provider에 대해 쿼리를 수행하려면 App에서 READ_CONTACTS 권한을 선언해야 한다.

하지만 Android 2.3부터는 Contact Provider가 결과를 반환할 때 자신의 app에서 그 결과를 읽어올 수 있도록 주소록 app이 임시 권한을 부여한다.

임시 권한은 해당 연락처 요청에만 적용되기 때문에 intent의 Uri로 지정된 연락처 외에는 쿼리할 수 없다. 다만, READ_CONTACTS 권한을 명시적으로 선언한 경우는 된다.

다른 App이 자신의 Activity를 시작할 수 있도록 허용하기

자신의 app이 다른 app에 유용할 수 있는 작업을 수행할 수 있다면 다른 app의 작업 요청에 응답할 수 있도록 준비해 두어야 한다.

예를 들어, 사용자가 친구와 메시지 또는 사진을 공유할 수 있는 소셜 app을 만드는 경우 사용자가 다른 app에서 “공유” 작업을 시작하고 이 작업을 수행하기 위해 내가 만든 app을 시작할 수 있도록 ACTION_SEND intent를 지원하는 것이 좋다.

다른 app이 자신의 activity를 시작할 수 있도록 하기 위해서는 manifest 파일에서 해당 <activity> 요소에 대해 <intent-filter> 요소를 추가해야 한다.

App이 기기에 설치되면 시스템이 intent filter를 식별한 후 설치된 모든 app에서 지원되는 intent의 내부 카탈로그에 해당 정보를 추가한다.

App이 암시적 intent를 사용하여 startActivity() 또는 startActivityForResult()를 호출하면 시스템이 intent에 응답 가능한 activity(들)을 찾는다.

Intent filter 추가하기

Activity가 처리 가능한 intent를 올바르게 정의하려면 activity가 받아들이는 데이터와 작업 유형 측면에서 추가하는 intent filter가 최대한 구체적이어야 한다.

Activity의 Intent filter가 Intent 객체의 아래 기준을 충족할 경우 시스템이 주어진 Intent를 해당 activity에 보낼 수 있다.

  1. Action
  2. Data
  3. Category

  4. Action

수행할 작업의 이름을 지정하는 문자열이다. 일반적으로, 플랫폼에서 정의하는 값 중 하나이다. (예. ACTION_SEND 또는 ACTION_VIEW)

<action> 요소를 사용하여 Intent filter에 지정해야 한다. 이 요소에 지정하는 값은 API 상수 대신 작업의 전체 문자열 이름이어야 한다.

  1. Data

Intent와 관련된 Data에 대한 설명이다.

<data> 요소를 사용하여 Intent filter에 지정한다. 이 요소에서 하나 이상의 특성을 사용하여 MIME 유형, URI 접두사, URI 구성표 또는 이들의 조합 그리고 수락된 데이터 유형을 나타내는 다른 요소들을 지정할 수 있다.

Activity가 URI가 아닌 다른 종류의 EXTRA 데이터를 처리할 때와 같이 데이터 Uri에 대한 세부사항을 선언할 필요가 없는 경우, text/plain 또는 image/jpeg과 같이 activity가 처리하는 데이터 유형을 선언하는데 android.mimeType 특성만 지정하면 된다.

  1. Category

Intent를 처리하는 activity의 특징을 지정할 수 있는 추가적인 방법을 제공한다. 일반적으로 사용자 제스쳐 또는 이러한 제스쳐가 시작된 위치와 관련이 있다.

시스템이 지원하는 category는 여러가지가 있지만 대부분은 거의 사용되지 않는다. 하지만 모든 암시적 intent는 기본적으로 CATEGORY_DEFAULT로 지정된다.

<category> 요소를 사용하여 Intent filter에 지정한다.

Intent filter에서 activity가 허용하는 기준을 선언할 수 있다. 이는 이러한 기준 각각을 <intent-filter> 요소 내에 해당 XML 요소를 중첩하여 선언하면 된다.

아래 코드는 데이터 유형이 텍스트 또는 이미지인 경우 ACTION_SEND intent를 처리하는 intent filter가 지정된 activity이다.

<activity android:name="ShareActivity">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plane" />
<data android:mimeType="image/*" />
</intent-filter>
</activity>

수신되는 intent는 각각 하나의 action 및 하나의 데이터 유형만 지정한다. 하지만 <intent-filter> 각각에 <action>, <category>, <data> 요소에 대한 여러 인스턴스를 선언해도 문제가 되지는 않는다.

action 및 data의 두 쌍이 서로 배타적으로 동작하는 경우 어떤 데이터 유형과 페어링되었을 때 어떤 작업이 가능한지를 지정하는 intent filter를 각각 따로 생성해야 한다.

예를 들어, activity가 ACTION_SEND 및 ACTION_SENDTO intent 모두에서 텍스트와 이미지를 모두 처리한다고 가정할 경우, 두 작업 각각에 별도의 intent filter를 정의해야 한다.

그 이유는 ACTION_SENDTO intent는 데이터 Uri를 사용해여 send 또는 sendto URI 구성표를 사용하는 수신자 주소를 지정해야 하기 때문이다.

<activity android:name="ShareActivity">
<!-- filter for sending text; accepts SENDTO action with sms URI schemes -->
<intent-filter>
<action android:name="android.intent.action.SENDTO"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="sms" />
<data android:scheme="smsto" />
</intent-filter>
<!-- filter for sending text or images; accepts SEND action and text or image data -->
<intent-filter>
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="image/*"/>
<data android:mimeType="text/plain"/>
</intent-filter>
</activity>

암시적 intent를 수신하기 위해서는 CATEGORY_DEFAULT category를 intent filter에 포함해야 한다.

startActivity() 및 startActivityForResult() 메서드는 모든 intent를 마치 CATEGORY_DEFAULT를 선언한 것처럼 취급한다.

Intent filter에서 이 category를 선언하지 않으면 어떠한 암시적 intent오 activity로 확인할 수 없다.

Activity 에서 intent 처리하기

Activity를 시작하는데 사용된 intent를 읽어서 Activity에서 할 작업을 결정할 수 있다.

Activity가 시작되면, getIntent()를 호출하여 activity를 시작한 intent를 검색한다. 이 작업은 activity의 수명 주기 동안 언제든지 가능하지만, 일반적으로 onCreate() 또는 onStart()와 같은 초기 콜백 과정에서 수행한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// 이 activity를 구동시킨 intent를 얻어온다.
Intent intent = getIntent();
Uri data = intent.getData();
// intent type에 기반하여 무엇을 할 것인지 정한다.
if (intent.getType().indexOf("image/") != -1) {
// 이미지 데이터를 다룬다.
} else if (intent.getType().equals("text/palain")) {
// 텍스트 데이터를 다룬다.
}
}

결과 반환하기

Activity를 호출한 activity로 결과를 반환하고자 하는 경우, 간단하게 setResult()를 호출하여 결과 코드 및 결과 intent를 지정하면 된다.

작업이 끝나고 사용자가 원래 activity로 되돌아갈 경우 finish()를 호출하여 activity를 종료(및 소멸)한다.

1
2
3
4
// 결과 데이터를 전달할 intent를 생성한다.
Intent result = new Intent("com.example.RESULT_ACTION", Uri.parse("content://result_uri"));
setResult(Activity.RESULT_OK, result);
finish();

결과 코드는 항상 결과와 함께 지정해야 한다. 일반적으로 RESULT_OK 또는 RESULT_CANCELED 이다.

Intent를 사용하여 추가 데이터를 제공할 수 있다.

결과는 기본적으로 RESULT_CANCELED로 설정된다. 따라서 작업을 완료하기 전 또는 개발자가 결과를 설정하기 전에 사용자가 Back 버튼을 누를 경우, 원래의 activity는 취소 결과를 받게된다.

단순히 여러 결과 옵션 중 하나를 나타내는 정수만 반환하면 되는 경우, 결과 코드를 0보다 큰 임의의 값으로 설정하면 된다.

결과 코드를 사용하여 정수만 제공하고 Intent를 포함할 필요는 없는 경우, setResult()를 호출하고 결과 코드만 전달하면 된다.

setResult(RESULT_COLOR_RED);
finish();

이 경우, 가능한 결과는 몇 개 없을 것이므로 결과 코드는 local로 정의된 0보다 큰 정수이다.

startActivity() 또는 startActivityForResult()로 activity가 시작되었는지 확인할 필요는 없다. activity를 시작한 intent가 결과를 원할 경우, setResult()를 호출하기만 하면 된다.

원래의 activity가 startActivityRForResult()를 호출한 경우, 시스템은 개발자가 setResult()에 제공하는 결과를 activity에 제공한다. 그렇지 않은 경우 결과는 무시된다.

출처

Share