Android/Theory

Intent는 택배 상자!

sh1mj1 2022. 8. 28. 14:04

Intent

 

Intent 은 메시징 객체(택배 박스📦)이며 다른 앱 컴포넌트로부터 작업을 요청하는데 사용할 수 있다. 크게 세 가지 사용 사례가 있다.

  • 액티비티 시작

이전 포스팅에서 나왔듯이 액티비티의 새로운 인스턴스를 시작하기 위해 Intent 을 startActivity() 로 전달한다.

액티비티가 완료되었을 때 결과를 받으려면 startActivityForResult() 을 호출한다. 액티비티는 해당 결과를 이 액티비티의 onActivityResult() 콜백에서 별도의 Intent 객체로 받는다.

  • 서비스 시작

Service은 사용자 인터페이스 없이 Background에서 작업을 수행하는 구성 요소이다.

Android 5.0 (API 레벨 21) 이상부터는 JobScheduler 로 서비스를 시작할 수 있다. 이하 버전은 Service 클래스 메서드를 사용하면 서비스를 시작할 수 있다. 파일 다운로드 같은 일회성 작업을 수행하려면 Intent 을 startService() 에 전달하면 된다.

https://developer.android.com/reference/android/app/job/JobScheduler

https://developer.android.com/guide/components/services?hl=ko

  • 브로드캐스트 전달 (broadcast)

broadcast 예시

broadcast 은 모든 앱이 수신할 수 있는 메시지이다.

시스템은 부팅, 충전을 시작할 때 등 시스템 이벤트에 대한 다양한 브로드캐스트를 전달한다.

Intent 을 sendBroadcast() 또는 sendOrderedBroadcast() 에 전달하면 다른 앱에 브로드캐스트를 전달할 수 있다.

Intent types

인텐트 타입에는 두 가지 인텐트 타입이 있다. 택배 상자가 두가지 타입이 있다고 생각하면 편할 것이다.

Explicit Intent (명시적 인텐트)

인텐트를 충족하는 애플리케이션이 무엇인지 지정한다. 즉, 택배를 받을 수신인이 정확히 특정되어 있는 것이다.

예를 들어 사용자 작업에 응답하여 새로운 액티비티를 시작하거나 Background에서 파일을 다운로드하기 위해 서비스를 시작하는 것 등이 여기에 해당한다.

아래 그림은 명시적 인텐트로 실행할 액티비티를 직접 지정했을 때의 그림이다.

액티비티를 시작할 때 Explicit Intent 을 사용하는 법.

  1. 액티비티 A 가 작업 설명이 있는 Intent 를 생성하여 이를 startActivity()에 전달.
  2. Android 시스템이 모든 앱에서 해당 인텐트와 일치하는 인텐트 필터 검색
  3. 일치하는 항목을 찾으면 시스템이 onCreate() 메서드 호출, 이를 Intent 에 전달하고 일치하는 액티비티 B을 시작.

아래 코드는 앱 안에 DownloadService 라는 서비스를 만들고, 이 서비스는 웹 상에서 파일을 다운로드하도록 설계되었다. 서비스 시작 예시 코드이다.

// Executed in an Activity, so 'this' is the **Context**
// The fileUrl is a string URL, such as "<http://www.example.com/image.png>"
val downloadIntent = Intent(this, DownloadService::class.java).apply {
    data =Uri.parse(fileUrl)
}
startService(downloadIntent)

Intent(Context, Class) 구조이다. 인텐트는 앱 내의 DownloadService 클래스를 명시적으로 시작한다.

Implicit Intent (암시적 인텐트)

특정 컴포넌트의 이름을 대지 않지만 그 대신 수행할 일반적인 작업을 선언하여 다른 앱의 컴포넌트가 이를 처리할 수 있도록 한다. 택배로 치면 수신인이 머리 색이 빨간색인 조건과 매일 아침 6시에 일어나는 조건, 등 여러 조건을 만족하는 사람이 되는 것이다.

암시적 인텐트를 사용하면 시스템에서 적절한 컴포넌트를 찾는다. 이 때 인텐트의 내용을 기기에 있는 다른 여러 앱의 Manifest 파일에서 선언된 intent filters 와 비교한다. 해당 인텐트와 일치하는 인텐트 필터가 있다면 시작 후 인텐트 객체를 전달한다. 여러 개가 호환된다면 위 사진처럼 하단에 표시된다.

예를 들어서 내 휴대기기 저장소에 사진 파일에 접근하는 코드를 실행하면 그 기능을 수행할 수 있는 여러 앱이 휴대폰 하단에 표시되는 것을 기억할 것이다.!

사용자가 다른 사람들과 공유했으면 하는 콘텐트를 가지고 있는 경우, ACTION\_SEND 작업이 있는 인텐트를 생성한 후 공유할 콘텐츠를 지정하는 extra을 추가하면 된다.

// Create the text message with a string
val sendIntent = Intent().apply {
    action = Intent.ACTION_SEND
    putExtra(Intent.EXTRA_TEXT, textMessage)
    type = "text/plain"
}

// Verify that the intent will resolve to an activity
if (sendIntent.resolveActivity(packageManager) != null) {
    startActivity(sendIntent)
}

시스템이 설치된 앱을 모두 살펴보고 이 종류의 인텐트를 처리할 수 있는 앱이 어느 것인지 알아본다.(ACTION_SEND 작업이 있는 인텐트이며 “텍스트/일반" 데이터가 담긴 것)

만약 처리할 수 있는 앱이 하나뿐이면 해당 앱이 바로 열린다. 처리 가능한 앱이 여러 개일 때 사용자는 어느 앱을 사용할 지 선택하고 이 작업에 대한 기본 앱으로 지정할 수 있다.

🤔: 그런데 사용자에게 매번 다른 앱을 사용하기를 원할 수도 있는 경우라면?????

선택기 대화상자를 명시적으로 표시해야 한다. 선택기를 표시하려면 createChooser() 을 사용하여 Intent을 만들고 startActivity() 에 전달한다. 아래 예시 코드이다.

val sendIntent = Intent(Intent.ACTION_SEND)
...

// Always use string resources for UI text.
// This says something like "Share this photo with"
val title: String = resources.getString(R.string.chooser_title)
// Create intent to show the chooser dialog
val chooser: Intent = Intent.createChooser(sendIntent, title)

// Verify the original intent will resolve to at least one activity
if (sendIntent.resolveActivity(packageManager) != null) {
    startActivity(chooser)
}

Intent Filter

위에서 계속 Intent Filter 에 대해 이야기 했는데 도당체 Intent Filter 가 뭐냐?!~

Activity, Service, Broadcast receiver는 시스템에게 처리할 암시적(Implicit) 인텐트를 알려주기 위해 하나 이상의 인텐트 필터를 가진다. 필터는 관계없는 인텐트를 제외하고 필요한 타입의 인텐트만을 걸러내는 기능을 한다.

즉, 인텐트 필터는 택배 수신인의 조건이라는 것이다!!

이 때 명시적 인텐트는 필터에 상관없이 항상 대상에게 전달된다. 인텐트 필터는 컴포넌트가 수신하고자 하는 인텐트의 타입을 명시하는 앱의 매니페스트 파일 안에 표현이다.

예를 들어서 액티비티에 대한 인텐트 필터를 선언해서 우리는 앱을 시작했을 때 시작 액티비티가 무엇일지를 지정할 수 있다.

비슷하게 액티비티에 대한 어떤 인텐트 필터도 선언을 하지 않으면 그 액티비티는 오직 명시적 인텐트로만 실행될 수 있는 것이다.

주의 인텐트 필터는 특성상 보안성이 뛰어나지 않다. 그러므로 앱의 Service을 시작할 때 항상 명시적 인텐트만 사용하고 서비스에 대한 인텐트 필터는 선언하지 않는 게 좋다. 암시적 인텐트를 사용하여 서비스를 시작하면 보안 위협을 초래하기 때문이다. 인텐트에서 어느 서비스가 응답할지 확신할 수 없고, 사용자는 어느 서비스가 시작되는지 볼수 없다. Android 5.0(API 21) 부터 시스템은 개발자가 암시적 인텐트로 bindService()을 호출하면 예외를 발생시킨다.

Building an intent (인텐트 빌드)

Intent 객체에는 시스템이 어떤 컴포넌트를 시작할지 판별하는데 사용하는 정보가 담겨 있다. 인텐트를 수신해야 하는 특정 컴포넌트 혹은 컴포넌트 카테고리같은 것들, 그리고 수신자 컴포넌트가 작업을 수행하기 위해 사용할 정보도 이 안에 담겨 있다.

  • 컴포넌트 이름

이는 선택사항이지만 명시적 인텐트에서는 필수이다.

Service을 시작하는 경우 보안의 이유로 항상 컴포넌트 이름을 지정해야 한다.

인텐트의 field는 ComponentName 객체로 정규화된 클래스 이름을 사용해야 한다.

우리가 익숙한 MainActivity::class.java 이런 것들 말이다.

  • Action (작업)

수행할 Action을 나타내는 문자열이다. 브로드캐스트 인텐트의 경우에는 단말기에 보고될 액션이 될 것이다.

상수 대상 컴포넌트 Action

상수 대상 컴포넌트 Action
ACTION_CALL activity 전화 걸기
ACTION_EDIT activity 사용자에게 편집할 수 있는 데이터 표시
ACTION_MAIN activity 데이터 입출력없이 task의 최초 액티비티 실행
ACTION_SYNC activity 단말기의 데이터와 서버의 데이터 동기화
ACTION_BATTERY_LOW broadcast receiver 배터리 부족 경고 통보
etc… etc.. etc…

기본 상수외에 커스텀된 Action-문자열을 정의할 수도 있다. 이 때는 아래처럼 앱의 패키지 이름을 앞에 포함시켜야 한다.

const val ACTION_TIMETRABEL = "com.example.action.TIMETRAVEL"
  • Data

처리되어야 할 데이터에 대한 URI와 MIME 타입 정보를 가진다. 서로 다른 액션들은 다른 종류의 데이터와 한쌍이 된다.

예를 들자면 액션 field 가 ACTION_EDIT 이면 데이터 field는 편집을 위해서 문서 URI 을 가질 것이다. 또 ACTION_CALL 이면 데이터는 통화번호를 가진 tel : URI 일 것이다.

인텐트를 데이터 처리 컴포넌트에 넘길 때에는 URI 에 대한 MIME 타입도 중요하다. 만약 이미지를 표시할 수 있는 액티비티라면 오디오 파일은 재생하지 못할 것이다. 물론 데이터 타입은 URI로부터 추측이 가능하지만 URI 형식이 비슷할 수 있다.

명시적으로도 Data를 지정이 가능하다. setData() 메소드는 URI만 사용하여 데이터를 지정하고 setType() 메소드는 MIME 타입으로만, setDataAndType() 메소드는 URI와 MIME 타입 모두를 지정한다.

URI는 getData(), 타입은 getType() 으로 얻을 수 있다.

  • Category

인텐트를 제어하는 컴포넌트의 종류에 대한 추가정보를 제공하는 문자열이다. 인텐트 객체는 여러 카테고리를 가질 수도 있다.

상수 의미

상수 의미
CATEGORY_BROWSABLE 대상 액티비티가 웹브라우저로 시작되게 허용하고, 링크로 참조된 데이터(이미지, 이메일 메시지 등)를 표시하게 한다.
CATEGORY_GADGET 액티비티는 가젯들을 보유한 다른 액티비티내에 포함될 수 있다.
CATEGORY_HOME 액티비티 홈 화면을 보여줌. 디바이스를 켯을때, 또는 HOME 키를 눌러 돌아갔을 때 사용자가 보게 될 첫번째 화면
CATEGORY_LAUNCHER 이 액티비티가 최초의 액티비티이며, 최상위 계층의 런처에 포함됨
CATEGORY_PREFERENCE 대상이 되는 액티비티는 설정을 위한 패널임

addCategory() 메소드는 인텐트 객체에 카테고리를 추가하고 removeCategory() 메서드는 추가된 카테고리를 삭제한다.

getCategories() 메소드가 있는 이것은 해당 객체의 모든 카테고리를 얻어온다.

  • Extras

Extras는 앱 컴포넌트로 확인되는 방법에 영향을 미치지 않는 추가 정보이다. 즉, 어떤 특정한 Extra을 가진다고 해서 어떠어떠한 컴포넌트이다’ 라고 특정할 수 없는 인텐트의 기타 정보이다.

요청된 작업을 수행하는 데 필요한 추가 정보가 담긴 key-value 쌍이다.

putExtra() 메서드로 엑스트라 데이터를 추가할 수 있다.

하나하나 데이터를 추가할 수도 있지만 모든 extra 데이터들을 모두 포함한 Bundle 객체를 만든 후 그 Bundle을 Intent 에 putExtras() 로 넣을 수 있다.

여기까지 보면 이것도 굉장히 익숙하다! 우리가 액티비티에서 액티비티로 데이터를 전달할 때 항상 intent.putExtra(”key”, value) 형식을 사용했었다!!!

Intent 클래스는 표준 데이터 타입에 대해 여러 EXTRA_* 상수를 지정한다. 만약 커스텀 extra 키를 선언해야 하는 경우 아래처럼 앱의 패키지 이름을 넣어주어야 한다.

const val EXTRA_GIGAWATTS = "com.example.EXTRA_GIGAWATTS"

** 주의 ** 만약 다른 앱이 수신할 인텐트를 전송할 때는 Parcelable 이나 Serializable 데이터를 사용하면 안 된다. 앱이 Bundle 객체 안에 있는 데이터에 접근하려고 하다가 Parcel 되거나 직렬화된 클래스에 접근하지 못하면 RuntimeException 이 발생한다.

  • Flags

Intent 클래스에서 정의되고 인텐트에 대한 메타데이터와 같은 기능을 한다. 이 부분은 어떻게 사용하는지를 전혀 기억나지 않는다….. 아마 사용해본 적이 없을지도 모른다. 기억해두었다가 사용하게 되었을 때 다시 작성할게요..

암시적 인텐트 수신

앱이 수신할 수 있는 암시적 인텐트가 어느 것인지 알리려면 요소를 사용해서 매니페스트 파일에 선언한다. 각 인텐트 필터는 인텐트의 action, data, category 를 기반으로 어느 유형의 인텐트를 수락하는지 지정한다. 시스템은 인텐트가 인텐트 필터 중 하나를 통과한 경우에만 암시적 인텐트를 앱 컴포넌트에 전달한다.

어떤 액티비티에는 필터가 두 개 이상 있을 수도 있다. 한 필터는 이미지를 보고, 다른 필터는 이미지를 편집하기 위한 것으로 말이다. 액티비티가 시작되면 Intent를 검사한 다음 Intent 에 있는 정보를 근거로 어떻게 동작할 것인지 결정한다.

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

: name 특성에서 허용된 인텐트 action. (리터럴 문자열값)

: 허용된 데이터 타입. URI(scheme, host, port, path) 와 MIME 타입 을 나타내는 특성

: name 특성에서 허용된 인텐트 카테고리 선언. (리터럴 문자열값)

** 주의 ** 암시적 인텐트를 수신하려면 CATEGROY_DEFAULT 카테고리를 인텐트 필터에 포함해야 한다. startActivity()startActivityForResult() 메서드는 CATEGORY_DEFAULT 카테고리를 선언한 것처럼 인텐트를 취급한다. 만약 이 카테고리를 인텐트 필터에서 선언하지 않으면 암시저 인텐트로 접근할 수 없다.

필터 예시

<activity android:name="MainActivity">
    <!-- This activity is the main entry, should appear in app launcher -->
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

<activity android:name="ShareActivity">
    <!-- This activity handles "SEND" actions with text data -->
    <intent-filter>
        <action android:name="android.intent.action.SEND"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <data android:mimeType="text/plain"/>
    </intent-filter>
    <!-- This activity also handles "SEND" and "SEND_MULTIPLE" with media data -->
    <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>
  • ACTION_MAIN 작업은 이것이 주요 진입 지점이며 어느 인텐트 데이터도 기대하지 않는다는 것을 나타냅니다.
  • CATEGORY_LAUNCHER 카테고리는 이 액티비티의 아이콘이 시스템의 앱 시작 관리자에 배치되어야 한다는 것을 나타냅니다. 요소가 아이콘을 icon으로 지정하지 않은 경우, 시스템은 요소로부터 가져온 아이콘을 사용합니다.

이것으로 기본적인 Intent 와 Intent Filters 을 정리했다.

다음 Intent에 대한 공부에서는 unsafe intent 실행을 감지하고 어떻게 대처하는지와 PendingIntent 에 대해 알아볼 것이다. 아마 바로 다음 포스팅에서는 하지 않을 것 같다.. 항상 그렇듯 공부할 것은 많고 아는 것은 적다. 화이팅!

 

참고한 사이트

https://underclub.tistory.com/358

https://namsieon.com/361

https://namsieon.com/361

https://limkydev.tistory.com/35

https://developer.android.com/guide/components/intents-filters#Types