인텐트
안드로이드 앱을 개발하면서 액티비티를 뛰우는 가정에서 인텐트라는 것을 만들고 그 인텐트의 파라미터로 액티비티 클래스를 전달하면 그 액티비티가 실행되는 것을 경험해봤을 것이다.
val intent = Intent(this, TestActivity::class.java)
startActivity(intent)
또한 인텐트 안에 웹 페이지 주소나 전화번호 등을 URI 객체로 만들어 넣으면, 웹 브라우저나 전화걸기 화면이 띄워지는 것도 모두 인텐트가 그 중심이 된다.
val marketIntent = Intent(
Intent.ACTION_VIEW,
Uri.parse("market://details?id=$packageName")
)
startActivity(marketIntent)
이번 포스팅에서는 이러한 인텐트 객체를 한 번 알아보는 시간을 가질 것이다.
인텐트란?
어떤 컴포넌트에서 다른 컴포넌트를 호출하려면 "다른 컴포넌트를 호출하고 싶다" 라는 의사 표현을 해야 한다.
의사 표현을 하려면 공통된 규약에 맞춰 이야기를 해야 한다.
안드로이드에서 이러한 의사 표현의 수단으로 인텐트 객체를 사용한다.
안드로이드에서 인텐트라 하면 일반적으로 인텐트 객체를 뜻한다. → Intent()
인텐트 객체는 안드로이드 어플리케이션 내의 컴포넌트를 호출하기 위한 여러 정보들을 담고 있으며, 이 정보들에는 호출 대상 컴포넌트의 이름이 명시되어 있을 수도 있고, 혹은 호출 대상 컴포넌트의 특성만 나열되어 있을 수 있다.
아래에서 좀 더 자세히 알아보자.
명시적 인텐트
호출 대상 컴포넌트의 이름이 명시되어 있는 인텐트를 명시적 인텐트라고 한다.
컴포넌트의 이름을 명시적으로 정의해야 하기에 보통 앱 내에 정의되어 있는 액티비티를 인텐트 객체에 담아 실행시킨다. (아래 그림 참고)
val intent = Intent(this, Activity2::class.java)
startActivity(intent)
그렇다면 명시적 인텐트로 다른 앱을 실행할 수는 없는 걸까?
-> 놀랍게도 가능하다고 한다. ComponentName을 정확히 명시해서 다른 앱의 컴포넌트를 실행시킬 수 있다.
ComponentName componentName = new ComponentName(packageName,className);
Intent intent = new Intent();
intent.setComponent(componentName);
intent.putExtra("key","1234");
startActivity(intent)
이를 위해선 다른 앱의 패키지 및 클래스 이름을 정확히 알고 있고 다른 앱의 컴포넌트가 exported="true" 로 설정되어 있어야 한다.
이렇게 명시적으로만 알 수 있고 모든 권한이 충족되면 다른 앱도 실행시키는 것이 가능할 수 있다.
암시적 인텐트
암시적 인텐트는 명시적 인텐트와 달리, 호출할 컴포넌트를 정확히 아는 것이 아니라서 인텐트 객체 내에 호출 대상 컴포넌트를 찾을 수 있는 정보들을 담는다.
이 정보들을 바탕으로 안드로이드 시스템에서는 적합한 앱을 실행해 주는 식이다.
// Create the text message with a string.
val sendIntent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, textMessage)
type = "text/plain"
}
// Try to invoke the intent.
try {
startActivity(sendIntent)
} catch (e: ActivityNotFoundException) {
// Define what your app should do if no activity can handle the intent.
}
위 코드를 보면 action, extra, type 이 있다.
이들이 바로 정보들이다.
정보의 종류
- Action
- Category
- Data & Type
- Extras
- Flags
위 정보를 가진 호출 대상 컴포넌트를 호출하게 된다.
인텐트 해석
호출 당하는 컴포넌트는 위 정보를 해석하여 어떻게 실행되는걸까?
이것이 바로 "인텐트 해석 과정"이라고 한다.
먼저, 호출 당하는 컴포넌트는 정보를 아래처럼 AndroidManifest.xml 의 intent-filter 에 정의해둔다.
<activity android:name="ShareActivity" android:exported="false">
<intent-filter>
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="text/plain"/>
</intent-filter>
</activity>
인텐트 해석 과정은 다음과 같이 이루어진다.
앱이 인텐트를 발송하면, 안드로이드 시스템은 우선 인텐트가 포함한 Action, Category, Data 등의 속성을 분석한다.
그런 다음, 시스템은 이 정보를 기준으로 AndroidManifest.xml 파일에 정의된 intent-filter 를 참조하여, 해당 인텐트를 처리할 수 있는 컴포넌트를 검색한다.
이렇게 조건에 맞는 컴포넌트가 발견된다면, 안드로이드 시스템은 이 컴포넌트에 해당 인텐트를 전달하여 적절한 작업을 수행하게 되는 방식이다.
그림으로 쉽게 표현하자면 다음과 같다.
암시적 인텐트의 정보
암시적 인텐트와 인텐트 해석 과정에 대해 알아봤으니 이번엔 암시적 인텐트를 이루는 정보들을 살펴보자.
1. Action
어떤 동작을 수행할지 말해주는 문자열이다.
즉 호출 대상 컴포넌트가 처리해줬으면 하는 작업이 action 에 정의된다.
액션에는 안드로이드 시스템에서 사용하는 액션 외에도 사용자가 직접 액션 이름을 지정한 후 그 액션을 사용할 수도 있다.
기본적으로 안드로이드에서 사용하는 액션은 아래와 같은 것들이 있다.
- Intent.ACTION_VIEW: 특정 데이터를 표시 (예: 웹 페이지 열기)
- Intent.ACTION_SEND: 데이터를 다른 애플리케이션에 전송 (예: 이메일 보내기)
- Intent.ACTION_DIAL: 전화번호를 다이얼러에 표시
이때 인텐트 객체에 정의된 액션과 인텐트 필터에 정의된 액션이 일치하는지 여부를 검사하는데 이 검사를 통과하려면 액션이 서로 일치해야 한다. 하지만 인텐트 객체에 정의된 액션이 없다면 인텐트 필터에 정의한 어떤 액션이든 관계없이 통과할 수 있게 된다.
2. Category
카테고리는 컴포넌트가 어느 범주인지 등 추가 정보를 담는다.
- 웹 브라우저(BROWSABLE), 계산기(APP_CALCULATOR) 등
- LAUNCHER : 런쳐가 실행하는 컴포넌트
여기서 알아두어야 할 점은 startActivity()나 startActivityForResult() 메서드로 암시적 인텐트를 실행할 경우 코드에서 지정하지 않아도 CATEGORY_DEFAULT 가 선언된 것처럼 행동한다. 따라서 호출 당하는 컴포넌트의 인텐트 필터에서는 category 태그로 DEFAULT를 무조건 넣어야 한다. (넣지 않으면 어떠한 암시적 인텐트도 받을 수 없게 된다)
<category android:name="android.intent.category.DEFAULT" />
또한 액션 검사에서는 인텐트 객체 내에 액션이 정의되어 있지 않는 경우 액션 검사를 통과할 수 있었던 것에 반해, 카테고리 검사는 인텐트 객체에 정의된 카테고리가 인텐트 필터에 정의된 카테고리들과 일치해야 한다.
예를 들면, 인텐트 필터에서 CATEGORY_DEFAULT 와 com.androidhuman.TEST_CATEGORY 가 정의되어 있을 경우 인텐트 객체는 이 두 개 범위 내에서 한 개 이상은 가지고 있어야 한다.
하지만, CATEGORY_DEFAULT 와 com.androidhuman.NEW_CATEGORY 를 가지는 인텐트 객체의 경우, 인텐트 필터에서 NEW_CATEGORY 가 정의되어있지 않으므로 카테고리 검사를 통과할 수 없게 된다.
3. Data & Type
Data & Type 는 컴포넌트가 어떤 성격의 데이터를 처리하는지 보여준다.
인텐트 객체 내의 Data 항목 및 Type 을 검사하여 인텐트 필터에 정의된 값과 비교하여 일치 여부를 검사한다.
데이터 검사는 크게 데이터의 주소(URI)를 검사하는 부분과 데이터의 유형(type, MIME type)을 검사하는 부분으로 나누어진다.
URI(Uniform Resource Identifier)
URI 는 어떤 자원을 나타내는 유일한 주소이다.
URL(Uniform Resource Location) 또한 URI의 하위개념이다. 인터넷의 특정 페이지를 나타내는 주소가 URL인것과 마찬가지로, 안드로이드 시스템 내의 자원의 주소를 표현하는 하나의 방식이다.
3-1. 데이터의 주소(URI) 검사
데이터의 주소를 세분화하여 검사할 수 있도록 되어 있다. URI 는 다음과 같은 구조로 되어 있다.
scheme://host:port/path
- 스킴 (scheme): 리소스의 종류를 지정한다. 예를 들어 http, https, tel, geo, content 등이 있다.
- 호스트 (host): URI의 주체가 되는 주소. 예를 들어, http://www.example.com 에서 www.example.com이 호스트이다.
- 경로 (path): 더 구체적인 리소스를 가리키기 위한 경로 정보이다. 예를 들어, 파일의 경로를 지정하거나, 웹 URL에서 페이지의 위치를 나타낼 수 있다.
예를 들어, 아래 URI 는 쇼핑 앱의 해당 제품 페이지가 열리도록 설정할 수 있다.
val intent = Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse("shoppingapp://product:443/details/67890")
}
startActivity(intent)
// 호출 당하는 컴포넌트의 인텐트 필터
<data android:scheme="shoppingapp"
android:host="product"
android:port="443"
android:pathPrefix="/details"/>
다른 예시 - 사용자가 특정 도메인의 링크를 클릭할 때 앱이 자동으로 열리는 앱링크 같은 경우, 아래와 같이 인텐트 필터로 설정되어 앱이 특정 URL 을 수신할 수 있도록 한다.
<data android:scheme="http" android:host="predict-dev.wwwww.io" />
<data android:scheme="https" android:host="predict-dev.wwwww.io"/>
3-2. 타입 검사
Type 검사는 데이터의 종류를 지정해 인텐트 해석 과정에서 정확하게 대상 컴포넌트를 찾을 수 있도록 해준다.
언제 MIME 타입을 설정할까?
→ 보통 웹페이지, 파일 이미지, 이메일 등과 같이 리소스의 구체적인 유형이 필요할 때 MIME 타입을 설정한다.
예를 들어, 특정 파일을 열거나 이미지를 보여줄 때 MIME 타입을 통해 시스템에 리소스 형식을 전달해 알맞은 앱이 실행되도록 할 수 있다.
val intent = Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse("file://path/to/image.jpg")
type = "image/jpeg"
}
startActivity(intent)
위의 예는 URI 는 파일 경로를, 타입은 image/jpeg 로 지정해 안드로이드가 이미지 파일임을 인식하게 한다.
<data android:mimeType = "video/mpeg" android:scheme = "http">
<data android:mimeType = "audio/*" android:scheme = "http">
인텐트 필터에서의 mineType 은 위와 같은 형식으로 정의된다.
이런 방식으로 첫 번째 data 필터를 해석하면 "http 스키마를 가진 mpeg 형식의 비디오 데이터" 를 가진 인텐트를 허용하는 것임을 알 수 있고, 두 번째 필터는 "http 스키마를 가진 모든 오디오 데이터" 를 가진 인텐트를 허용하는 것임을 알 수 있다.
4. Extras
extra 는 인텐트 객체에 실제로 데이터를 첨부하여 보내는 것이다.
이 데이터들은 키-값 쌍을 이루어 인텐트 객체에 저장된다.
아래 코드는 안드로이드에서 인텐트(Intent)를 통해 데이터를 전달하고, 이를 다른 컴포넌트에서 받아오는 방식을 보여준다.
Intent i = new Intent();
i.putExtra("TEST", "Test string"); // String을 넣습니다.
// 인텐트를 받은 다른 컴포넌트가 Extras 데이터를 받아옵니다.
String str = getIntent().getExtras().getString("TEST");
예시
https://developer.android.com/guide/components/intents-common?hl=ko
위의 예시에서 각종 인텐트의 예제가 잘 나타나 있다.
아래 몇 가지 예를 직접 테스트해보았다.
- 알람 설정 테스트
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.provider.AlarmClock
class TestActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test)
val intent = Intent(AlarmClock.ACTION_SET_ALARM).apply {
putExtra(AlarmClock.EXTRA_MESSAGE, "test message")
putExtra(AlarmClock.EXTRA_HOUR, 0)
putExtra(AlarmClock.EXTRA_MINUTES, 1)
}
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent)
}
}
}
위처럼 암시적 인텐트를 작성하면 "test message" 라는 텍스트인 알람이 설정된다.
다만, 위처럼 인텐트를 호출하기 위해서 user-permission 권한을 설정해주어야 오류가 나지 않는다. (권한을 설정해주지 않으면 앱이 runtime exception 오류를 내며 종료된다)
<uses-permission android:name="com.android.alarm.permission.SET_ALARM" />
그리고 알람 앱은 아마 intent-filter 가 아래처럼 설정되어 있을 것이다.
카테고리 DEFAULT 는 아까 설명했던 것처럼 암시적 인텐트를 실행할 경우 코드에서 지정하지 않아도 CATEGORY_DEFAULT 가 선언된 것처럼 행동하기에 호출 당하는 컴포넌트에는 반드시 선언해주어야 한다.
<activity ...>
<intent-filter>
<action android:name="android.intent.action.SET_ALARM" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
- 연락처 추가 테스트
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.provider.AlarmClock
import android.provider.ContactsContract
import android.provider.MediaStore
class TestActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test)
val intent = Intent(Intent.ACTION_INSERT).apply {
type = ContactsContract.Contacts.CONTENT_TYPE
putExtra(ContactsContract.Intents.Insert.NAME, "JH")
putExtra(ContactsContract.Intents.Insert.EMAIL, "ix219@naver.com")
}
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent)
}
}
}
위처럼 Intent.ACTION_INSERT를 사용해 연락처 앱의 새 연락처 삽입 화면을 열고,
기본값으로 이름과 이메일을 입력하도록 한다.
그리고 호출 대상 컴포넌트는 아마 아래처럼 intent-filter 가 설정되어 있을 것이다.
<intent-filter>
<action android:name="android.intent.action.INSERT" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="vnd.android.cursor.dir/contact" />
</intent-filter>
지금까지 우리는 안드로이드에서 인텐트가 무엇인지, 인텐트 객체의 구조, 그리고 인텐트 필터와 그 통과 조건에 대해 알아보았다.
안드로이드에서 인텐트는 앱 컴포넌트 간의 상호작용을 위한 중요한 수단이라는 것을 알았으며,
컴포넌트가 처리할 수 있는 Action, Category, Data 등의 조건을 설정하여 어떤 컴포넌트에 전달될지 결정할 수 있게 하여 앱은 다양한 상황과 사용자 요청을 유연하게 대응할 수 있게 만든다.
개인적으로 위와 같이 공부해보면서 안드로이드가 이렇게 앱과의 상호작용을 처리한다는 것이 흥미로웠다.
다음 포스팅도 안드로이드 글로 찾아뵙겠다. :)