Android

인텐트 (1) : 인텐트와 인텐트 필터

까망사과 2022. 4. 4. 22:00

인텐트는 컴포넌트에 작업을 요청할 때 사용하는 메시징 객체이다.

인텐트가 컴포넌트와 통신하는 방법은 다양하지만 기본적인 용도는 다음과 같다.

  • 액티비티 시작
    startActivity()Intent 객체를 전달하여 새 액티비티를 시작한다.
    시작한 액티비티가 종료될 때 결과를 수신하려면 대신 startActivityForResult()를 호출한다.
  • 서비스 시작
    startService(Intent)로 서비스를 시작한다.
    서비스를 다른 컴포넌트에 바인딩하려면 bindService()Intent 객체를 전달한다.
  • 브로드캐스트 전달
    sendBroadcast() 또는 sendOrderedBroadcast()Intent 객체를 전달하여 다른 앱에 브로드캐스트를 전달한다.

 

인텐트 유형

인텐트는 두 유형으로 분류된다.

  • 명시적 인텐트
    대상 컴포넌트가 명시적으로 지정된 인텐트.
    컴포넌트의 클래스 이름/패키지 이름이 필요하기 때문에 일반적으로 같은 앱의 컴포넌트를 시작할 때 사용한다.
    Intent 객체에 특정 컴포넌트 이름을 지정하면 Android 시스템이 해당 컴포넌트를 즉시 시작한다.
  • 암시적 인텐트
    컴포넌트 이름을 지정하지 않고 수행할 작업(액션)이 지정된 인텐트.
    Android 시스템은 인텐트를 매니페스트 파일 안에 선언된 인텐트 필터와 비교하여 적합한 컴포넌트를 찾는다.
    일치하는 필터가 여러 개일 경우 다이얼로그를 표시하여 어떤 앱을 사용할지 선택할 수 있다.

컴포넌트에 인텐트 필터를 선언하면 다른 앱에서 특정한 인텐트를 사용하여 이를 시작할 수 있다.
필터를 하나도 선언하지 않으면 해당 컴포넌트는 명시적 인텐트로만 시작할 수 있다.

 

서비스를 시작할 때는 항상 명시적 인텐트를 사용해야 하며 서비스에 인텐트 필터를 선언하지 않아야 한다.
해당 인텐트에 어떤 서비스가 응답하는지 알 수 없기 때문이다.
Android 5.0(API 레벨 21) 이상 버전에서는 bindService()에 암시적 인텐트를 전달하면 예외가 발생한다.


인텐트 빌드하기

Intent 객체는 다음 정보를 포함한다.

  • 컴포넌트 이름
    시작하려는 컴포넌트의 이름. 명시적 인텐트로 사용할 때는 필수 사항이다.
    setComponent(), setClass(), setClassName(), Intent 생성자로 지정할 수 있다.
  • 액션
    수행하고자 하는 작업을 나타내는 문자열. 일반적으로 Intent나 다른 클래스의 상수를 사용한다.
    자체적으로 정의하려면 앱의 패키지 이름을 문자열의 접두어로 써야 한다.
    setAction(), Intent 생성자로 지정할 수 있다.
  • 데이터
    액션을 수행할 때 사용할 데이터 또는 이의 MIME 타입을 참조하는 URI(Uri 객체). 일반적으로 액션에 의해 결정된다.
    MIME 타입은 URI로부터 추론할 수도 있다.
    데이터의 URI만 설정하려면 setData()를 사용하고 MIME 타입만 설정하려면 setType()을 사용한다.
    두 메서드는 서로의 값을 무효화하기 때문에 둘 다 설정하려면 setDataAndType()을 사용해야 한다.
  • 카테고리
    인텐트를 처리할 컴포넌트에 대한 추가 정보를 포함하는 문자열. 개수 제한이 없지만 보통 필요 없다.
    addCategory()로 지정할 수 있다.
  • 엑스트라
    기타 정보를 포함하는 키-값 쌍. putExtra()로 여러 타입의 엑스트라를 추가할 수 있다.
    putExtras(Bundle)을 사용하면 여러 개의 엑스트라를 한 번에 추가할 수 있다.
    자체적으로 키 문자열을 정의하려면 앱의 패키지 이름을 접두어로 써야 한다.

    다른 앱이 수신할 수 있는 인텐트를 전송할 때 Parcelable 또는 Serializable 데이터를 사용하면 안 된다.
    이에 접근할 수 있는 권한이 없는 상태에서 접근하려 하면 RuntimeException이 발생한다.
  • 플래그
    시스템이 인텐트를 처리하는 방식을 나타내는 값. setFlags()에 플래그 값을 전달하여 지정한다.

 

인텐트 예시

암시적 인텐트 예시

// this는 액티비티
val downloadIntent = Intent(this, DownloadService::class.java).apply {
    data = Uri.parse("http://www.example.com/image.png")
}
startService(downloadIntent)

암시적 인텐트 예시

// 메시지를 보내는 인텐트
val sendIntent = Intent().apply {
    action = Intent.ACTION_SEND
    type = "text/plain"
    putExtra(Intent.EXTRA_TEXT, "Hello")
}

// 인텐트 호출 시도
try {
    startActivity(sendIntent)
} catch (e: ActivityNotFoundException) { // 인텐트를 처리할 수 있는 액티비티가 없음
}

startActivity()로 시작할 수 있는 액티비티의 개수에 따른 결과는 다음과 같다.

  • 1개
    해당 액티비티를 즉시 시작
  • 2개 이상
    사용할 앱을 선택하는 다이얼로그 표시
  • 없음
    ActivityNotFoundException 발생

 

앱 선택 다이얼로그 강제로 표시하기

암시적 인텐트를 처리할 수 있는 앱이 여러 개인 경우 시스템이 다이얼로그를 표시하여 사용할 앱을 선택할 수 있으며, 해당 액션에 대한 기본 앱을 지정할 수 있다.

하지만 해당 인텐트를 매번 다른 앱으로 처리하도록 하려면 앱 선택 다이얼로그를 명시적으로 표시해야 한다.

그렇게 하려면 createChooser()로 다이얼로그를 표시하는 인텐트를 생성한 다음 이를 startActivity()에 전달해야 한다.

 

예시

// 실행하려는 인텐트
val sendIntent = Intent(Intent.ACTION_SEND).apply {
    /* ... */
}

// 다이얼로그의 제목으로 사용할 문자열
val title = resources.getString(R.string.chooser_title)

// 다이얼로그를 표시하는 인텐트
val chooser = Intent.createChooser(sendIntent, title)

// sendIntent를 처리할 수 있는 액티비티가 존재하면 다이얼로그를 표시
sendIntent.resolveActivity(packageManager)?.run {
    startActivity(chooser)
}

암시적 인텐트 수신

암시적 인텐트를 수신하려면 매니페스트 파일 안의 각 컴포넌트에 <intent-filter> 태그를 1개 이상 선언해야 한다.
각 인텐트 필터는 액션, 데이터, 카테고리 정보를 바탕으로 인텐트 유형을 지정한다.
시스템은 필터 중 하나를 통과할 수 있는 경우에만 암시적 인텐트를 컴포넌트에 전달한다.

 

명시적 인텐트는 인텐트 필터와 상관없이 컴포넌트에게 전달된다.

 

<intent-filter> 태그를 포함하는 컴포넌트는 exported 속성 값을 설정해야 한다.
이 속성은 다른 앱에서 해당 컴포넌트에 접근할 수 있는지 여부를 나타낸다.

 

컴포넌트가 인텐트 필터를 사용하지만 exported 속성값이 설정되지 않으면 Android 12 이상 버전을 실행하는 디바이스에 앱이 설치되지 않는다.

 

<intent-filter> 태그는 다음 하위 태그를 포함할 수 있다.

  • <action>
    name 속성으로 액션을 지정한다. 값은 문자열 리터럴이다.
  • <data>
    데이터 URI(scheme, host, port 등의 속성)와 MIME 타입(mimeType 속성)을 지정한다.
  • <category>
    name 속성으로 카테고리를 지정한다. 값은 문자열 리터럴이다.

    암시적 인텐트를 수신하려면 CATEGORY_DEFAULT 카테고리를 반드시 포함해야 한다.
    startActivity()startActivityForResult()는 모든 인텐트를 CATEGORY_DEFAULT 카테고리를 선언한 것처럼 취급한다.
    이를 인텐트 필터 안에 선언하지 않으면 어떤 암시적 인텐트도 확인되지 않는다.

 

인텐트 필터로 사용하더라도 컴포넌트 이름을 알아내서 명시적 인텐트를 사용하면 외부 앱이 내부 앱의 컴포넌트를 시작할 수 있다.
외부 앱에서 내부 앱의 컴포넌트를 시작하지 않게 하려면 인텐트 필터를 선언하지 않고 exported 속성 값을 false로 설정해야 한다.

이와 마찬가지로 외부 앱의 서비스가 실행되지 않게 하려면 항상 명시적 인텐트를 사용해야 한다.

 

모든 액티비티는 매니페스트 파일 안에 인텐트 필터를 선언해야 한다.

하지만 브로드캐스트 리시버에 대한 인텐트 필터는 registerReceiver()를 호출하여 동적으로 등록할 수 있다.

 

인텐트 필터 예시

<activity android:name="MainActivity" android:exported="true">
    <!-- 이 액티비티는 메인 진입점으로 앱 런처에 나타난다 -->
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>
	
<activity android:name="ShareActivity" android:exported="false">
    <!-- 텍스트 데이터에 대한 SEND 액션을 처리함 -->
    <intent-filter>
        <action android:name="android.intent.action.SEND"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <data android:mimeType="text/plain"/>		   
    </intent-filter>
    <!-- 미디어 데이터에 대한 SEND, SEND_MULTIPLE 액션도 처리함 -->
    <intent-filter>
        <action android:name="android.intent.action.SEND"/>
        <action android:name="android.intent.action.SEND_MULTIPLE"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <data android:mimeType="application/vnd.google.panorama360+jpg"/>
        <data android:mimeType="image/*"/>
        <data android:mimeType="video/*"/>
    </intent-filter>
</activity>

MainActivity는 앱의 메인 진입점으로, 사용자가 런처 아이콘을 눌러 앱을 처음 실행했을 때 열린다.

액티비티가 앱 런처에 나타나도록 하려면 아래 2가지를 함께 선언해야 한다.

  • ACTION_MAIN
    이 액티비티가 메인 진입점이며 어떤 인텐트 데이터도 필요하지 않음을 나타낸다.
  • CATEGORY_LAUNCHER
    이 액티비티의 아이콘이 시스템의 앱 런처에 위치해야 함을 나타낸다.
    <activity> 요소에 icon 속성을 따로 지정하지 않으면 <application> 요소의 아이콘을 사용한다.

ShareActivity는 텍스트 및 미디어 컨텐츠를 공유하기 위해 사용한다.

MainActivity에서 진입할 수도 있지만 다른 앱에서 인텐트 필터와 일치하는 암시적 인텐트를 실행하여 진입할 수도 있다.